Skip to content

Commit 9a8ba4d

Browse files
committed
wip
1 parent b983b62 commit 9a8ba4d

File tree

7 files changed

+249
-28
lines changed

7 files changed

+249
-28
lines changed

components/BUILD.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ scripts:
268268
echo "No input."
269269
else
270270
echo "User: $user"
271-
query="update d_b_user set rolesOrPermissions = '[\"admin\"]', fgaRelationshipsVersion=0 where name=\"$user\";"
271+
query="update d_b_user set rolesOrPermissions = '[\"admin\", \"admin-permissions\"]', fgaRelationshipsVersion=0 where name=\"$user\";"
272272
mysql -e "$query" -u$DB_USERNAME -p$DB_PASSWORD -h 127.0.0.1 gitpod
273273
fi
274274
kill $PID || true

components/dashboard/craco.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ module.exports = {
4949
Buffer: ["buffer", "Buffer"],
5050
}),
5151
],
52+
output: !!process.env.STATIC_MAIN
53+
? {
54+
filename: (pathData) => {
55+
return pathData.chunk.name === "main" ? "static/js/main.js" : undefined;
56+
},
57+
}
58+
: undefined,
5259
},
5360
},
5461
devServer: {

components/dashboard/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@
9494
"web-vitals": "^1.1.1"
9595
},
9696
"scripts": {
97-
"start": "BROWSER=none HMR_HOST=`gp url 3001` craco start",
98-
"start-local": "BROWSER=none HMR_HOST=`gp url 3000` craco start",
97+
"start": "BROWSER=none HMR_HOST=`gp url 3001` env",
98+
"start-local": "export BROWSER=none; export HMR_HOST=`gp url 3000`; export STATIC_MAIN=true; craco start",
9999
"build": "craco build --verbose",
100100
"lint": "eslint --max-warnings=0 --ext=.jsx,.js,.tsx,.ts ./src",
101101
"test": "yarn test:unit",

components/proxy/conf/Caddyfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,9 @@ https://{$GITPOD_DOMAIN} {
376376
header_up -Upgrade
377377
}
378378
# Then handle it with our plugin!
379-
gitpod.frontend_dev
379+
gitpod.frontend_dev {
380+
upstream http://dashboard.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:3001
381+
}
380382
}
381383

