Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/proxy/plugins/frontend_dev/frontend_dev.go
2500 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 frontend_dev
6
7
import (
8
"bytes"
9
"fmt"
10
"io"
11
"net/http"
12
"net/http/httputil"
13
"net/url"
14
"os"
15
"regexp"
16
"strings"
17
18
"github.com/caddyserver/caddy/v2"
19
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
20
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
21
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
22
)
23
24
const (
25
frontendDevModule = "gitpod.frontend_dev"
26
devURLHeaderName = "X-Frontend-Dev-URL"
27
frontendDevEnabledEnvVarName = "FRONTEND_DEV_ENABLED"
28
)
29
30
func init() {
31
caddy.RegisterModule(FrontendDev{})
32
httpcaddyfile.RegisterHandlerDirective(frontendDevModule, parseCaddyfile)
33
}
34
35
// FrontendDev implements an HTTP handler that extracts gitpod headers
36
type FrontendDev struct {
37
Upstream string `json:"upstream,omitempty"`
38
UpstreamUrl *url.URL
39
}
40
41
// CaddyModule returns the Caddy module information.
42
func (FrontendDev) CaddyModule() caddy.ModuleInfo {
43
return caddy.ModuleInfo{
44
ID: "http.handlers.frontend_dev",
45
New: func() caddy.Module { return new(FrontendDev) },
46
}
47
}
48
49
// ServeHTTP implements caddyhttp.MiddlewareHandler.
50
func (m FrontendDev) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
51
enabled := os.Getenv(frontendDevEnabledEnvVarName)
52
if enabled != "true" {
53
caddy.Log().Sugar().Debugf("Dev URL header present but disabled")
54
return caddyhttp.Error(http.StatusBadRequest, fmt.Errorf("frontend dev module disabled"))
55
}
56
57
devURLStr := r.Header.Get(devURLHeaderName)
58
if devURLStr == "" {
59
caddy.Log().Sugar().Errorf("Dev URL header empty")
60
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error forwarding to dev URL"))
61
}
62
devURL, err := url.Parse(devURLStr)
63
if err != nil {
64
caddy.Log().Sugar().Errorf("Cannot parse dev URL")
65
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error forwarding to dev URL"))
66
}
67
68
director := func(req *http.Request) {
69
req.URL.Scheme = m.UpstreamUrl.Scheme
70
req.URL.Host = m.UpstreamUrl.Host
71
req.Host = m.UpstreamUrl.Host
72
if _, ok := req.Header["User-Agent"]; !ok {
73
// explicitly disable User-Agent so it's not set to default value
74
req.Header.Set("User-Agent", "")
75
}
76
req.Header.Set("Accept-Encoding", "") // we can't handle other than plain text
77
// caddy.Log().Sugar().Infof("director request (mod): %v", req.URL.String())
78
}
79
proxy := httputil.ReverseProxy{Director: director, Transport: &RedirectingTransport{baseUrl: devURL}}
80
proxy.ServeHTTP(w, r)
81
82
return nil
83
}
84
85
type RedirectingTransport struct {
86
baseUrl *url.URL
87
}
88
89
func (rt *RedirectingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
90
// caddy.Log().Sugar().Infof("issuing upstream request: %s", req.URL.Path)
91
resp, err := http.DefaultTransport.RoundTrip(req)
92
if err != nil {
93
return nil, err
94
}
95
96
// gpl: Do we have better means to avoid checking the body?
97
if resp.StatusCode < 300 && strings.HasPrefix(resp.Header.Get("Content-type"), "text/html") {
98
// caddy.Log().Sugar().Infof("trying to match request: %s", req.URL.Path)
99
modifiedResp := MatchAndRewriteRootRequest(resp, rt.baseUrl)
100
if modifiedResp != nil {
101
caddy.Log().Sugar().Debugf("using modified upstream response: %s", req.URL.Path)
102
return modifiedResp, nil
103
}
104
}
105
caddy.Log().Sugar().Debugf("forwarding upstream response: %s", req.URL.Path)
106
107
return resp, nil
108
}
109
110
func MatchAndRewriteRootRequest(or *http.Response, baseUrl *url.URL) *http.Response {
111
// match index.html?
112
prefix := []byte("<!doctype html>")
113
var buf bytes.Buffer
114
bodyReader := io.TeeReader(or.Body, &buf)
115
prefixBuf := make([]byte, len(prefix))
116
_, err := io.ReadAtLeast(bodyReader, prefixBuf, len(prefix))
117
if err != nil {
118
caddy.Log().Sugar().Debugf("prefix match: can't read response body: %w", err)
119
return nil
120
}
121
if !bytes.Equal(prefix, prefixBuf) {
122
caddy.Log().Sugar().Debugf("prefix mismatch: %s", string(prefixBuf))
123
return nil
124
}
125
126
caddy.Log().Sugar().Debugf("match index.html")
127
_, err = io.Copy(&buf, or.Body)
128
if err != nil {
129
caddy.Log().Sugar().Debugf("unable to copy response body: %w, path: %s", err, or.Request.URL.Path)
130
return nil
131
}
132
fullBody := buf.String()
133
134
mainJs := regexp.MustCompile(`"[^"]+?main\.[0-9a-z]+\.js"`)
135
fullBody = mainJs.ReplaceAllStringFunc(fullBody, func(s string) string {
136
return fmt.Sprintf(`"%s/static/js/main.js"`, baseUrl.String())
137
})
138
139
mainCss := regexp.MustCompile(`<link[^>]+?rel="stylesheet">`)
140
fullBody = mainCss.ReplaceAllString(fullBody, "")
141
142
hrefs := regexp.MustCompile(`href="/`)
143
fullBody = hrefs.ReplaceAllString(fullBody, fmt.Sprintf(`href="%s/`, baseUrl.String()))
144
145
or.Body = io.NopCloser(strings.NewReader(fullBody))
146
or.Header.Set("Content-Length", fmt.Sprintf("%d", len(fullBody)))
147
or.Header.Set("Etag", "")
148
return or
149
}
150
151
// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
152
func (m *FrontendDev) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
153
if !d.Next() {
154
return d.Err("expected token following filter")
155
}
156
157
for d.NextBlock(0) {
158
key := d.Val()
159
var value string
160
d.Args(&value)
161
if d.NextArg() {
162
return d.ArgErr()
163
}
164
165
switch key {
166
case "upstream":
167
m.Upstream = value
168
169
default:
170
return d.Errf("unrecognized subdirective '%s'", value)
171
}
172
}
173
174
if m.Upstream == "" {
175
return fmt.Errorf("frontend_dev: 'upstream' config field may not be empty")
176
}
177
178
upstreamURL, err := url.Parse(m.Upstream)
179
if err != nil {
180
return fmt.Errorf("frontend_dev: 'upstream' is not a valid URL: %w", err)
181
}
182
m.UpstreamUrl = upstreamURL
183
184
return nil
185
}
186
187
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
188
m := new(FrontendDev)
189
err := m.UnmarshalCaddyfile(h.Dispenser)
190
if err != nil {
191
return nil, err
192
}
193
194
return m, nil
195
}
196
197
// Interface guards
198
var (
199
_ caddyhttp.MiddlewareHandler = (*FrontendDev)(nil)
200
_ caddyfile.Unmarshaler = (*FrontendDev)(nil)
201
)
202
203