package main import ( "encoding/base64" "io" "net" "net/http" "net/http/httptest" "os" "strconv" "strings" "testing" "time" ) // The httptest servers listen on loopback, which the tool refuses; the tests allow it, and the refusal // tests put the refusal back. func TestMain(m *testing.M) { allowLoopback = true os.Exit(m.Run()) } func refusing(t *testing.T) { t.Helper() allowLoopback = false t.Cleanup(func() { allowLoopback = true }) } func TestRequestRefusesLocalAddresses(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Error("the server was reached") })) defer srv.Close() port := srv.Listener.Addr().(*net.TCPAddr).Port refusing(t) for _, host := range []string{ "127.0.0.1", "localhost", "127.1", "2130706433", "0x7f.0.0.1", "0x7f.1", "0177.0.0.1", "[::1]", "[::ffff:127.0.0.1]", "0.0.0.0", "[::]", "169.254.169.254", "[fe80::1]", } { u := "http://" + host + ":" + strconv.Itoa(port) + "/" _, err := asked(t, map[string]any{"url": u, "timeout_ms": float64(2000)}) if err == nil || !strings.Contains(err.Error(), "refus") { t.Errorf("%s: want a refusal, got %v", u, err) } } } func TestShortFormAddressIsRefusedOnItsOwnWords(t *testing.T) { for _, host := range []string{"127.1", "2130706433", "0x7f.0.0.1", "0x7f.1", "0177.0.0.1"} { _, err := asked(t, map[string]any{"url": "http://" + host + ":1/", "timeout_ms": float64(2000)}) if err == nil || !strings.Contains(err.Error(), "short or legacy form") { t.Errorf("%s: want the short-form refusal, got %v", host, err) } } for _, host := range []string{"example.com", "127.0.0.1", "1.example.com", "[::1]", "10.0.0.1"} { if shortFormAddress(strings.Trim(host, "[]")) { t.Errorf("%s taken for a short form", host) } } } func TestRequestIgnoresProxyEnvironment(t *testing.T) { t.Setenv("HTTP_PROXY", "http://127.0.0.1:1") t.Setenv("http_proxy", "http://127.0.0.1:1") srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) defer srv.Close() if _, err := asked(t, map[string]any{"url": "http://example.invalid:80/"}); err == nil || strings.Contains(err.Error(), "127.0.0.1:1") { t.Fatalf("a proxy seems to have been used: %v", err) } } func asked(t *testing.T, args map[string]any) (RequestResult, error) { t.Helper() got, err := runRequest(args) if err != nil { return RequestResult{}, err } return got.(RequestResult), nil } func TestRequestGet(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Seen", r.Method+" "+r.URL.RequestURI()) _, _ = io.WriteString(w, "hello") })) defer srv.Close() res, err := asked(t, map[string]any{"url": srv.URL + "/mail/config-v1.1.xml?a=1"}) if err != nil { t.Fatal(err) } if res.Status != 200 || res.StatusLine != "200 OK" || res.Body != "hello" || res.Cut || res.Encoding != "text" { t.Fatalf("unexpected answer %+v", res) } if res.Headers["X-Seen"][0] != "GET /mail/config-v1.1.xml?a=1" { t.Fatalf("headers %+v", res.Headers) } } func TestRequestHeadHasNoBody(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Method", r.Method) })) defer srv.Close() res, err := asked(t, map[string]any{"url": srv.URL, "method": "HEAD"}) if err != nil || res.Headers["X-Method"][0] != "HEAD" || res.Body != "" { t.Fatalf("%+v %v", res, err) } } func TestRequestPostWithBodyAndHeaders(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) w.WriteHeader(http.StatusCreated) _, _ = io.WriteString(w, r.Method+"|"+r.Header.Get("Content-Type")+"|"+string(b)) })) defer srv.Close() res, err := asked(t, map[string]any{ "url": srv.URL, "method": "POST", "body": "", "headers": map[string]any{"Content-Type": "text/xml"}, }) if err != nil { t.Fatal(err) } if res.Status != 201 || res.Body != "POST|text/xml|" { t.Fatalf("%+v", res) } } func TestRequestBodyTooLarge(t *testing.T) { _, err := asked(t, map[string]any{"url": "http://127.0.0.1:1", "method": "POST", "body": strings.Repeat("x", MostRequestBody+1)}) if err == nil || !strings.Contains(err.Error(), "body") { t.Fatalf("want a body error, got %v", err) } } func TestRequestCutBody(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, strings.Repeat("a", MostResponseBody+10)) })) defer srv.Close() res, err := asked(t, map[string]any{"url": srv.URL}) if err != nil { t.Fatal(err) } if !res.Cut || len(res.Body) != MostResponseBody { t.Fatalf("cut=%v len=%d", res.Cut, len(res.Body)) } } func TestRequestBinaryBodyIsBase64(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte{0xff, 0xfe, 0x00, 0x01}) })) defer srv.Close() res, err := asked(t, map[string]any{"url": srv.URL}) if err != nil { t.Fatal(err) } raw, derr := base64.StdEncoding.DecodeString(res.Body) if res.Encoding != "base64" || derr != nil || len(raw) != 4 { t.Fatalf("%+v", res) } } func TestRequestRedirectReportedNotFollowed(t *testing.T) { hit := false srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/there" { hit = true } http.Redirect(w, r, "/there", http.StatusFound) })) defer srv.Close() res, err := asked(t, map[string]any{"url": srv.URL}) if err != nil { t.Fatal(err) } if res.Status != 302 || res.Redirect != "/there" || hit { t.Fatalf("%+v hit=%v", res, hit) } } func TestRequestRefusesOtherSchemes(t *testing.T) { for _, u := range []string{"file:///etc/passwd", "ftp://example.org/x", "gopher://x", "//x/y", "example.org"} { if _, err := asked(t, map[string]any{"url": u}); err == nil { t.Errorf("%q was not refused", u) } } } func TestRequestRefusesOtherMethods(t *testing.T) { if _, err := asked(t, map[string]any{"url": "http://127.0.0.1:1", "method": "DELETE"}); err == nil { t.Fatal("DELETE was not refused") } } func TestRequestTimeout(t *testing.T) { release := make(chan struct{}) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { select { case <-release: case <-r.Context().Done(): } })) defer srv.Close() defer close(release) start := time.Now() _, err := asked(t, map[string]any{"url": srv.URL, "timeout_ms": float64(100)}) if err == nil || time.Since(start) > 3*time.Second { t.Fatalf("want a timeout error quickly, got %v after %v", err, time.Since(start)) } }