Skip to content

Commit

Permalink
internal/dl: add CORS support to JSON endpoint
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
andybons committed Jul 16, 2020
1 parent a8d8c12 commit 51b22ef
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 16 deletions.
44 changes: 28 additions & 16 deletions internal/dl/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

Expand All @@ -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.
Expand Down
92 changes: 92 additions & 0 deletions internal/dl/server_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}

0 comments on commit 51b22ef

Please sign in to comment.