Christian Weichel 6cb7b610d2 [ws-proxy] Proxy pass static IDE assets from registry-facade's blobserve
/werft https
/werft ws-feature-flags=registry_facade
2020-09-04 14:12:06 +02:00

177 lines
5.6 KiB
Go

// Copyright (c) 2020 TypeFox 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"
"syscall"
"time"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/sirupsen/logrus"
"golang.org/x/xerrors"
"github.com/koding/websocketproxy"
)
// ProxyPassConfig is used as intermediate struct to assemble a configurable proxy
type proxyPassConfig struct {
TargetResolver targetResolver
ErrorHandler errorHandler
Transport http.RoundTripper
WebsocketSupport bool
}
// 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, *http.Request) (*url.URL, error)
// 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, resolver targetResolver, opts ...proxyPassOpt) http.HandlerFunc {
h := proxyPassConfig{
Transport: config.DefaultTransport,
}
for _, o := range opts {
o(&h)
}
h.TargetResolver = resolver
errorHandler := func(w http.ResponseWriter, req *http.Request, connectErr error) {
log.Debugf("could not connect to backend %s: %s", req.URL.String(), connectErrorToCause(connectErr))
if h.ErrorHandler != nil {
h.ErrorHandler(w, req, connectErr)
}
}
// proxy constructors
createWebsocketProxy := func(h *proxyPassConfig, targetURL *url.URL) http.Handler {
// TODO configure custom IdleConnTimeout for websockets
proxy := websocketproxy.NewProxy(targetURL)
return proxy
}
isWebsocketRequest := func(req *http.Request) bool {
return req.Header.Get("Connection") == "upgrade" && req.Header.Get("Upgrade") == "websocket"
}
createRegularProxy := func(h *proxyPassConfig, targetURL *url.URL) http.Handler {
proxy := httputil.NewSingleHostReverseProxy(targetURL)
proxy.Transport = h.Transport
proxy.ErrorHandler = errorHandler
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.StatusOK {
dmp, _ := httputil.DumpRequest(resp.Request, false)
log.WithField("url", url.String()).WithField("req", dmp).WithField("status", resp.Status).Debug("proxied request failed")
}
return nil
}
return proxy
}
return func(w http.ResponseWriter, req *http.Request) {
targetURL, err := h.TargetResolver(config.Config, req)
if err != nil {
log.WithError(err).Errorf("Unable to resolve targetURL: %s", req.URL.String())
return
}
// TODO Would it make sense to cache these constructs per target URL?
var proxy http.Handler
if h.WebsocketSupport && isWebsocketRequest(req) {
if targetURL.Scheme == "https" {
targetURL.Scheme = "wss"
} else if targetURL.Scheme == "http" {
targetURL.Scheme = "ws"
}
proxy = createWebsocketProxy(&h, targetURL)
} else {
proxy = createRegularProxy(&h, targetURL)
}
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 "unknown"
}
// withOnProxyErrorRedirectToWorkspaceStartHandler is an error handler that redirects to gitpod.io/start/#<wsid>
func withOnProxyErrorRedirectToWorkspaceStartHandler(config *Config) proxyPassOpt {
return func(h *proxyPassConfig) {
h.ErrorHandler = func(w http.ResponseWriter, req *http.Request, err error) {
// the default impl reports all errors as 502, so we'll do the same with the rest
ws := getWorkspaceCoords(req)
redirectURL := fmt.Sprintf("%s://%s/start/#%s", config.GitpodInstallation.Scheme, config.GitpodInstallation.HostName, ws.ID)
http.Redirect(w, req, redirectURL, 302)
}
}
}
// // withTransport allows to configure a http.RoundTripper that handles the actual sending and receiving of the HTTP request to the proxy target
// func withTransport(transport http.RoundTripper) proxyPassOpt {
// return func(h *proxyPassConfig) {
// h.Transport = transport
// }
// }
// withWebsocketSupport treats this route as websocket route
func withWebsocketSupport() proxyPassOpt {
return func(h *proxyPassConfig) {
h.WebsocketSupport = true
}
}
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: 100
IdleConnTimeout: time.Duration(config.IdleConnTimeout), // default: 90s
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}