server_test.go RawHistoryFolder

1package webterm
2
3import (
4	"context"
5	"encoding/json"
6	"io"
7	"net/http"
8	"net/http/httptest"
9	"net/url"
10	"strings"
11	"testing"
12	"time"
13
14	"github.com/coder/websocket"
15	"github.com/stretchr/testify/require"
16)
17
18func TestHTTPTrustBoundary(t *testing.T) {
19	token := strings.Repeat("a", 43)
20	server, err := NewServer(t.Context(), Settings{}, token, func(context.Context, *Terminal) error { return nil })
21	require.NoError(t, err)
22	tests := []struct {
23		name, method, path, host, origin, site, credential string
24		want                                               int
25	}{
26		{"shell", "GET", "/", Address, "", "", "", 200},
27		{"no auth", "GET", "/health", Address, "", "", "", 401},
28		{"authenticated", "GET", "/health", Address, "", "", token, 204},
29		{"rebinding", "GET", "/health", "attacker.invalid", "", "", token, 403},
30		{"cross origin", "GET", "/health", Address, "https://attacker.invalid", "", token, 403},
31		{"cross site", "GET", "/", Address, "", "cross-site", "", 403},
32		{"ws requires origin", "GET", "/ws", Address, "", "", token, 403},
33		{"health is read only", "POST", "/health", Address, Origin, "", token, 405},
34		{"license is read only", "POST", "/icons-license", Address, "", "", "", 405},
35		{"uppercase host", "GET", "/health", strings.ToUpper(Address), "", "", token, 204},
36		{"trailing dot host", "GET", "/health", "127.0.0.1.:8778", "", "", token, 204},
37	}
38	for _, tt := range tests {
39		t.Run(tt.name, func(t *testing.T) {
40			request := httptest.NewRequest(tt.method, Origin+tt.path, nil)
41			request.Host = tt.host
42			request.Header.Set("Origin", tt.origin)
43			request.Header.Set("Sec-Fetch-Site", tt.site)
44			if tt.credential != "" {
45				request.AddCookie(&http.Cookie{Name: cookieName, Value: tt.credential})
46			}
47			response := httptest.NewRecorder()
48			server.ServeHTTP(response, request)
49			require.Equal(t, tt.want, response.Code)
50			require.Contains(t, response.Header().Get("Content-Security-Policy"), "frame-ancestors 'none'")
51			require.NotContains(t, response.Body.String(), token)
52		})
53	}
54}
55
56func TestLogin(t *testing.T) {
57	token := strings.Repeat("b", 43)
58	server, err := NewServer(t.Context(), Settings{}, token, func(context.Context, *Terminal) error { return nil })
59	require.NoError(t, err)
60	for _, valid := range []bool{false, true} {
61		t.Run(map[bool]string{false: "invalid", true: "valid"}[valid], func(t *testing.T) {
62			supplied := "wrong"
63			if valid {
64				supplied = token
65			}
66			request := httptest.NewRequest("POST", Origin+"/login", strings.NewReader(url.Values{"token": {supplied}}.Encode()))
67			request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
68			request.Header.Set("Origin", Origin)
69			response := httptest.NewRecorder()
70			server.ServeHTTP(response, request)
71			if !valid {
72				require.Equal(t, 403, response.Code)
73				return
74			}
75			require.Equal(t, 204, response.Code)
76			cookies := response.Result().Cookies()
77			require.Len(t, cookies, 1)
78			require.True(t, cookies[0].HttpOnly)
79			require.Equal(t, http.SameSiteStrictMode, cookies[0].SameSite)
80		})
81	}
82}
83
84func TestWebSocketRoundTripAndDetach(t *testing.T) {
85	ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
86	defer cancel()
87	exited := make(chan struct{})
88	token := strings.Repeat("c", 43)
89	handler, err := NewServer(ctx, Settings{}, token, func(ctx context.Context, terminal *Terminal) error {
90		defer close(exited)
91		if _, err := terminal.EnterRaw(); err != nil {
92			return err
93		}
94		data := make([]byte, 1)
95		if _, err := io.ReadFull(terminal.In(), data); err != nil {
96			return err
97		}
98		if _, err := terminal.Write(data); err != nil {
99			return err
100		}
101		if err := terminal.Flush(); err != nil {
102			return err
103		}
104		<-ctx.Done()
105		return ctx.Err()
106	})
107	require.NoError(t, err)
108	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.Host = Address; handler.ServeHTTP(w, r) }))
109	defer server.Close()
110	headers := http.Header{"Origin": {Origin}, "Cookie": {cookieName + "=" + token}}
111	conn, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(server.URL, "http")+"/ws", &websocket.DialOptions{HTTPHeader: headers})
112	require.NoError(t, err)
113	defer conn.CloseNow()
114	conn.SetReadLimit(64 << 20)
115	_, data, err := conn.Read(ctx)
116	require.NoError(t, err)
117	require.True(t, json.Valid(data))
118	require.NoError(t, conn.Write(ctx, websocket.MessageText, []byte(`{"schemaVersion":1,"type":"text","text":"Z"}`)))
119	for {
120		_, data, err = conn.Read(ctx)
121		require.NoError(t, err)
122		if strings.Contains(string(data), `"text":"Z"`) {
123			break
124		}
125	}
126	require.NoError(t, conn.CloseNow())
127	select {
128	case <-exited:
129	case <-ctx.Done():
130		t.Fatal("attachment did not detach")
131	}
132	handler.Wait()
133}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close