cloudflare/cloudflared

Public

mirrored from https://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
d78a5ba5da61c3df6cebf2c8254bdfc2aa6a0106

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

connection/connection_test.go

228lines · modecode

1package connection
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "math/rand"
8 "net/http"
9 "testing"
10 "time"
11
12 "github.com/rs/zerolog"
13 "github.com/stretchr/testify/assert"
14
15 tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
16 "github.com/cloudflare/cloudflared/websocket"
17)
18
19const (
20 largeFileSize = 2 * 1024 * 1024
21 testGracePeriod = time.Millisecond * 100
22)
23
24var (
25 testOrchestrator = &mockOrchestrator{
26 originProxy: &mockOriginProxy{},
27 }
28 log = zerolog.Nop()
29 testLargeResp = make([]byte, largeFileSize)
30)
31
32type testRequest struct {
33 name string
34 endpoint string
35 expectedStatus int
36 expectedBody []byte
37 isProxyError bool
38}
39
40type mockOrchestrator struct {
41 originProxy OriginProxy
42}
43
44func (*mockOrchestrator) UpdateConfig(version int32, config []byte) *tunnelpogs.UpdateConfigurationResponse {
45 return &tunnelpogs.UpdateConfigurationResponse{
46 LastAppliedVersion: version,
47 }
48}
49
50func (mcr *mockOrchestrator) GetOriginProxy() (OriginProxy, error) {
51 return mcr.originProxy, nil
52}
53
54type mockOriginProxy struct{}
55
56func (moc *mockOriginProxy) ProxyHTTP(
57 w ResponseWriter,
58 req *http.Request,
59 isWebsocket bool,
60) error {
61 if isWebsocket {
62 switch req.URL.Path {
63 case "/ws/echo":
64 return wsEchoEndpoint(w, req)
65 case "/ws/flaky":
66 return wsFlakyEndpoint(w, req)
67 default:
68 originRespEndpoint(w, http.StatusNotFound, []byte("ws endpoint not found"))
69 return fmt.Errorf("Unknwon websocket endpoint %s", req.URL.Path)
70 }
71 }
72 switch req.URL.Path {
73 case "/ok":
74 originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
75 case "/large_file":
76 originRespEndpoint(w, http.StatusOK, testLargeResp)
77 case "/400":
78 originRespEndpoint(w, http.StatusBadRequest, []byte(http.StatusText(http.StatusBadRequest)))
79 case "/500":
80 originRespEndpoint(w, http.StatusInternalServerError, []byte(http.StatusText(http.StatusInternalServerError)))
81 case "/error":
82 return fmt.Errorf("Failed to proxy to origin")
83 default:
84 originRespEndpoint(w, http.StatusNotFound, []byte("page not found"))
85 }
86 return nil
87
88}
89
90func (moc *mockOriginProxy) ProxyTCP(
91 ctx context.Context,
92 rwa ReadWriteAcker,
93 r *TCPRequest,
94) error {
95 return nil
96}
97
98type echoPipe struct {
99 reader *io.PipeReader
100 writer *io.PipeWriter
101}
102
103func (ep *echoPipe) Read(p []byte) (int, error) {
104 return ep.reader.Read(p)
105}
106
107func (ep *echoPipe) Write(p []byte) (int, error) {
108 return ep.writer.Write(p)
109}
110
111// A mock origin that echos data by streaming like a tcpOverWSConnection
112// https://github.com/cloudflare/cloudflared/blob/master/ingress/origin_connection.go
113func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
114 resp := &http.Response{
115 StatusCode: http.StatusSwitchingProtocols,
116 }
117 if err := w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil {
118 return err
119 }
120 wsCtx, cancel := context.WithCancel(r.Context())
121 readPipe, writePipe := io.Pipe()
122 wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
123 go func() {
124 select {
125 case <-wsCtx.Done():
126 case <-r.Context().Done():
127 }
128 readPipe.Close()
129 writePipe.Close()
130 }()
131
132 originConn := &echoPipe{reader: readPipe, writer: writePipe}
133 websocket.Stream(wsConn, originConn, &log)
134 cancel()
135 wsConn.Close()
136 return nil
137}
138
139type flakyConn struct {
140 closeAt time.Time
141}
142
143func (fc *flakyConn) Read(p []byte) (int, error) {
144 if time.Now().After(fc.closeAt) {
145 return 0, io.EOF
146 }
147 n := copy(p, "Read from flaky connection")
148 return n, nil
149}
150
151func (fc *flakyConn) Write(p []byte) (int, error) {
152 if time.Now().After(fc.closeAt) {
153 return 0, fmt.Errorf("flaky connection closed")
154 }
155 return len(p), nil
156}
157
158func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error {
159 resp := &http.Response{
160 StatusCode: http.StatusSwitchingProtocols,
161 }
162 if err := w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil {
163 return err
164 }
165 wsCtx, cancel := context.WithCancel(r.Context())
166
167 wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
168
169 closedAfter := time.Millisecond * time.Duration(rand.Intn(50))
170 originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
171 websocket.Stream(wsConn, originConn, &log)
172 cancel()
173 wsConn.Close()
174 return nil
175}
176
177func originRespEndpoint(w ResponseWriter, status int, data []byte) {
178 resp := &http.Response{
179 StatusCode: status,
180 }
181 _ = w.WriteRespHeaders(resp.StatusCode, resp.Header)
182 _, _ = w.Write(data)
183}
184
185type mockConnectedFuse struct{}
186
187func (mcf mockConnectedFuse) Connected() {}
188
189func (mcf mockConnectedFuse) IsConnected() bool {
190 return true
191}
192
193func TestIsEventStream(t *testing.T) {
194 tests := []struct {
195 headers http.Header
196 isEventStream bool
197 }{
198 {
199 headers: newHeader("Content-Type", "text/event-stream"),
200 isEventStream: true,
201 },
202 {
203 headers: newHeader("content-type", "text/event-stream"),
204 isEventStream: true,
205 },
206 {
207 headers: newHeader("Content-Type", "text/event-stream; charset=utf-8"),
208 isEventStream: true,
209 },
210 {
211 headers: newHeader("Content-Type", "application/json"),
212 isEventStream: false,
213 },
214 {
215 headers: http.Header{},
216 isEventStream: false,
217 },
218 }
219 for _, test := range tests {
220 assert.Equal(t, test.isEventStream, IsServerSentEvent(test.headers))
221 }
222}
223
224func newHeader(key, value string) http.Header {
225 header := http.Header{}
226 header.Add(key, value)
227 return header
228}
229