cmd/coordinator: allow CORS requests to /try.json

Allow any origin to make cross-origin requests to get builder
statuses.

Also fixes a bug with an ineffectual Header().Set(...) call since
WriteHeader was called before it.

Change-Id: Ic2867624a286dad7268ddb94c3bb5afb52bf84ac
Reviewed-on: https://go-review.googlesource.com/98995
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index cdd147a..a2e48e6 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -553,7 +553,7 @@
 			ts.mu.Unlock()
 		}
 		if json {
-			serveTryStatusJSON(w, ts, tss)
+			serveTryStatusJSON(w, r, ts, tss)
 			return
 		}
 		serveTryStatusHTML(w, ts, tss)
@@ -561,7 +561,12 @@
 }
 
 // tss is a clone that does not require ts' lock.
-func serveTryStatusJSON(w http.ResponseWriter, ts *trySet, tss trySetState) {
+func serveTryStatusJSON(w http.ResponseWriter, r *http.Request, ts *trySet, tss trySetState) {
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	if r.Method == "OPTIONS" {
+		// This is likely a pre-flight CORS request.
+		return
+	}
 	var resp struct {
 		Success bool        `json:"success"`
 		Error   string      `json:"error,omitempty"`
@@ -574,9 +579,9 @@
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		w.WriteHeader(http.StatusNotFound)
 		w.Header().Set("Content-Type", "application/json")
-		io.Copy(w, &buf)
+		w.WriteHeader(http.StatusNotFound)
+		w.Write(buf.Bytes())
 		return
 	}
 	type litebuild struct {
diff --git a/cmd/coordinator/coordinator_test.go b/cmd/coordinator/coordinator_test.go
index 5033ad9..d920ad4 100644
--- a/cmd/coordinator/coordinator_test.go
+++ b/cmd/coordinator/coordinator_test.go
@@ -29,19 +29,30 @@
 func TestTryStatusJSON(t *testing.T) {
 	testCases := []struct {
 		desc   string
+		method string
 		ts     *trySet
 		tss    trySetState
 		status int
 		body   string
 	}{
 		{
+			"pre-flight CORS header",
+			"OPTIONS",
+			nil,
+			trySetState{},
+			http.StatusOK,
+			``,
+		},
+		{
 			"nil trySet",
+			"GET",
 			nil,
 			trySetState{},
 			http.StatusNotFound,
 			`{"success":false,"error":"TryBot result not found (already done, invalid, or not yet discovered from Gerrit). Check Gerrit for results."}` + "\n",
 		},
 		{"non-nil trySet",
+			"GET",
 			&trySet{
 				tryKey: tryKey{
 					Commit:   "deadbeef",
@@ -69,8 +80,16 @@
 	for _, tc := range testCases {
 		t.Run(tc.desc, func(t *testing.T) {
 			w := httptest.NewRecorder()
-			serveTryStatusJSON(w, tc.ts, tc.tss)
+			r, err := http.NewRequest(tc.method, "", nil)
+			if err != nil {
+				t.Fatalf("could not create http.Request: %v", err)
+			}
+			serveTryStatusJSON(w, r, tc.ts, tc.tss)
 			resp := w.Result()
+			hdr := "Access-Control-Allow-Origin"
+			if got, want := resp.Header.Get(hdr), "*"; got != want {
+				t.Errorf("unexpected %q header: got %q; want %q", hdr, got, want)
+			}
 			if got, want := resp.StatusCode, tc.status; got != want {
 				t.Errorf("response status code: got %d; want %d", got, want)
 			}