1
16
17 package http
18
19 import (
20 "bytes"
21 "fmt"
22 "net"
23 "net/http"
24 "net/http/httptest"
25 "net/url"
26 "os"
27 "sort"
28 "strconv"
29 "strings"
30 "testing"
31 "time"
32
33 "github.com/stretchr/testify/assert"
34 "github.com/stretchr/testify/require"
35 "k8s.io/apimachinery/pkg/util/wait"
36 "k8s.io/kubernetes/pkg/probe"
37 )
38
39 const FailureCode int = -1
40
41 func unsetEnv(t testing.TB, key string) {
42 if originalValue, ok := os.LookupEnv(key); ok {
43 t.Cleanup(func() { os.Setenv(key, originalValue) })
44 os.Unsetenv(key)
45 }
46 }
47
48 func TestHTTPProbeProxy(t *testing.T) {
49 res := "welcome to http probe proxy"
50
51 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
52 fmt.Fprint(w, res)
53 }))
54 defer server.Close()
55
56 localProxy := server.URL
57
58 t.Setenv("http_proxy", localProxy)
59 t.Setenv("HTTP_PROXY", localProxy)
60 unsetEnv(t, "no_proxy")
61 unsetEnv(t, "NO_PROXY")
62
63 followNonLocalRedirects := true
64 prober := New(followNonLocalRedirects)
65
66
67 time.Sleep(2 * time.Second)
68 url, err := url.Parse("http://example.com")
69 if err != nil {
70 t.Errorf("proxy test unexpected error: %v", err)
71 }
72
73 req, err := NewProbeRequest(url, http.Header{})
74 if err != nil {
75 t.Fatal(err)
76 }
77
78 _, response, _ := prober.Probe(req, time.Second*3)
79
80 if response == res {
81 t.Errorf("proxy test unexpected error: the probe is using proxy")
82 }
83 }
84
85 func TestHTTPProbeChecker(t *testing.T) {
86 handleReq := func(s int, body string) func(w http.ResponseWriter, r *http.Request) {
87 return func(w http.ResponseWriter, r *http.Request) {
88 w.WriteHeader(s)
89 w.Write([]byte(body))
90 }
91 }
92
93
94 headerEchoHandler := func(w http.ResponseWriter, r *http.Request) {
95 w.WriteHeader(200)
96 output := ""
97 for k, arr := range r.Header {
98 for _, v := range arr {
99 output += fmt.Sprintf("%s: %s\n", k, v)
100 }
101 }
102 w.Write([]byte(output))
103 }
104
105
106 headerCounterHandler := func(w http.ResponseWriter, r *http.Request) {
107 w.WriteHeader(200)
108 w.Write([]byte(strconv.Itoa(len(r.Header))))
109 }
110
111
112 headerKeysNamesHandler := func(w http.ResponseWriter, r *http.Request) {
113 w.WriteHeader(200)
114 keys := make([]string, 0, len(r.Header))
115 for k := range r.Header {
116 keys = append(keys, k)
117 }
118 sort.Strings(keys)
119
120 w.Write([]byte(strings.Join(keys, "\n")))
121 }
122
123 redirectHandler := func(s int, bad bool) func(w http.ResponseWriter, r *http.Request) {
124 return func(w http.ResponseWriter, r *http.Request) {
125 if r.URL.Path == "/" {
126 http.Redirect(w, r, "/new", s)
127 } else if bad && r.URL.Path == "/new" {
128 http.Error(w, "", http.StatusInternalServerError)
129 }
130 }
131 }
132
133 redirectHandlerWithBody := func(s int, body string) func(w http.ResponseWriter, r *http.Request) {
134 return func(w http.ResponseWriter, r *http.Request) {
135 if r.URL.Path == "/" {
136 http.Redirect(w, r, "/new", s)
137 } else if r.URL.Path == "/new" {
138 w.WriteHeader(s)
139 w.Write([]byte(body))
140 }
141 }
142 }
143
144 followNonLocalRedirects := true
145 prober := New(followNonLocalRedirects)
146 testCases := []struct {
147 handler func(w http.ResponseWriter, r *http.Request)
148 reqHeaders http.Header
149 health probe.Result
150 accBody string
151 notBody string
152 }{
153
154 {
155 handler: handleReq(http.StatusOK, "ok body"),
156 health: probe.Success,
157 accBody: "ok body",
158 },
159 {
160 handler: headerCounterHandler,
161 reqHeaders: http.Header{},
162 health: probe.Success,
163 accBody: "3",
164 },
165 {
166 handler: headerKeysNamesHandler,
167 reqHeaders: http.Header{},
168 health: probe.Success,
169 accBody: "Accept\nConnection\nUser-Agent",
170 },
171 {
172 handler: headerEchoHandler,
173 reqHeaders: http.Header{
174 "Accept-Encoding": {"gzip"},
175 },
176 health: probe.Success,
177 accBody: "Accept-Encoding: gzip",
178 },
179 {
180 handler: headerEchoHandler,
181 reqHeaders: http.Header{
182 "Accept-Encoding": {"foo"},
183 },
184 health: probe.Success,
185 accBody: "Accept-Encoding: foo",
186 },
187 {
188 handler: headerEchoHandler,
189 reqHeaders: http.Header{
190 "Accept-Encoding": {""},
191 },
192 health: probe.Success,
193 accBody: "Accept-Encoding: \n",
194 },
195 {
196 handler: headerEchoHandler,
197 reqHeaders: http.Header{
198 "X-Muffins-Or-Cupcakes": {"muffins"},
199 },
200 health: probe.Success,
201 accBody: "X-Muffins-Or-Cupcakes: muffins",
202 },
203 {
204 handler: headerEchoHandler,
205 reqHeaders: http.Header{
206 "User-Agent": {"foo/1.0"},
207 },
208 health: probe.Success,
209 accBody: "User-Agent: foo/1.0",
210 },
211 {
212 handler: headerEchoHandler,
213 reqHeaders: http.Header{
214 "User-Agent": {""},
215 },
216 health: probe.Success,
217 notBody: "User-Agent",
218 },
219 {
220 handler: headerEchoHandler,
221 reqHeaders: http.Header{},
222 health: probe.Success,
223 accBody: "User-Agent: kube-probe/",
224 },
225 {
226 handler: headerEchoHandler,
227 reqHeaders: http.Header{
228 "User-Agent": {"foo/1.0"},
229 "Accept": {"text/html"},
230 },
231 health: probe.Success,
232 accBody: "Accept: text/html",
233 },
234 {
235 handler: headerEchoHandler,
236 reqHeaders: http.Header{
237 "User-Agent": {"foo/1.0"},
238 "Accept": {"foo/*"},
239 },
240 health: probe.Success,
241 accBody: "User-Agent: foo/1.0",
242 },
243 {
244 handler: headerEchoHandler,
245 reqHeaders: http.Header{
246 "X-Muffins-Or-Cupcakes": {"muffins"},
247 "Accept": {"foo/*"},
248 },
249 health: probe.Success,
250 accBody: "X-Muffins-Or-Cupcakes: muffins",
251 },
252 {
253 handler: headerEchoHandler,
254 reqHeaders: http.Header{
255 "Accept": {"foo/*"},
256 },
257 health: probe.Success,
258 accBody: "Accept: foo/*",
259 },
260 {
261 handler: headerEchoHandler,
262 reqHeaders: http.Header{
263 "Accept": {""},
264 },
265 health: probe.Success,
266 notBody: "Accept:",
267 },
268 {
269 handler: headerEchoHandler,
270 reqHeaders: http.Header{
271 "User-Agent": {"foo/1.0"},
272 "Accept": {""},
273 },
274 health: probe.Success,
275 notBody: "Accept:",
276 },
277 {
278 handler: headerEchoHandler,
279 reqHeaders: http.Header{},
280 health: probe.Success,
281 accBody: "Accept: */*",
282 },
283 {
284
285 handler: func(w http.ResponseWriter, r *http.Request) {
286 w.WriteHeader(200)
287 w.Write([]byte(r.Host))
288 },
289 reqHeaders: http.Header{
290 "Host": {"muffins.cupcakes.org"},
291 },
292 health: probe.Success,
293 accBody: "muffins.cupcakes.org",
294 },
295 {
296 handler: handleReq(FailureCode, "fail body"),
297 health: probe.Failure,
298 },
299 {
300 handler: handleReq(http.StatusInternalServerError, "fail body"),
301 health: probe.Failure,
302 },
303 {
304 handler: func(w http.ResponseWriter, r *http.Request) {
305 time.Sleep(3 * time.Second)
306 },
307 health: probe.Failure,
308 },
309 {
310 handler: redirectHandler(http.StatusMovedPermanently, false),
311 health: probe.Success,
312 },
313 {
314 handler: redirectHandler(http.StatusMovedPermanently, true),
315 health: probe.Failure,
316 },
317 {
318 handler: redirectHandler(http.StatusFound, false),
319 health: probe.Success,
320 },
321 {
322 handler: redirectHandler(http.StatusFound, true),
323 health: probe.Failure,
324 },
325 {
326 handler: redirectHandler(http.StatusTemporaryRedirect, false),
327 health: probe.Success,
328 },
329 {
330 handler: redirectHandler(http.StatusTemporaryRedirect, true),
331 health: probe.Failure,
332 },
333 {
334 handler: redirectHandler(http.StatusPermanentRedirect, false),
335 health: probe.Success,
336 },
337 {
338 handler: redirectHandler(http.StatusPermanentRedirect, true),
339 health: probe.Failure,
340 },
341 {
342 handler: redirectHandlerWithBody(http.StatusPermanentRedirect, ""),
343 health: probe.Warning,
344 accBody: "Probe terminated redirects, Response body:",
345 },
346 {
347 handler: redirectHandlerWithBody(http.StatusPermanentRedirect, "ok body"),
348 health: probe.Warning,
349 accBody: "Probe terminated redirects, Response body: ok body",
350 },
351 }
352 for i, test := range testCases {
353 t.Run(fmt.Sprintf("case-%2d", i), func(t *testing.T) {
354 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
355 test.handler(w, r)
356 }))
357 defer server.Close()
358 u, err := url.Parse(server.URL)
359 if err != nil {
360 t.Errorf("case %d: unexpected error: %v", i, err)
361 }
362 _, port, err := net.SplitHostPort(u.Host)
363 if err != nil {
364 t.Errorf("case %d: unexpected error: %v", i, err)
365 }
366 _, err = strconv.Atoi(port)
367 if err != nil {
368 t.Errorf("case %d: unexpected error: %v", i, err)
369 }
370 req, err := NewProbeRequest(u, test.reqHeaders)
371 if err != nil {
372 t.Fatal(err)
373 }
374 health, output, err := prober.Probe(req, 1*time.Second)
375 if test.health == probe.Unknown && err == nil {
376 t.Errorf("case %d: expected error", i)
377 }
378 if test.health != probe.Unknown && err != nil {
379 t.Errorf("case %d: unexpected error: %v", i, err)
380 }
381 if health != test.health {
382 t.Errorf("case %d: expected %v, got %v", i, test.health, health)
383 }
384 if health != probe.Failure && test.health != probe.Failure {
385 if !strings.Contains(output, test.accBody) {
386 t.Errorf("Expected response body to contain %v, got %v", test.accBody, output)
387 }
388 if test.notBody != "" && strings.Contains(output, test.notBody) {
389 t.Errorf("Expected response not to contain %v, got %v", test.notBody, output)
390 }
391 }
392 })
393 }
394 }
395
396 func TestHTTPProbeChecker_NonLocalRedirects(t *testing.T) {
397 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
398 switch r.URL.Path {
399 case "/redirect":
400 loc, _ := url.QueryUnescape(r.URL.Query().Get("loc"))
401 http.Redirect(w, r, loc, http.StatusFound)
402 case "/loop":
403 http.Redirect(w, r, "/loop", http.StatusFound)
404 case "/success":
405 w.WriteHeader(http.StatusOK)
406 default:
407 http.Error(w, "", http.StatusInternalServerError)
408 }
409 })
410 server := httptest.NewServer(handler)
411 defer server.Close()
412
413 newportServer := httptest.NewServer(handler)
414 defer newportServer.Close()
415
416 testCases := map[string]struct {
417 redirect string
418 expectLocalResult probe.Result
419 expectNonLocalResult probe.Result
420 }{
421 "local success": {"/success", probe.Success, probe.Success},
422 "local fail": {"/fail", probe.Failure, probe.Failure},
423 "newport success": {newportServer.URL + "/success", probe.Success, probe.Success},
424 "newport fail": {newportServer.URL + "/fail", probe.Failure, probe.Failure},
425 "bogus nonlocal": {"http://0.0.0.0/fail", probe.Warning, probe.Failure},
426 "redirect loop": {"/loop", probe.Failure, probe.Failure},
427 }
428 for desc, test := range testCases {
429 t.Run(desc+"-local", func(t *testing.T) {
430 followNonLocalRedirects := false
431 prober := New(followNonLocalRedirects)
432 target, err := url.Parse(server.URL + "/redirect?loc=" + url.QueryEscape(test.redirect))
433 require.NoError(t, err)
434 req, err := NewProbeRequest(target, nil)
435 require.NoError(t, err)
436 result, _, _ := prober.Probe(req, wait.ForeverTestTimeout)
437 assert.Equal(t, test.expectLocalResult, result)
438 })
439 t.Run(desc+"-nonlocal", func(t *testing.T) {
440 followNonLocalRedirects := true
441 prober := New(followNonLocalRedirects)
442 target, err := url.Parse(server.URL + "/redirect?loc=" + url.QueryEscape(test.redirect))
443 require.NoError(t, err)
444 req, err := NewProbeRequest(target, nil)
445 require.NoError(t, err)
446 result, _, _ := prober.Probe(req, wait.ForeverTestTimeout)
447 assert.Equal(t, test.expectNonLocalResult, result)
448 })
449 }
450 }
451
452 func TestHTTPProbeChecker_HostHeaderPreservedAfterRedirect(t *testing.T) {
453 successHostHeader := "www.success.com"
454 failHostHeader := "www.fail.com"
455
456 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
457 switch r.URL.Path {
458 case "/redirect":
459 http.Redirect(w, r, "/success", http.StatusFound)
460 case "/success":
461 if r.Host == successHostHeader {
462 w.WriteHeader(http.StatusOK)
463 } else {
464 http.Error(w, "", http.StatusBadRequest)
465 }
466 default:
467 http.Error(w, "", http.StatusInternalServerError)
468 }
469 })
470 server := httptest.NewServer(handler)
471 defer server.Close()
472
473 testCases := map[string]struct {
474 hostHeader string
475 expectedResult probe.Result
476 }{
477 "success": {successHostHeader, probe.Success},
478 "fail": {failHostHeader, probe.Failure},
479 }
480 for desc, test := range testCases {
481 headers := http.Header{}
482 headers.Add("Host", test.hostHeader)
483 t.Run(desc+"local", func(t *testing.T) {
484 followNonLocalRedirects := false
485 prober := New(followNonLocalRedirects)
486 target, err := url.Parse(server.URL + "/redirect")
487 require.NoError(t, err)
488 req, err := NewProbeRequest(target, headers)
489 require.NoError(t, err)
490 result, _, _ := prober.Probe(req, wait.ForeverTestTimeout)
491 assert.Equal(t, test.expectedResult, result)
492 })
493 t.Run(desc+"nonlocal", func(t *testing.T) {
494 followNonLocalRedirects := true
495 prober := New(followNonLocalRedirects)
496 target, err := url.Parse(server.URL + "/redirect")
497 require.NoError(t, err)
498 req, err := NewProbeRequest(target, headers)
499 require.NoError(t, err)
500 result, _, _ := prober.Probe(req, wait.ForeverTestTimeout)
501 assert.Equal(t, test.expectedResult, result)
502 })
503 }
504 }
505
506 func TestHTTPProbeChecker_PayloadTruncated(t *testing.T) {
507 successHostHeader := "www.success.com"
508 oversizePayload := bytes.Repeat([]byte("a"), maxRespBodyLength+1)
509 truncatedPayload := bytes.Repeat([]byte("a"), maxRespBodyLength)
510
511 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
512 switch r.URL.Path {
513 case "/success":
514 if r.Host == successHostHeader {
515 w.WriteHeader(http.StatusOK)
516 w.Write(oversizePayload)
517 } else {
518 http.Error(w, "", http.StatusBadRequest)
519 }
520 default:
521 http.Error(w, "", http.StatusInternalServerError)
522 }
523 })
524 server := httptest.NewServer(handler)
525 defer server.Close()
526
527 headers := http.Header{}
528 headers.Add("Host", successHostHeader)
529 t.Run("truncated payload", func(t *testing.T) {
530 prober := New(false)
531 target, err := url.Parse(server.URL + "/success")
532 require.NoError(t, err)
533 req, err := NewProbeRequest(target, headers)
534 require.NoError(t, err)
535 result, body, err := prober.Probe(req, wait.ForeverTestTimeout)
536 assert.NoError(t, err)
537 assert.Equal(t, probe.Success, result)
538 assert.Equal(t, string(truncatedPayload), body)
539 })
540 }
541
542 func TestHTTPProbeChecker_PayloadNormal(t *testing.T) {
543 successHostHeader := "www.success.com"
544 normalPayload := bytes.Repeat([]byte("a"), maxRespBodyLength-1)
545
546 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
547 switch r.URL.Path {
548 case "/success":
549 if r.Host == successHostHeader {
550 w.WriteHeader(http.StatusOK)
551 w.Write(normalPayload)
552 } else {
553 http.Error(w, "", http.StatusBadRequest)
554 }
555 default:
556 http.Error(w, "", http.StatusInternalServerError)
557 }
558 })
559 server := httptest.NewServer(handler)
560 defer server.Close()
561
562 headers := http.Header{}
563 headers.Add("Host", successHostHeader)
564 t.Run("normal payload", func(t *testing.T) {
565 prober := New(false)
566 target, err := url.Parse(server.URL + "/success")
567 require.NoError(t, err)
568 req, err := NewProbeRequest(target, headers)
569 require.NoError(t, err)
570 result, body, err := prober.Probe(req, wait.ForeverTestTimeout)
571 assert.NoError(t, err)
572 assert.Equal(t, probe.Success, result)
573 assert.Equal(t, string(normalPayload), body)
574 })
575 }
576
View as plain text