Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/openvsx-proxy/pkg/run.go
2498 views
1
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package pkg
6
7
import (
8
"context"
9
"net/http"
10
"net/http/httputil"
11
"net/url"
12
"strings"
13
"time"
14
15
"github.com/eko/gocache/cache"
16
"github.com/gitpod-io/gitpod/common-go/experiments"
17
"github.com/gitpod-io/gitpod/common-go/log"
18
"github.com/sirupsen/logrus"
19
"golang.org/x/xerrors"
20
)
21
22
const (
23
REQUEST_CACHE_KEY_CTX = "gitpod-cache-key"
24
REQUEST_ID_CTX = "gitpod-request-id"
25
UPSTREAM_CTX = "gitpod-upstream"
26
LOG_FIELD_REQUEST_ID = "request_id"
27
LOG_FIELD_REQUEST = "request"
28
LOG_FIELD_FUNC = "func"
29
LOG_FIELD_STATUS = "status"
30
)
31
32
type OpenVSXProxy struct {
33
Config *Config
34
defaultUpstreamURL *url.URL
35
cacheManager *cache.Cache
36
metrics *Prometheus
37
experiments experiments.Client
38
}
39
40
func (o *OpenVSXProxy) GetUpstreamUrl(r *http.Request) *url.URL {
41
reqid := r.Context().Value(REQUEST_ID_CTX).(string)
42
43
clientID := r.Header.Get("x-market-client-id")
44
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
45
defer cancel()
46
upstream := o.experiments.GetStringValue(ctx, "openvsx_proxy_upstream", o.Config.URLUpstream, experiments.Attributes{
47
UserID: reqid,
48
VSCodeClientID: clientID,
49
})
50
upstreamUrl, err := url.Parse(upstream)
51
if err != nil {
52
return o.defaultUpstreamURL
53
}
54
return upstreamUrl
55
}
56
57
func (o *OpenVSXProxy) IsDisabledCache(u *url.URL) bool {
58
for _, v := range o.Config.AllowCacheDomain {
59
if strings.ToLower(u.Host) == v {
60
return false
61
}
62
}
63
return true
64
}
65
66
func (o *OpenVSXProxy) Setup() error {
67
o.experiments = experiments.NewClient()
68
o.metrics = &Prometheus{}
69
o.metrics.Start(o.Config)
70
71
err := o.SetupCache()
72
if err != nil {
73
return xerrors.Errorf("error setting up cache: %v", err)
74
}
75
76
o.defaultUpstreamURL, err = url.Parse(o.Config.URLUpstream)
77
if err != nil {
78
return xerrors.Errorf("error parsing upstream URL: %v", err)
79
}
80
81
http.DefaultTransport.(*http.Transport).MaxIdleConns = o.Config.MaxIdleConns
82
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = o.Config.MaxIdleConnsPerHost
83
return nil
84
}
85
86
func (o *OpenVSXProxy) Start() (shutdown func(context.Context) error, err error) {
87
if o.defaultUpstreamURL == nil {
88
if err := o.Setup(); err != nil {
89
return nil, err
90
}
91
}
92
proxy := newSingleHostReverseProxy()
93
proxy.ErrorHandler = o.ErrorHandler
94
proxy.ModifyResponse = o.ModifyResponse
95
proxy.Transport = &DurationTrackingTransport{o: o}
96
97
http.HandleFunc("/", o.Handler(proxy))
98
http.HandleFunc("/openvsx-proxy-status", func(rw http.ResponseWriter, r *http.Request) {
99
if _, _, err := o.ReadCache("does-not-exist"); err != nil {
100
log.WithError(err).Debug("status not ready")
101
rw.WriteHeader(http.StatusInternalServerError)
102
rw.Write([]byte(err.Error()))
103
return
104
}
105
rw.WriteHeader(http.StatusOK)
106
rw.Write([]byte("ok"))
107
})
108
109
srv := &http.Server{Addr: ":8080"}
110
111
go func() {
112
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
113
log.WithError(err).Panic("error starting HTTP server")
114
}
115
}()
116
return func(c context.Context) error {
117
return srv.Shutdown(c)
118
}, nil
119
}
120
121
type DurationTrackingTransport struct {
122
o *OpenVSXProxy
123
}
124
125
func (t *DurationTrackingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
126
reqid := r.Context().Value(REQUEST_ID_CTX).(string)
127
key, _ := r.Context().Value(REQUEST_CACHE_KEY_CTX).(string)
128
129
logFields := logrus.Fields{
130
LOG_FIELD_FUNC: "transport_roundtrip",
131
LOG_FIELD_REQUEST_ID: reqid,
132
LOG_FIELD_REQUEST: key,
133
}
134
135
start := time.Now()
136
defer func(ts time.Time) {
137
duration := time.Since(ts)
138
t.o.metrics.DurationUpstreamCallHistorgram.Observe(duration.Seconds())
139
log.
140
WithFields(logFields).
141
WithFields(t.o.DurationLogFields(duration)).
142
Info("upstream call finished")
143
}(start)
144
return http.DefaultTransport.RoundTrip(r)
145
}
146
147
// From go/src/net/http/httputil/reverseproxy.go
148
149
func singleJoiningSlash(a, b string) string {
150
aslash := strings.HasSuffix(a, "/")
151
bslash := strings.HasPrefix(b, "/")
152
switch {
153
case aslash && bslash:
154
return a + b[1:]
155
case !aslash && !bslash:
156
return a + "/" + b
157
}
158
return a + b
159
}
160
161
func joinURLPath(a, b *url.URL) (path, rawpath string) {
162
if a.RawPath == "" && b.RawPath == "" {
163
return singleJoiningSlash(a.Path, b.Path), ""
164
}
165
// Same as singleJoiningSlash, but uses EscapedPath to determine
166
// whether a slash should be added
167
apath := a.EscapedPath()
168
bpath := b.EscapedPath()
169
170
aslash := strings.HasSuffix(apath, "/")
171
bslash := strings.HasPrefix(bpath, "/")
172
173
switch {
174
case aslash && bslash:
175
return a.Path + b.Path[1:], apath + bpath[1:]
176
case !aslash && !bslash:
177
return a.Path + "/" + b.Path, apath + "/" + bpath
178
}
179
return a.Path + b.Path, apath + bpath
180
}
181
182
func newSingleHostReverseProxy() *httputil.ReverseProxy {
183
director := func(req *http.Request) {
184
target := req.Context().Value(UPSTREAM_CTX).(*url.URL)
185
targetQuery := target.RawQuery
186
187
originalHost := req.Host
188
189
req.URL.Scheme = target.Scheme
190
req.URL.Host = target.Host
191
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
192
if targetQuery == "" || req.URL.RawQuery == "" {
193
req.URL.RawQuery = targetQuery + req.URL.RawQuery
194
} else {
195
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
196
}
197
req.Host = target.Host
198
199
if _, ok := req.Header["User-Agent"]; !ok {
200
// explicitly disable User-Agent so it's not set to default value
201
req.Header.Set("User-Agent", "")
202
}
203
204
// From https://github.com/golang/go/pull/36678
205
prior, ok := req.Header["X-Forwarded-Host"]
206
omit := ok && prior == nil // nil means don't populate the header
207
if !omit {
208
req.Header.Set("X-Forwarded-Host", originalHost)
209
}
210
211
prior, ok = req.Header["X-Forwarded-Proto"]
212
omit = ok && prior == nil // nil means don't populate the header
213
if !omit {
214
if req.TLS == nil {
215
req.Header.Set("X-Forwarded-Proto", "http")
216
} else {
217
req.Header.Set("X-Forwarded-Proto", "https")
218
}
219
}
220
// ReverseProxy will add X-Forwarded-For internally
221
}
222
return &httputil.ReverseProxy{Director: director}
223
}
224
225