mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
224 lines
6.0 KiB
Go
224 lines
6.0 KiB
Go
// Copyright (c) 2021 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 pkg
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/eko/gocache/cache"
|
|
"github.com/gitpod-io/gitpod/common-go/experiments"
|
|
"github.com/gitpod-io/gitpod/common-go/log"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
const (
|
|
REQUEST_CACHE_KEY_CTX = "gitpod-cache-key"
|
|
REQUEST_ID_CTX = "gitpod-request-id"
|
|
UPSTREAM_CTX = "gitpod-upstream"
|
|
LOG_FIELD_REQUEST_ID = "request_id"
|
|
LOG_FIELD_REQUEST = "request"
|
|
LOG_FIELD_FUNC = "func"
|
|
LOG_FIELD_STATUS = "status"
|
|
)
|
|
|
|
type OpenVSXProxy struct {
|
|
Config *Config
|
|
defaultUpstreamURL *url.URL
|
|
cacheManager *cache.Cache
|
|
metrics *Prometheus
|
|
experiments experiments.Client
|
|
}
|
|
|
|
func (o *OpenVSXProxy) GetUpstreamUrl(r *http.Request) *url.URL {
|
|
reqid := r.Context().Value(REQUEST_ID_CTX).(string)
|
|
|
|
clientID := r.Header.Get("x-market-client-id")
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
upstream := o.experiments.GetStringValue(ctx, "openvsx_proxy_upstream", o.Config.URLUpstream, experiments.Attributes{
|
|
UserID: reqid,
|
|
VSCodeClientID: clientID,
|
|
})
|
|
upstreamUrl, err := url.Parse(upstream)
|
|
if err != nil {
|
|
return o.defaultUpstreamURL
|
|
}
|
|
return upstreamUrl
|
|
}
|
|
|
|
func (o *OpenVSXProxy) IsDisabledCache(u *url.URL) bool {
|
|
for _, v := range o.Config.AllowCacheDomain {
|
|
if strings.ToLower(u.Host) == v {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (o *OpenVSXProxy) Setup() error {
|
|
o.experiments = experiments.NewClient()
|
|
o.metrics = &Prometheus{}
|
|
o.metrics.Start(o.Config)
|
|
|
|
err := o.SetupCache()
|
|
if err != nil {
|
|
return xerrors.Errorf("error setting up cache: %v", err)
|
|
}
|
|
|
|
o.defaultUpstreamURL, err = url.Parse(o.Config.URLUpstream)
|
|
if err != nil {
|
|
return xerrors.Errorf("error parsing upstream URL: %v", err)
|
|
}
|
|
|
|
http.DefaultTransport.(*http.Transport).MaxIdleConns = o.Config.MaxIdleConns
|
|
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = o.Config.MaxIdleConnsPerHost
|
|
return nil
|
|
}
|
|
|
|
func (o *OpenVSXProxy) Start() (shutdown func(context.Context) error, err error) {
|
|
if o.defaultUpstreamURL == nil {
|
|
if err := o.Setup(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
proxy := newSingleHostReverseProxy()
|
|
proxy.ErrorHandler = o.ErrorHandler
|
|
proxy.ModifyResponse = o.ModifyResponse
|
|
proxy.Transport = &DurationTrackingTransport{o: o}
|
|
|
|
http.HandleFunc("/", o.Handler(proxy))
|
|
http.HandleFunc("/openvsx-proxy-status", func(rw http.ResponseWriter, r *http.Request) {
|
|
if _, _, err := o.ReadCache("does-not-exist"); err != nil {
|
|
log.WithError(err).Debug("status not ready")
|
|
rw.WriteHeader(http.StatusInternalServerError)
|
|
rw.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
rw.WriteHeader(http.StatusOK)
|
|
rw.Write([]byte("ok"))
|
|
})
|
|
|
|
srv := &http.Server{Addr: ":8080"}
|
|
|
|
go func() {
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.WithError(err).Panic("error starting HTTP server")
|
|
}
|
|
}()
|
|
return func(c context.Context) error {
|
|
return srv.Shutdown(c)
|
|
}, nil
|
|
}
|
|
|
|
type DurationTrackingTransport struct {
|
|
o *OpenVSXProxy
|
|
}
|
|
|
|
func (t *DurationTrackingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
reqid := r.Context().Value(REQUEST_ID_CTX).(string)
|
|
key, _ := r.Context().Value(REQUEST_CACHE_KEY_CTX).(string)
|
|
|
|
logFields := logrus.Fields{
|
|
LOG_FIELD_FUNC: "transport_roundtrip",
|
|
LOG_FIELD_REQUEST_ID: reqid,
|
|
LOG_FIELD_REQUEST: key,
|
|
}
|
|
|
|
start := time.Now()
|
|
defer func(ts time.Time) {
|
|
duration := time.Since(ts)
|
|
t.o.metrics.DurationUpstreamCallHistorgram.Observe(duration.Seconds())
|
|
log.
|
|
WithFields(logFields).
|
|
WithFields(t.o.DurationLogFields(duration)).
|
|
Info("upstream call finished")
|
|
}(start)
|
|
return http.DefaultTransport.RoundTrip(r)
|
|
}
|
|
|
|
// From go/src/net/http/httputil/reverseproxy.go
|
|
|
|
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() *httputil.ReverseProxy {
|
|
director := func(req *http.Request) {
|
|
target := req.Context().Value(UPSTREAM_CTX).(*url.URL)
|
|
targetQuery := target.RawQuery
|
|
|
|
originalHost := req.Host
|
|
|
|
req.URL.Scheme = target.Scheme
|
|
req.URL.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
|
|
}
|
|
req.Host = target.Host
|
|
|
|
if _, ok := req.Header["User-Agent"]; !ok {
|
|
// explicitly disable User-Agent so it's not set to default value
|
|
req.Header.Set("User-Agent", "")
|
|
}
|
|
|
|
// From https://github.com/golang/go/pull/36678
|
|
prior, ok := req.Header["X-Forwarded-Host"]
|
|
omit := ok && prior == nil // nil means don't populate the header
|
|
if !omit {
|
|
req.Header.Set("X-Forwarded-Host", originalHost)
|
|
}
|
|
|
|
prior, ok = req.Header["X-Forwarded-Proto"]
|
|
omit = ok && prior == nil // nil means don't populate the header
|
|
if !omit {
|
|
if req.TLS == nil {
|
|
req.Header.Set("X-Forwarded-Proto", "http")
|
|
} else {
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
}
|
|
}
|
|
// ReverseProxy will add X-Forwarded-For internally
|
|
}
|
|
return &httputil.ReverseProxy{Director: director}
|
|
}
|