Implement a MountableRouter

The httprouter used in Chronograf did not support prefixing every route
with some basepath. This caused problems for those using the --basepath
parameter in combination with a load balancer that did not strip the
basepath prefix from requests that it forwarded onto Chronograf.

To support this, MountableRouter prefixes all routes at definition time
with the supplied prefix.
pull/10616/head
Tim Raymond 2017-03-31 11:20:44 -04:00
parent 92ad16e210
commit e1d2949b18
3 changed files with 311 additions and 0 deletions

View File

@ -42,6 +42,19 @@ type Logger interface {
Writer() *io.PipeWriter
}
// Router is an abstracted Router based on the API provided by the
// julienschmidt/httprouter package.
type Router interface {
http.Handler
GET(string, http.HandlerFunc)
PATCH(string, http.HandlerFunc)
POST(string, http.HandlerFunc)
DELETE(string, http.HandlerFunc)
PUT(string, http.HandlerFunc)
Handler(string, string, http.Handler)
}
// Assets returns a handler to serve the website.
type Assets interface {
Handler() http.Handler

View File

@ -0,0 +1,58 @@
package server
import (
"net/http"
"github.com/influxdata/chronograf"
)
var _ chronograf.Router = &MountableRouter{}
// MountableRouter is an implementation of a chronograf.Router which supports
// prefixing each route of a Delegated chronograf.Router with a prefix.
type MountableRouter struct {
Prefix string
Delegate chronograf.Router
}
// DELETE defines a route responding to a DELETE request that will be prefixed
// with the configured route prefix
func (mr *MountableRouter) DELETE(path string, handler http.HandlerFunc) {
mr.Delegate.DELETE(mr.Prefix+path, handler)
}
// GET defines a route responding to a GET request that will be prefixed
// with the configured route prefix
func (mr *MountableRouter) GET(path string, handler http.HandlerFunc) {
mr.Delegate.GET(mr.Prefix+path, handler)
}
// POST defines a route responding to a POST request that will be prefixed
// with the configured route prefix
func (mr *MountableRouter) POST(path string, handler http.HandlerFunc) {
mr.Delegate.POST(mr.Prefix+path, handler)
}
// PUT defines a route responding to a PUT request that will be prefixed
// with the configured route prefix
func (mr *MountableRouter) PUT(path string, handler http.HandlerFunc) {
mr.Delegate.PUT(mr.Prefix+path, handler)
}
// PATCH defines a route responding to a PATCH request that will be prefixed
// with the configured route prefix
func (mr *MountableRouter) PATCH(path string, handler http.HandlerFunc) {
mr.Delegate.PATCH(mr.Prefix+path, handler)
}
// Handler defines a prefixed route responding to a request type specified in
// the method parameter
func (mr *MountableRouter) Handler(method string, path string, handler http.Handler) {
mr.Delegate.Handler(method, mr.Prefix+path, handler)
}
// ServeHTTP is an implementation of http.Handler which delegates to the
// configured Delegate's implementation of http.Handler
func (mr *MountableRouter) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
mr.Delegate.ServeHTTP(rw, r)
}

View File

