1
16
17 package proxy
18
19 import (
20 "bytes"
21 "compress/flate"
22 "compress/gzip"
23 "fmt"
24 "io"
25 "net/http"
26 "net/http/httptest"
27 "net/url"
28 "strings"
29 "testing"
30 )
31
32 func parseURLOrDie(inURL string) *url.URL {
33 parsed, err := url.Parse(inURL)
34 if err != nil {
35 panic(err)
36 }
37 return parsed
38 }
39
40 func TestProxyTransport(t *testing.T) {
41 testTransport := &Transport{
42 Scheme: "http",
43 Host: "foo.com",
44 PathPrepend: "/proxy/node/node1:10250",
45 }
46 testTransport2 := &Transport{
47 Scheme: "https",
48 Host: "foo.com",
49 PathPrepend: "/proxy/node/node1:8080",
50 }
51 emptyHostTransport := &Transport{
52 Scheme: "https",
53 PathPrepend: "/proxy/node/node1:10250",
54 }
55 emptySchemeTransport := &Transport{
56 Host: "foo.com",
57 PathPrepend: "/proxy/node/node1:10250",
58 }
59 emptyHostAndSchemeTransport := &Transport{
60 PathPrepend: "/proxy/node/node1:10250",
61 }
62 type Item struct {
63 input string
64 sourceURL string
65 transport *Transport
66 output string
67 contentType string
68 forwardedURI string
69 redirect string
70 redirectWant string
71 reqHost string
72 }
73
74 table := map[string]Item{
75 "normal": {
76 input: `<pre><a href="kubelet.log">kubelet.log</a><a href="/google.log">google.log</a></pre>`,
77 sourceURL: "http://mynode.com/logs/log.log",
78 transport: testTransport,
79 output: `<pre><a href="kubelet.log">kubelet.log</a><a href="http://foo.com/proxy/node/node1:10250/google.log">google.log</a></pre>`,
80 contentType: "text/html",
81 forwardedURI: "/proxy/node/node1:10250/logs/log.log",
82 },
83 "full document": {
84 input: `<html><header></header><body><pre><a href="kubelet.log">kubelet.log</a><a href="/google.log">google.log</a></pre></body></html>`,
85 sourceURL: "http://mynode.com/logs/log.log",
86 transport: testTransport,
87 output: `<html><header></header><body><pre><a href="kubelet.log">kubelet.log</a><a href="http://foo.com/proxy/node/node1:10250/google.log">google.log</a></pre></body></html>`,
88 contentType: "text/html",
89 forwardedURI: "/proxy/node/node1:10250/logs/log.log",
90 },
91 "trailing slash": {
92 input: `<pre><a href="kubelet.log">kubelet.log</a><a href="/google.log/">google.log</a></pre>`,
93 sourceURL: "http://mynode.com/logs/log.log",
94 transport: testTransport,
95 output: `<pre><a href="kubelet.log">kubelet.log</a><a href="http://foo.com/proxy/node/node1:10250/google.log/">google.log</a></pre>`,
96 contentType: "text/html",
97 forwardedURI: "/proxy/node/node1:10250/logs/log.log",
98 },
99 "content-type charset": {
100 input: `<pre><a href="kubelet.log">kubelet.log</a><a href="/google.log">google.log</a></pre>`,
101 sourceURL: "http://mynode.com/logs/log.log",
102 transport: testTransport,
103 output: `<pre><a href="kubelet.log">kubelet.log</a><a href="http://foo.com/proxy/node/node1:10250/google.log">google.log</a></pre>`,
104 contentType: "text/html; charset=utf-8",
105 forwardedURI: "/proxy/node/node1:10250/logs/log.log",
106 },
107 "content-type passthrough": {
108 input: `<pre><a href="kubelet.log">kubelet.log</a><a href="/google.log">google.log</a></pre>`,
109 sourceURL: "http://mynode.com/logs/log.log",
110 transport: testTransport,
111 output: `<pre><a href="kubelet.log">kubelet.log</a><a href="/google.log">google.log</a></pre>`,
112 contentType: "text/plain",
113 forwardedURI: "/proxy/node/node1:10250/logs/log.log",
114 },
115 "subdir": {
116 input: `<a href="kubelet.log">kubelet.log</a><a href="/google.log">google.log</a>`,
117 sourceURL: "http://mynode.com/whatever/apt/somelog.log",
118 transport: testTransport2,
119 output: `<a href="kubelet.log">kubelet.log</a><a href="https://foo.com/proxy/node/node1:8080/google.log">google.log</a>`,
120 contentType: "text/html",
121 forwardedURI: "/proxy/node/node1:8080/whatever/apt/somelog.log",
122 },
123 "image": {
124 input: `<pre><img src="kubernetes.jpg"/><img src="/kubernetes_abs.jpg"/></pre>`,
125 sourceURL: "http://mynode.com/",
126 transport: testTransport,
127 output: `<pre><img src="kubernetes.jpg"/><img src="http://foo.com/proxy/node/node1:10250/kubernetes_abs.jpg"/></pre>`,
128 contentType: "text/html",
129 forwardedURI: "/proxy/node/node1:10250/",
130 },
131 "abs": {
132 input: `<script src="http://google.com/kubernetes.js"/>`,
133 sourceURL: "http://mynode.com/any/path/",
134 transport: testTransport,
135 output: `<script src="http://google.com/kubernetes.js"/>`,
136 contentType: "text/html",
137 forwardedURI: "/proxy/node/node1:10250/any/path/",
138 },
139 "abs but same host": {
140 input: `<script src="http://mynode.com/kubernetes.js"/>`,
141 sourceURL: "http://mynode.com/any/path/",
142 transport: testTransport,
143 output: `<script src="http://foo.com/proxy/node/node1:10250/kubernetes.js"/>`,
144 contentType: "text/html",
145 forwardedURI: "/proxy/node/node1:10250/any/path/",
146 },
147 "redirect rel": {
148 sourceURL: "http://mynode.com/redirect",
149 transport: testTransport,
150 redirect: "/redirected/target/",
151 redirectWant: "http://foo.com/proxy/node/node1:10250/redirected/target/",
152 forwardedURI: "/proxy/node/node1:10250/redirect",
153 },
154 "redirect abs same host": {
155 sourceURL: "http://mynode.com/redirect",
156 transport: testTransport,
157 redirect: "http://mynode.com/redirected/target/",
158 redirectWant: "http://foo.com/proxy/node/node1:10250/redirected/target/",
159 forwardedURI: "/proxy/node/node1:10250/redirect",
160 },
161 "redirect abs other host": {
162 sourceURL: "http://mynode.com/redirect",
163 transport: testTransport,
164 redirect: "http://example.com/redirected/target/",
165 redirectWant: "http://example.com/redirected/target/",
166 forwardedURI: "/proxy/node/node1:10250/redirect",
167 },
168 "redirect abs use reqHost no host no scheme": {
169 sourceURL: "http://mynode.com/redirect",
170 transport: emptyHostAndSchemeTransport,
171 redirect: "http://10.0.0.1:8001/redirected/target/",
172 redirectWant: "http://10.0.0.1:8001/proxy/node/node1:10250/redirected/target/",
173 forwardedURI: "/proxy/node/node1:10250/redirect",
174 reqHost: "10.0.0.1:8001",
175 },
176 "source contains the redirect already": {
177 input: `<pre><a href="kubelet.log">kubelet.log</a><a href="http://foo.com/proxy/node/node1:10250/google.log">google.log</a></pre>`,
178 sourceURL: "http://foo.com/logs/log.log",
179 transport: testTransport,
180 output: `<pre><a href="kubelet.log">kubelet.log</a><a href="http://foo.com/proxy/node/node1:10250/google.log">google.log</a></pre>`,
181 contentType: "text/html",
182 forwardedURI: "/proxy/node/node1:10250/logs/log.log",
183 },
184 "no host": {
185 input: "<html></html>",
186 sourceURL: "http://mynode.com/logs/log.log",
187 transport: emptyHostTransport,
188 output: "<html></html>",
189 contentType: "text/html",
190 forwardedURI: "/proxy/node/node1:10250/logs/log.log",
191 },
192 "no scheme": {
193 input: "<html></html>",
194 sourceURL: "http://mynode.com/logs/log.log",
195 transport: emptySchemeTransport,
196 output: "<html></html>",
197 contentType: "text/html",
198 forwardedURI: "/proxy/node/node1:10250/logs/log.log",
199 },
200 "forwarded URI must be escaped": {
201 input: "<html></html>",
202 sourceURL: "http://mynode.com/logs/log.log%00<script>alert(1)</script>",
203 transport: testTransport,
204 output: "<html></html>",
205 contentType: "text/html",
206 forwardedURI: "/proxy/node/node1:10250/logs/log.log%00%3Cscript%3Ealert%281%29%3C/script%3E",
207 },
208 "redirect rel must be escaped": {
209 sourceURL: "http://mynode.com/redirect",
210 transport: testTransport,
211 redirect: "/redirected/target/%00<script>alert(1)</script>/",
212 redirectWant: "http://foo.com/proxy/node/node1:10250/redirected/target/%00%3Cscript%3Ealert%281%29%3C/script%3E/",
213 forwardedURI: "/proxy/node/node1:10250/redirect",
214 },
215 "redirect abs same host must be escaped": {
216 sourceURL: "http://mynode.com/redirect",
217 transport: testTransport,
218 redirect: "http://mynode.com/redirected/target/%00<script>alert(1)</script>/",
219 redirectWant: "http://foo.com/proxy/node/node1:10250/redirected/target/%00%3Cscript%3Ealert%281%29%3C/script%3E/",
220 forwardedURI: "/proxy/node/node1:10250/redirect",
221 },
222 "redirect abs other host must be escaped": {
223 sourceURL: "http://mynode.com/redirect",
224 transport: testTransport,
225 redirect: "http://example.com/redirected/target/%00<script>alert(1)</script>/",
226 redirectWant: "http://example.com/redirected/target/%00%3Cscript%3Ealert%281%29%3C/script%3E/",
227 forwardedURI: "/proxy/node/node1:10250/redirect",
228 },
229 "redirect abs use reqHost no host no scheme must be escaped": {
230 sourceURL: "http://mynode.com/redirect",
231 transport: emptyHostAndSchemeTransport,
232 redirect: "http://10.0.0.1:8001/redirected/target/%00<script>alert(1)</script>/",
233 redirectWant: "http://10.0.0.1:8001/proxy/node/node1:10250/redirected/target/%00%3Cscript%3Ealert%281%29%3C/script%3E/",
234 forwardedURI: "/proxy/node/node1:10250/redirect",
235 reqHost: "10.0.0.1:8001",
236 },
237 }
238
239 testItem := func(name string, item *Item) {
240 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
241
242 if got, want := r.Header.Get("X-Forwarded-Uri"), item.forwardedURI; got != want {
243 t.Errorf("%v: X-Forwarded-Uri = %q, want %q", name, got, want)
244 }
245 if len(item.transport.Host) == 0 {
246 _, present := r.Header["X-Forwarded-Host"]
247 if present {
248 t.Errorf("%v: X-Forwarded-Host header should not be present", name)
249 }
250 } else {
251 if got, want := r.Header.Get("X-Forwarded-Host"), item.transport.Host; got != want {
252 t.Errorf("%v: X-Forwarded-Host = %q, want %q", name, got, want)
253 }
254 }
255 if len(item.transport.Scheme) == 0 {
256 _, present := r.Header["X-Forwarded-Proto"]
257 if present {
258 t.Errorf("%v: X-Forwarded-Proto header should not be present", name)
259 }
260 } else {
261 if got, want := r.Header.Get("X-Forwarded-Proto"), item.transport.Scheme; got != want {
262 t.Errorf("%v: X-Forwarded-Proto = %q, want %q", name, got, want)
263 }
264 }
265
266
267 if item.redirect != "" {
268 http.Redirect(w, r, item.redirect, http.StatusMovedPermanently)
269 return
270 }
271 w.Header().Set("Content-Type", item.contentType)
272 fmt.Fprint(w, item.input)
273 }))
274 defer server.Close()
275
276
277 sourceURL := parseURLOrDie(item.sourceURL)
278 serverURL := parseURLOrDie(server.URL)
279 item.input = strings.Replace(item.input, sourceURL.Host, serverURL.Host, -1)
280 item.redirect = strings.Replace(item.redirect, sourceURL.Host, serverURL.Host, -1)
281 sourceURL.Host = serverURL.Host
282
283 req, err := http.NewRequest("GET", sourceURL.String(), nil)
284 if err != nil {
285 t.Errorf("%v: Unexpected error: %v", name, err)
286 return
287 }
288 if item.reqHost != "" {
289 req.Host = item.reqHost
290 }
291 resp, err := item.transport.RoundTrip(req)
292 if err != nil {
293 t.Errorf("%v: Unexpected error: %v", name, err)
294 return
295 }
296 if item.redirect != "" {
297
298 if got, want := resp.Header.Get("Location"), item.redirectWant; got != want {
299 t.Errorf("%v: Location header = %q, want %q", name, got, want)
300 }
301 return
302 }
303 body, err := io.ReadAll(resp.Body)
304 if err != nil {
305 t.Errorf("%v: Unexpected error: %v", name, err)
306 return
307 }
308 if e, a := item.output, string(body); e != a {
309 t.Errorf("%v: expected %v, but got %v", name, e, a)
310 }
311 }
312
313 for name, item := range table {
314 testItem(name, &item)
315 }
316 }
317
318 func TestRewriteResponse(t *testing.T) {
319 gzipbuf := bytes.NewBuffer(nil)
320 flatebuf := bytes.NewBuffer(nil)
321
322 testTransport := &Transport{
323 Scheme: "http",
324 Host: "foo.com",
325 PathPrepend: "/proxy/node/node1:10250",
326 }
327 expected := []string{
328 "short body test",
329 strings.Repeat("long body test", 4097),
330 }
331 test := []struct {
332 encodeType string
333 writer func(string) *http.Response
334 reader func(*http.Response) string
335 }{
336 {
337 encodeType: "gzip",
338 writer: func(ept string) *http.Response {
339 gzw := gzip.NewWriter(gzipbuf)
340 defer gzw.Close()
341
342 gzw.Write([]byte(ept))
343 gzw.Flush()
344 return &http.Response{
345 Body: io.NopCloser(gzipbuf),
346 }
347 },
348 reader: func(rep *http.Response) string {
349 reader, _ := gzip.NewReader(rep.Body)
350 s, _ := io.ReadAll(reader)
351 return string(s)
352 },
353 },
354 {
355 encodeType: "deflate",
356 writer: func(ept string) *http.Response {
357 flw, _ := flate.NewWriter(flatebuf, flate.BestCompression)
358 defer flw.Close()
359
360 flw.Write([]byte(ept))
361 flw.Flush()
362 return &http.Response{
363 Body: io.NopCloser(flatebuf),
364 }
365 },
366 reader: func(rep *http.Response) string {
367 reader := flate.NewReader(rep.Body)
368 s, _ := io.ReadAll(reader)
369 return string(s)
370 },
371 },
372 }
373
374 errFn := func(encode string, err error) {
375 t.Errorf("%s failed to read and write: %v", encode, err)
376 }
377 for _, v := range test {
378 request, _ := http.NewRequest("GET", "http://mynode.com/", nil)
379 request.Header.Set("Content-Encoding", v.encodeType)
380 request.Header.Add("Accept-Encoding", v.encodeType)
381
382 for _, exp := range expected {
383 resp := v.writer(exp)
384 gotResponse, err := testTransport.rewriteResponse(request, resp)
385
386 if err != nil {
387 errFn(v.encodeType, err)
388 }
389
390 result := v.reader(gotResponse)
391 if result != exp {
392 errFn(v.encodeType, fmt.Errorf("expected %s, get %s", exp, result))
393 }
394 }
395 }
396 }
397
View as plain text