Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-proxy/pkg/proxy/pass.go
2500 views
1
// Copyright (c) 2020 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 proxy
6
7
import (
8
"crypto/tls"
9
"fmt"
10
"net"
11
"net/http"
12
"net/http/httputil"
13
"net/url"
14
"strings"
15
"syscall"
16
"time"
17
18
"github.com/sirupsen/logrus"
19
"golang.org/x/xerrors"
20
21
"github.com/gitpod-io/gitpod/common-go/log"
22
"github.com/gitpod-io/gitpod/ws-proxy/pkg/common"
23
)
24
25
// ProxyPassConfig is used as intermediate struct to assemble a configurable proxy.
26
type proxyPassConfig struct {
27
TargetResolver targetResolver
28
ResponseHandler []responseHandler
29
ErrorHandler errorHandler
30
Transport http.RoundTripper
31
UseTargetHost bool
32
}
33
34
func (ppc *proxyPassConfig) appendResponseHandler(handler responseHandler) {
35
ppc.ResponseHandler = append(ppc.ResponseHandler, handler)
36
}
37
38
// proxyPassOpt allows to compose ProxyHandler options.
39
type proxyPassOpt func(h *proxyPassConfig)
40
41
// createHttpTransportOpt allows to compose create http Transport options.
42
type createHttpTransportOpt func(h *http.Transport)
43
44
// errorHandler is a function that handles an error that occurred during proxying of a HTTP request.
45
type errorHandler func(http.ResponseWriter, *http.Request, error)
46
47
// targetResolver is a function that determines to which target to forward the given HTTP request to.
48
type targetResolver func(*Config, common.WorkspaceInfoProvider, *http.Request) (*url.URL, string, error)
49
50
type responseHandler func(*http.Response, *http.Request) error
51
52
func singleJoiningSlash(a, b string) string {
53
aslash := strings.HasSuffix(a, "/")
54
bslash := strings.HasPrefix(b, "/")
55
switch {
56
case aslash && bslash:
57
return a + b[1:]
58
case !aslash && !bslash:
59
return a + "/" + b
60
}
61
return a + b
62
}
63
64
func joinURLPath(a, b *url.URL) (path, rawpath string) {
65
if a.RawPath == "" && b.RawPath == "" {
66
return singleJoiningSlash(a.Path, b.Path), ""
67
}
68
// Same as singleJoiningSlash, but uses EscapedPath to determine
69
// whether a slash should be added
70
apath := a.EscapedPath()
71
bpath := b.EscapedPath()
72
73
aslash := strings.HasSuffix(apath, "/")
74
bslash := strings.HasPrefix(bpath, "/")
75
76
switch {
77
case aslash && bslash:
78
return a.Path + b.Path[1:], apath + bpath[1:]
79
case !aslash && !bslash:
80
return a.Path + "/" + b.Path, apath + "/" + bpath
81
}
82
return a.Path + b.Path, apath + bpath
83
}
84
85
func NewSingleHostReverseProxy(target *url.URL, useTargetHost bool) *httputil.ReverseProxy {
86
targetQuery := target.RawQuery
87
director := func(req *http.Request) {
88
req.URL.Scheme = target.Scheme
89
req.URL.Host = target.Host
90
if useTargetHost {
91
req.Host = target.Host
92
}
93
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
94
if targetQuery == "" || req.URL.RawQuery == "" {
95
req.URL.RawQuery = targetQuery + req.URL.RawQuery
96
} else {
97
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
98
}
99
if _, ok := req.Header["User-Agent"]; !ok {
100
// explicitly disable User-Agent so it's not set to default value
101
req.Header.Set("User-Agent", "")
102
}
103
}
104
return &httputil.ReverseProxy{Director: director}
105
}
106
107
// proxyPass is the function that assembles a ProxyHandler from the config, a resolver and various options and returns a http.HandlerFunc.
108
func proxyPass(config *RouteHandlerConfig, infoProvider common.WorkspaceInfoProvider, resolver targetResolver, opts ...proxyPassOpt) http.HandlerFunc {
109
h := proxyPassConfig{
110
Transport: config.DefaultTransport,
111
}
112
for _, o := range opts {
113
o(&h)
114
}
115
h.TargetResolver = resolver
116
117
if h.ErrorHandler != nil {
118
oeh := h.ErrorHandler
119
h.ErrorHandler = func(w http.ResponseWriter, req *http.Request, connectErr error) {
120
log.Debugf("could not connect to backend %s: %s", req.URL.String(), connectErrorToCause(connectErr))
121
oeh(w, req, connectErr)
122
}
123
}
124
125
return func(w http.ResponseWriter, req *http.Request) {
126
targetURL, targetResource, err := h.TargetResolver(config.Config, infoProvider, req)
127
if err != nil {
128
if h.ErrorHandler != nil {
129
h.ErrorHandler(w, req, err)
130
} else {
131
log.WithError(err).Errorf("Unable to resolve targetURL: %s", req.URL.String())
132
}
133
return
134
}
135
req = withResourceMetricsLabel(req, targetResource)
136
req = withHttpVersionMetricsLabel(req)
137
138
originalURL := *req.URL
139
140
// TODO(cw): we should cache the proxy for some time for each target URL
141
142
proxy := NewSingleHostReverseProxy(targetURL, h.UseTargetHost)
143
proxy.Transport = h.Transport
144
proxy.ModifyResponse = func(resp *http.Response) error {
145
url := resp.Request.URL
146
if url == nil {
147
return xerrors.Errorf("response's request without URL")
148
}
149
150
if log.Log.Level <= logrus.DebugLevel && resp.StatusCode >= http.StatusBadRequest {
151
dmp, _ := httputil.DumpRequest(resp.Request, false)
152
log.WithField("url", url.String()).WithField("req", dmp).WithField("status", resp.Status).Debug("proxied request failed")
153
}
154
155
// execute response handlers in order of registration
156
for _, handler := range h.ResponseHandler {
157
err := handler(resp, req)
158
if err != nil {
159
return err
160
}
161
}
162
163
return nil
164
}
165
166
proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
167
if h.ErrorHandler != nil {
168
req.URL = &originalURL
169
h.ErrorHandler(w, req, err)
170
return
171
}
172
173
if !strings.HasPrefix(originalURL.Path, "/_supervisor/") {
174
log.WithField("url", originalURL.String()).WithError(err).Debug("proxied request failed")
175
}
176
177
rw.WriteHeader(http.StatusBadGateway)
178
}
179
180
getLog(req.Context()).WithField("targetURL", targetURL.String()).Debug("proxy-passing request")
181
proxy.ServeHTTP(w, req)
182
}
183
}
184
185
func connectErrorToCause(err error) string {
186
if err == nil {
187
return ""
188
}
189
190
if netError, ok := err.(net.Error); ok && netError.Timeout() {
191
return "Connect timeout"
192
}
193
194
switch t := err.(type) {
195
case *net.OpError:
196
if t.Op == "dial" {
197
return fmt.Sprintf("Unknown host: %s", err.Error())
198
} else if t.Op == "read" {
199
return fmt.Sprintf("Connection refused: %s", err.Error())
200
}
201
202
case syscall.Errno:
203
if t == syscall.ECONNREFUSED {
204
return "Connection refused"
205
}
206
}
207
208
return err.Error()
209
}
210
211
func withHTTPErrorHandler(h http.Handler) proxyPassOpt {
212
return func(cfg *proxyPassConfig) {
213
cfg.ErrorHandler = func(w http.ResponseWriter, req *http.Request, err error) {
214
h.ServeHTTP(w, req)
215
}
216
}
217
}
218
219
func withErrorHandler(h errorHandler) proxyPassOpt {
220
return func(cfg *proxyPassConfig) {
221
cfg.ErrorHandler = h
222
}
223
}
224
225
func withSkipTLSVerify() createHttpTransportOpt {
226
return func(tr *http.Transport) {
227
tr.TLSClientConfig = &tls.Config{
228
InsecureSkipVerify: true,
229
}
230
}
231
}
232
233
func createDefaultTransport(config *TransportConfig, opts ...createHttpTransportOpt) http.RoundTripper {
234
transport := &http.Transport{
235
Proxy: http.ProxyFromEnvironment,
236
DialContext: (&net.Dialer{
237
Timeout: time.Duration(config.ConnectTimeout), // default: 30s
238
KeepAlive: 30 * time.Second,
239
DualStack: true,
240
}).DialContext,
241
ForceAttemptHTTP2: true,
242
MaxIdleConns: config.MaxIdleConns, // default: 0 (unlimited connections in pool)
243
MaxIdleConnsPerHost: config.MaxIdleConnsPerHost, // default: 100 (max connections per host in pool)
244
IdleConnTimeout: time.Duration(config.IdleConnTimeout), // default: 90s
245
TLSHandshakeTimeout: 10 * time.Second,
246
ExpectContinueTimeout: 1 * time.Second,
247
}
248
for _, o := range opts {
249
o(transport)
250
}
251
// TODO equivalent of client_max_body_size 2048m; necessary ???
252
// this is based on http.DefaultTransport, with some values exposed to config
253
return instrumentClientMetrics(transport)
254
}
255
256
// tell the browser to cache for 1 year and don't ask the server during this period.
257
func withLongTermCaching() proxyPassOpt {
258
return func(cfg *proxyPassConfig) {
259
cfg.appendResponseHandler(func(resp *http.Response, req *http.Request) error {
260
if resp.StatusCode < http.StatusBadRequest {
261
resp.Header.Set("Cache-Control", "public, max-age=31536000")
262
}
263
264
return nil
265
})
266
}
267
}
268
269
func withXFrameOptionsFilter() proxyPassOpt {
270
return func(cfg *proxyPassConfig) {
271
cfg.appendResponseHandler(func(resp *http.Response, req *http.Request) error {
272
resp.Header.Del("X-Frame-Options")
273
return nil
274
})
275
}
276
}
277
278
func withUseTargetHost() proxyPassOpt {
279
return func(cfg *proxyPassConfig) {
280
cfg.UseTargetHost = true
281
}
282
}
283
284