Skip to content

Commit

Permalink
Allow CORS access to /preloadable and /status. Closes #48.
Browse files Browse the repository at this point in the history
At the moment, requests are only allowed from localhost or whitelisted origins.
This prevents us from being locked into an API before we're ready to commit to
being an internet-wide service.
  • Loading branch information
lgarron committed Jun 23, 2016
1 parent 4bd3657 commit 421f373
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 9 deletions.
99 changes: 90 additions & 9 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ func TestAPI(t *testing.T) {
api, mc, h, c := mockAPI()

pr1 := map[string]hstspreload.Issues{
"garron.net": emptyIssues,
"badssl.com": issuesWithWarnings,
"example.com": issuesWithErrors,
"garron.net": emptyIssues,
"badssl.com": issuesWithWarnings,
"mozilla.github.io": issuesWithErrors,
}
rr1 := map[string]hstspreload.Issues{
"removable.test": emptyIssues,
Expand Down Expand Up @@ -132,7 +132,7 @@ func TestAPI(t *testing.T) {
200, wantBody{issues: &emptyIssues}},
{"preloadable warning", data1, failNone, api.Preloadable, "GET", "?domain=badssl.com",
200, wantBody{issues: &issuesWithWarnings}},
{"preloadable error", data1, failNone, api.Preloadable, "GET", "?domain=example.com",
{"preloadable error", data1, failNone, api.Preloadable, "GET", "?domain=mozilla.github.io",
200, wantBody{issues: &issuesWithErrors}},

// removable
Expand All @@ -145,9 +145,9 @@ func TestAPI(t *testing.T) {
{"garron.net initial", data1, failNone, api.Status, "GET", "?domain=garron.net",
200, wantBody{state: &database.DomainState{
Name: "garron.net", Status: database.StatusUnknown}}},
{"example.com initial", data1, failNone, api.Status, "GET", "?domain=example.com",
{"mozilla.github.io initial", data1, failNone, api.Status, "GET", "?domain=mozilla.github.io",
200, wantBody{state: &database.DomainState{
Name: "example.com", Status: database.StatusUnknown}}},
Name: "mozilla.github.io", Status: database.StatusUnknown}}},
{"pending 1", data1, failNone, api.Pending, "GET", "",
200, wantBody{text: "[\n]\n"}},

Expand All @@ -158,7 +158,7 @@ func TestAPI(t *testing.T) {
500, wantBody{text: "Internal error: could not retrieve status. (forced failure)\n\n"}},

// submit
{"bad submit", data1, failNone, api.Submit, "POST", "?domain=example.com",
{"bad submit", data1, failNone, api.Submit, "POST", "?domain=mozilla.github.io",
200, wantBody{issues: &issuesWithErrors}},
{"submit database failure", data1, failDatabase, api.Submit, "POST", "?domain=garron.net",
500, wantBody{text: "Internal error: could not get current domain status. (forced failure)\n\n"}},
Expand Down Expand Up @@ -191,9 +191,9 @@ func TestAPI(t *testing.T) {
200, wantBody{issues: &hstspreload.Issues{
Errors: []hstspreload.Issue{{Code: "server.preload.already_preloaded"}},
}}},
{"example.com after update", data1, failNone, api.Status, "GET", "?domain=example.com",
{"mozilla.github.io after update", data1, failNone, api.Status, "GET", "?domain=mozilla.github.io",
200, wantBody{state: &database.DomainState{
Name: "example.com", Status: database.StatusUnknown}}},
Name: "mozilla.github.io", Status: database.StatusUnknown}}},
{"garron.net after update", data1, failNone, api.Status, "GET", "?domain=garron.net",
200, wantBody{state: &database.DomainState{
Name: "garron.net", Status: database.StatusPreloaded}}},
Expand Down Expand Up @@ -265,3 +265,84 @@ func TestAPI(t *testing.T) {
}
}
}

