2
2
// Licensed under the GNU Affero General Public License (AGPL).
3
3
// See License.AGPL.txt in the project root for license information.
4
4
5
- package workspacedownload
5
+ package frontend_dev
6
6
7
7
import (
8
+ "bytes"
8
9
"fmt"
10
+ "io"
9
11
"net/http"
10
12
"net/http/httputil"
11
13
"net/url"
12
14
"os"
15
+ "regexp"
13
16
"strings"
14
17
15
18
"github.com/caddyserver/caddy/v2"
@@ -25,24 +28,26 @@ const (
25
28
)
26
29
27
30
func init () {
28
- caddy .RegisterModule (Config {})
31
+ caddy .RegisterModule (FrontendDev {})
29
32
httpcaddyfile .RegisterHandlerDirective (frontendDevModule , parseCaddyfile )
30
33
}
31
34
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
34
39
}
35
40
36
41
// CaddyModule returns the Caddy module information.
37
- func (Config ) CaddyModule () caddy.ModuleInfo {
42
+ func (FrontendDev ) CaddyModule () caddy.ModuleInfo {
38
43
return caddy.ModuleInfo {
39
44
ID : "http.handlers.frontend_dev" ,
40
- New : func () caddy.Module { return new (Config ) },
45
+ New : func () caddy.Module { return new (FrontendDev ) },
41
46
}
42
47
}
43
48
44
49
// 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 {
46
51
enabled := os .Getenv (frontendDevEnabledEnvVarName )
47
52
if enabled != "true" {
48
53
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
60
65
return caddyhttp .Error (http .StatusInternalServerError , fmt .Errorf ("unexpected error forwarding to dev URL" ))
61
66
}
62
67
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
+
64
86
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
75
90
if _ , ok := req .Header ["User-Agent" ]; ! ok {
76
91
// explicitly disable User-Agent so it's not set to default value
77
92
req .Header .Set ("User-Agent" , "" )
78
93
}
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 ())
79
96
}
80
- proxy := httputil.ReverseProxy {Director : director }
97
+ proxy := httputil.ReverseProxy {Director : director , Transport : & RedirectingTransport { baseUrl : devURL } }
81
98
proxy .ServeHTTP (w , r )
82
99
83
100
return nil
84
101
}
85
102
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
+
86
169
func joinURLPath (a , b * url.URL ) (path , rawpath string ) {
87
170
if a .RawPath == "" && b .RawPath == "" {
88
171
return singleJoiningSlash (a .Path , b .Path ), ""
@@ -117,13 +200,43 @@ func singleJoiningSlash(a, b string) string {
117
200
}
118
201
119
202
// 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
121
234
122
235
return nil
123
236
}
124
237
125
238
func parseCaddyfile (h httpcaddyfile.Helper ) (caddyhttp.MiddlewareHandler , error ) {
126
- m := new (Config )
239
+ m := new (FrontendDev )
127
240
err := m .UnmarshalCaddyfile (h .Dispenser )
128
241
if err != nil {
129
242
return nil , err
@@ -134,6 +247,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
134
247
135
248
// Interface guards
136
249
var (
137
- _ caddyhttp.MiddlewareHandler = (* Config )(nil )
138
- _ caddyfile.Unmarshaler = (* Config )(nil )
250
+ _ caddyhttp.MiddlewareHandler = (* FrontendDev )(nil )
251
+ _ caddyfile.Unmarshaler = (* FrontendDev )(nil )
139
252
)
0 commit comments