|
@@ -16,6 +16,7 @@ import (
|
|
|
"net"
|
|
|
"net/http"
|
|
|
"net/http/httptest"
|
|
|
+ "strconv"
|
|
|
"strings"
|
|
|
"testing"
|
|
|
"time"
|
|
@@ -460,7 +461,6 @@ func TestHTTPLogin(t *testing.T) {
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
t.Errorf("Unexpected non-200 return code %d for authed request (ISO-8859-1)", resp.StatusCode)
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
|
|
|
func startHTTP(cfg *mockedConfig) (string, error) {
|
|
@@ -491,7 +491,12 @@ func startHTTP(cfg *mockedConfig) (string, error) {
|
|
|
if err != nil {
|
|
|
return "", fmt.Errorf("Weird address from API service: %v", err)
|
|
|
}
|
|
|
- baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
|
|
|
+
|
|
|
+ host, _, _ := net.SplitHostPort(cfg.gui.RawAddress)
|
|
|
+ if host == "" || host == "0.0.0.0" {
|
|
|
+ host = "127.0.0.1"
|
|
|
+ }
|
|
|
+ baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)))
|
|
|
|
|
|
return baseURL, nil
|
|
|
}
|
|
@@ -666,3 +671,189 @@ func testConfigPost(data io.Reader) (*http.Response, error) {
|
|
|
req.Header.Set("X-API-Key", testAPIKey)
|
|
|
return cli.Do(req)
|
|
|
}
|
|
|
+
|
|
|
+func TestHostCheck(t *testing.T) {
|
|
|
+ // An API service bound to localhost should reject non-localhost host Headers
|
|
|
+
|
|
|
+ cfg := new(mockedConfig)
|
|
|
+ cfg.gui.RawAddress = "127.0.0.1:0"
|
|
|
+ baseURL, err := startHTTP(cfg)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A normal HTTP get to the localhost-bound service should succeed
|
|
|
+
|
|
|
+ resp, err := http.Get(baseURL)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ resp.Body.Close()
|
|
|
+ if resp.StatusCode != http.StatusOK {
|
|
|
+ t.Error("Regular HTTP get: expected 200 OK, not", resp.Status)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A request with a suspicious Host header should fail
|
|
|
+
|
|
|
+ req, _ := http.NewRequest("GET", baseURL, nil)
|
|
|
+ req.Host = "example.com"
|
|
|
+ resp, err = http.DefaultClient.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ resp.Body.Close()
|
|
|
+ if resp.StatusCode != http.StatusForbidden {
|
|
|
+ t.Error("Suspicious Host header: expected 403 Forbidden, not", resp.Status)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A request with an explicit "localhost:8384" Host header should pass
|
|
|
+
|
|
|
+ req, _ = http.NewRequest("GET", baseURL, nil)
|
|
|
+ req.Host = "localhost:8384"
|
|
|
+ resp, err = http.DefaultClient.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ resp.Body.Close()
|
|
|
+ if resp.StatusCode != http.StatusOK {
|
|
|
+ t.Error("Explicit localhost:8384: expected 200 OK, not", resp.Status)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A request with an explicit "localhost" Host header (no port) should pass
|
|
|
+
|
|
|
+ req, _ = http.NewRequest("GET", baseURL, nil)
|
|
|
+ req.Host = "localhost"
|
|
|
+ resp, err = http.DefaultClient.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ resp.Body.Close()
|
|
|
+ if resp.StatusCode != http.StatusOK {
|
|
|
+ t.Error("Explicit localhost: expected 200 OK, not", resp.Status)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A server with InsecureSkipHostCheck set behaves differently
|
|
|
+
|
|
|
+ cfg = new(mockedConfig)
|
|
|
+ cfg.gui.RawAddress = "127.0.0.1:0"
|
|
|
+ cfg.gui.InsecureSkipHostCheck = true
|
|
|
+ baseURL, err = startHTTP(cfg)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A request with a suspicious Host header should be allowed
|
|
|
+
|
|
|
+ req, _ = http.NewRequest("GET", baseURL, nil)
|
|
|
+ req.Host = "example.com"
|
|
|
+ resp, err = http.DefaultClient.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ resp.Body.Close()
|
|
|
+ if resp.StatusCode != http.StatusOK {
|
|
|
+ t.Error("Incorrect host header, check disabled: expected 200 OK, not", resp.Status)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A server bound to a wildcard address also doesn't do the check
|
|
|
+
|
|
|
+ cfg = new(mockedConfig)
|
|
|
+ cfg.gui.RawAddress = "0.0.0.0:0"
|
|
|
+ cfg.gui.InsecureSkipHostCheck = true
|
|
|
+ baseURL, err = startHTTP(cfg)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A request with a suspicious Host header should be allowed
|
|
|
+
|
|
|
+ req, _ = http.NewRequest("GET", baseURL, nil)
|
|
|
+ req.Host = "example.com"
|
|
|
+ resp, err = http.DefaultClient.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ resp.Body.Close()
|
|
|
+ if resp.StatusCode != http.StatusOK {
|
|
|
+ t.Error("Incorrect host header, wildcard bound: expected 200 OK, not", resp.Status)
|
|
|
+ }
|
|
|
+
|
|
|
+ // This should all work over IPv6 as well
|
|
|
+
|
|
|
+ cfg = new(mockedConfig)
|
|
|
+ cfg.gui.RawAddress = "[::1]:0"
|
|
|
+ baseURL, err = startHTTP(cfg)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A normal HTTP get to the localhost-bound service should succeed
|
|
|
+
|
|
|
+ resp, err = http.Get(baseURL)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ resp.Body.Close()
|
|
|
+ if resp.StatusCode != http.StatusOK {
|
|
|
+ t.Error("Regular HTTP get (IPv6): expected 200 OK, not", resp.Status)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A request with a suspicious Host header should fail
|
|
|
+
|
|
|
+ req, _ = http.NewRequest("GET", baseURL, nil)
|
|
|
+ req.Host = "example.com"
|
|
|
+ resp, err = http.DefaultClient.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ resp.Body.Close()
|
|
|
+ if resp.StatusCode != http.StatusForbidden {
|
|
|
+ t.Error("Suspicious Host header (IPv6): expected 403 Forbidden, not", resp.Status)
|
|
|
+ }
|
|
|
+
|
|
|
+ // A request with an explicit "localhost:8384" Host header should pass
|
|
|
+
|
|
|
+ req, _ = http.NewRequest("GET", baseURL, nil)
|
|
|
+ req.Host = "localhost:8384"
|
|
|
+ resp, err = http.DefaultClient.Do(req)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ resp.Body.Close()
|
|
|
+ if resp.StatusCode != http.StatusOK {
|
|
|
+ t.Error("Explicit localhost:8384 (IPv6): expected 200 OK, not", resp.Status)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestAddressIsLocalhost(t *testing.T) {
|
|
|
+ testcases := []struct {
|
|
|
+ address string
|
|
|
+ result bool
|
|
|
+ }{
|
|
|
+ // These are all valid localhost addresses
|
|
|
+ {"localhost", true},
|
|
|
+ {"::1", true},
|
|
|
+ {"127.0.0.1", true},
|
|
|
+ {"localhost:8080", true},
|
|
|
+ {"[::1]:8080", true},
|
|
|
+ {"127.0.0.1:8080", true},
|
|
|
+
|
|
|
+ // These are all non-localhost addresses
|
|
|
+ {"example.com", false},
|
|
|
+ {"example.com:8080", false},
|
|
|
+ {"192.0.2.10", false},
|
|
|
+ {"192.0.2.10:8080", false},
|
|
|
+ {"0.0.0.0", false},
|
|
|
+ {"0.0.0.0:8080", false},
|
|
|
+ {"::", false},
|
|
|
+ {"[::]:8080", false},
|
|
|
+ {":8080", false},
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tc := range testcases {
|
|
|
+ result := addressIsLocalhost(tc.address)
|
|
|
+ if result != tc.result {
|
|
|
+ t.Errorf("addressIsLocalhost(%q)=%v, expected %v", tc.address, result, tc.result)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|