carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package tunnelclient
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "testing"
10 "time"
11
12 "golang.org/x/net/http2"
13)
14
15// fakeTunnelEdge accepts one registration, answers 101, then drives ONE h2
16// request over the inverted conn and returns the response body it read.
17func fakeTunnelEdge(t *testing.T, wantToken, wantName string) (*httptest.Server, chan string) {
18 t.Helper()
19 got := make(chan string, 1)
20 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21 if r.Header.Get("Authorization") != "Bearer "+wantToken ||
22 r.Header.Get("Carlos-Tunnel-Name") != wantName {
23 http.Error(w, "unauthorized", http.StatusUnauthorized)
24 return
25 }
26 conn, bufrw, err := w.(http.Hijacker).Hijack()
27 if err != nil {
28 t.Errorf("hijack: %v", err)
29 return
30 }
31 fmt.Fprintf(bufrw, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: carlos-tunnel/1\r\nConnection: Upgrade\r\n\r\n")
32 bufrw.Flush()
33 cc, err := (&http2.Transport{}).NewClientConn(conn)
34 if err != nil {
35 t.Errorf("h2 client: %v", err)
36 return
37 }
38 req, _ := http.NewRequest("GET", "https://demo.example/hello", nil)
39 resp, err := cc.RoundTrip(req)
40 if err != nil {
41 t.Errorf("roundtrip over tunnel: %v", err)
42 got <- ""
43 return
44 }
45 body, _ := io.ReadAll(resp.Body)
46 got <- resp.Header.Get("X-From-Host") + ":" + string(body)
47 }))
48 return srv, got
49}
50
51func TestDialAndServeInvertsConnection(t *testing.T) {
52 srv, got := fakeTunnelEdge(t, "sekrit", "pi-box")
53 defer srv.Close()
54 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55 w.Header().Set("X-From-Host", r.Host)
56 fmt.Fprint(w, "pong")
57 })
58 conn, err := Dial(context.Background(), srv.URL, "pi-box", "sekrit")
59 if err != nil {
60 t.Fatalf("Dial: %v", err)
61 }
62 go Serve(conn, handler)
63 if v := <-got; v != "demo.example:pong" {
64 t.Fatalf("over-tunnel response = %q, want demo.example:pong", v)
65 }
66}
67
68func TestDialRejectsNon101(t *testing.T) {
69 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
70 http.Error(w, "unauthorized", http.StatusUnauthorized)
71 }))
72 defer srv.Close()
73 if _, err := Dial(context.Background(), srv.URL, "pi-box", "wrong"); err == nil {
74 t.Fatal("want error on 401 handshake")
75 }
76}
77
78// A cancelled ctx must stop the loop from BOTH of its blocking positions:
79// parked in backoff between dials (no edge at all), and parked in ServeConn on
80// a live channel that is perfectly healthy. The second one is the load-bearing
81// case — http2.Server.ServeConn has no stop signal of its own, so cancellation
82// has to reach it by closing the conn — and it is what lets a test tear the
83// fake host down deterministically instead of leaking a dialer for the rest of
84// the run.
85func TestLoopStopsOnContextCancel(t *testing.T) {
86 t.Run("while backing off", func(t *testing.T) {
87 // A server that is already gone: every dial is refused, so the loop
88 // spends essentially all of its time parked in wait().
89 dead := httptest.NewServer(http.NotFoundHandler())
90 url := dead.URL
91 dead.Close()
92 assertLoopReturns(t, url, nil)
93 })
94 t.Run("while connected", func(t *testing.T) {
95 release := make(chan struct{})
96 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
97 conn, bufrw, err := w.(http.Hijacker).Hijack()
98 if err != nil {
99 t.Errorf("hijack: %v", err)
100 return
101 }
102 fmt.Fprintf(bufrw, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: carlos-tunnel/1\r\nConnection: Upgrade\r\n\r\n")
103 bufrw.Flush()
104 // Hold the channel open from this side: the edge never hangs up,
105 // so the ONLY thing that can end ServeConn is cancellation
106 // closing the conn — which is the property under test.
107 <-release
108 conn.Close()
109 }))
110 // LIFO: release the fake edge's handler first, then shut it down.
111 defer srv.Close()
112 defer close(release)
113 assertLoopReturns(t, srv.URL, make(chan struct{}, 1))
114 })
115}
116
117// assertLoopReturns runs Loop against url, waits for it to reach the state
118// gate (connected, when connected is non-nil), cancels, and requires a prompt
119// return.
120func assertLoopReturns(t *testing.T, url string, connected chan struct{}) {
121 t.Helper()
122 ctx, cancel := context.WithCancel(context.Background())
123 done := make(chan struct{})
124 go func() {
125 defer close(done)
126 Loop(ctx, Config{
127 URL: url, Name: "pi-box", Token: "sekrit",
128 Handler: http.NewServeMux(),
129 OnState: func(s string) {
130 if s == StateConnected && connected != nil {
131 select {
132 case connected <- struct{}{}:
133 default:
134 }
135 }
136 },
137 Logf: func(string, ...any) {}, // the failing-dial case is noisy
138 })
139 }()
140 if connected != nil {
141 select {
142 case <-connected:
143 case <-time.After(5 * time.Second):
144 cancel()
145 t.Fatal("loop never connected")
146 }
147 }
148 cancel()
149 select {
150 case <-done:
151 case <-time.After(5 * time.Second):
152 t.Fatal("Loop did not return after its context was cancelled")
153 }
154}
155