From 51b22ef31af8bf2cb877aab9d56031bae8313a18 Mon Sep 17 00:00:00 2001 From: Andrew Bonventre Date: Thu, 16 Jul 2020 13:05:25 -0400 Subject: [PATCH] internal/dl: add CORS support to JSON endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 TryBot-Result: Gobot Gobot Reviewed-by: Alexander Rakoczy Reviewed-by: Dmitri Shuralyov --- internal/dl/server.go | 44 +++++++++++------- internal/dl/server_test.go | 92 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 internal/dl/server_test.go diff --git a/internal/dl/server.go b/internal/dl/server.go index 8b3bd7c7b3..c837584b78 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 @@ func RegisterHandlers(mux *http.ServeMux, dc *datastore.Client, mc *memcache.Cli 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 @@ func (h server) listHandler(w http.ResponseWriter, r *http.Request) { } 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 @@ func (h server) listHandler(w http.ResponseWriter, r *http.Request) { } } +// 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 0000000000..2cff8d7991 --- /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) + } + } + }) + } +}