1package webterm
2
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "net/url"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/coder/websocket"
13 "github.com/stretchr/testify/require"
14)
15
16func TestWebProxyTrustBoundary(t *testing.T) {
17 for _, scheme := range []string{"http", "https"} {
18 t.Run(scheme, func(t *testing.T) {
19 settings, err := ParseSettings("", scheme+"://terminal.example.internal")
20 require.NoError(t, err)
21 token := strings.Repeat("f", 43)
22 handler, err := NewServer(t.Context(), settings, token, func(context.Context, *Terminal) error { return nil })
23 require.NoError(t, err)
24 for _, tt := range []struct {
25 name, host, origin string
26 want int
27 }{
28 {"valid", settings.Host(), settings.Origin, 204},
29 {"upstream host", Address, settings.Origin, 403},
30 {"foreign host", "attacker.invalid", settings.Origin, 403},
31 {"foreign origin", settings.Host(), "https://attacker.invalid", 403},
32 {"missing origin", settings.Host(), "", 403},
33 } {
34 t.Run(tt.name, func(t *testing.T) {
35 r := httptest.NewRequest("POST", "http://"+Address+"/login", strings.NewReader(url.Values{"token": {token}}.Encode()))
36 r.Host = tt.host
37 r.Header.Set("Origin", tt.origin)
38 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
39 r.Header.Set("X-Forwarded-Host", settings.Host())
40 r.Header.Set("X-Forwarded-Proto", "https")
41 w := httptest.NewRecorder()
42 handler.ServeHTTP(w, r)
43 require.Equal(t, tt.want, w.Code)
44 if tt.want == 204 {
45 cookies := w.Result().Cookies()
46 require.Len(t, cookies, 1)
47 require.Equal(t, scheme == "https", cookies[0].Secure)
48 require.True(t, cookies[0].HttpOnly)
49 require.Equal(t, http.SameSiteStrictMode, cookies[0].SameSite)
50 } else {
51 require.Empty(t, w.Result().Cookies())
52 }
53 })
54 }
55 })
56 }
57}
58
59func TestWebProxyWebSocket(t *testing.T) {
60 ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
61 defer cancel()
62 settings, err := ParseSettings("", "https://terminal.example.internal")
63 require.NoError(t, err)
64 token := strings.Repeat("g", 43)
65 handler, err := NewServer(ctx, settings, token, func(ctx context.Context, terminal *Terminal) error {
66 <-ctx.Done()
67 return ctx.Err()
68 })
69 require.NoError(t, err)
70 // Model a proxy that preserves the browser-facing Host on HTTP upstream.
71 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
72 r.Host = settings.Host()
73 handler.ServeHTTP(w, r)
74 }))
75 defer server.Close()
76 headers := http.Header{"Origin": {settings.Origin}, "Cookie": {cookieName + "=" + token}}
77 conn, _, err := websocket.Dial(ctx, server.URL+"/ws", &websocket.DialOptions{HTTPHeader: headers})
78 require.NoError(t, err)
79 conn.CloseNow()
80 cancel()
81 handler.Wait()
82}