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)
}