1package http
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "net"
8 "net/http"
9 "net/http/httptest"
10 "net/url"
11 "os"
12 "path/filepath"
13 "strings"
14 "testing"
15
16 "golang.org/x/net/websocket"
17
18 "github.com/mjl-/mox/mox-"
19)
20
21func tcheck(t *testing.T, err error, msg string) {
22 t.Helper()
23 if err != nil {
24 t.Fatalf("%s: %s", msg, err)
25 }
26}
27
28func TestWebserver(t *testing.T) {
29 os.RemoveAll("../testdata/webserver/data")
30 mox.ConfigStaticPath = filepath.FromSlash("../testdata/webserver/mox.conf")
31 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
32 mox.MustLoadConfig(true, false)
33
34 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 1024*1024)
35
36 srv := &serve{Webserver: true}
37
38 test := func(method, target string, reqhdrs map[string]string, expCode int, expContent string, expHeaders map[string]string) {
39 t.Helper()
40
41 req := httptest.NewRequest(method, target, nil)
42 for k, v := range reqhdrs {
43 req.Header.Add(k, v)
44 }
45 rw := httptest.NewRecorder()
46 rw.Body = &bytes.Buffer{}
47 srv.ServeHTTP(rw, req)
48 resp := rw.Result()
49 if resp.StatusCode != expCode {
50 t.Fatalf("got statuscode %d, expected %d", resp.StatusCode, expCode)
51 }
52 if expContent != "" {
53 s := rw.Body.String()
54 if s != expContent {
55 t.Fatalf("got response data %q, expected %q", s, expContent)
56 }
57 }
58 for k, v := range expHeaders {
59 if xv := resp.Header.Get(k); xv != v {
60 t.Fatalf("got %q for header %q, expected %q", xv, k, v)
61 }
62 }
63 }
64
65 test("GET", "http://redir.mox.example", nil, http.StatusPermanentRedirect, "", map[string]string{"Location": "https://mox.example/"})
66
67 // http to https redirect, and stay on https afterwards without redirect loop.
68 test("GET", "http://schemeredir.example", nil, http.StatusPermanentRedirect, "", map[string]string{"Location": "https://schemeredir.example/"})
69 test("GET", "https://schemeredir.example", nil, http.StatusNotFound, "", nil)
70
71 accgzip := map[string]string{"Accept-Encoding": "gzip"}
72 test("GET", "http://mox.example/static/", accgzip, http.StatusOK, "", map[string]string{"X-Test": "mox", "Content-Encoding": "gzip"}) // index.html
73 test("GET", "http://mox.example/static/dir/hi.txt", accgzip, http.StatusOK, "", map[string]string{"X-Test": "mox", "Content-Encoding": ""}) // too small to compress
74 test("GET", "http://mox.example/static/dir/", accgzip, http.StatusOK, "", map[string]string{"X-Test": "mox", "Content-Encoding": "gzip"}) // listing
75 test("GET", "http://mox.example/static/dir", accgzip, http.StatusTemporaryRedirect, "", map[string]string{"Location": "/static/dir/"}) // redirect to dir
76 test("GET", "http://mox.example/static/bogus", accgzip, http.StatusNotFound, "", map[string]string{"Content-Encoding": ""})
77
78 test("GET", "http://mox.example/nolist/", nil, http.StatusOK, "", nil) // index.html
79 test("GET", "http://mox.example/nolist/dir/", nil, http.StatusForbidden, "", nil) // no listing
80
81 test("GET", "http://mox.example/tls/", nil, http.StatusPermanentRedirect, "", map[string]string{"Location": "https://mox.example/tls/"}) // redirect to tls
82
83 test("GET", "http://mox.example/baseurl/x?y=2", nil, http.StatusPermanentRedirect, "", map[string]string{"Location": "https://tls.mox.example/baseurl/x?q=1&y=2#fragment"})
84 test("GET", "http://mox.example/pathonly/old/x?q=2", nil, http.StatusTemporaryRedirect, "", map[string]string{"Location": "http://mox.example/pathonly/new/x?q=2"})
85 test("GET", "http://mox.example/baseurlpath/old/x?y=2", nil, http.StatusPermanentRedirect, "", map[string]string{"Location": "//other.mox.example/baseurlpath/new/x?q=1&y=2#fragment"})
86
87 test("GET", "http://mox.example/strip/x", nil, http.StatusBadGateway, "", nil) // no server yet
88 test("GET", "http://mox.example/nostrip/x", nil, http.StatusBadGateway, "", nil) // no server yet
89
90 badForwarded := map[string]string{
91 "Forwarded": "bad",
92 "X-Forwarded-For": "bad",
93 "X-Forwarded-Proto": "bad",
94 "X-Forwarded-Host": "bad",
95 "X-Forwarded-Ext": "bad",
96 }
97
98 // Server that echoes path, and forwarded request headers.
99 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
100 for k, v := range badForwarded {
101 if r.Header.Get(k) == v {
102 w.WriteHeader(http.StatusInternalServerError)
103 return
104 }
105 }
106
107 for k, vl := range r.Header {
108 if k == "Forwarded" || k == "X-Forwarded" || strings.HasPrefix(k, "X-Forwarded-") {
109 w.Header()[k] = vl
110 }
111 }
112 w.Write([]byte(r.URL.Path))
113 }))
114 defer server.Close()
115
116 serverURL, err := url.Parse(server.URL)
117 if err != nil {
118 t.Fatalf("parsing url: %v", err)
119 }
120 serverURL.Path = "/a"
121
122 // warning: it is not normally allowed to access the dynamic config without lock. don't propagate accesses like this!
123 mox.Conf.Dynamic.WebHandlers[len(mox.Conf.Dynamic.WebHandlers)-2].WebForward.TargetURL = serverURL
124 mox.Conf.Dynamic.WebHandlers[len(mox.Conf.Dynamic.WebHandlers)-1].WebForward.TargetURL = serverURL
125
126 test("GET", "http://mox.example/strip/x", badForwarded, http.StatusOK, "/a/x", map[string]string{
127 "X-Test": "mox",
128 "X-Forwarded-For": "192.0.2.1", // IP is hardcoded in Go's src/net/http/httptest/httptest.go
129 "X-Forwarded-Proto": "http",
130 "X-Forwarded-Host": "mox.example",
131 "X-Forwarded-Ext": "",
132 })
133 test("GET", "http://mox.example/nostrip/x", map[string]string{"X-OK": "ok"}, http.StatusOK, "/a/nostrip/x", map[string]string{"X-Test": "mox"})
134
135 test("GET", "http://mox.example/bogus", nil, http.StatusNotFound, "", nil) // path not registered.
136 test("GET", "http://bogus.mox.example/static/", nil, http.StatusNotFound, "", nil) // domain not registered.
137 test("GET", "http://mox.example/xadmin/", nil, http.StatusOK, "", nil) // internal admin service
138 test("GET", "http://mox.example/xaccount/", nil, http.StatusOK, "", nil) // internal account service
139 test("GET", "http://mox.example/xwebmail/", nil, http.StatusOK, "", nil) // internal webmail service
140 test("GET", "http://mox.example/xwebapi/v0/", nil, http.StatusOK, "", nil) // internal webapi service
141
142 npaths := len(staticgzcache.paths)
143 if npaths != 1 {
144 t.Fatalf("%d file(s) in staticgzcache, expected 1", npaths)
145 }
146 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 1024*1024)
147 npaths = len(staticgzcache.paths)
148 if npaths != 1 {
149 t.Fatalf("%d file(s) in staticgzcache after loading from disk, expected 1", npaths)
150 }
151 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 0)
152 npaths = len(staticgzcache.paths)
153 if npaths != 0 {
154 t.Fatalf("%d file(s) in staticgzcache after setting max size to 0, expected 0", npaths)
155 }
156 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 0)
157 npaths = len(staticgzcache.paths)
158 if npaths != 0 {
159 t.Fatalf("%d file(s) in staticgzcache after setting max size to 0 and reloading from disk, expected 0", npaths)
160 }
161}
162
163func TestWebsocket(t *testing.T) {
164 os.RemoveAll("../testdata/websocket/data")
165 mox.ConfigStaticPath = filepath.FromSlash("../testdata/websocket/mox.conf")
166 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
167 mox.MustLoadConfig(true, false)
168
169 srv := &serve{Webserver: true}
170
171 var handler http.Handler // Active handler during test.
172 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
173 handler.ServeHTTP(w, r)
174 }))
175
176 defer backend.Close()
177 backendURL, err := url.Parse(backend.URL)
178 if err != nil {
179 t.Fatalf("parsing backend url: %v", err)
180 }
181 backendURL.Path = "/"
182
183 // warning: it is not normally allowed to access the dynamic config without lock. don't propagate accesses like this!
184 mox.Conf.Dynamic.WebHandlers[len(mox.Conf.Dynamic.WebHandlers)-1].WebForward.TargetURL = backendURL
185
186 server := httptest.NewServer(srv)
187 defer server.Close()
188
189 serverURL, err := url.Parse(server.URL)
190 tcheck(t, err, "parsing server url")
191 _, port, err := net.SplitHostPort(serverURL.Host)
192 tcheck(t, err, "parsing host port in server url")
193 wsurl := fmt.Sprintf("ws://%s/ws/", net.JoinHostPort("localhost", port))
194
195 handler = websocket.Handler(func(c *websocket.Conn) {
196 io.Copy(c, c)
197 })
198
199 // Test a correct websocket connection.
200 wsconn, err := websocket.Dial(wsurl, "ignored", "http://ignored.example")
201 tcheck(t, err, "websocket dial")
202 _, err = fmt.Fprint(wsconn, "test")
203 tcheck(t, err, "write to websocket")
204 buf := make([]byte, 128)
205 n, err := wsconn.Read(buf)
206 tcheck(t, err, "read from websocket")
207 if string(buf[:n]) != "test" {
208 t.Fatalf(`got websocket data %q, expected "test"`, buf[:n])
209 }
210 err = wsconn.Close()
211 tcheck(t, err, "closing websocket connection")
212
213 // Test with server.ServeHTTP directly.
214 test := func(method string, reqhdrs map[string]string, expCode int, expHeaders map[string]string) {
215 t.Helper()
216
217 req := httptest.NewRequest(method, wsurl, nil)
218 for k, v := range reqhdrs {
219 req.Header.Add(k, v)
220 }
221 rw := httptest.NewRecorder()
222 rw.Body = &bytes.Buffer{}
223 srv.ServeHTTP(rw, req)
224 resp := rw.Result()
225 if resp.StatusCode != expCode {
226 t.Fatalf("got statuscode %d, expected %d", resp.StatusCode, expCode)
227 }
228 for k, v := range expHeaders {
229 if xv := resp.Header.Get(k); xv != v {
230 t.Fatalf("got %q for header %q, expected %q", xv, k, v)
231 }
232 }
233 }
234
235 wsreqhdrs := map[string]string{
236 "Upgrade": "keep-alive, websocket",
237 "Connection": "X, Upgrade",
238 "Sec-Websocket-Version": "13",
239 "Sec-Websocket-Key": "AAAAAAAAAAAAAAAAAAAAAA==",
240 }
241
242 test("POST", wsreqhdrs, http.StatusBadRequest, nil)
243
244 clone := func(m map[string]string) map[string]string {
245 r := map[string]string{}
246 for k, v := range m {
247 r[k] = v
248 }
249 return r
250 }
251
252 hdrs := clone(wsreqhdrs)
253 hdrs["Sec-Websocket-Version"] = "14"
254 test("GET", hdrs, http.StatusBadRequest, map[string]string{"Sec-Websocket-Version": "13"})
255
256 httpurl := fmt.Sprintf("http://%s/ws/", net.JoinHostPort("localhost", port))
257
258 // Must now do actual HTTP requests and read the HTTP response. Cannot call
259 // ServeHTTP because ResponseRecorder is not a http.Hijacker.
260 test = func(method string, reqhdrs map[string]string, expCode int, expHeaders map[string]string) {
261 t.Helper()
262
263 req, err := http.NewRequest(method, httpurl, nil)
264 tcheck(t, err, "http newrequest")
265 for k, v := range reqhdrs {
266 req.Header.Add(k, v)
267 }
268 resp, err := http.DefaultClient.Do(req)
269 tcheck(t, err, "http transaction")
270 if resp.StatusCode != expCode {
271 t.Fatalf("got statuscode %d, expected %d", resp.StatusCode, expCode)
272 }
273 for k, v := range expHeaders {
274 if xv := resp.Header.Get(k); xv != v {
275 t.Fatalf("got %q for header %q, expected %q", xv, k, v)
276 }
277 }
278 }
279
280 hdrs = clone(wsreqhdrs)
281 hdrs["Sec-Websocket-Key"] = "malformed"
282 test("GET", hdrs, http.StatusBadRequest, nil)
283
284 hdrs = clone(wsreqhdrs)
285 hdrs["Sec-Websocket-Key"] = "c2hvcnQK" // "short"
286 test("GET", hdrs, http.StatusBadRequest, nil)
287
288 // Not responding with a 101, but with regular 200 OK response.
289 handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
290 http.Error(w, "bad", http.StatusOK)
291 })
292 test("GET", wsreqhdrs, http.StatusBadRequest, nil)
293
294 // Respond with 101, but other websocket response headers missing.
295 handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
296 w.WriteHeader(http.StatusSwitchingProtocols)
297 })
298 test("GET", wsreqhdrs, http.StatusBadRequest, nil)
299
300 // With Upgrade: websocket, without Connection: Upgrade
301 handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
302 w.Header().Set("Upgrade", "websocket")
303 w.WriteHeader(http.StatusSwitchingProtocols)
304 })
305 test("GET", wsreqhdrs, http.StatusBadRequest, nil)
306
307 // With malformed Sec-WebSocket-Accept response header.
308 handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
309 h := w.Header()
310 h.Set("Upgrade", "websocket")
311 h.Set("Connection", "Upgrade")
312 h.Set("Sec-WebSocket-Accept", "malformed")
313 w.WriteHeader(http.StatusSwitchingProtocols)
314 })
315 test("GET", wsreqhdrs, http.StatusBadRequest, nil)
316
317 // With malformed Sec-WebSocket-Accept response header.
318 handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
319 h := w.Header()
320 h.Set("Upgrade", "websocket")
321 h.Set("Connection", "Upgrade")
322 h.Set("Sec-WebSocket-Accept", "YmFk") // "bad"
323 w.WriteHeader(http.StatusSwitchingProtocols)
324 })
325 test("GET", wsreqhdrs, http.StatusBadRequest, nil)
326
327 // All good.
328 wsresphdrs := map[string]string{
329 "Connection": "Upgrade",
330 "Upgrade": "websocket",
331 "Sec-Websocket-Accept": "ICX+Yqv66kxgM0FcWaLWlFLwTAI=",
332 "X-Test": "mox",
333 }
334 handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
335 h := w.Header()
336 h.Set("Upgrade", "websocket")
337 h.Set("Connection", "Upgrade")
338 h.Set("Sec-WebSocket-Accept", "ICX+Yqv66kxgM0FcWaLWlFLwTAI=")
339 w.WriteHeader(http.StatusSwitchingProtocols)
340 })
341 test("GET", wsreqhdrs, http.StatusSwitchingProtocols, wsresphdrs)
342}
343