382384
reverse_proxy dashboard.{$KUBE_NAMESPACE}.{$KUBE_DOMAIN}:3001 {

components/proxy/plugins/frontend_dev/frontend_dev.go

Lines changed: 136 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
// Licensed under the GNU Affero General Public License (AGPL).
33
// See License.AGPL.txt in the project root for license information.
44

5-
package workspacedownload
5+
package frontend_dev
66

77
import (
8+
"bytes"
89
"fmt"
10+
"io"
911
"net/http"
1012
"net/http/httputil"
1113
"net/url"
1214
"os"
15+
"regexp"
1316
"strings"
1417

1518
"github.com/caddyserver/caddy/v2"
@@ -25,24 +28,26 @@ const (
2528
)
2629

2730
func init() {
28-
caddy.RegisterModule(Config{})
31+
caddy.RegisterModule(FrontendDev{})
2932
httpcaddyfile.RegisterHandlerDirective(frontendDevModule, parseCaddyfile)
3033
}
3134

32-
// Config implements an HTTP handler that extracts gitpod headers
33-
type Config struct {
35+
// FrontendDev implements an HTTP handler that extracts gitpod headers
36+
type FrontendDev struct {
37+
Upstream string `json:"upstream,omitempty"`
38+
UpstreamUrl *url.URL
3439
}
3540

3641
// CaddyModule returns the Caddy module information.
37-
func (Config) CaddyModule() caddy.ModuleInfo {
42+
func (FrontendDev) CaddyModule() caddy.ModuleInfo {
3843
return caddy.ModuleInfo{
3944
ID: "http.handlers.frontend_dev",
40-
New: func() caddy.Module { return new(Config) },
45+
New: func() caddy.Module { return new(FrontendDev) },
4146
}
4247
}
4348

4449
// ServeHTTP implements caddyhttp.MiddlewareHandler.
45-
func (m Config) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
50+
func (m FrontendDev) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
4651
enabled := os.Getenv(frontendDevEnabledEnvVarName)
4752
if enabled != "true" {
4853
caddy.Log().Sugar().Debugf("Dev URL header present but disabled")
@@ -60,29 +65,107 @@ func (m Config) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp
6065
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error forwarding to dev URL"))
6166
}
6267

63-
targetQuery := devURL.RawQuery
68+
// targetQuery := devURL.RawQuery
69+
// director := func(req *http.Request) {
70+
// req.URL.Scheme = devURL.Scheme
71+
// req.URL.Host = devURL.Host
72+
// req.Host = devURL.Host // override host header so target proxy can handle this request properly
73+
74+
// req.URL.Path, req.URL.RawPath = joinURLPath(devURL, req.URL)
75+
// if targetQuery == "" || req.URL.RawQuery == "" {
76+
// req.URL.RawQuery = targetQuery + req.URL.RawQuery
77+
// } else {
78+
// req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
79+
// }
80+
// if _, ok := req.Header["User-Agent"]; !ok {
81+
// // explicitly disable User-Agent so it's not set to default value
82+
// req.Header.Set("User-Agent", "")
83+
// }
84+
// }
85+
6486
director := func(req *http.Request) {
65-
req.URL.Scheme = devURL.Scheme
66-
req.URL.Host = devURL.Host
67-
req.Host = devURL.Host // override host header so target proxy can handle this request properly
68-
69-
req.URL.Path, req.URL.RawPath = joinURLPath(devURL, req.URL)
70-
if targetQuery == "" || req.URL.RawQuery == "" {
71-
req.URL.RawQuery = targetQuery + req.URL.RawQuery
72-
} else {
73-
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
74-
}
87+
req.URL.Scheme = m.UpstreamUrl.Scheme
88+
req.URL.Host = m.UpstreamUrl.Host
89+
req.Host = m.UpstreamUrl.Host
7590
if _, ok := req.Header["User-Agent"]; !ok {
7691
// explicitly disable User-Agent so it's not set to default value
7792
req.Header.Set("User-Agent", "")
7893
}
94+
req.Header.Set("Accept-Encoding", "") // we can't handle other than plain text
95+
caddy.Log().Sugar().Infof("director request (mod): %v", req.URL.String())
7996
}
80-
proxy := httputil.ReverseProxy{Director: director}
97+
proxy := httputil.ReverseProxy{Director: director, Transport: &RedirectingTransport{baseUrl: devURL}}
8198
proxy.ServeHTTP(w, r)
8299

83100
return nil
84101
}
85102

103+
type RedirectingTransport struct {
104+
baseUrl *url.URL
105+
}
106+
107+
func (rt *RedirectingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
108+
caddy.Log().Sugar().Infof("issuing upstream request: %s", req.URL.Path)
109+
resp, err := http.DefaultTransport.RoundTrip(req)
110+
if err != nil {
111+
return nil, err
112+
}
113+
caddy.Log().Sugar().Infof("RESPONSE: %v", resp)
114+
115+
// gpl: Do we have better means to avoid checking the body?
116+
if resp.StatusCode < 300 && strings.HasPrefix(resp.Header.Get("Content-type"), "text/html") {
117+
caddy.Log().Sugar().Infof("trying to match request: %s", req.URL.Path)
118+
modifiedResp := MatchAndRewriteRootRequest(resp, rt.baseUrl)
119+
if modifiedResp != nil {
120+
return modifiedResp, nil
121+
}
122+
}
123+
caddy.Log().Sugar().Infof("forwarding upstream response: %s", req.URL.Path)
124+
125+
return resp, nil
126+
}
127+
128+
func MatchAndRewriteRootRequest(or *http.Response, baseUrl *url.URL) *http.Response {
129+
// match index.html?
130+
prefix := []byte("<!doctype html>")
131+
var buf bytes.Buffer
132+
bodyReader := io.TeeReader(or.Body, &buf)
133+
prefixBuf := make([]byte, len(prefix))
134+
_, err := io.ReadAtLeast(bodyReader, prefixBuf, len(prefix))
135+
if err != nil {
136+
caddy.Log().Sugar().Warnf("prefix match: can't read response body: %w", err)
137+
return nil
138+
}
139+
if !bytes.Equal(prefix, prefixBuf) {
140+
caddy.Log().Sugar().Infof("prefix mismatch: %s", string(prefixBuf))
141+
return nil
142+
}
143+
144+
caddy.Log().Sugar().Infof("match index.html")
145+
_, err = io.Copy(&buf, or.Body)
146+
if err != nil {
147+
caddy.Log().Sugar().Errorf("unable to copy response body: %w, path: %s", err, or.Request.URL.Path)
148+
return nil
149+
}
150+
fullBody := buf.String()
151+
152+
mainJs := regexp.MustCompile(`"[^"]+?main\.[0-9a-z]+\.js"`)
153+
fullBody = mainJs.ReplaceAllStringFunc(fullBody, func(s string) string {
154+
return fmt.Sprintf(`"%s/static/js/main.js"`, baseUrl.String())
155+
})
156+
157+
mainCss := regexp.MustCompile(`<link[^>]+?rel="stylesheet">`)
158+
fullBody = mainCss.ReplaceAllString(fullBody, "")
159+
160+
hrefs := regexp.MustCompile(`href="/`)
161+
fullBody = hrefs.ReplaceAllString(fullBody, fmt.Sprintf(`href="%s/`, baseUrl.String()))
162+
163+
or.Body = io.NopCloser(strings.NewReader(fullBody))
164+
or.Header.Set("Content-Length", fmt.Sprintf("%d", len(fullBody)))
165+
or.Header.Set("Etag", "")
166+
return or
167+
}
168+
86169
func joinURLPath(a, b *url.URL) (path, rawpath string) {
87170
if a.RawPath == "" && b.RawPath == "" {
88171
return singleJoiningSlash(a.Path, b.Path), ""
@@ -117,13 +200,43 @@ func singleJoiningSlash(a, b string) string {
117200
}
118201

119202
// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
120-
func (m *Config) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
203+
func (m *FrontendDev) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
204+
if !d.Next() {
205+
return d.Err("expected token following filter")
206+
}
207+
208+
for d.NextBlock(0) {
209+
key := d.Val()
210+
var value string
211+
d.Args(&value)
212+
if d.NextArg() {
213+
return d.ArgErr()
214+
}
215+
216+
switch key {
217+
case "upstream":
218+
m.Upstream = value
219+
220+
default:
221+
return d.Errf("unrecognized subdirective '%s'", value)
222+
}
223+
}
224+
225+
if m.Upstream == "" {
226+
return fmt.Errorf("frontend_dev: 'upstream' config field may not be empty")
227+
}
228+
229+
upstreamURL, err := url.Parse(m.Upstream)
230+
if err != nil {
231+
return fmt.Errorf("frontend_dev: 'upstream' is not a valid URL: %w", err)
232+
}
233+
m.UpstreamUrl = upstreamURL
121234

122235
return nil
123236
}
124237

125238
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
126-
m := new(Config)
239+
m := new(FrontendDev)
127240
err := m.UnmarshalCaddyfile(h.Dispenser)
128241
if err != nil {
129242
return nil, err
@@ -134,6 +247,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
134247

135248
// Interface guards
136249
var (
137-
_ caddyhttp.MiddlewareHandler = (*Config)(nil)
138-
_ caddyfile.Unmarshaler = (*Config)(nil)
250+
_ caddyhttp.MiddlewareHandler = (*FrontendDev)(nil)
251+
_ caddyfile.Unmarshaler = (*FrontendDev)(nil)
139252
)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) 2023 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+
"io/ioutil"
9+
"net/http"
10+
"net/url"
11+
"strings"
12+
"testing"
13+
)
14+
15+
const index_html = `<!doctype html>
16+
<html lang="en">
17+
18+
<head>
19+
<meta charset="utf-8" />
20+
<link rel="icon" href="/favicon256.png" />
21+
<meta name="viewport" content="width=device-width,initial-scale=1" />
22+
<meta name="theme-color" content="#000000" />
23+
<meta name="robots" content="noindex">
24+
<meta name="Gitpod" content="Always Ready-to-Code" />
25+
<link rel="apple-touch-icon" href="/favicon192.png" />
26+
<link rel="manifest" href="/manifest.json" />
27+
<title>Dashboard</title>
28+
<script defer="defer" src="/static/js/main.b009793d.js"></script>
29+
<link href="/static/css/main.32e61b25.css" rel="stylesheet">
30+
</head>
31+
32+
<body><noscript>You need to enable JavaScript to run this app.</noscript>
33+
<div id="root"></div>
34+
</body>
35+
36+
</html>`
37+
38+
func Test_MatchAndRewriteRootRequest(t *testing.T) {
39+
40+
type Test struct {
41+
name string
42+
response *http.Response
43+
newBaseUrl string
44+
expectedBody string
45+
}
46+
tests := []Test{
47+
{
48+
name: "should match and rewrite root request",
49+
response: &http.Response{
50+
StatusCode: 200,
51+
Header: http.Header{
52+
"Content-Type": []string{"text/html"},
53+
},
54+
Body: ioutil.NopCloser(strings.NewReader(index_html)),
55+
},
56+
newBaseUrl: "https://3000-gitpodio-gitpod-hk3453q4csi.ws-eu108.gitpod.io",
57+
expectedBody: `<!doctype html>
58+
<html lang="en">
59+
60+
<head>
61+
<meta charset="utf-8" />
62+
<link rel="icon" href="https://3000-gitpodio-gitpod-hk3453q4csi.ws-eu108.gitpod.io/favicon256.png" />
63+
<meta name="viewport" content="width=device-width,initial-scale=1" />
64+
<meta name="theme-color" content="#000000" />
65+
<meta name="robots" content="noindex">
66+
<meta name="Gitpod" content="Always Ready-to-Code" />
67+
<link rel="apple-touch-icon" href="https://3000-gitpodio-gitpod-hk3453q4csi.ws-eu108.gitpod.io/favicon192.png" />
68+
<link rel="manifest" href="https://3000-gitpodio-gitpod-hk3453q4csi.ws-eu108.gitpod.io/manifest.json" />
69+
<title>Dashboard</title>
70+
<script defer="defer" src="https://3000-gitpodio-gitpod-hk3453q4csi.ws-eu108.gitpod.io/static/js/main.js"></script>
71+
72+
</head>
73+
74+
<body><noscript>You need to enable JavaScript to run this app.</noscript>
75+
<div id="root"></div>
76+
</body>
77+
78+
</html>`,
79+
},
80+
}
81+
82+
for _, test := range tests {
83+
t.Run(test.name, func(t *testing.T) {
84+
newBase, err := url.Parse(test.newBaseUrl)
85+
if err != nil {
86+
t.Errorf("error parsing new base url: %v", err)
87+
}
88+
actual := MatchAndRewriteRootRequest(test.response, newBase)
89+
actualBodyBytes, err := ioutil.ReadAll(actual.Body)
90+
if err != nil {
91+
t.Errorf("error reading response body: %v", err)
92+
}
93+
actualBody := string(actualBodyBytes)
94+
if strings.Compare(actualBody, test.expectedBody) != 0 {
95+
t.Errorf("got %v, want %v", actualBody, test.expectedBody)
96+
}
97+
})
98+
}
99+
}

components/proxy/plugins/frontend_dev/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module github.com/gitpod-io/gitpod/proxy/plugins/workspacedownload
1+
module github.com/gitpod-io/gitpod/proxy/plugins/frontend_dev
22

33
go 1.21
44

0 commit comments

Comments
 (0)