mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
258 lines
7.4 KiB
Go
258 lines
7.4 KiB
Go
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
|
|
// Licensed under the GNU Affero General Public License (AGPL).
|
|
// See License.AGPL.txt in the project root for license information.
|
|
|
|
package proxy
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/gitpod-io/gitpod/common-go/log"
|
|
)
|
|
|
|
// ProxyPassConfig is used as intermediate struct to assemble a configurable proxy.
|
|
type proxyPassConfig struct {
|
|
TargetResolver targetResolver
|
|
ResponseHandler []responseHandler
|
|
ErrorHandler errorHandler
|
|
Transport http.RoundTripper
|
|
UseTargetHost bool
|
|
}
|
|
|
|
func (ppc *proxyPassConfig) appendResponseHandler(handler responseHandler) {
|
|
ppc.ResponseHandler = append(ppc.ResponseHandler, handler)
|
|
}
|
|
|
|
// proxyPassOpt allows to compose ProxyHandler options.
|
|
type proxyPassOpt func(h *proxyPassConfig)
|
|
|
|
// errorHandler is a function that handles an error that occurred during proxying of a HTTP request.
|
|
type errorHandler func(http.ResponseWriter, *http.Request, error)
|
|
|
|
// targetResolver is a function that determines to which target to forward the given HTTP request to.
|
|
type targetResolver func(*Config, WorkspaceInfoProvider, *http.Request) (*url.URL, error)
|
|
|
|
type responseHandler func(*http.Response, *http.Request) error
|
|
|
|
func singleJoiningSlash(a, b string) string {
|
|
aslash := strings.HasSuffix(a, "/")
|
|
bslash := strings.HasPrefix(b, "/")
|
|
switch {
|
|
case aslash && bslash:
|
|
return a + b[1:]
|
|
case !aslash && !bslash:
|
|
return a + "/" + b
|
|
}
|
|
return a + b
|
|
}
|
|
|
|
func joinURLPath(a, b *url.URL) (path, rawpath string) {
|
|
if a.RawPath == "" && b.RawPath == "" {
|
|
return singleJoiningSlash(a.Path, b.Path), ""
|
|
}
|
|
// Same as singleJoiningSlash, but uses EscapedPath to determine
|
|
// whether a slash should be added
|
|
apath := a.EscapedPath()
|
|
bpath := b.EscapedPath()
|
|
|
|
aslash := strings.HasSuffix(apath, "/")
|
|
bslash := strings.HasPrefix(bpath, "/")
|
|
|
|
switch {
|
|
case aslash && bslash:
|
|
return a.Path + b.Path[1:], apath + bpath[1:]
|
|
case !aslash && !bslash:
|
|
return a.Path + "/" + b.Path, apath + "/" + bpath
|
|
}
|
|
return a.Path + b.Path, apath + bpath
|
|
}
|
|
|
|
func NewSingleHostReverseProxy(target *url.URL, useTargetHost bool) *httputil.ReverseProxy {
|
|
targetQuery := target.RawQuery
|
|
director := func(req *http.Request) {
|
|
req.URL.Scheme = target.Scheme
|
|
req.URL.Host = target.Host
|
|
if useTargetHost {
|
|
req.Host = target.Host
|
|
}
|
|
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
|
|
if targetQuery == "" || req.URL.RawQuery == "" {
|
|
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
|
} else {
|
|
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
|
}
|
|
if _, ok := req.Header["User-Agent"]; !ok {
|
|
// explicitly disable User-Agent so it's not set to default value
|
|
req.Header.Set("User-Agent", "")
|
|
}
|
|
}
|
|
return &httputil.ReverseProxy{Director: director}
|
|
}
|
|
|
|
// proxyPass is the function that assembles a ProxyHandler from the config, a resolver and various options and returns a http.HandlerFunc.
|
|
func proxyPass(config *RouteHandlerConfig, infoProvider WorkspaceInfoProvider, resolver targetResolver, opts ...proxyPassOpt) http.HandlerFunc {
|
|
h := proxyPassConfig{
|
|
Transport: config.DefaultTransport,
|
|
}
|
|
for _, o := range opts {
|
|
o(&h)
|
|
}
|
|
h.TargetResolver = resolver
|
|
|
|
if h.ErrorHandler != nil {
|
|
oeh := h.ErrorHandler
|
|
h.ErrorHandler = func(w http.ResponseWriter, req *http.Request, connectErr error) {
|
|
log.Debugf("could not connect to backend %s: %s", req.URL.String(), connectErrorToCause(connectErr))
|
|
oeh(w, req, connectErr)
|
|
}
|
|
}
|
|
|
|
return func(w http.ResponseWriter, req *http.Request) {
|
|
targetURL, err := h.TargetResolver(config.Config, infoProvider, req)
|
|
if err != nil {
|
|
if h.ErrorHandler != nil {
|
|
h.ErrorHandler(w, req, err)
|
|
} else {
|
|
log.WithError(err).Errorf("Unable to resolve targetURL: %s", req.URL.String())
|
|
}
|
|
return
|
|
}
|
|
|
|
originalURL := *req.URL
|
|
|
|
// TODO(cw): we should cache the proxy for some time for each target URL
|
|
|
|
proxy := NewSingleHostReverseProxy(targetURL, h.UseTargetHost)
|
|
proxy.Transport = h.Transport
|
|
proxy.ModifyResponse = func(resp *http.Response) error {
|
|
url := resp.Request.URL
|
|
if url == nil {
|
|
return xerrors.Errorf("response's request without URL")
|
|
}
|
|
|
|
if log.Log.Level <= logrus.DebugLevel && resp.StatusCode >= http.StatusBadRequest {
|
|
dmp, _ := httputil.DumpRequest(resp.Request, false)
|
|
log.WithField("url", url.String()).WithField("req", dmp).WithField("status", resp.Status).Debug("proxied request failed")
|
|
}
|
|
|
|
// execute response handlers in order of registration
|
|
for _, handler := range h.ResponseHandler {
|
|
err := handler(resp, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
|
|
if h.ErrorHandler != nil {
|
|
req.URL = &originalURL
|
|
h.ErrorHandler(w, req, err)
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(originalURL.Path, "/_supervisor/") {
|
|
log.WithField("url", originalURL.String()).WithError(err).Debug("proxied request failed")
|
|
}
|
|
|
|
rw.WriteHeader(http.StatusBadGateway)
|
|
}
|
|
|
|
getLog(req.Context()).WithField("targetURL", targetURL.String()).Debug("proxy-passing request")
|
|
proxy.ServeHTTP(w, req)
|
|
}
|
|
}
|
|
|
|
func connectErrorToCause(err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
|
|
if netError, ok := err.(net.Error); ok && netError.Timeout() {
|
|
return "Connect timeout"
|
|
}
|
|
|
|
switch t := err.(type) {
|
|
case *net.OpError:
|
|
if t.Op == "dial" {
|
|
return fmt.Sprintf("Unknown host: %s", err.Error())
|
|
} else if t.Op == "read" {
|
|
return fmt.Sprintf("Connection refused: %s", err.Error())
|
|
}
|
|
|
|
case syscall.Errno:
|
|
if t == syscall.ECONNREFUSED {
|
|
return "Connection refused"
|
|
}
|
|
}
|
|
|
|
return err.Error()
|
|
}
|
|
|
|
func withHTTPErrorHandler(h http.Handler) proxyPassOpt {
|
|
return func(cfg *proxyPassConfig) {
|
|
cfg.ErrorHandler = func(w http.ResponseWriter, req *http.Request, err error) {
|
|
h.ServeHTTP(w, req)
|
|
}
|
|
}
|
|
}
|
|
|
|
func createDefaultTransport(config *TransportConfig) *http.Transport {
|
|
// TODO equivalent of client_max_body_size 2048m; necessary ???
|
|
// this is based on http.DefaultTransport, with some values exposed to config
|
|
return &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: (&net.Dialer{
|
|
Timeout: time.Duration(config.ConnectTimeout), // default: 30s
|
|
KeepAlive: 30 * time.Second,
|
|
DualStack: true,
|
|
}).DialContext,
|
|
ForceAttemptHTTP2: true,
|
|
MaxIdleConns: config.MaxIdleConns, // default: 0 (unlimited connections in pool)
|
|
MaxIdleConnsPerHost: config.MaxIdleConnsPerHost, // default: 100 (max connections per host in pool)
|
|
IdleConnTimeout: time.Duration(config.IdleConnTimeout), // default: 90s
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
}
|
|
|
|
// tell the browser to cache for 1 year and don't ask the server during this period.
|
|
func withLongTermCaching() proxyPassOpt {
|
|
return func(cfg *proxyPassConfig) {
|
|
cfg.appendResponseHandler(func(resp *http.Response, req *http.Request) error {
|
|
if resp.StatusCode < http.StatusBadRequest {
|
|
resp.Header.Set("Cache-Control", "public, max-age=31536000")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
|
|
func withXFrameOptionsFilter() proxyPassOpt {
|
|
return func(cfg *proxyPassConfig) {
|
|
cfg.appendResponseHandler(func(resp *http.Response, req *http.Request) error {
|
|
resp.Header.Del("X-Frame-Options")
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
|
|
func withUseTargetHost() proxyPassOpt {
|
|
return func(cfg *proxyPassConfig) {
|
|
cfg.UseTargetHost = true
|
|
}
|
|
}
|