From 55fa8c2d0a82123645294f3c8d5158df16e56ed5 Mon Sep 17 00:00:00 2001 From: Samantha Date: Sat, 15 Jun 2024 22:23:21 +0100 Subject: [PATCH 1/3] RF: hsts --- checks/checks.go | 2 + checks/hsts.go | 98 +++++++++++++++++++++++++++++++++ checks/hsts_test.go | 125 ++++++++++++++++++++++++++++++++++++++++++ handlers/hsts.go | 50 ++--------------- handlers/hsts_test.go | 20 +++---- server/server.go | 2 +- server/server_test.go | 2 +- 7 files changed, 239 insertions(+), 60 deletions(-) create mode 100644 checks/hsts.go create mode 100644 checks/hsts_test.go diff --git a/checks/checks.go b/checks/checks.go index 6c92238..ea7b766 100644 --- a/checks/checks.go +++ b/checks/checks.go @@ -14,6 +14,7 @@ type Checks struct { Rank *Rank SocialTags *SocialTags Tls *Tls + Hsts *Hsts } func NewChecks() *Checks { @@ -27,5 +28,6 @@ func NewChecks() *Checks { Rank: NewRank(client), SocialTags: NewSocialTags(client), Tls: NewTls(client), + Hsts: NewHsts(client), } } diff --git a/checks/hsts.go b/checks/hsts.go new file mode 100644 index 0000000..da24593 --- /dev/null +++ b/checks/hsts.go @@ -0,0 +1,98 @@ +package checks + +import ( + "context" + "net/http" + "strconv" + "strings" + "unicode" +) + +const ( + StrictTransportSecurity = "Strict-Transport-Security" + includeSubDomains = "includeSubDomains" + preload = "preload" + NilHeadersError = "Site does not serve any HSTS headers." + MaxAgeError = "HSTS max-age is less than 10886400." + SubdomainsError = "HSTS header does not include all subdomains." + PreloadError = "HSTS header does not contain the preload directive." + HstsSuccess = "Site is compatible with the HSTS preload list!" +) + +type HSTSResponse struct { + Message string `json:"message"` + Compatible bool `json:"compatible"` + HSTSHeader string `json:"hstsHeader"` +} + +type Hsts struct { + client *http.Client +} + +func NewHsts(client *http.Client) *Hsts { + return &Hsts{client: client} +} + +func (h *Hsts) Validate(ctx context.Context, url string) (*HSTSResponse, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) + if err != nil { + return nil, err + } + + resp, err := h.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + hstsHeader := resp.Header.Get(StrictTransportSecurity) + if hstsHeader == "" { + return &HSTSResponse{ + Message: NilHeadersError, + }, nil + } + + maxAge := extractMaxAgeFromHeader(hstsHeader) + if maxAge == "" { + return &HSTSResponse{Message: MaxAgeError}, nil + } + + maxAgeInt, err := convertMaxAgeStringToInt(maxAge) + if err != nil { + return nil, err + } + + if maxAgeInt < 10886400 { + return &HSTSResponse{Message: MaxAgeError}, nil + } + + if !strings.Contains(hstsHeader, includeSubDomains) { + return &HSTSResponse{Message: SubdomainsError}, nil + } + + if !strings.Contains(hstsHeader, preload) { + return &HSTSResponse{Message: PreloadError}, nil + } + + return &HSTSResponse{ + Message: HstsSuccess, + Compatible: true, + HSTSHeader: hstsHeader, + }, nil +} + +func extractMaxAgeFromHeader(header string) string { + var maxAge strings.Builder + + for _, b := range header { + if unicode.IsDigit(b) { + maxAge.WriteRune(b) + } + } + + return maxAge.String() +} + +func convertMaxAgeStringToInt(maxAge string) (int, error) { + return strconv.Atoi(maxAge) +} diff --git a/checks/hsts_test.go b/checks/hsts_test.go new file mode 100644 index 0000000..b06687a --- /dev/null +++ b/checks/hsts_test.go @@ -0,0 +1,125 @@ +package checks + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/xray-web/web-check-api/testutils" +) + +func TestValidate(t *testing.T) { + t.Parallel() + + t.Run("given an empty header", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{""}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, NilHeadersError, actual.Message) + assert.False(t, actual.Compatible) + assert.Empty(t, actual.HSTSHeader) + }) + + t.Run("given a header without max age", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{"includeSubDomains; preload"}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, MaxAgeError, actual.Message) + assert.False(t, actual.Compatible) + assert.Empty(t, actual.HSTSHeader) + }) + + t.Run("given max age less than 10886400", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{"max-age=47; includeSubDomains; preload"}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, MaxAgeError, actual.Message) + assert.False(t, actual.Compatible) + assert.Empty(t, actual.HSTSHeader) + }) + + t.Run("given a header without includeSubDomains", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{"max-age=47474747; preload"}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, SubdomainsError, actual.Message) + assert.False(t, actual.Compatible) + assert.Empty(t, actual.HSTSHeader) + }) + + t.Run("given a header without preload", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{"max-age=47474747; includeSubDomains"}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, PreloadError, actual.Message) + assert.False(t, actual.Compatible) + assert.Empty(t, actual.HSTSHeader) + }) + + t.Run("given a valid header", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{"max-age=47474747; includeSubDomains; preload"}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, HstsSuccess, actual.Message) + assert.True(t, actual.Compatible) + assert.NotEmpty(t, actual.HSTSHeader) + }) +} + +func TestExtractMaxAgeFromHeader(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + header string + expected string + }{ + {"give valid header", "max-age=47474747; includeSubDomains; preload", "47474747"}, + {"given an empty header", "", ""}, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + actual := extractMaxAgeFromHeader(tc.header) + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/handlers/hsts.go b/handlers/hsts.go index 647cba7..83c3eb1 100644 --- a/handlers/hsts.go +++ b/handlers/hsts.go @@ -1,54 +1,12 @@ package handlers import ( - "fmt" "net/http" - "regexp" - "strings" -) - -type HSTSResponse struct { - Message string `json:"message"` - Compatible bool `json:"compatible"` - HSTSHeader string `json:"hstsHeader"` -} - -func checkHSTS(url string) (HSTSResponse, error) { - client := &http.Client{} - - req, err := http.NewRequest("HEAD", url, nil) - if err != nil { - return HSTSResponse{}, fmt.Errorf("error creating request: %s", err.Error()) - } - - resp, err := client.Do(req) - if err != nil { - return HSTSResponse{}, fmt.Errorf("error making request: %s", err.Error()) - } - defer resp.Body.Close() - hstsHeader := resp.Header.Get("strict-transport-security") - if hstsHeader == "" { - return HSTSResponse{Message: "Site does not serve any HSTS headers."}, nil - } - - maxAgeMatch := regexp.MustCompile(`max-age=(\d+)`).FindStringSubmatch(hstsHeader) - if maxAgeMatch == nil || len(maxAgeMatch) < 2 || maxAgeMatch[1] == "" || maxAgeMatch[1] < "10886400" { - return HSTSResponse{Message: "HSTS max-age is less than 10886400."}, nil - } - - if !strings.Contains(hstsHeader, "includeSubDomains") { - return HSTSResponse{Message: "HSTS header does not include all subdomains."}, nil - } - - if !strings.Contains(hstsHeader, "preload") { - return HSTSResponse{Message: "HSTS header does not contain the preload directive."}, nil - } - - return HSTSResponse{Message: "Site is compatible with the HSTS preload list!", Compatible: true, HSTSHeader: hstsHeader}, nil -} + "github.com/xray-web/web-check-api/checks" +) -func HandleHsts() http.Handler { +func HandleHsts(h *checks.Hsts) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rawURL, err := extractURL(r) if err != nil { @@ -56,7 +14,7 @@ func HandleHsts() http.Handler { return } - result, err := checkHSTS(rawURL.String()) + result, err := h.Validate(r.Context(), rawURL.String()) if err != nil { JSONError(w, err, http.StatusInternalServerError) return diff --git a/handlers/hsts_test.go b/handlers/hsts_test.go index 983ae22..c9ba27d 100644 --- a/handlers/hsts_test.go +++ b/handlers/hsts_test.go @@ -1,7 +1,6 @@ package handlers import ( - "encoding/json" "net/http" "net/http/httptest" "testing" @@ -11,18 +10,15 @@ import ( func TestHandleHsts(t *testing.T) { t.Parallel() - req := httptest.NewRequest("GET", "/check-hsts?url=example.com", nil) - rec := httptest.NewRecorder() - HandleHsts().ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) + t.Run("missing URL parameter", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(http.MethodGet, "/check-hsts", nil) + rec := httptest.NewRecorder() - var response HSTSResponse - err := json.Unmarshal(rec.Body.Bytes(), &response) - assert.NoError(t, err) + HandleHsts(nil).ServeHTTP(rec, req) - assert.NotNil(t, response) - assert.Equal(t, "Site does not serve any HSTS headers.", response.Message) - assert.False(t, response.Compatible) - assert.Empty(t, response.HSTSHeader) + assert.Equal(t, http.StatusBadRequest, rec.Code) + assert.JSONEq(t, `{"error": "missing URL parameter"}`, rec.Body.String()) + }) } diff --git a/server/server.go b/server/server.go index 47843ac..21bb945 100644 --- a/server/server.go +++ b/server/server.go @@ -41,7 +41,7 @@ func (s *Server) routes() { s.mux.Handle("GET /api/firewall", handlers.HandleFirewall()) s.mux.Handle("GET /api/get-ip", handlers.HandleGetIP(s.checks.IpAddress)) s.mux.Handle("GET /api/headers", handlers.HandleGetHeaders()) - s.mux.Handle("GET /api/hsts", handlers.HandleHsts()) + s.mux.Handle("GET /api/hsts", handlers.HandleHsts(s.checks.Hsts)) s.mux.Handle("GET /api/http-security", handlers.HandleHttpSecurity()) s.mux.Handle("GET /api/legacy-rank", handlers.HandleLegacyRank(s.checks.LegacyRank)) s.mux.Handle("GET /api/linked-pages", handlers.HandleGetLinks()) diff --git a/server/server_test.go b/server/server_test.go index 5fa34cb..c340ee8 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1,6 +1,7 @@ package server import ( + "context" "net/http" "net/http/httptest" "testing" @@ -8,7 +9,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/xray-web/web-check-api/config" - "golang.org/x/net/context" ) func TestServer(t *testing.T) { From d1c94547952e404a3099a099cf85139a4db99335 Mon Sep 17 00:00:00 2001 From: Samantha Date: Sat, 15 Jun 2024 22:43:14 +0100 Subject: [PATCH 2/3] resolve conflicts --- checks/checks.go | 2 + checks/hsts.go | 98 +++++++++++++++++++++++++++++++++ checks/hsts_test.go | 125 ++++++++++++++++++++++++++++++++++++++++++ handlers/hsts.go | 50 ++--------------- handlers/hsts_test.go | 20 +++---- server/server.go | 2 +- server/server_test.go | 2 +- 7 files changed, 239 insertions(+), 60 deletions(-) create mode 100644 checks/hsts.go create mode 100644 checks/hsts_test.go diff --git a/checks/checks.go b/checks/checks.go index ebc64d5..e33df4c 100644 --- a/checks/checks.go +++ b/checks/checks.go @@ -12,6 +12,7 @@ type Checks struct { BlockList *BlockList Carbon *Carbon Headers *Headers + Hsts *Hsts IpAddress *Ip LegacyRank *LegacyRank LinkedPages *LinkedPages @@ -28,6 +29,7 @@ func NewChecks() *Checks { BlockList: NewBlockList(&ip.NetDNSLookup{}), Carbon: NewCarbon(client), Headers: NewHeaders(client), + Hsts: NewHsts(client), IpAddress: NewIp(NewNetIp()), LegacyRank: NewLegacyRank(legacyrank.NewInMemoryStore()), LinkedPages: NewLinkedPages(client), diff --git a/checks/hsts.go b/checks/hsts.go new file mode 100644 index 0000000..da24593 --- /dev/null +++ b/checks/hsts.go @@ -0,0 +1,98 @@ +package checks + +import ( + "context" + "net/http" + "strconv" + "strings" + "unicode" +) + +const ( + StrictTransportSecurity = "Strict-Transport-Security" + includeSubDomains = "includeSubDomains" + preload = "preload" + NilHeadersError = "Site does not serve any HSTS headers." + MaxAgeError = "HSTS max-age is less than 10886400." + SubdomainsError = "HSTS header does not include all subdomains." + PreloadError = "HSTS header does not contain the preload directive." + HstsSuccess = "Site is compatible with the HSTS preload list!" +) + +type HSTSResponse struct { + Message string `json:"message"` + Compatible bool `json:"compatible"` + HSTSHeader string `json:"hstsHeader"` +} + +type Hsts struct { + client *http.Client +} + +func NewHsts(client *http.Client) *Hsts { + return &Hsts{client: client} +} + +func (h *Hsts) Validate(ctx context.Context, url string) (*HSTSResponse, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) + if err != nil { + return nil, err + } + + resp, err := h.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + hstsHeader := resp.Header.Get(StrictTransportSecurity) + if hstsHeader == "" { + return &HSTSResponse{ + Message: NilHeadersError, + }, nil + } + + maxAge := extractMaxAgeFromHeader(hstsHeader) + if maxAge == "" { + return &HSTSResponse{Message: MaxAgeError}, nil + } + + maxAgeInt, err := convertMaxAgeStringToInt(maxAge) + if err != nil { + return nil, err + } + + if maxAgeInt < 10886400 { + return &HSTSResponse{Message: MaxAgeError}, nil + } + + if !strings.Contains(hstsHeader, includeSubDomains) { + return &HSTSResponse{Message: SubdomainsError}, nil + } + + if !strings.Contains(hstsHeader, preload) { + return &HSTSResponse{Message: PreloadError}, nil + } + + return &HSTSResponse{ + Message: HstsSuccess, + Compatible: true, + HSTSHeader: hstsHeader, + }, nil +} + +func extractMaxAgeFromHeader(header string) string { + var maxAge strings.Builder + + for _, b := range header { + if unicode.IsDigit(b) { + maxAge.WriteRune(b) + } + } + + return maxAge.String() +} + +func convertMaxAgeStringToInt(maxAge string) (int, error) { + return strconv.Atoi(maxAge) +} diff --git a/checks/hsts_test.go b/checks/hsts_test.go new file mode 100644 index 0000000..b06687a --- /dev/null +++ b/checks/hsts_test.go @@ -0,0 +1,125 @@ +package checks + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/xray-web/web-check-api/testutils" +) + +func TestValidate(t *testing.T) { + t.Parallel() + + t.Run("given an empty header", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{""}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, NilHeadersError, actual.Message) + assert.False(t, actual.Compatible) + assert.Empty(t, actual.HSTSHeader) + }) + + t.Run("given a header without max age", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{"includeSubDomains; preload"}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, MaxAgeError, actual.Message) + assert.False(t, actual.Compatible) + assert.Empty(t, actual.HSTSHeader) + }) + + t.Run("given max age less than 10886400", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{"max-age=47; includeSubDomains; preload"}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, MaxAgeError, actual.Message) + assert.False(t, actual.Compatible) + assert.Empty(t, actual.HSTSHeader) + }) + + t.Run("given a header without includeSubDomains", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{"max-age=47474747; preload"}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, SubdomainsError, actual.Message) + assert.False(t, actual.Compatible) + assert.Empty(t, actual.HSTSHeader) + }) + + t.Run("given a header without preload", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{"max-age=47474747; includeSubDomains"}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, PreloadError, actual.Message) + assert.False(t, actual.Compatible) + assert.Empty(t, actual.HSTSHeader) + }) + + t.Run("given a valid header", func(t *testing.T) { + t.Parallel() + + client := testutils.MockClient(&http.Response{ + Header: http.Header{StrictTransportSecurity: []string{"max-age=47474747; includeSubDomains; preload"}}}) + h := NewHsts(client) + + actual, err := h.Validate(context.Background(), "test.com") + assert.NoError(t, err) + + assert.Equal(t, HstsSuccess, actual.Message) + assert.True(t, actual.Compatible) + assert.NotEmpty(t, actual.HSTSHeader) + }) +} + +func TestExtractMaxAgeFromHeader(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + header string + expected string + }{ + {"give valid header", "max-age=47474747; includeSubDomains; preload", "47474747"}, + {"given an empty header", "", ""}, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + actual := extractMaxAgeFromHeader(tc.header) + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/handlers/hsts.go b/handlers/hsts.go index 647cba7..83c3eb1 100644 --- a/handlers/hsts.go +++ b/handlers/hsts.go @@ -1,54 +1,12 @@ package handlers import ( - "fmt" "net/http" - "regexp" - "strings" -) - -type HSTSResponse struct { - Message string `json:"message"` - Compatible bool `json:"compatible"` - HSTSHeader string `json:"hstsHeader"` -} - -func checkHSTS(url string) (HSTSResponse, error) { - client := &http.Client{} - - req, err := http.NewRequest("HEAD", url, nil) - if err != nil { - return HSTSResponse{}, fmt.Errorf("error creating request: %s", err.Error()) - } - - resp, err := client.Do(req) - if err != nil { - return HSTSResponse{}, fmt.Errorf("error making request: %s", err.Error()) - } - defer resp.Body.Close() - hstsHeader := resp.Header.Get("strict-transport-security") - if hstsHeader == "" { - return HSTSResponse{Message: "Site does not serve any HSTS headers."}, nil - } - - maxAgeMatch := regexp.MustCompile(`max-age=(\d+)`).FindStringSubmatch(hstsHeader) - if maxAgeMatch == nil || len(maxAgeMatch) < 2 || maxAgeMatch[1] == "" || maxAgeMatch[1] < "10886400" { - return HSTSResponse{Message: "HSTS max-age is less than 10886400."}, nil - } - - if !strings.Contains(hstsHeader, "includeSubDomains") { - return HSTSResponse{Message: "HSTS header does not include all subdomains."}, nil - } - - if !strings.Contains(hstsHeader, "preload") { - return HSTSResponse{Message: "HSTS header does not contain the preload directive."}, nil - } - - return HSTSResponse{Message: "Site is compatible with the HSTS preload list!", Compatible: true, HSTSHeader: hstsHeader}, nil -} + "github.com/xray-web/web-check-api/checks" +) -func HandleHsts() http.Handler { +func HandleHsts(h *checks.Hsts) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rawURL, err := extractURL(r) if err != nil { @@ -56,7 +14,7 @@ func HandleHsts() http.Handler { return } - result, err := checkHSTS(rawURL.String()) + result, err := h.Validate(r.Context(), rawURL.String()) if err != nil { JSONError(w, err, http.StatusInternalServerError) return diff --git a/handlers/hsts_test.go b/handlers/hsts_test.go index 983ae22..c9ba27d 100644 --- a/handlers/hsts_test.go +++ b/handlers/hsts_test.go @@ -1,7 +1,6 @@ package handlers import ( - "encoding/json" "net/http" "net/http/httptest" "testing" @@ -11,18 +10,15 @@ import ( func TestHandleHsts(t *testing.T) { t.Parallel() - req := httptest.NewRequest("GET", "/check-hsts?url=example.com", nil) - rec := httptest.NewRecorder() - HandleHsts().ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) + t.Run("missing URL parameter", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(http.MethodGet, "/check-hsts", nil) + rec := httptest.NewRecorder() - var response HSTSResponse - err := json.Unmarshal(rec.Body.Bytes(), &response) - assert.NoError(t, err) + HandleHsts(nil).ServeHTTP(rec, req) - assert.NotNil(t, response) - assert.Equal(t, "Site does not serve any HSTS headers.", response.Message) - assert.False(t, response.Compatible) - assert.Empty(t, response.HSTSHeader) + assert.Equal(t, http.StatusBadRequest, rec.Code) + assert.JSONEq(t, `{"error": "missing URL parameter"}`, rec.Body.String()) + }) } diff --git a/server/server.go b/server/server.go index 5ee1493..9513a86 100644 --- a/server/server.go +++ b/server/server.go @@ -41,7 +41,7 @@ func (s *Server) routes() { s.mux.Handle("GET /api/firewall", handlers.HandleFirewall()) s.mux.Handle("GET /api/get-ip", handlers.HandleGetIP(s.checks.IpAddress)) s.mux.Handle("GET /api/headers", handlers.HandleGetHeaders(s.checks.Headers)) - s.mux.Handle("GET /api/hsts", handlers.HandleHsts()) + s.mux.Handle("GET /api/hsts", handlers.HandleHsts(s.checks.Hsts)) s.mux.Handle("GET /api/http-security", handlers.HandleHttpSecurity()) s.mux.Handle("GET /api/legacy-rank", handlers.HandleLegacyRank(s.checks.LegacyRank)) s.mux.Handle("GET /api/linked-pages", handlers.HandleGetLinks(s.checks.LinkedPages)) diff --git a/server/server_test.go b/server/server_test.go index 5fa34cb..c340ee8 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1,6 +1,7 @@ package server import ( + "context" "net/http" "net/http/httptest" "testing" @@ -8,7 +9,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/xray-web/web-check-api/config" - "golang.org/x/net/context" ) func TestServer(t *testing.T) { From 07b487c5cd7fafbad06d693782b2867f21150ffa Mon Sep 17 00:00:00 2001 From: Samantha Date: Sat, 22 Jun 2024 12:01:17 +0100 Subject: [PATCH 3/3] resolve comments --- checks/hsts.go | 49 ++++++++++++++++++--------------------------- checks/hsts_test.go | 26 ++++++++++++------------ 2 files changed, 32 insertions(+), 43 deletions(-) diff --git a/checks/hsts.go b/checks/hsts.go index da24593..19ace02 100644 --- a/checks/hsts.go +++ b/checks/hsts.go @@ -8,17 +8,6 @@ import ( "unicode" ) -const ( - StrictTransportSecurity = "Strict-Transport-Security" - includeSubDomains = "includeSubDomains" - preload = "preload" - NilHeadersError = "Site does not serve any HSTS headers." - MaxAgeError = "HSTS max-age is less than 10886400." - SubdomainsError = "HSTS header does not include all subdomains." - PreloadError = "HSTS header does not contain the preload directive." - HstsSuccess = "Site is compatible with the HSTS preload list!" -) - type HSTSResponse struct { Message string `json:"message"` Compatible bool `json:"compatible"` @@ -45,37 +34,41 @@ func (h *Hsts) Validate(ctx context.Context, url string) (*HSTSResponse, error) } defer resp.Body.Close() - hstsHeader := resp.Header.Get(StrictTransportSecurity) + hstsHeader := resp.Header.Get("Strict-Transport-Security") if hstsHeader == "" { - return &HSTSResponse{ - Message: NilHeadersError, - }, nil + return &HSTSResponse{Message: "Site does not serve any HSTS headers."}, nil } - maxAge := extractMaxAgeFromHeader(hstsHeader) - if maxAge == "" { - return &HSTSResponse{Message: MaxAgeError}, nil + if !strings.Contains(hstsHeader, "max-age") { + return &HSTSResponse{Message: "HSTS max-age is less than 10886400."}, nil } - maxAgeInt, err := convertMaxAgeStringToInt(maxAge) + var maxAgeString string + for _, h := range strings.Split(hstsHeader, " ") { + if strings.Contains(h, "max-age=") { + maxAgeString = extractMaxAgeFromHeader(h) + } + } + + maxAge, err := strconv.Atoi(maxAgeString) if err != nil { return nil, err } - if maxAgeInt < 10886400 { - return &HSTSResponse{Message: MaxAgeError}, nil + if maxAge < 10886400 { + return &HSTSResponse{Message: "HSTS max-age is less than 10886400."}, nil } - if !strings.Contains(hstsHeader, includeSubDomains) { - return &HSTSResponse{Message: SubdomainsError}, nil + if !strings.Contains(hstsHeader, "includeSubDomains") { + return &HSTSResponse{Message: "HSTS header does not include all subdomains."}, nil } - if !strings.Contains(hstsHeader, preload) { - return &HSTSResponse{Message: PreloadError}, nil + if !strings.Contains(hstsHeader, "preload") { + return &HSTSResponse{Message: "HSTS header does not contain the preload directive."}, nil } return &HSTSResponse{ - Message: HstsSuccess, + Message: "Site is compatible with the HSTS preload list!", Compatible: true, HSTSHeader: hstsHeader, }, nil @@ -92,7 +85,3 @@ func extractMaxAgeFromHeader(header string) string { return maxAge.String() } - -func convertMaxAgeStringToInt(maxAge string) (int, error) { - return strconv.Atoi(maxAge) -} diff --git a/checks/hsts_test.go b/checks/hsts_test.go index b06687a..e64eb59 100644 --- a/checks/hsts_test.go +++ b/checks/hsts_test.go @@ -16,13 +16,13 @@ func TestValidate(t *testing.T) { t.Parallel() client := testutils.MockClient(&http.Response{ - Header: http.Header{StrictTransportSecurity: []string{""}}}) + Header: http.Header{"Strict-Transport-Security": []string{""}}}) h := NewHsts(client) actual, err := h.Validate(context.Background(), "test.com") assert.NoError(t, err) - assert.Equal(t, NilHeadersError, actual.Message) + assert.Equal(t, "Site does not serve any HSTS headers.", actual.Message) assert.False(t, actual.Compatible) assert.Empty(t, actual.HSTSHeader) }) @@ -31,13 +31,13 @@ func TestValidate(t *testing.T) { t.Parallel() client := testutils.MockClient(&http.Response{ - Header: http.Header{StrictTransportSecurity: []string{"includeSubDomains; preload"}}}) + Header: http.Header{"Strict-Transport-Security": []string{"includeSubDomains; preload"}}}) h := NewHsts(client) actual, err := h.Validate(context.Background(), "test.com") assert.NoError(t, err) - assert.Equal(t, MaxAgeError, actual.Message) + assert.Equal(t, "HSTS max-age is less than 10886400.", actual.Message) assert.False(t, actual.Compatible) assert.Empty(t, actual.HSTSHeader) }) @@ -46,13 +46,13 @@ func TestValidate(t *testing.T) { t.Parallel() client := testutils.MockClient(&http.Response{ - Header: http.Header{StrictTransportSecurity: []string{"max-age=47; includeSubDomains; preload"}}}) + Header: http.Header{"Strict-Transport-Security": []string{"max-age=47; includeSubDomains; preload"}}}) h := NewHsts(client) actual, err := h.Validate(context.Background(), "test.com") assert.NoError(t, err) - assert.Equal(t, MaxAgeError, actual.Message) + assert.Equal(t, "HSTS max-age is less than 10886400.", actual.Message) assert.False(t, actual.Compatible) assert.Empty(t, actual.HSTSHeader) }) @@ -61,13 +61,13 @@ func TestValidate(t *testing.T) { t.Parallel() client := testutils.MockClient(&http.Response{ - Header: http.Header{StrictTransportSecurity: []string{"max-age=47474747; preload"}}}) + Header: http.Header{"Strict-Transport-Security": []string{"max-age=47474747; preload"}}}) h := NewHsts(client) actual, err := h.Validate(context.Background(), "test.com") assert.NoError(t, err) - assert.Equal(t, SubdomainsError, actual.Message) + assert.Equal(t, "HSTS header does not include all subdomains.", actual.Message) assert.False(t, actual.Compatible) assert.Empty(t, actual.HSTSHeader) }) @@ -76,13 +76,13 @@ func TestValidate(t *testing.T) { t.Parallel() client := testutils.MockClient(&http.Response{ - Header: http.Header{StrictTransportSecurity: []string{"max-age=47474747; includeSubDomains"}}}) + Header: http.Header{"Strict-Transport-Security": []string{"max-age=47474747; includeSubDomains"}}}) h := NewHsts(client) actual, err := h.Validate(context.Background(), "test.com") assert.NoError(t, err) - assert.Equal(t, PreloadError, actual.Message) + assert.Equal(t, "HSTS header does not contain the preload directive.", actual.Message) assert.False(t, actual.Compatible) assert.Empty(t, actual.HSTSHeader) }) @@ -91,13 +91,13 @@ func TestValidate(t *testing.T) { t.Parallel() client := testutils.MockClient(&http.Response{ - Header: http.Header{StrictTransportSecurity: []string{"max-age=47474747; includeSubDomains; preload"}}}) + Header: http.Header{"Strict-Transport-Security": []string{"max-age=47474747; includeSubDomains; preload"}}}) h := NewHsts(client) actual, err := h.Validate(context.Background(), "test.com") assert.NoError(t, err) - assert.Equal(t, HstsSuccess, actual.Message) + assert.Equal(t, "Site is compatible with the HSTS preload list!", actual.Message) assert.True(t, actual.Compatible) assert.NotEmpty(t, actual.HSTSHeader) }) @@ -111,7 +111,7 @@ func TestExtractMaxAgeFromHeader(t *testing.T) { header string expected string }{ - {"give valid header", "max-age=47474747; includeSubDomains; preload", "47474747"}, + {"give valid header", "max-age=47474747;", "47474747"}, {"given an empty header", "", ""}, } { tc := tc