playground: enable CORS on /fmt and /p/ routes

Previously, only the /vet, /compile, and /share routes supported
Cross-Origin Resource Sharing (CORS) from any domain. This change
enables CORS on the /p/ and /fmt endpoints as well.

That makes it possible for third-party sites to load and format
playground snippets from the frontend.

Fixes golang/go#35019

Change-Id: I185f518257082cbb5ccd848b410ff408bc077340
Reviewed-on: https://go-review.googlesource.com/c/playground/+/193798
Reviewed-by: Alexander Rakoczy <alex@golang.org>
diff --git a/edit.go b/edit.go
index 81923e4..c8cb974 100644
--- a/edit.go
+++ b/edit.go
@@ -26,6 +26,12 @@
 }
 
 func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	if r.Method == "OPTIONS" {
+		// This is likely a pre-flight CORS request.
+		return
+	}
+
 	// Redirect foo.play.golang.org to play.golang.org.
 	if strings.HasSuffix(r.Host, "."+hostname) {
 		http.Redirect(w, r, "https://"+hostname, http.StatusFound)
diff --git a/fmt.go b/fmt.go
index 88c0746..91027be 100644
--- a/fmt.go
+++ b/fmt.go
@@ -21,6 +21,11 @@
 }
 
 func handleFmt(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	if r.Method == "OPTIONS" {
+		// This is likely a pre-flight CORS request.
+		return
+	}
 	w.Header().Set("Content-Type", "application/json")
 
 	fs, err := splitFiles([]byte(r.FormValue("body")))
diff --git a/fmt_test.go b/fmt_test.go
index e601dc3..ceea152 100644
--- a/fmt_test.go
+++ b/fmt_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"encoding/json"
+	"net/http"
 	"net/http/httptest"
 	"net/url"
 	"strings"
@@ -15,49 +16,62 @@
 func TestHandleFmt(t *testing.T) {
 	for _, tt := range []struct {
 		name    string
+		method  string
 		body    string
 		imports bool
 		want    string
 		wantErr string
 	}{
 		{
-			name: "classic",
-			body: " package main\n    func main( ) {  }\n",
-			want: "package main\n\nfunc main() {}\n",
+			name:   "OPTIONS no-op",
+			method: http.MethodOptions,
+		},
+		{
+			name:   "classic",
+			method: http.MethodPost,
+			body:   " package main\n    func main( ) {  }\n",
+			want:   "package main\n\nfunc main() {}\n",
 		},
 		{
 			name:    "classic_goimports",
+			method:  http.MethodPost,
 			body:    " package main\nvar _ = fmt.Printf",
 			imports: true,
 			want:    "package main\n\nimport \"fmt\"\n\nvar _ = fmt.Printf\n",
 		},
 		{
-			name: "single_go_with_header",
-			body: "-- prog.go --\n  package main",
-			want: "-- prog.go --\npackage main\n",
+			name:   "single_go_with_header",
+			method: http.MethodPost,
+			body:   "-- prog.go --\n  package main",
+			want:   "-- prog.go --\npackage main\n",
 		},
 		{
-			name: "multi_go_with_header",
-			body: "-- prog.go --\n  package main\n\n\n-- two.go --\n   package main\n  var X = 5",
-			want: "-- prog.go --\npackage main\n-- two.go --\npackage main\n\nvar X = 5\n",
+			name:   "multi_go_with_header",
+			method: http.MethodPost,
+			body:   "-- prog.go --\n  package main\n\n\n-- two.go --\n   package main\n  var X = 5",
+			want:   "-- prog.go --\npackage main\n-- two.go --\npackage main\n\nvar X = 5\n",
 		},
 		{
-			name: "multi_go_without_header",
-			body: "    package main\n\n\n-- two.go --\n   package main\n  var X = 5",
-			want: "package main\n-- two.go --\npackage main\n\nvar X = 5\n",
+			name:   "multi_go_without_header",
+			method: http.MethodPost,
+			body:   "    package main\n\n\n-- two.go --\n   package main\n  var X = 5",
+			want:   "package main\n-- two.go --\npackage main\n\nvar X = 5\n",
 		},
 		{
-			name: "single_go.mod_with_header",
-			body: "-- go.mod --\n   module   \"foo\"   ",
-			want: "-- go.mod --\nmodule foo\n",
+			name:   "single_go.mod_with_header",
+			method: http.MethodPost,
+			body:   "-- go.mod --\n   module   \"foo\"   ",
+			want:   "-- go.mod --\nmodule foo\n",
 		},
 		{
-			name: "multi_go.mod_with_header",
-			body: "-- a/go.mod --\n  module foo\n\n\n-- b/go.mod --\n   module  \"bar\"",
-			want: "-- a/go.mod --\nmodule foo\n-- b/go.mod --\nmodule bar\n",
+			name:   "multi_go.mod_with_header",
+			method: http.MethodPost,
+			body:   "-- a/go.mod --\n  module foo\n\n\n-- b/go.mod --\n   module  \"bar\"",
+			want:   "-- a/go.mod --\nmodule foo\n-- b/go.mod --\nmodule bar\n",
 		},
 		{
-			name: "only_format_go_and_go.mod",
+			name:   "only_format_go_and_go.mod",
+			method: http.MethodPost,
 			body: "    package   main   \n\n\n" +
 				"-- go.mod --\n   module   foo   \n\n\n" +
 				"-- plain.txt --\n   plain   text   \n\n\n",
@@ -65,33 +79,39 @@
 		},
 		{
 			name:    "error_gofmt",
+			method:  http.MethodPost,
 			body:    "package 123\n",
 			wantErr: "prog.go:1:9: expected 'IDENT', found 123",
 		},
 		{
 			name:    "error_gofmt_with_header",
+			method:  http.MethodPost,
 			body:    "-- dir/one.go --\npackage 123\n",
 			wantErr: "dir/one.go:1:9: expected 'IDENT', found 123",
 		},
 		{
 			name:    "error_goimports",
+			method:  http.MethodPost,
 			body:    "package 123\n",
 			imports: true,
 			wantErr: "prog.go:1:9: expected 'IDENT', found 123",
 		},
 		{
 			name:    "error_goimports_with_header",
+			method:  http.MethodPost,
 			body:    "-- dir/one.go --\npackage 123\n",
 			imports: true,
 			wantErr: "dir/one.go:1:9: expected 'IDENT', found 123",
 		},
 		{
 			name:    "error_go.mod",
+			method:  http.MethodPost,
 			body:    "-- go.mod --\n123\n",
 			wantErr: "go.mod:1: unknown directive: 123",
 		},
 		{
 			name:    "error_go.mod_with_header",
+			method:  http.MethodPost,
 			body:    "-- dir/go.mod --\n123\n",
 			wantErr: "dir/go.mod:1: unknown directive: 123",
 		},
@@ -110,6 +130,10 @@
 			if resp.StatusCode != 200 {
 				t.Fatalf("code = %v", resp.Status)
 			}
+			corsHeader := "Access-Control-Allow-Origin"
+			if got, want := resp.Header.Get(corsHeader), "*"; got != want {
+				t.Errorf("Header %q: got %q; want %q", corsHeader, got, want)
+			}
 			if ct := resp.Header.Get("Content-Type"); ct != "application/json" {
 				t.Fatalf("Content-Type = %q; want application/json", ct)
 			}
diff --git a/server_test.go b/server_test.go
index 40ba065..a7d9fac 100644
--- a/server_test.go
+++ b/server_test.go
@@ -51,24 +51,30 @@
 
 	testCases := []struct {
 		desc       string
+		method     string
 		url        string
 		statusCode int
 		headers    map[string]string
 		respBody   []byte
 	}{
-		{"foo.play.golang.org to play.golang.org", "https://foo.play.golang.org", http.StatusFound, map[string]string{"Location": "https://play.golang.org"}, nil},
-		{"Non-existent page", "https://play.golang.org/foo", http.StatusNotFound, nil, nil},
-		{"Unknown snippet", "https://play.golang.org/p/foo", http.StatusNotFound, nil, nil},
-		{"Existing snippet", "https://play.golang.org/p/" + id, http.StatusOK, nil, nil},
-		{"Plaintext snippet", "https://play.golang.org/p/" + id + ".go", http.StatusOK, nil, barBody},
-		{"Download snippet", "https://play.golang.org/p/" + id + ".go?download=true", http.StatusOK, map[string]string{"Content-Disposition": fmt.Sprintf(`attachment; filename="%s.go"`, id)}, barBody},
+		{"OPTIONS no-op", http.MethodOptions, "https://play.golang.org/p/foo", http.StatusOK, nil, nil},
+		{"foo.play.golang.org to play.golang.org", http.MethodGet, "https://foo.play.golang.org", http.StatusFound, map[string]string{"Location": "https://play.golang.org"}, nil},
+		{"Non-existent page", http.MethodGet, "https://play.golang.org/foo", http.StatusNotFound, nil, nil},
+		{"Unknown snippet", http.MethodGet, "https://play.golang.org/p/foo", http.StatusNotFound, nil, nil},
+		{"Existing snippet", http.MethodGet, "https://play.golang.org/p/" + id, http.StatusOK, nil, nil},
+		{"Plaintext snippet", http.MethodGet, "https://play.golang.org/p/" + id + ".go", http.StatusOK, nil, barBody},
+		{"Download snippet", http.MethodGet, "https://play.golang.org/p/" + id + ".go?download=true", http.StatusOK, map[string]string{"Content-Disposition": fmt.Sprintf(`attachment; filename="%s.go"`, id)}, barBody},
 	}
 
 	for _, tc := range testCases {
-		req := httptest.NewRequest(http.MethodGet, tc.url, nil)
+		req := httptest.NewRequest(tc.method, tc.url, nil)
 		w := httptest.NewRecorder()
 		s.handleEdit(w, req)
 		resp := w.Result()
+		corsHeader := "Access-Control-Allow-Origin"
+		if got, want := resp.Header.Get(corsHeader), "*"; got != want {
+			t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want)
+		}
 		if got, want := resp.StatusCode, tc.statusCode; got != want {
 			t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want)
 		}
@@ -115,6 +121,10 @@
 		w := httptest.NewRecorder()
 		s.handleShare(w, req)
 		resp := w.Result()
+		corsHeader := "Access-Control-Allow-Origin"
+		if got, want := resp.Header.Get(corsHeader), "*"; got != want {
+			t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want)
+		}
 		if got, want := resp.StatusCode, tc.statusCode; got != want {
 			t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want)
 		}