@ -0,0 +1,240 @@
package server_test
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf/server"
)
func Test_MountableRouter_MountsRoutesUnderPrefix(t *testing.T) {
t.Parallel()
mr := &server.MountableRouter{
Prefix: "/chronograf",
Delegate: httprouter.New(),
}
expected := "Hello?! McFly?! Anybody in there?!"
mr.GET("/biff", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
fmt.Fprintf(rw, expected)
}))
ts := httptest.NewServer(mr)
defer ts.Close()
resp, err := http.Get(ts.URL + "/chronograf/biff")
if err != nil {
t.Fatal("Unexpected error fetching from mounted router: err:", err)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal("Unexpected error decoding response body: err:", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatal("Expected 200 but received", resp.StatusCode)
}
if string(body) != expected {
t.Fatalf("Unexpected response body: Want: \"%s\". Got: \"%s\"", expected, string(body))
}
}
func Test_MountableRouter_PrefixesPosts(t *testing.T) {
t.Parallel()
mr := &server.MountableRouter{
Prefix: "/chronograf",
Delegate: httprouter.New(),
}
expected := "Great Scott!"
actual := make([]byte, len(expected))
mr.POST("/doc", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if _, err := io.ReadFull(r.Body, actual); err != nil {
rw.WriteHeader(http.StatusInternalServerError)
} else {
rw.WriteHeader(http.StatusOK)
}
}))
ts := httptest.NewServer(mr)
defer ts.Close()
resp, err := http.Post(ts.URL+"/chronograf/doc", "text/plain", strings.NewReader(expected))
if err != nil {
t.Fatal("Unexpected error posting to mounted router: err:", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatal("Expected 200 but received", resp.StatusCode)
}
if string(actual) != expected {
t.Fatalf("Unexpected request body: Want: \"%s\". Got: \"%s\"", expected, string(actual))
}
}
func Test_MountableRouter_PrefixesPuts(t *testing.T) {
t.Parallel()
mr := &server.MountableRouter{
Prefix: "/chronograf",
Delegate: httprouter.New(),
}
expected := "Great Scott!"
actual := make([]byte, len(expected))
mr.PUT("/doc", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if _, err := io.ReadFull(r.Body, actual); err != nil {
rw.WriteHeader(http.StatusInternalServerError)
} else {
rw.WriteHeader(http.StatusOK)
}
}))
ts := httptest.NewServer(mr)
defer ts.Close()
req := httptest.NewRequest(http.MethodPut, ts.URL+"/chronograf/doc", strings.NewReader(expected))
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
req.Header.Set("Content-Length", fmt.Sprintf("%d", len(expected)))
req.RequestURI = ""
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatal("Unexpected error posting to mounted router: err:", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatal("Expected 200 but received", resp.StatusCode)
}
if string(actual) != expected {
t.Fatalf("Unexpected request body: Want: \"%s\". Got: \"%s\"", expected, string(actual))
}
}
func Test_MountableRouter_PrefixesDeletes(t *testing.T) {
t.Parallel()
mr := &server.MountableRouter{
Prefix: "/chronograf",
Delegate: httprouter.New(),
}
mr.DELETE("/proto1985", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNoContent)
}))
ts := httptest.NewServer(mr)
defer ts.Close()
req := httptest.NewRequest(http.MethodDelete, ts.URL+"/chronograf/proto1985", nil)
req.RequestURI = ""
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatal("Unexpected error sending request to mounted router: err:", err)
}
if resp.StatusCode != http.StatusNoContent {
t.Fatal("Expected 204 but received", resp.StatusCode)
}
}
func Test_MountableRouter_PrefixesPatches(t *testing.T) {
t.Parallel()
type Character struct {
Name string
Items []string
}
mr := &server.MountableRouter{
Prefix: "/chronograf",
Delegate: httprouter.New(),
}
biff := Character{"biff", []string{"sports almanac"}}
mr.PATCH("/1955", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
c := Character{}
err := json.NewDecoder(r.Body).Decode(&c)
if err != nil {
rw.WriteHeader(http.StatusBadRequest)
} else {
biff.Items = c.Items
rw.WriteHeader(http.StatusOK)
}
}))
ts := httptest.NewServer(mr)
defer ts.Close()
r, w := io.Pipe()
go func() {
_ = json.NewEncoder(w).Encode(Character{"biff", []string{}})
w.Close()
}()
req := httptest.NewRequest(http.MethodPatch, ts.URL+"/chronograf/1955", r)
req.RequestURI = ""
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatal("Unexpected error sending request to mounted router: err:", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatal("Expected 200 but received", resp.StatusCode)
}
if len(biff.Items) != 0 {
t.Fatal("Failed to alter history, biff still has the sports almanac")
}
}
func Test_MountableRouter_PrefixesHandler(t *testing.T) {
t.Parallel()
mr := &server.MountableRouter{
Prefix: "/chronograf",
Delegate: httprouter.New(),
}
mr.Handler(http.MethodGet, "/recklessAmountOfPower", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
rw.Write([]byte("1.21 Gigawatts!"))
}))
ts := httptest.NewServer(mr)
defer ts.Close()
req := httptest.NewRequest(http.MethodGet, ts.URL+"/chronograf/recklessAmountOfPower", nil)
req.RequestURI = ""
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatal("Unexpected error sending request to mounted router: err:", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatal("Expected 200 but received", resp.StatusCode)
}
}