internal/dl: add CORS support to JSON endpoint
This change allows users to request golang.org/dl/?mode=json
via Cross-Origin Resource Sharing (CORS).
It also removes the golangorg build tag as it did not seem
necessary and adds tests for the “include” GET parameter.
Updates golang/go#29206
Fixes golang/go#40253
Change-Id: I5306a264c4ac2a6e6f49cfb53db01eef6b7f4473
Reviewed-on: https://go-review.googlesource.com/c/website/+/243118
Run-TryBot: Andrew Bonventre <andybons@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Alexander Rakoczy <alex@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/internal/dl/server.go b/internal/dl/server.go
index 8b3bd7c..c837584 100644
--- a/internal/dl/server.go
+++ b/internal/dl/server.go
@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// +build golangorg
-
package dl
import (
@@ -46,7 +44,7 @@
var rootKey = datastore.NameKey("FileRoot", "root", nil)
func (h server) listHandler(w http.ResponseWriter, r *http.Request) {
- if r.Method != "GET" {
+ if r.Method != "GET" && r.Method != "OPTIONS" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
@@ -80,19 +78,7 @@
}
if r.URL.Query().Get("mode") == "json" {
- var releases []Release
- switch r.URL.Query().Get("include") {
- case "all":
- releases = append(append(d.Stable, d.Archive...), d.Unstable...)
- default:
- releases = d.Stable
- }
- w.Header().Set("Content-Type", "application/json")
- enc := json.NewEncoder(w)
- enc.SetIndent("", " ")
- if err := enc.Encode(releases); err != nil {
- log.Printf("ERROR rendering JSON for releases: %v", err)
- }
+ serveJSON(w, r, d)
return
}
@@ -101,6 +87,32 @@
}
}
+// serveJSON serves a JSON representation of d. It assumes that requests are
+// limited to GET and OPTIONS, the latter used for CORS requests, which this
+// endpoint supports.
+func serveJSON(w http.ResponseWriter, r *http.Request, d listTemplateData) {
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
+ if r.Method == "OPTIONS" {
+ // Likely a CORS preflight request.
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+ var releases []Release
+ switch r.URL.Query().Get("include") {
+ case "all":
+ releases = append(append(d.Stable, d.Archive...), d.Unstable...)
+ default:
+ releases = d.Stable
+ }
+ w.Header().Set("Content-Type", "application/json")
+ enc := json.NewEncoder(w)
+ enc.SetIndent("", " ")
+ if err := enc.Encode(releases); err != nil {
+ log.Printf("ERROR rendering JSON for releases: %v", err)
+ }
+}
+
// googleCN reports whether request r is considered
// to be served from golang.google.cn.
// TODO: This is duplicated within internal/proxy. Move to a common location.
diff --git a/internal/dl/server_test.go b/internal/dl/server_test.go
new file mode 100644
index 0000000..2cff8d7
--- /dev/null
+++ b/internal/dl/server_test.go
@@ -0,0 +1,92 @@
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package dl
+
+import (
+ "encoding/json"
+ "net/http/httptest"
+ "sort"
+ "testing"
+)
+
+func TestServeJSON(t *testing.T) {
+ data := listTemplateData{
+ Stable: []Release{{Version: "Stable"}},
+ Unstable: []Release{{Version: "Unstable"}},
+ Archive: []Release{{Version: "Archived"}},
+ }
+ testCases := []struct {
+ desc string
+ method string
+ target string
+ status int
+ versions []string
+ }{
+ {
+ desc: "basic",
+ method: "GET",
+ target: "/",
+ status: 200,
+ versions: []string{"Stable"},
+ },
+ {
+ desc: "include all versions",
+ method: "GET",
+ target: "/?include=all",
+ status: 200,
+ versions: []string{"Stable", "Unstable", "Archived"},
+ },
+ {
+ desc: "CORS preflight request",
+ method: "OPTIONS",
+ target: "/",
+ status: 204,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ r := httptest.NewRequest(tc.method, tc.target, nil)
+ w := httptest.NewRecorder()
+ serveJSON(w, r, data)
+
+ resp := w.Result()
+ defer resp.Body.Close()
+ if got, want := resp.StatusCode, tc.status; got != want {
+ t.Errorf("Response status code = %d; want %d", got, want)
+ }
+ for k, v := range map[string]string{
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
+ } {
+ if got, want := resp.Header.Get(k), v; got != want {
+ t.Errorf("%s = %q; want %q", k, got, want)
+ }
+ }
+ if tc.versions == nil {
+ return
+ }
+
+ if got, want := resp.Header.Get("Content-Type"), "application/json"; got != want {
+ t.Errorf("Content-Type = %q; want %q", got, want)
+ }
+ var rs []Release
+ if err := json.NewDecoder(resp.Body).Decode(&rs); err != nil {
+ t.Fatalf("json.Decode: got unexpected error: %v", err)
+ }
+ sort.Slice(rs, func(i, j int) bool {
+ return rs[i].Version < rs[j].Version
+ })
+ sort.Strings(tc.versions)
+ if got, want := len(rs), len(tc.versions); got != want {
+ t.Fatalf("Number of releases = %d; want %d", got, want)
+ }
+ for i := range rs {
+ if got, want := rs[i].Version, tc.versions[i]; got != want {
+ t.Errorf("Got version %q; want %q", got, want)
+ }
+ }
+ })
+ }
+}