// This file is part of MinIO Console Server // Copyright (c) 2021 MinIO, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package operatorapi import ( "bytes" "crypto/sha1" "crypto/tls" "encoding/json" "errors" "fmt" "io" "log" "net/http" "net/http/cookiejar" url2 "net/url" "strings" "time" "github.com/gorilla/websocket" v2 "github.com/minio/operator/pkg/apis/minio.min.io/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/minio/console/cluster" "github.com/minio/console/pkg/auth" ) func serveProxy(responseWriter http.ResponseWriter, req *http.Request) { urlParts := strings.Split(req.URL.Path, "/") // Either proxy or hop, will decide the type of session proxyMethod := urlParts[2] if len(urlParts) < 5 { log.Println(len(urlParts)) return } namespace := urlParts[3] tenantName := urlParts[4] // validate the tenantName token, err := auth.GetTokenFromRequest(req) if err != nil { log.Println(err) responseWriter.WriteHeader(401) return } claims, err := auth.SessionTokenAuthenticate(token) if err != nil { log.Printf("Unable to validate the session token %s: %v\n", token, err) responseWriter.WriteHeader(401) return } STSSessionToken := claims.STSSessionToken opClientClientSet, err := cluster.OperatorClient(STSSessionToken) if err != nil { log.Println(err) responseWriter.WriteHeader(404) return } opClient := operatorClient{ client: opClientClientSet, } tenant, err := opClient.TenantGet(req.Context(), namespace, tenantName, metav1.GetOptions{}) if err != nil { log.Println(err) responseWriter.WriteHeader(502) return } nsTenant := fmt.Sprintf("%s/%s", tenant.Namespace, tenant.Name) tenantSchema := "http" tenantPort := fmt.Sprintf(":%d", v2.ConsolePort) if tenant.AutoCert() || tenant.ExternalCert() { tenantSchema = "https" tenantPort = fmt.Sprintf(":%d", v2.ConsoleTLSPort) } tenantURL := fmt.Sprintf("%s://%s.%s.svc.%s%s", tenantSchema, tenant.ConsoleCIServiceName(), tenant.Namespace, v2.GetClusterDomain(), tenantPort) // for development // tenantURL = "http://localhost:9091" // tenantURL = "https://localhost:9443" h := sha1.New() h.Write([]byte(nsTenant)) tenantCookieName := fmt.Sprintf("token-%s-%s-%x", proxyMethod, claims.AccountAccessKey, string(h.Sum(nil))) tenantCookie, err := req.Cookie(tenantCookieName) if err != nil { // login to tenantName loginURL := fmt.Sprintf("%s/api/v1/login", tenantURL) // get the tenant credentials clientSet, err := cluster.K8sClient(STSSessionToken) if err != nil { log.Println(err) responseWriter.WriteHeader(500) return } k8sClient := k8sClient{ client: clientSet, } tenantConfiguration, err := GetTenantConfiguration(req.Context(), &k8sClient, tenant) if err != nil { log.Println(err) responseWriter.WriteHeader(500) return } data := map[string]interface{}{ "accessKey": tenantConfiguration["accesskey"], "secretKey": tenantConfiguration["secretkey"], } // if this a proxy request hide the menu if proxyMethod == "proxy" { data["features"] = map[string]bool{ "hide_menu": true, } } payload, _ := json.Marshal(data) loginReq, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewReader(payload)) if err != nil { log.Println(err) responseWriter.WriteHeader(500) return } loginReq.Header.Add("Content-Type", "application/json") // FIXME: in the future we should use restapi.GetConsoleHTTPClient() tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client := &http.Client{Transport: tr} loginResp, err := client.Do(loginReq) if err != nil { log.Println(err) responseWriter.WriteHeader(500) return } if loginResp.StatusCode < 200 && loginResp.StatusCode <= 299 { log.Println(fmt.Printf("Status: %d. Couldn't complete login", loginResp.StatusCode)) responseWriter.WriteHeader(500) return } for _, c := range loginResp.Cookies() { if c.Name == "token" { tenantCookie = c c := &http.Cookie{ Name: tenantCookieName, Value: c.Value, Path: c.Path, Expires: c.Expires, HttpOnly: c.HttpOnly, } http.SetCookie(responseWriter, c) break } } defer loginResp.Body.Close() } if tenantCookie == nil { log.Println(errors.New("couldn't login to tenant and get cookie")) responseWriter.WriteHeader(403) return } // at this point we have a valid cookie ready to either route HTTP or WS // now we need to know if we are doing an /api/ call (http) or /ws/ call (ws) callType := urlParts[5] targetURL, err := url2.Parse(tenantURL) if err != nil { log.Println(err) responseWriter.WriteHeader(500) return } tenantBase := fmt.Sprintf("/api/%s/%s/%s", proxyMethod, tenant.Namespace, tenant.Name) targetURL.Path = strings.ReplaceAll(req.URL.Path, tenantBase, "") proxiedCookie := &http.Cookie{ Name: "token", Value: tenantCookie.Value, Path: tenantCookie.Path, Expires: tenantCookie.Expires, HttpOnly: tenantCookie.HttpOnly, } proxyCookieJar, _ := cookiejar.New(nil) proxyCookieJar.SetCookies(targetURL, []*http.Cookie{proxiedCookie}) switch callType { case "ws": handleWSRequest(responseWriter, req, proxyCookieJar, targetURL, tenantSchema) default: handleHTTPRequest(responseWriter, req, proxyCookieJar, tenantBase, targetURL) } } func handleHTTPRequest(responseWriter http.ResponseWriter, req *http.Request, proxyCookieJar *cookiejar.Jar, tenantBase string, targetURL *url2.URL) { tr := &http.Transport{ // FIXME: use restapi.GetConsoleHTTPClient() TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client := &http.Client{ Transport: tr, Jar: proxyCookieJar, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } // are we proxying something with cp=y? (console proxy) then add cpb (console proxy base) so the console // on the other side updates the to this value overriding sub path or root if v := req.URL.Query().Get("cp"); v == "y" { q := req.URL.Query() q.Add("cpb", tenantBase) req.URL.RawQuery = q.Encode() } // copy query params targetURL.RawQuery = req.URL.Query().Encode() proxRequest, err := http.NewRequest(req.Method, targetURL.String(), req.Body) if err != nil { log.Println(err) responseWriter.WriteHeader(500) return } for _, v := range req.Header.Values("Content-Type") { proxRequest.Header.Add("Content-Type", v) } resp, err := client.Do(proxRequest) if err != nil { log.Println(err) responseWriter.WriteHeader(500) return } for hk, hv := range resp.Header { if hk != "X-Frame-Options" { for _, v := range hv { responseWriter.Header().Add(hk, v) } } } // Allow iframes responseWriter.Header().Set("X-Frame-Options", "SAMEORIGIN") responseWriter.Header().Set("X-XSS-Protection", "1; mode=block") // Pass HTTP status code to proxy response responseWriter.WriteHeader(resp.StatusCode) io.Copy(responseWriter, resp.Body) } var upgrader = websocket.Upgrader{ ReadBufferSize: 0, WriteBufferSize: 1024, } func handleWSRequest(responseWriter http.ResponseWriter, req *http.Request, proxyCookieJar *cookiejar.Jar, targetURL *url2.URL, schema string) { dialer := &websocket.Dialer{ Proxy: http.ProxyFromEnvironment, HandshakeTimeout: 45 * time.Second, Jar: proxyCookieJar, } upgrader.CheckOrigin = func(r *http.Request) bool { return true } c, err := upgrader.Upgrade(responseWriter, req, nil) if err != nil { log.Print("error upgrade connection:", err) return } defer c.Close() if schema == "http" { targetURL.Scheme = "ws" } else { targetURL.Scheme = "wss" } // establish a websocket to the tenant tenantConn, _, err := dialer.Dial(targetURL.String(), nil) if err != nil { log.Println("dial:", err) return } defer tenantConn.Close() doneTenant := make(chan struct{}) done := make(chan struct{}) // start read pump from tenant connection go func() { defer close(doneTenant) for { msgType, message, err := tenantConn.ReadMessage() if err != nil { log.Println("error read from tenant:", err) return } c.WriteMessage(msgType, message) } }() // start read pump from tenant connection go func() { defer close(done) for { msgType, message, err := c.ReadMessage() if err != nil { log.Println("error read from client:", err) return } tenantConn.WriteMessage(msgType, message) } }() select { case <-done: case <-doneTenant: } }