func TestCORS(t *testing.T) {
api, _, _, _ := mockAPI()

cases := []struct {
handlerName string
handlerFunc http.HandlerFunc
method string
clientOrigin string
wantCORS string
}{
// Handlers that should allow CORS.
{"Preloadable", api.Preloadable, http.MethodGet, "http://localhost", "*"},
{"Preloadable", api.Preloadable, http.MethodGet, "http://localhost:8080", "*"},
{"Preloadable", api.Preloadable, http.MethodGet, "http://example.com", "null"},
{"Preloadable", api.Preloadable, http.MethodGet, "https://example.com", "null"},
{"Preloadable", api.Preloadable, http.MethodGet, "http://mozilla.github.io", "null"},
{"Preloadable", api.Preloadable, http.MethodGet, "http://mozilla.example.com", "null"},
{"Preloadable", api.Preloadable, http.MethodGet, "https://mozilla.example.com", "null"},
{"Preloadable", api.Preloadable, http.MethodGet, "http://mozilla.github.io:443", "null"},
{"Preloadable", api.Preloadable, http.MethodGet, "https://mozilla.github.io", "*"},
{"Preloadable", api.Preloadable, http.MethodGet, "https://mozilla.github.io:443", "*"},
{"Preloadable", api.Preloadable, http.MethodGet, "https://mozilla.github.io:80", "*"},
{"Preloadable", api.Preloadable, http.MethodGet, "https://mozilla.github.io:443", "*"},
{"Preloadable", api.Preloadable, http.MethodOptions, "http://localhost", "*"},
{"Preloadable", api.Preloadable, http.MethodOptions, "http://example.com", "null"},
{"Preloadable", api.Preloadable, http.MethodOptions, "https://example.com", "null"},
{"Preloadable", api.Preloadable, http.MethodOptions, "http://mozilla.github.io", "null"},
{"Preloadable", api.Preloadable, http.MethodOptions, "https://mozilla.github.io", "*"},
{"Preloadable", api.Preloadable, http.MethodPost, "https://mozilla.github.io", "*"},
{"Status", api.Status, http.MethodGet, "http://localhost:8080", "*"},
{"Status", api.Status, http.MethodGet, "http://example.com", "null"},
{"Status", api.Status, http.MethodGet, "https://example.com", "null"},
{"Status", api.Status, http.MethodGet, "http://mozilla.github.io", "null"},
{"Status", api.Status, http.MethodGet, "https://mozilla.github.io", "*"},
{"Status", api.Status, http.MethodOptions, "http://localhost:8080", "*"},
{"Status", api.Status, http.MethodOptions, "http://example.com", "null"},
{"Status", api.Status, http.MethodOptions, "https://example.com", "null"},
{"Status", api.Status, http.MethodOptions, "http://mozilla.github.io", "null"},
{"Status", api.Status, http.MethodOptions, "https://mozilla.github.io", "*"},
// Handlers that should not allow CORS.
{"Removable", api.Removable, http.MethodGet, "http://localhost:8080", ""},
{"Removable", api.Removable, http.MethodGet, "http://example.com", ""},
{"Removable", api.Removable, http.MethodGet, "https://example.com", ""},
{"Removable", api.Removable, http.MethodGet, "http://mozilla.github.io", ""},
{"Removable", api.Removable, http.MethodGet, "https://mozilla.github.io", ""},
{"Removable", api.Removable, http.MethodOptions, "https://mozilla.github.io", ""},
{"Submit", api.Submit, http.MethodGet, "https://mozilla.github.io", ""},
{"Submit", api.Submit, http.MethodOptions, "https://mozilla.github.io", ""},
{"Pending", api.Pending, http.MethodGet, "https://mozilla.github.io", ""},
{"Pending", api.Pending, http.MethodOptions, "https://mozilla.github.io", ""},
{"Update", api.Update, http.MethodGet, "https://mozilla.github.io", ""},
{"Update", api.Update, http.MethodOptions, "https://mozilla.github.io", ""},
}

for _, tt := range cases {
r, err := http.NewRequest(tt.method, "", nil)
if err != nil {
t.Fatalf("%s", err)
}
r.Header.Set("Origin", tt.clientOrigin)

w := httptest.NewRecorder()
w.Body = &bytes.Buffer{}

tt.handlerFunc(w, r)

key := http.CanonicalHeaderKey(corsOriginHeader)
actual := w.Header().Get(key)
if tt.wantCORS != actual {
t.Errorf(
"[%s][%s][%s] CORS header `%s` does not match expected value `%s`.",
tt.handlerName,
tt.method,
tt.clientOrigin,
actual,
tt.wantCORS,
)
}
}
}
57 changes: 57 additions & 0 deletions api/cors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package api

import (
"net/http"

"github.com/chromium/hstspreload.appspot.com/origin"
)

const (
corsOriginHeader = "Access-Control-Allow-Origin"
)

// If you have a project that could use client-side API access
// to hstspreload.appspot.com, feel free to send a pull request
// to add your domain on GitHub:
// https://github.com/chromium/hstspreload.appspot.com/edit/master/api/cors.go
var whitelistedHosts = map[string]bool{
"mozilla.github.io": true,
"infinitude.me.uk": true,
}

func allowOrigin(clientOrigin string) bool {
o, err := origin.Parse(clientOrigin)
if err != nil {
return false
}

switch {
case o.HostName == "localhost":
return true
case o.Scheme == "https" && whitelistedHosts[o.HostName]:
return true
default:
return false
}
}

func (api API) allowCORS(w http.ResponseWriter, r *http.Request) (cont bool) {
isOptions := (r.Method == http.MethodOptions)

key := http.CanonicalHeaderKey("Origin")
clientOrigin := r.Header.Get(key)
if clientOrigin == "" && !isOptions {
return true
}

if allowOrigin(clientOrigin) {
w.Header().Set(corsOriginHeader, "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Max-Age", "86400")
w.Header().Set("Vary", "Origin")
} else {
w.Header().Set(corsOriginHeader, "null")
}

return !isOptions
}
8 changes: 8 additions & 0 deletions api/domain_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func getASCIIDomain(wantMethod string, w http.ResponseWriter, r *http.Request) (
//
// Example: GET /preloadable?domain=garron.net
func (api API) Preloadable(w http.ResponseWriter, r *http.Request) {
if cont := api.allowCORS(w, r); !cont {
return
}

domain, ok := getASCIIDomain(http.MethodGet, w, r)
if !ok {
return
Expand All @@ -63,6 +67,10 @@ func (api API) Removable(w http.ResponseWriter, r *http.Request) {
//
// Example: GET /status?domain=garron.net
func (api API) Status(w http.ResponseWriter, r *http.Request) {
if cont := api.allowCORS(w, r); !cont {
return
}

domain, ok := getASCIIDomain(http.MethodGet, w, r)
if !ok {
return
Expand Down

0 comments on commit 421f373

Please sign in to comment.