diff options
author | Francisco Javier López <fjlopez@gitlab.com> | 2019-05-29 19:03:25 +0300 |
---|---|---|
committer | Francisco Javier López <fjlopez@gitlab.com> | 2019-05-29 19:03:25 +0300 |
commit | 209939c0edb2512f5801ca2c90f962058c678120 (patch) | |
tree | 739f9eaea74efcff682177356c08f1f09beaf425 | |
parent | ef7fff4fa64c9cb3ca57faef3f26fa59f4f51ecb (diff) |
backupfj-webide-http-proxy
-rw-r--r-- | app.go | 32 | ||||
-rw-r--r-- | client.html | 45 | ||||
-rw-r--r-- | internal/auth/auth.go | 42 | ||||
-rw-r--r-- | internal/buildservice/buildservice.go | 120 |
4 files changed, 205 insertions, 34 deletions
@@ -20,6 +20,7 @@ import ( "gitlab.com/gitlab-org/gitlab-pages/internal/admin" "gitlab.com/gitlab-org/gitlab-pages/internal/artifact" "gitlab.com/gitlab-org/gitlab-pages/internal/auth" + "gitlab.com/gitlab-org/gitlab-pages/internal/buildservice" "gitlab.com/gitlab-org/gitlab-pages/internal/domain" "gitlab.com/gitlab-org/gitlab-pages/internal/httperrors" "gitlab.com/gitlab-org/gitlab-pages/internal/netutil" @@ -38,10 +39,11 @@ var ( type theApp struct { appConfig - dm domain.Map - lock sync.RWMutex - Artifact *artifact.Artifact - Auth *auth.Auth + dm domain.Map + lock sync.RWMutex + Artifact *artifact.Artifact + BuildService *buildservice.BuildService + Auth *auth.Auth } func (a *theApp) isReady() bool { @@ -95,12 +97,15 @@ func (a *theApp) getHostAndDomain(r *http.Request) (host string, domain *domain. return host, a.domain(host) } +// IsAuthSupported checks if pages is running with the authentication support +func (a *theApp) IsAuthSupported() bool { + return a.Auth != nil +} + func (a *theApp) checkAuthenticationIfNotExists(domain *domain.D, w http.ResponseWriter, r *http.Request) bool { if domain == nil || !domain.HasProject(r) { - // Only if auth is supported - if a.Auth.IsAuthSupported() { - + if a.IsAuthSupported() { // To avoid user knowing if pages exist, we will force user to login and authorize pages if a.Auth.CheckAuthenticationWithoutProject(w, r) { return true @@ -140,6 +145,14 @@ func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, ht return true } + if a.IsAuthSupported() && a.BuildService != nil { + if token, err := a.Auth.GetSessionAccessToken(r); err == nil && token != "" { + if a.BuildService.TryMakeRequest(host, token, w, r) { + return true + } + } + } + if !a.isReady() { httperrors.Serve503(w) return true @@ -160,12 +173,10 @@ func (a *theApp) tryAuxiliaryHandlers(w http.ResponseWriter, r *http.Request, ht func (a *theApp) serveContent(ww http.ResponseWriter, r *http.Request, https bool) { w := newLoggingResponseWriter(ww) defer w.Log(r) - metrics.SessionsActive.Inc() defer metrics.SessionsActive.Dec() host, domain := a.getHostAndDomain(r) - if a.Auth.TryAuthenticate(&w, r, a.dm, &a.lock) { return } @@ -200,7 +211,6 @@ func (a *theApp) serveFileOrNotFound(domain *domain.D) http.HandlerFunc { // because the projects override the paths of the namespace project and they might be private even though // namespace project is public. if domain.IsNamespaceProject(r) { - if a.Auth.CheckAuthenticationWithoutProject(w, r) { return } @@ -358,6 +368,8 @@ func runApp(config appConfig) { if config.ClientID != "" { a.Auth = auth.New(config.Domain, config.StoreSecret, config.ClientID, config.ClientSecret, config.RedirectURI, config.GitLabServer) + + a.BuildService = buildservice.New(config.GitLabServer, config.ArtifactsServerTimeout, config.Domain) } configureLogging(config.LogFormat, config.LogVerbose) diff --git a/client.html b/client.html new file mode 100644 index 00000000..47d77de1 --- /dev/null +++ b/client.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> +<head> + <title></title> +</head> +<body> + <select id="method" value="get"> + <option value="get">GET</option> + <option value="post">POST</option> + <option value="delete">DELETE</option> + <option value="put">PUT</option> + </select> + + <input id="url" type="text" value="http://root.proxy.5029.service_name.service_port.127.0.0.1.xip.io:3010" style="width:100%"></input> + <button id="perform">PERFORM</button> + +<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.4.0.min.js"></script> +<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script> +<script type="text/javascript"> + +$("#perform").on("click", function(){ + axios({ + url: $("#url").val(), + method: $("#method").val(), + headers: { + // 'Access-Control-Allow-Origin': null, + // 'Access-Control-Allow-Origin': 'http://root.proxy.5029.service_name.service_port.127.0.0.1.xip.io:3010', + } + }) + .then(function (response) { + console.log("ENTRO") + // handle success + console.log(response); + }) + .catch(function (error) { + console.log("lalal") + // handle error + console.log(error); + }) +}) + + +</script> +</body> +</html> diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 02879568..9f28d879 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -91,11 +91,6 @@ func (a *Auth) checkSession(w http.ResponseWriter, r *http.Request) (*sessions.S // TryAuthenticate tries to authenticate user and fetch access token if request is a callback to auth func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, dm domain.Map, lock *sync.RWMutex) bool { - - if a == nil { - return false - } - session, err := a.checkSession(w, r) if err != nil { return true @@ -105,7 +100,6 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, dm domain if r.URL.Path != callbackPath { return false } - logRequest(r).Info("Receive OAuth authentication callback") if a.handleProxyingAuth(session, w, r, dm, lock) { @@ -125,12 +119,10 @@ func (a *Auth) TryAuthenticate(w http.ResponseWriter, r *http.Request, dm domain a.checkAuthenticationResponse(session, w, r) return true } - return false } func (a *Auth) checkAuthenticationResponse(session *sessions.Session, w http.ResponseWriter, r *http.Request) { - if !validateState(r, session) { // State is NOT ok logRequest(r).Warn("Authentication state did not match expected") @@ -380,22 +372,8 @@ func destroySession(session *sessions.Session, w http.ResponseWriter, r *http.Re http.Redirect(w, r, getRequestAddress(r), 302) } -// IsAuthSupported checks if pages is running with the authentication support -func (a *Auth) IsAuthSupported() bool { - if a == nil { - return false - } - return true -} - // CheckAuthenticationWithoutProject checks if user is authenticated and has a valid token func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http.Request) bool { - - if a == nil { - // No auth supported - return false - } - session, err := a.checkSession(w, r) if err != nil { return true @@ -408,7 +386,6 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. // Access token exists, authorize request url := fmt.Sprintf(apiURLUserTemplate, a.gitLabServer) req, err := http.NewRequest("GET", url, nil) - if err != nil { logRequest(r).WithError(err).Error("Failed to authenticate request") @@ -418,7 +395,6 @@ func (a *Auth) CheckAuthenticationWithoutProject(w http.ResponseWriter, r *http. req.Header.Add("Authorization", "Bearer "+session.Values["access_token"].(string)) resp, err := a.apiClient.Do(req) - if checkResponseForInvalidToken(resp, err) { logRequest(r).Warn("Access token was invalid, destroying session") @@ -535,3 +511,21 @@ func New(pagesDomain string, storeSecret string, clientID string, clientSecret s store: sessions.NewCookieStore([]byte(storeSecret)), } } + +func (a *Auth) GetSessionAccessToken(r *http.Request) (string, error) { + session, err := a.getSessionFromStore(r) + if err != nil { + return "", err + } + + if session == nil { + return "", fmt.Errorf("session not present") + } + + value := session.Values["access_token"] + if value == nil { + return "", fmt.Errorf("access_token is not present in the session") + } + + return value.(string), nil +} diff --git a/internal/buildservice/buildservice.go b/internal/buildservice/buildservice.go new file mode 100644 index 00000000..a6c82766 --- /dev/null +++ b/internal/buildservice/buildservice.go @@ -0,0 +1,120 @@ +package buildservice + +import ( + "fmt" + "log" + "net/http" + "net/http/httputil" + "net/url" + "regexp" + "strings" + "time" + + "gitlab.com/gitlab-org/gitlab-pages/internal/httptransport" +) + +const ( + proxyURLTemplate = "%s/api/v4/jobs/%s/proxy/%s/%s" + domainKeyword = "proxy" +) + +// The domain structure is: +// username.domainKeywork.build_id.service_name.service_port.pages.domain +// Ex: root.proxy.1.service_name.service_port.xip.io:3010 +// Service_port can be either a number or a string +var ( + // Captures subgroup + project, job ID and artifacts path + domainExtractor = regexp.MustCompile(`(?i)\A(.+)\.` + domainKeyword + `\.(\d+)\.(.+)\.(.+)\z`) +) + +// Artifact proxies requests for artifact files to the GitLab artifacts API +type BuildService struct { + server string + suffix string + client *http.Client +} + +// New when provided the arguments defined herein, returns a pointer to an +// BuildService that is used to proxy requests. +func New(server string, timeoutSeconds int, pagesDomain string) *BuildService { + return &BuildService{ + server: strings.TrimRight(server, "/"), + suffix: "." + strings.ToLower(pagesDomain), + client: &http.Client{ + Timeout: time.Second * time.Duration(timeoutSeconds), + Transport: httptransport.Transport, + }, + } +} + +// TryMakeRequest will attempt to proxy a request and write it to the argument +// http.ResponseWriter, ultimately returning a bool that indicates if the +// http.ResponseWriter has been written to in any capacity. +func (a *BuildService) TryMakeRequest(host string, token string, w http.ResponseWriter, r *http.Request) bool { + if a.server == "" || host == "" || token == "" { + return false + } + + rewrittenReq, ok := a.rewriteRequest(host, r) + if !ok { + return false + } + + a.makeRequest(w, rewrittenReq, token) + return true +} + +func (a *BuildService) makeRequest(w http.ResponseWriter, req *http.Request, token string) { + u, _ := url.Parse(a.server) + proxy := httputil.NewSingleHostReverseProxy(u) + + req.Header.Add("Authorization", "Bearer "+token) + proxy.ServeHTTP(w, req) +} + +// BuildURL returns a pointer to a url.URL for where the request should be +// proxied to. The returned bool will indicate if there is some sort of issue +// with the url while it is being generated. +// +// The URL is generated from the host (which contains the top-level group and +// ends with the pagesDomain) and the path (which contains any subgroups, the +// project, a job ID +func (a *BuildService) rewriteRequest(host string, r *http.Request) (*http.Request, bool) { + if !strings.HasSuffix(strings.ToLower(host), a.suffix) { + return nil, false + } + + topDomain := host[0 : len(host)-len(a.suffix)] + domainParts := domainExtractor.FindAllStringSubmatch(topDomain, 1) + if len(domainParts) != 1 || len(domainParts[0]) != 5 { + return nil, false + } + + jobID := domainParts[0][2] + serviceName := domainParts[0][3] + servicePort := domainParts[0][4] + generated := fmt.Sprintf(proxyURLTemplate, a.server, jobID, serviceName, servicePort) + + u, err := url.Parse(generated) + if err != nil { + return nil, false + } + + log.Printf("r.URL.RawQuery: %#+v\n", r.URL.RawQuery) + q := url.Values{"path": []string{r.URL.Path}} + q["wrapped_raw_query_"+jobID] = []string{r.URL.RawQuery} + u.RawQuery = q.Encode() + + // Rewritten request + r.URL = u + log.Printf("r.URL.Pathj: %#+v\n", r.URL.Path) + log.Printf("r.URL.Query(): %#+v\n", r.URL.Query()) + + // if err := r.ParseForm(); err != nil { + // return nil, false + // } + + // r.Form.Set("HTTP_JOB_TOKEN", "2z3OV406fjsxgAF3xUjS0JA/+u9sVpr0vKhU0FuLRwquFJx5") + + return r, true +} |