diff --git a/go.mod b/go.mod
index a71417a5a7964118169c3df5069f0a82843fe39c..ce82a21e1b48ec92227a50bcd1315bdbac37291f 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
 	github.com/golang/lint v0.0.0-20180702182130-06c8688daad7 // indirect
 	github.com/golang/protobuf v1.4.3
 	github.com/google/subcommands v1.2.0
+	github.com/gorilla/handlers v1.5.1
 	github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff
 	github.com/lpar/gzipped v1.1.1-0.20190413023519-5d9a18ea7f47
 	github.com/miekg/dns v1.1.35
diff --git a/go.sum b/go.sum
index e81805a14fbc7820c4f62ba7dd5df3da63b360e5..c665cd6e4a1af217914d29dfbd82967df2cb5db4 100644
--- a/go.sum
+++ b/go.sum
@@ -62,6 +62,8 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
+github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
 github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -121,6 +123,8 @@ github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
+github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c h1:Lh2aW+HnU2Nbe1gqD9SOJLJxW1jBMmQOktN2acDyJk8=
diff --git a/node/server.go b/node/server.go
index efffd44027ae489094156d6601fd88e55bc75f55..321fc5445f3b1c4fd3238cf7d1ebc9c713220ba4 100644
--- a/node/server.go
+++ b/node/server.go
@@ -6,11 +6,13 @@ import (
 	"log"
 	"net"
 	"net/http"
+	"os"
 	"strconv"
 	"strings"
 	"time"
 
 	"git.autistici.org/ale/autoradio/node/acme"
+	"github.com/gorilla/handlers"
 	"go.etcd.io/etcd/clientv3"
 	"golang.org/x/sync/errgroup"
 )
@@ -109,7 +111,7 @@ func NewServer(ctx context.Context, etcd *clientv3.Client, n *Node, config *Conf
 		}
 
 		httpsHandler := httpHandler
-		servers = append(servers, newHTTPSServer("https", fmt.Sprintf(":%d", config.HTTPSPort), httpsHandler, acmeMgr))
+		servers = append(servers, newHTTPSServer("https", fmt.Sprintf(":%d", config.HTTPSPort), handlers.LoggingHandler(os.Stdout, httpsHandler), acmeMgr))
 		httpHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 			if strings.HasPrefix(req.URL.Path, "/.well-known/acme-challenge/") {
 				acmeMgr.ServeHTTP(w, req)
@@ -120,7 +122,7 @@ func NewServer(ctx context.Context, etcd *clientv3.Client, n *Node, config *Conf
 	}
 
 	servers = append(servers, newStatusServer(mkaddr(config.PeerAddr, config.GossipPort), n.statusMgr))
-	servers = append(servers, newHTTPServer("main", fmt.Sprintf(":%d", config.HTTPPort), httpHandler))
+	servers = append(servers, newHTTPServer("main", fmt.Sprintf(":%d", config.HTTPPort), handlers.LoggingHandler(os.Stdout, httpHandler)))
 	servers = append(servers, newHTTPServer("metrics", fmt.Sprintf(":%d", config.MetricsPort), newMetricsHandler()))
 
 	for _, ip := range config.DNSAddrs {
diff --git a/vendor/github.com/felixge/httpsnoop/.gitignore b/vendor/github.com/felixge/httpsnoop/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/vendor/github.com/felixge/httpsnoop/.travis.yml b/vendor/github.com/felixge/httpsnoop/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bfc421200d0e2698a3d55bb0ec3ae9109cd99bf8
--- /dev/null
+++ b/vendor/github.com/felixge/httpsnoop/.travis.yml
@@ -0,0 +1,6 @@
+language: go
+
+go:
+  - 1.6
+  - 1.7
+  - 1.8
diff --git a/vendor/github.com/felixge/httpsnoop/LICENSE.txt b/vendor/github.com/felixge/httpsnoop/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e028b46a9b042927836e7b38d64b7bf4c51269c6
--- /dev/null
+++ b/vendor/github.com/felixge/httpsnoop/LICENSE.txt
@@ -0,0 +1,19 @@
+Copyright (c) 2016 Felix Geisendörfer (felix@debuggable.com)
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
diff --git a/vendor/github.com/felixge/httpsnoop/Makefile b/vendor/github.com/felixge/httpsnoop/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..2d84889aed79240b321e0143a330c5a635fcecc6
--- /dev/null
+++ b/vendor/github.com/felixge/httpsnoop/Makefile
@@ -0,0 +1,10 @@
+.PHONY: ci generate clean
+
+ci: clean generate
+	go test -v ./...
+
+generate:
+	go generate .
+
+clean:
+	rm -rf *_generated*.go
diff --git a/vendor/github.com/felixge/httpsnoop/README.md b/vendor/github.com/felixge/httpsnoop/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ae44137e9b04b4d192eaa97eabb63bd9995dbedc
--- /dev/null
+++ b/vendor/github.com/felixge/httpsnoop/README.md
@@ -0,0 +1,94 @@
+# httpsnoop
+
+Package httpsnoop provides an easy way to capture http related metrics (i.e.
+response time, bytes written, and http status code) from your application's
+http.Handlers.
+
+Doing this requires non-trivial wrapping of the http.ResponseWriter interface,
+which is also exposed for users interested in a more low-level API.
+
+[![GoDoc](https://godoc.org/github.com/felixge/httpsnoop?status.svg)](https://godoc.org/github.com/felixge/httpsnoop)
+[![Build Status](https://travis-ci.org/felixge/httpsnoop.svg?branch=master)](https://travis-ci.org/felixge/httpsnoop)
+
+## Usage Example
+
+```go
+// myH is your app's http handler, perhaps a http.ServeMux or similar.
+var myH http.Handler
+// wrappedH wraps myH in order to log every request.
+wrappedH := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	m := httpsnoop.CaptureMetrics(myH, w, r)
+	log.Printf(
+		"%s %s (code=%d dt=%s written=%d)",
+		r.Method,
+		r.URL,
+		m.Code,
+		m.Duration,
+		m.Written,
+	)
+})
+http.ListenAndServe(":8080", wrappedH)
+```
+
+## Why this package exists
+
+Instrumenting an application's http.Handler is surprisingly difficult.
+
+However if you google for e.g. "capture ResponseWriter status code" you'll find
+lots of advise and code examples that suggest it to be a fairly trivial
+undertaking. Unfortunately everything I've seen so far has a high chance of
+breaking your application.
+
+The main problem is that a `http.ResponseWriter` often implements additional
+interfaces such as `http.Flusher`, `http.CloseNotifier`, `http.Hijacker`, `http.Pusher`, and
+`io.ReaderFrom`. So the naive approach of just wrapping `http.ResponseWriter`
+in your own struct that also implements the `http.ResponseWriter` interface
+will hide the additional interfaces mentioned above. This has a high change of
+introducing subtle bugs into any non-trivial application.
+
+Another approach I've seen people take is to return a struct that implements
+all of the interfaces above. However, that's also problematic, because it's
+difficult to fake some of these interfaces behaviors when the underlying
+`http.ResponseWriter` doesn't have an implementation. It's also dangerous,
+because an application may choose to operate differently, merely because it
+detects the presence of these additional interfaces.
+
+This package solves this problem by checking which additional interfaces a
+`http.ResponseWriter` implements, returning a wrapped version implementing the
+exact same set of interfaces.
+
+Additionally this package properly handles edge cases such as `WriteHeader` not
+being called, or called more than once, as well as concurrent calls to
+`http.ResponseWriter` methods, and even calls happening after the wrapped
+`ServeHTTP` has already returned.
+
+Unfortunately this package is not perfect either. It's possible that it is
+still missing some interfaces provided by the go core (let me know if you find
+one), and it won't work for applications adding their own interfaces into the
+mix.
+
+However, hopefully the explanation above has sufficiently scared you of rolling
+your own solution to this problem. httpsnoop may still break your application,
+but at least it tries to avoid it as much as possible.
+
+Anyway, the real problem here is that smuggling additional interfaces inside
+`http.ResponseWriter` is a problematic design choice, but it probably goes as
+deep as the Go language specification itself. But that's okay, I still prefer
+Go over the alternatives ;).
+
+## Performance
+
+```
+BenchmarkBaseline-8      	   20000	     94912 ns/op
+BenchmarkCaptureMetrics-8	   20000	     95461 ns/op
+```
+
+As you can see, using `CaptureMetrics` on a vanilla http.Handler introduces an
+overhead of ~500 ns per http request on my machine. However, the margin of
+error appears to be larger than that, therefor it should be reasonable to
+assume that the overhead introduced by `CaptureMetrics` is absolutely
+negligible.
+
+## License
+
+MIT
diff --git a/vendor/github.com/felixge/httpsnoop/capture_metrics.go b/vendor/github.com/felixge/httpsnoop/capture_metrics.go
new file mode 100644
index 0000000000000000000000000000000000000000..4c45b1a8c15f6c263bf42295f3c78e7a89d42920
--- /dev/null
+++ b/vendor/github.com/felixge/httpsnoop/capture_metrics.go
@@ -0,0 +1,84 @@
+package httpsnoop
+
+import (
+	"io"
+	"net/http"
+	"sync"
+	"time"
+)
+
+// Metrics holds metrics captured from CaptureMetrics.
+type Metrics struct {
+	// Code is the first http response code passed to the WriteHeader func of
+	// the ResponseWriter. If no such call is made, a default code of 200 is
+	// assumed instead.
+	Code int
+	// Duration is the time it took to execute the handler.
+	Duration time.Duration
+	// Written is the number of bytes successfully written by the Write or
+	// ReadFrom function of the ResponseWriter. ResponseWriters may also write
+	// data to their underlaying connection directly (e.g. headers), but those
+	// are not tracked. Therefor the number of Written bytes will usually match
+	// the size of the response body.
+	Written int64
+}
+
+// CaptureMetrics wraps the given hnd, executes it with the given w and r, and
+// returns the metrics it captured from it.
+func CaptureMetrics(hnd http.Handler, w http.ResponseWriter, r *http.Request) Metrics {
+	return CaptureMetricsFn(w, func(ww http.ResponseWriter) {
+		hnd.ServeHTTP(ww, r)
+	})
+}
+
+// CaptureMetricsFn wraps w and calls fn with the wrapped w and returns the
+// resulting metrics. This is very similar to CaptureMetrics (which is just
+// sugar on top of this func), but is a more usable interface if your
+// application doesn't use the Go http.Handler interface.
+func CaptureMetricsFn(w http.ResponseWriter, fn func(http.ResponseWriter)) Metrics {
+	var (
+		start         = time.Now()
+		m             = Metrics{Code: http.StatusOK}
+		headerWritten bool
+		lock          sync.Mutex
+		hooks         = Hooks{
+			WriteHeader: func(next WriteHeaderFunc) WriteHeaderFunc {
+				return func(code int) {
+					next(code)
+					lock.Lock()
+					defer lock.Unlock()
+					if !headerWritten {
+						m.Code = code
+						headerWritten = true
+					}
+				}
+			},
+
+			Write: func(next WriteFunc) WriteFunc {
+				return func(p []byte) (int, error) {
+					n, err := next(p)
+					lock.Lock()
+					defer lock.Unlock()
+					m.Written += int64(n)
+					headerWritten = true
+					return n, err
+				}
+			},
+
+			ReadFrom: func(next ReadFromFunc) ReadFromFunc {
+				return func(src io.Reader) (int64, error) {
+					n, err := next(src)
+					lock.Lock()
+					defer lock.Unlock()
+					headerWritten = true
+					m.Written += n
+					return n, err
+				}
+			},
+		}
+	)
+
+	fn(Wrap(w, hooks))
+	m.Duration = time.Since(start)
+	return m
+}
diff --git a/vendor/github.com/felixge/httpsnoop/docs.go b/vendor/github.com/felixge/httpsnoop/docs.go
new file mode 100644
index 0000000000000000000000000000000000000000..203c35b3c6d10502990706a6f7362a9778599260
--- /dev/null
+++ b/vendor/github.com/felixge/httpsnoop/docs.go
@@ -0,0 +1,10 @@
+// Package httpsnoop provides an easy way to capture http related metrics (i.e.
+// response time, bytes written, and http status code) from your application's
+// http.Handlers.
+//
+// Doing this requires non-trivial wrapping of the http.ResponseWriter
+// interface, which is also exposed for users interested in a more low-level
+// API.
+package httpsnoop
+
+//go:generate go run codegen/main.go
diff --git a/vendor/github.com/felixge/httpsnoop/go.mod b/vendor/github.com/felixge/httpsnoop/go.mod
new file mode 100644
index 0000000000000000000000000000000000000000..73b3946905ab0f6f287c9990f450b07fc602b8b2
--- /dev/null
+++ b/vendor/github.com/felixge/httpsnoop/go.mod
@@ -0,0 +1,3 @@
+module github.com/felixge/httpsnoop
+
+go 1.13
diff --git a/vendor/github.com/felixge/httpsnoop/wrap_generated_gteq_1.8.go b/vendor/github.com/felixge/httpsnoop/wrap_generated_gteq_1.8.go
new file mode 100644
index 0000000000000000000000000000000000000000..41a20da9eab483b21ebd685ac905644d024225dd
--- /dev/null
+++ b/vendor/github.com/felixge/httpsnoop/wrap_generated_gteq_1.8.go
@@ -0,0 +1,385 @@
+// +build go1.8
+// Code generated by "httpsnoop/codegen"; DO NOT EDIT
+
+package httpsnoop
+
+import (
+	"bufio"
+	"io"
+	"net"
+	"net/http"
+)
+
+// HeaderFunc is part of the http.ResponseWriter interface.
+type HeaderFunc func() http.Header
+
+// WriteHeaderFunc is part of the http.ResponseWriter interface.
+type WriteHeaderFunc func(code int)
+
+// WriteFunc is part of the http.ResponseWriter interface.
+type WriteFunc func(b []byte) (int, error)
+
+// FlushFunc is part of the http.Flusher interface.
+type FlushFunc func()
+
+// CloseNotifyFunc is part of the http.CloseNotifier interface.
+type CloseNotifyFunc func() <-chan bool
+
+// HijackFunc is part of the http.Hijacker interface.
+type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
+
+// ReadFromFunc is part of the io.ReaderFrom interface.
+type ReadFromFunc func(src io.Reader) (int64, error)
+
+// PushFunc is part of the http.Pusher interface.
+type PushFunc func(target string, opts *http.PushOptions) error
+
+// Hooks defines a set of method interceptors for methods included in
+// http.ResponseWriter as well as some others. You can think of them as
+// middleware for the function calls they target. See Wrap for more details.
+type Hooks struct {
+	Header      func(HeaderFunc) HeaderFunc
+	WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
+	Write       func(WriteFunc) WriteFunc
+	Flush       func(FlushFunc) FlushFunc
+	CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
+	Hijack      func(HijackFunc) HijackFunc
+	ReadFrom    func(ReadFromFunc) ReadFromFunc
+	Push        func(PushFunc) PushFunc
+}
+
+// Wrap returns a wrapped version of w that provides the exact same interface
+// as w. Specifically if w implements any combination of:
+//
+// - http.Flusher
+// - http.CloseNotifier
+// - http.Hijacker
+// - io.ReaderFrom
+// - http.Pusher
+//
+// The wrapped version will implement the exact same combination. If no hooks
+// are set, the wrapped version also behaves exactly as w. Hooks targeting
+// methods not supported by w are ignored. Any other hooks will intercept the
+// method they target and may modify the call's arguments and/or return values.
+// The CaptureMetrics implementation serves as a working example for how the
+// hooks can be used.
+func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
+	rw := &rw{w: w, h: hooks}
+	_, i0 := w.(http.Flusher)
+	_, i1 := w.(http.CloseNotifier)
+	_, i2 := w.(http.Hijacker)
+	_, i3 := w.(io.ReaderFrom)
+	_, i4 := w.(http.Pusher)
+	switch {
+	// combination 1/32
+	case !i0 && !i1 && !i2 && !i3 && !i4:
+		return struct {
+			http.ResponseWriter
+		}{rw}
+	// combination 2/32
+	case !i0 && !i1 && !i2 && !i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Pusher
+		}{rw, rw}
+	// combination 3/32
+	case !i0 && !i1 && !i2 && i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			io.ReaderFrom
+		}{rw, rw}
+	// combination 4/32
+	case !i0 && !i1 && !i2 && i3 && i4:
+		return struct {
+			http.ResponseWriter
+			io.ReaderFrom
+			http.Pusher
+		}{rw, rw, rw}
+	// combination 5/32
+	case !i0 && !i1 && i2 && !i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.Hijacker
+		}{rw, rw}
+	// combination 6/32
+	case !i0 && !i1 && i2 && !i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Hijacker
+			http.Pusher
+		}{rw, rw, rw}
+	// combination 7/32
+	case !i0 && !i1 && i2 && i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.Hijacker
+			io.ReaderFrom
+		}{rw, rw, rw}
+	// combination 8/32
+	case !i0 && !i1 && i2 && i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Hijacker
+			io.ReaderFrom
+			http.Pusher
+		}{rw, rw, rw, rw}
+	// combination 9/32
+	case !i0 && i1 && !i2 && !i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+		}{rw, rw}
+	// combination 10/32
+	case !i0 && i1 && !i2 && !i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+			http.Pusher
+		}{rw, rw, rw}
+	// combination 11/32
+	case !i0 && i1 && !i2 && i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+			io.ReaderFrom
+		}{rw, rw, rw}
+	// combination 12/32
+	case !i0 && i1 && !i2 && i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+			io.ReaderFrom
+			http.Pusher
+		}{rw, rw, rw, rw}
+	// combination 13/32
+	case !i0 && i1 && i2 && !i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+			http.Hijacker
+		}{rw, rw, rw}
+	// combination 14/32
+	case !i0 && i1 && i2 && !i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+			http.Hijacker
+			http.Pusher
+		}{rw, rw, rw, rw}
+	// combination 15/32
+	case !i0 && i1 && i2 && i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+			http.Hijacker
+			io.ReaderFrom
+		}{rw, rw, rw, rw}
+	// combination 16/32
+	case !i0 && i1 && i2 && i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+			http.Hijacker
+			io.ReaderFrom
+			http.Pusher
+		}{rw, rw, rw, rw, rw}
+	// combination 17/32
+	case i0 && !i1 && !i2 && !i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+		}{rw, rw}
+	// combination 18/32
+	case i0 && !i1 && !i2 && !i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.Pusher
+		}{rw, rw, rw}
+	// combination 19/32
+	case i0 && !i1 && !i2 && i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			io.ReaderFrom
+		}{rw, rw, rw}
+	// combination 20/32
+	case i0 && !i1 && !i2 && i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			io.ReaderFrom
+			http.Pusher
+		}{rw, rw, rw, rw}
+	// combination 21/32
+	case i0 && !i1 && i2 && !i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.Hijacker
+		}{rw, rw, rw}
+	// combination 22/32
+	case i0 && !i1 && i2 && !i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.Hijacker
+			http.Pusher
+		}{rw, rw, rw, rw}
+	// combination 23/32
+	case i0 && !i1 && i2 && i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.Hijacker
+			io.ReaderFrom
+		}{rw, rw, rw, rw}
+	// combination 24/32
+	case i0 && !i1 && i2 && i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.Hijacker
+			io.ReaderFrom
+			http.Pusher
+		}{rw, rw, rw, rw, rw}
+	// combination 25/32
+	case i0 && i1 && !i2 && !i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+		}{rw, rw, rw}
+	// combination 26/32
+	case i0 && i1 && !i2 && !i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+			http.Pusher
+		}{rw, rw, rw, rw}
+	// combination 27/32
+	case i0 && i1 && !i2 && i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+			io.ReaderFrom
+		}{rw, rw, rw, rw}
+	// combination 28/32
+	case i0 && i1 && !i2 && i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+			io.ReaderFrom
+			http.Pusher
+		}{rw, rw, rw, rw, rw}
+	// combination 29/32
+	case i0 && i1 && i2 && !i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+			http.Hijacker
+		}{rw, rw, rw, rw}
+	// combination 30/32
+	case i0 && i1 && i2 && !i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+			http.Hijacker
+			http.Pusher
+		}{rw, rw, rw, rw, rw}
+	// combination 31/32
+	case i0 && i1 && i2 && i3 && !i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+			http.Hijacker
+			io.ReaderFrom
+		}{rw, rw, rw, rw, rw}
+	// combination 32/32
+	case i0 && i1 && i2 && i3 && i4:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+			http.Hijacker
+			io.ReaderFrom
+			http.Pusher
+		}{rw, rw, rw, rw, rw, rw}
+	}
+	panic("unreachable")
+}
+
+type rw struct {
+	w http.ResponseWriter
+	h Hooks
+}
+
+func (w *rw) Header() http.Header {
+	f := w.w.(http.ResponseWriter).Header
+	if w.h.Header != nil {
+		f = w.h.Header(f)
+	}
+	return f()
+}
+
+func (w *rw) WriteHeader(code int) {
+	f := w.w.(http.ResponseWriter).WriteHeader
+	if w.h.WriteHeader != nil {
+		f = w.h.WriteHeader(f)
+	}
+	f(code)
+}
+
+func (w *rw) Write(b []byte) (int, error) {
+	f := w.w.(http.ResponseWriter).Write
+	if w.h.Write != nil {
+		f = w.h.Write(f)
+	}
+	return f(b)
+}
+
+func (w *rw) Flush() {
+	f := w.w.(http.Flusher).Flush
+	if w.h.Flush != nil {
+		f = w.h.Flush(f)
+	}
+	f()
+}
+
+func (w *rw) CloseNotify() <-chan bool {
+	f := w.w.(http.CloseNotifier).CloseNotify
+	if w.h.CloseNotify != nil {
+		f = w.h.CloseNotify(f)
+	}
+	return f()
+}
+
+func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	f := w.w.(http.Hijacker).Hijack
+	if w.h.Hijack != nil {
+		f = w.h.Hijack(f)
+	}
+	return f()
+}
+
+func (w *rw) ReadFrom(src io.Reader) (int64, error) {
+	f := w.w.(io.ReaderFrom).ReadFrom
+	if w.h.ReadFrom != nil {
+		f = w.h.ReadFrom(f)
+	}
+	return f(src)
+}
+
+func (w *rw) Push(target string, opts *http.PushOptions) error {
+	f := w.w.(http.Pusher).Push
+	if w.h.Push != nil {
+		f = w.h.Push(f)
+	}
+	return f(target, opts)
+}
diff --git a/vendor/github.com/felixge/httpsnoop/wrap_generated_lt_1.8.go b/vendor/github.com/felixge/httpsnoop/wrap_generated_lt_1.8.go
new file mode 100644
index 0000000000000000000000000000000000000000..36bb59b837c9dc201b8e167b5b42e34cbda14cc9
--- /dev/null
+++ b/vendor/github.com/felixge/httpsnoop/wrap_generated_lt_1.8.go
@@ -0,0 +1,243 @@
+// +build !go1.8
+// Code generated by "httpsnoop/codegen"; DO NOT EDIT
+
+package httpsnoop
+
+import (
+	"bufio"
+	"io"
+	"net"
+	"net/http"
+)
+
+// HeaderFunc is part of the http.ResponseWriter interface.
+type HeaderFunc func() http.Header
+
+// WriteHeaderFunc is part of the http.ResponseWriter interface.
+type WriteHeaderFunc func(code int)
+
+// WriteFunc is part of the http.ResponseWriter interface.
+type WriteFunc func(b []byte) (int, error)
+
+// FlushFunc is part of the http.Flusher interface.
+type FlushFunc func()
+
+// CloseNotifyFunc is part of the http.CloseNotifier interface.
+type CloseNotifyFunc func() <-chan bool
+
+// HijackFunc is part of the http.Hijacker interface.
+type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
+
+// ReadFromFunc is part of the io.ReaderFrom interface.
+type ReadFromFunc func(src io.Reader) (int64, error)
+
+// Hooks defines a set of method interceptors for methods included in
+// http.ResponseWriter as well as some others. You can think of them as
+// middleware for the function calls they target. See Wrap for more details.
+type Hooks struct {
+	Header      func(HeaderFunc) HeaderFunc
+	WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
+	Write       func(WriteFunc) WriteFunc
+	Flush       func(FlushFunc) FlushFunc
+	CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
+	Hijack      func(HijackFunc) HijackFunc
+	ReadFrom    func(ReadFromFunc) ReadFromFunc
+}
+
+// Wrap returns a wrapped version of w that provides the exact same interface
+// as w. Specifically if w implements any combination of:
+//
+// - http.Flusher
+// - http.CloseNotifier
+// - http.Hijacker
+// - io.ReaderFrom
+//
+// The wrapped version will implement the exact same combination. If no hooks
+// are set, the wrapped version also behaves exactly as w. Hooks targeting
+// methods not supported by w are ignored. Any other hooks will intercept the
+// method they target and may modify the call's arguments and/or return values.
+// The CaptureMetrics implementation serves as a working example for how the
+// hooks can be used.
+func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
+	rw := &rw{w: w, h: hooks}
+	_, i0 := w.(http.Flusher)
+	_, i1 := w.(http.CloseNotifier)
+	_, i2 := w.(http.Hijacker)
+	_, i3 := w.(io.ReaderFrom)
+	switch {
+	// combination 1/16
+	case !i0 && !i1 && !i2 && !i3:
+		return struct {
+			http.ResponseWriter
+		}{rw}
+	// combination 2/16
+	case !i0 && !i1 && !i2 && i3:
+		return struct {
+			http.ResponseWriter
+			io.ReaderFrom
+		}{rw, rw}
+	// combination 3/16
+	case !i0 && !i1 && i2 && !i3:
+		return struct {
+			http.ResponseWriter
+			http.Hijacker
+		}{rw, rw}
+	// combination 4/16
+	case !i0 && !i1 && i2 && i3:
+		return struct {
+			http.ResponseWriter
+			http.Hijacker
+			io.ReaderFrom
+		}{rw, rw, rw}
+	// combination 5/16
+	case !i0 && i1 && !i2 && !i3:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+		}{rw, rw}
+	// combination 6/16
+	case !i0 && i1 && !i2 && i3:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+			io.ReaderFrom
+		}{rw, rw, rw}
+	// combination 7/16
+	case !i0 && i1 && i2 && !i3:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+			http.Hijacker
+		}{rw, rw, rw}
+	// combination 8/16
+	case !i0 && i1 && i2 && i3:
+		return struct {
+			http.ResponseWriter
+			http.CloseNotifier
+			http.Hijacker
+			io.ReaderFrom
+		}{rw, rw, rw, rw}
+	// combination 9/16
+	case i0 && !i1 && !i2 && !i3:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+		}{rw, rw}
+	// combination 10/16
+	case i0 && !i1 && !i2 && i3:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			io.ReaderFrom
+		}{rw, rw, rw}
+	// combination 11/16
+	case i0 && !i1 && i2 && !i3:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.Hijacker
+		}{rw, rw, rw}
+	// combination 12/16
+	case i0 && !i1 && i2 && i3:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.Hijacker
+			io.ReaderFrom
+		}{rw, rw, rw, rw}
+	// combination 13/16
+	case i0 && i1 && !i2 && !i3:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+		}{rw, rw, rw}
+	// combination 14/16
+	case i0 && i1 && !i2 && i3:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+			io.ReaderFrom
+		}{rw, rw, rw, rw}
+	// combination 15/16
+	case i0 && i1 && i2 && !i3:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+			http.Hijacker
+		}{rw, rw, rw, rw}
+	// combination 16/16
+	case i0 && i1 && i2 && i3:
+		return struct {
+			http.ResponseWriter
+			http.Flusher
+			http.CloseNotifier
+			http.Hijacker
+			io.ReaderFrom
+		}{rw, rw, rw, rw, rw}
+	}
+	panic("unreachable")
+}
+
+type rw struct {
+	w http.ResponseWriter
+	h Hooks
+}
+
+func (w *rw) Header() http.Header {
+	f := w.w.(http.ResponseWriter).Header
+	if w.h.Header != nil {
+		f = w.h.Header(f)
+	}
+	return f()
+}
+
+func (w *rw) WriteHeader(code int) {
+	f := w.w.(http.ResponseWriter).WriteHeader
+	if w.h.WriteHeader != nil {
+		f = w.h.WriteHeader(f)
+	}
+	f(code)
+}
+
+func (w *rw) Write(b []byte) (int, error) {
+	f := w.w.(http.ResponseWriter).Write
+	if w.h.Write != nil {
+		f = w.h.Write(f)
+	}
+	return f(b)
+}
+
+func (w *rw) Flush() {
+	f := w.w.(http.Flusher).Flush
+	if w.h.Flush != nil {
+		f = w.h.Flush(f)
+	}
+	f()
+}
+
+func (w *rw) CloseNotify() <-chan bool {
+	f := w.w.(http.CloseNotifier).CloseNotify
+	if w.h.CloseNotify != nil {
+		f = w.h.CloseNotify(f)
+	}
+	return f()
+}
+
+func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	f := w.w.(http.Hijacker).Hijack
+	if w.h.Hijack != nil {
+		f = w.h.Hijack(f)
+	}
+	return f()
+}
+
+func (w *rw) ReadFrom(src io.Reader) (int64, error) {
+	f := w.w.(io.ReaderFrom).ReadFrom
+	if w.h.ReadFrom != nil {
+		f = w.h.ReadFrom(f)
+	}
+	return f(src)
+}
diff --git a/vendor/github.com/gorilla/handlers/LICENSE b/vendor/github.com/gorilla/handlers/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..66ea3c8ae71435611df429a98806f00df00b306f
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2013 The Gorilla Handlers Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+  Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+  Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/gorilla/handlers/README.md b/vendor/github.com/gorilla/handlers/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..6eba66bf302720f31c1e1f54a2737ef95980b851
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/README.md
@@ -0,0 +1,56 @@
+gorilla/handlers
+================
+[![GoDoc](https://godoc.org/github.com/gorilla/handlers?status.svg)](https://godoc.org/github.com/gorilla/handlers)
+[![CircleCI](https://circleci.com/gh/gorilla/handlers.svg?style=svg)](https://circleci.com/gh/gorilla/handlers)
+[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/handlers/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/handlers?badge)
+
+
+Package handlers is a collection of handlers (aka "HTTP middleware") for use
+with Go's `net/http` package (or any framework supporting `http.Handler`), including:
+
+* [**LoggingHandler**](https://godoc.org/github.com/gorilla/handlers#LoggingHandler) for logging HTTP requests in the Apache [Common Log
+  Format](http://httpd.apache.org/docs/2.2/logs.html#common).
+* [**CombinedLoggingHandler**](https://godoc.org/github.com/gorilla/handlers#CombinedLoggingHandler) for logging HTTP requests in the Apache [Combined Log
+  Format](http://httpd.apache.org/docs/2.2/logs.html#combined) commonly used by
+  both Apache and nginx.
+* [**CompressHandler**](https://godoc.org/github.com/gorilla/handlers#CompressHandler) for gzipping responses.
+* [**ContentTypeHandler**](https://godoc.org/github.com/gorilla/handlers#ContentTypeHandler) for validating requests against a list of accepted
+  content types.
+* [**MethodHandler**](https://godoc.org/github.com/gorilla/handlers#MethodHandler) for matching HTTP methods against handlers in a
+  `map[string]http.Handler`
+* [**ProxyHeaders**](https://godoc.org/github.com/gorilla/handlers#ProxyHeaders) for populating `r.RemoteAddr` and `r.URL.Scheme` based on the
+  `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto` and RFC7239 `Forwarded`
+  headers when running a Go server behind a HTTP reverse proxy.
+* [**CanonicalHost**](https://godoc.org/github.com/gorilla/handlers#CanonicalHost) for re-directing to the preferred host when handling multiple 
+  domains (i.e. multiple CNAME aliases).
+* [**RecoveryHandler**](https://godoc.org/github.com/gorilla/handlers#RecoveryHandler) for recovering from unexpected panics.
+
+Other handlers are documented [on the Gorilla
+website](https://www.gorillatoolkit.org/pkg/handlers).
+
+## Example
+
+A simple example using `handlers.LoggingHandler` and `handlers.CompressHandler`:
+
+```go
+import (
+    "net/http"
+    "github.com/gorilla/handlers"
+)
+
+func main() {
+    r := http.NewServeMux()
+
+    // Only log requests to our admin dashboard to stdout
+    r.Handle("/admin", handlers.LoggingHandler(os.Stdout, http.HandlerFunc(ShowAdminDashboard)))
+    r.HandleFunc("/", ShowIndex)
+
+    // Wrap our server with our gzip handler to gzip compress all responses.
+    http.ListenAndServe(":8000", handlers.CompressHandler(r))
+}
+```
+
+## License
+
+BSD licensed. See the included LICENSE file for details.
+
diff --git a/vendor/github.com/gorilla/handlers/canonical.go b/vendor/github.com/gorilla/handlers/canonical.go
new file mode 100644
index 0000000000000000000000000000000000000000..8437fefc1ef6826f585f9de2f4c6371ac035c2bc
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/canonical.go
@@ -0,0 +1,74 @@
+package handlers
+
+import (
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+type canonical struct {
+	h      http.Handler
+	domain string
+	code   int
+}
+
+// CanonicalHost is HTTP middleware that re-directs requests to the canonical
+// domain. It accepts a domain and a status code (e.g. 301 or 302) and
+// re-directs clients to this domain. The existing request path is maintained.
+//
+// Note: If the provided domain is considered invalid by url.Parse or otherwise
+// returns an empty scheme or host, clients are not re-directed.
+//
+// Example:
+//
+//  r := mux.NewRouter()
+//  canonical := handlers.CanonicalHost("http://www.gorillatoolkit.org", 302)
+//  r.HandleFunc("/route", YourHandler)
+//
+//  log.Fatal(http.ListenAndServe(":7000", canonical(r)))
+//
+func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler {
+	fn := func(h http.Handler) http.Handler {
+		return canonical{h, domain, code}
+	}
+
+	return fn
+}
+
+func (c canonical) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	dest, err := url.Parse(c.domain)
+	if err != nil {
+		// Call the next handler if the provided domain fails to parse.
+		c.h.ServeHTTP(w, r)
+		return
+	}
+
+	if dest.Scheme == "" || dest.Host == "" {
+		// Call the next handler if the scheme or host are empty.
+		// Note that url.Parse won't fail on in this case.
+		c.h.ServeHTTP(w, r)
+		return
+	}
+
+	if !strings.EqualFold(cleanHost(r.Host), dest.Host) {
+		// Re-build the destination URL
+		dest := dest.Scheme + "://" + dest.Host + r.URL.Path
+		if r.URL.RawQuery != "" {
+			dest += "?" + r.URL.RawQuery
+		}
+		http.Redirect(w, r, dest, c.code)
+		return
+	}
+
+	c.h.ServeHTTP(w, r)
+}
+
+// cleanHost cleans invalid Host headers by stripping anything after '/' or ' '.
+// This is backported from Go 1.5 (in response to issue #11206) and attempts to
+// mitigate malformed Host headers that do not match the format in RFC7230.
+func cleanHost(in string) string {
+	if i := strings.IndexAny(in, " /"); i != -1 {
+		return in[:i]
+	}
+	return in
+}
diff --git a/vendor/github.com/gorilla/handlers/compress.go b/vendor/github.com/gorilla/handlers/compress.go
new file mode 100644
index 0000000000000000000000000000000000000000..1e95f1ccbfa511c70d3f296a9f11554f6c9911aa
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/compress.go
@@ -0,0 +1,143 @@
+// Copyright 2013 The Gorilla 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 handlers
+
+import (
+	"compress/flate"
+	"compress/gzip"
+	"io"
+	"net/http"
+	"strings"
+
+	"github.com/felixge/httpsnoop"
+)
+
+const acceptEncoding string = "Accept-Encoding"
+
+type compressResponseWriter struct {
+	compressor io.Writer
+	w          http.ResponseWriter
+}
+
+func (cw *compressResponseWriter) WriteHeader(c int) {
+	cw.w.Header().Del("Content-Length")
+	cw.w.WriteHeader(c)
+}
+
+func (cw *compressResponseWriter) Write(b []byte) (int, error) {
+	h := cw.w.Header()
+	if h.Get("Content-Type") == "" {
+		h.Set("Content-Type", http.DetectContentType(b))
+	}
+	h.Del("Content-Length")
+
+	return cw.compressor.Write(b)
+}
+
+func (cw *compressResponseWriter) ReadFrom(r io.Reader) (int64, error) {
+	return io.Copy(cw.compressor, r)
+}
+
+type flusher interface {
+	Flush() error
+}
+
+func (w *compressResponseWriter) Flush() {
+	// Flush compressed data if compressor supports it.
+	if f, ok := w.compressor.(flusher); ok {
+		f.Flush()
+	}
+	// Flush HTTP response.
+	if f, ok := w.w.(http.Flusher); ok {
+		f.Flush()
+	}
+}
+
+// CompressHandler gzip compresses HTTP responses for clients that support it
+// via the 'Accept-Encoding' header.
+//
+// Compressing TLS traffic may leak the page contents to an attacker if the
+// page contains user input: http://security.stackexchange.com/a/102015/12208
+func CompressHandler(h http.Handler) http.Handler {
+	return CompressHandlerLevel(h, gzip.DefaultCompression)
+}
+
+// CompressHandlerLevel gzip compresses HTTP responses with specified compression level
+// for clients that support it via the 'Accept-Encoding' header.
+//
+// The compression level should be gzip.DefaultCompression, gzip.NoCompression,
+// or any integer value between gzip.BestSpeed and gzip.BestCompression inclusive.
+// gzip.DefaultCompression is used in case of invalid compression level.
+func CompressHandlerLevel(h http.Handler, level int) http.Handler {
+	if level < gzip.DefaultCompression || level > gzip.BestCompression {
+		level = gzip.DefaultCompression
+	}
+
+	const (
+		gzipEncoding  = "gzip"
+		flateEncoding = "deflate"
+	)
+
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		// detect what encoding to use
+		var encoding string
+		for _, curEnc := range strings.Split(r.Header.Get(acceptEncoding), ",") {
+			curEnc = strings.TrimSpace(curEnc)
+			if curEnc == gzipEncoding || curEnc == flateEncoding {
+				encoding = curEnc
+				break
+			}
+		}
+
+		// always add Accept-Encoding to Vary to prevent intermediate caches corruption
+		w.Header().Add("Vary", acceptEncoding)
+
+		// if we weren't able to identify an encoding we're familiar with, pass on the
+		// request to the handler and return
+		if encoding == "" {
+			h.ServeHTTP(w, r)
+			return
+		}
+
+		if r.Header.Get("Upgrade") != "" {
+			h.ServeHTTP(w, r)
+			return
+		}
+
+		// wrap the ResponseWriter with the writer for the chosen encoding
+		var encWriter io.WriteCloser
+		if encoding == gzipEncoding {
+			encWriter, _ = gzip.NewWriterLevel(w, level)
+		} else if encoding == flateEncoding {
+			encWriter, _ = flate.NewWriter(w, level)
+		}
+		defer encWriter.Close()
+
+		w.Header().Set("Content-Encoding", encoding)
+		r.Header.Del(acceptEncoding)
+
+		cw := &compressResponseWriter{
+			w:          w,
+			compressor: encWriter,
+		}
+
+		w = httpsnoop.Wrap(w, httpsnoop.Hooks{
+			Write: func(httpsnoop.WriteFunc) httpsnoop.WriteFunc {
+				return cw.Write
+			},
+			WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
+				return cw.WriteHeader
+			},
+			Flush: func(httpsnoop.FlushFunc) httpsnoop.FlushFunc {
+				return cw.Flush
+			},
+			ReadFrom: func(rff httpsnoop.ReadFromFunc) httpsnoop.ReadFromFunc {
+				return cw.ReadFrom
+			},
+		})
+
+		h.ServeHTTP(w, r)
+	})
+}
diff --git a/vendor/github.com/gorilla/handlers/cors.go b/vendor/github.com/gorilla/handlers/cors.go
new file mode 100644
index 0000000000000000000000000000000000000000..0dcdffb3d32e2dffdc6c2dd89c377506ffcdc343
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/cors.go
@@ -0,0 +1,355 @@
+package handlers
+
+import (
+	"net/http"
+	"strconv"
+	"strings"
+)
+
+// CORSOption represents a functional option for configuring the CORS middleware.
+type CORSOption func(*cors) error
+
+type cors struct {
+	h                      http.Handler
+	allowedHeaders         []string
+	allowedMethods         []string
+	allowedOrigins         []string
+	allowedOriginValidator OriginValidator
+	exposedHeaders         []string
+	maxAge                 int
+	ignoreOptions          bool
+	allowCredentials       bool
+	optionStatusCode       int
+}
+
+// OriginValidator takes an origin string and returns whether or not that origin is allowed.
+type OriginValidator func(string) bool
+
+var (
+	defaultCorsOptionStatusCode = 200
+	defaultCorsMethods          = []string{"GET", "HEAD", "POST"}
+	defaultCorsHeaders          = []string{"Accept", "Accept-Language", "Content-Language", "Origin"}
+	// (WebKit/Safari v9 sends the Origin header by default in AJAX requests)
+)
+
+const (
+	corsOptionMethod           string = "OPTIONS"
+	corsAllowOriginHeader      string = "Access-Control-Allow-Origin"
+	corsExposeHeadersHeader    string = "Access-Control-Expose-Headers"
+	corsMaxAgeHeader           string = "Access-Control-Max-Age"
+	corsAllowMethodsHeader     string = "Access-Control-Allow-Methods"
+	corsAllowHeadersHeader     string = "Access-Control-Allow-Headers"
+	corsAllowCredentialsHeader string = "Access-Control-Allow-Credentials"
+	corsRequestMethodHeader    string = "Access-Control-Request-Method"
+	corsRequestHeadersHeader   string = "Access-Control-Request-Headers"
+	corsOriginHeader           string = "Origin"
+	corsVaryHeader             string = "Vary"
+	corsOriginMatchAll         string = "*"
+)
+
+func (ch *cors) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	origin := r.Header.Get(corsOriginHeader)
+	if !ch.isOriginAllowed(origin) {
+		if r.Method != corsOptionMethod || ch.ignoreOptions {
+			ch.h.ServeHTTP(w, r)
+		}
+
+		return
+	}
+
+	if r.Method == corsOptionMethod {
+		if ch.ignoreOptions {
+			ch.h.ServeHTTP(w, r)
+			return
+		}
+
+		if _, ok := r.Header[corsRequestMethodHeader]; !ok {
+			w.WriteHeader(http.StatusBadRequest)
+			return
+		}
+
+		method := r.Header.Get(corsRequestMethodHeader)
+		if !ch.isMatch(method, ch.allowedMethods) {
+			w.WriteHeader(http.StatusMethodNotAllowed)
+			return
+		}
+
+		requestHeaders := strings.Split(r.Header.Get(corsRequestHeadersHeader), ",")
+		allowedHeaders := []string{}
+		for _, v := range requestHeaders {
+			canonicalHeader := http.CanonicalHeaderKey(strings.TrimSpace(v))
+			if canonicalHeader == "" || ch.isMatch(canonicalHeader, defaultCorsHeaders) {
+				continue
+			}
+
+			if !ch.isMatch(canonicalHeader, ch.allowedHeaders) {
+				w.WriteHeader(http.StatusForbidden)
+				return
+			}
+
+			allowedHeaders = append(allowedHeaders, canonicalHeader)
+		}
+
+		if len(allowedHeaders) > 0 {
+			w.Header().Set(corsAllowHeadersHeader, strings.Join(allowedHeaders, ","))
+		}
+
+		if ch.maxAge > 0 {
+			w.Header().Set(corsMaxAgeHeader, strconv.Itoa(ch.maxAge))
+		}
+
+		if !ch.isMatch(method, defaultCorsMethods) {
+			w.Header().Set(corsAllowMethodsHeader, method)
+		}
+	} else {
+		if len(ch.exposedHeaders) > 0 {
+			w.Header().Set(corsExposeHeadersHeader, strings.Join(ch.exposedHeaders, ","))
+		}
+	}
+
+	if ch.allowCredentials {
+		w.Header().Set(corsAllowCredentialsHeader, "true")
+	}
+
+	if len(ch.allowedOrigins) > 1 {
+		w.Header().Set(corsVaryHeader, corsOriginHeader)
+	}
+
+	returnOrigin := origin
+	if ch.allowedOriginValidator == nil && len(ch.allowedOrigins) == 0 {
+		returnOrigin = "*"
+	} else {
+		for _, o := range ch.allowedOrigins {
+			// A configuration of * is different than explicitly setting an allowed
+			// origin. Returning arbitrary origin headers in an access control allow
+			// origin header is unsafe and is not required by any use case.
+			if o == corsOriginMatchAll {
+				returnOrigin = "*"
+				break
+			}
+		}
+	}
+	w.Header().Set(corsAllowOriginHeader, returnOrigin)
+
+	if r.Method == corsOptionMethod {
+		w.WriteHeader(ch.optionStatusCode)
+		return
+	}
+	ch.h.ServeHTTP(w, r)
+}
+
+// CORS provides Cross-Origin Resource Sharing middleware.
+// Example:
+//
+//  import (
+//      "net/http"
+//
+//      "github.com/gorilla/handlers"
+//      "github.com/gorilla/mux"
+//  )
+//
+//  func main() {
+//      r := mux.NewRouter()
+//      r.HandleFunc("/users", UserEndpoint)
+//      r.HandleFunc("/projects", ProjectEndpoint)
+//
+//      // Apply the CORS middleware to our top-level router, with the defaults.
+//      http.ListenAndServe(":8000", handlers.CORS()(r))
+//  }
+//
+func CORS(opts ...CORSOption) func(http.Handler) http.Handler {
+	return func(h http.Handler) http.Handler {
+		ch := parseCORSOptions(opts...)
+		ch.h = h
+		return ch
+	}
+}
+
+func parseCORSOptions(opts ...CORSOption) *cors {
+	ch := &cors{
+		allowedMethods:   defaultCorsMethods,
+		allowedHeaders:   defaultCorsHeaders,
+		allowedOrigins:   []string{},
+		optionStatusCode: defaultCorsOptionStatusCode,
+	}
+
+	for _, option := range opts {
+		option(ch)
+	}
+
+	return ch
+}
+
+//
+// Functional options for configuring CORS.
+//
+
+// AllowedHeaders adds the provided headers to the list of allowed headers in a
+// CORS request.
+// This is an append operation so the headers Accept, Accept-Language,
+// and Content-Language are always allowed.
+// Content-Type must be explicitly declared if accepting Content-Types other than
+// application/x-www-form-urlencoded, multipart/form-data, or text/plain.
+func AllowedHeaders(headers []string) CORSOption {
+	return func(ch *cors) error {
+		for _, v := range headers {
+			normalizedHeader := http.CanonicalHeaderKey(strings.TrimSpace(v))
+			if normalizedHeader == "" {
+				continue
+			}
+
+			if !ch.isMatch(normalizedHeader, ch.allowedHeaders) {
+				ch.allowedHeaders = append(ch.allowedHeaders, normalizedHeader)
+			}
+		}
+
+		return nil
+	}
+}
+
+// AllowedMethods can be used to explicitly allow methods in the
+// Access-Control-Allow-Methods header.
+// This is a replacement operation so you must also
+// pass GET, HEAD, and POST if you wish to support those methods.
+func AllowedMethods(methods []string) CORSOption {
+	return func(ch *cors) error {
+		ch.allowedMethods = []string{}
+		for _, v := range methods {
+			normalizedMethod := strings.ToUpper(strings.TrimSpace(v))
+			if normalizedMethod == "" {
+				continue
+			}
+
+			if !ch.isMatch(normalizedMethod, ch.allowedMethods) {
+				ch.allowedMethods = append(ch.allowedMethods, normalizedMethod)
+			}
+		}
+
+		return nil
+	}
+}
+
+// AllowedOrigins sets the allowed origins for CORS requests, as used in the
+// 'Allow-Access-Control-Origin' HTTP header.
+// Note: Passing in a []string{"*"} will allow any domain.
+func AllowedOrigins(origins []string) CORSOption {
+	return func(ch *cors) error {
+		for _, v := range origins {
+			if v == corsOriginMatchAll {
+				ch.allowedOrigins = []string{corsOriginMatchAll}
+				return nil
+			}
+		}
+
+		ch.allowedOrigins = origins
+		return nil
+	}
+}
+
+// AllowedOriginValidator sets a function for evaluating allowed origins in CORS requests, represented by the
+// 'Allow-Access-Control-Origin' HTTP header.
+func AllowedOriginValidator(fn OriginValidator) CORSOption {
+	return func(ch *cors) error {
+		ch.allowedOriginValidator = fn
+		return nil
+	}
+}
+
+// OptionStatusCode sets a custom status code on the OPTIONS requests.
+// Default behaviour sets it to 200 to reflect best practices. This is option is not mandatory
+// and can be used if you need a custom status code (i.e 204).
+//
+// More informations on the spec:
+// https://fetch.spec.whatwg.org/#cors-preflight-fetch
+func OptionStatusCode(code int) CORSOption {
+	return func(ch *cors) error {
+		ch.optionStatusCode = code
+		return nil
+	}
+}
+
+// ExposedHeaders can be used to specify headers that are available
+// and will not be stripped out by the user-agent.
+func ExposedHeaders(headers []string) CORSOption {
+	return func(ch *cors) error {
+		ch.exposedHeaders = []string{}
+		for _, v := range headers {
+			normalizedHeader := http.CanonicalHeaderKey(strings.TrimSpace(v))
+			if normalizedHeader == "" {
+				continue
+			}
+
+			if !ch.isMatch(normalizedHeader, ch.exposedHeaders) {
+				ch.exposedHeaders = append(ch.exposedHeaders, normalizedHeader)
+			}
+		}
+
+		return nil
+	}
+}
+
+// MaxAge determines the maximum age (in seconds) between preflight requests. A
+// maximum of 10 minutes is allowed. An age above this value will default to 10
+// minutes.
+func MaxAge(age int) CORSOption {
+	return func(ch *cors) error {
+		// Maximum of 10 minutes.
+		if age > 600 {
+			age = 600
+		}
+
+		ch.maxAge = age
+		return nil
+	}
+}
+
+// IgnoreOptions causes the CORS middleware to ignore OPTIONS requests, instead
+// passing them through to the next handler. This is useful when your application
+// or framework has a pre-existing mechanism for responding to OPTIONS requests.
+func IgnoreOptions() CORSOption {
+	return func(ch *cors) error {
+		ch.ignoreOptions = true
+		return nil
+	}
+}
+
+// AllowCredentials can be used to specify that the user agent may pass
+// authentication details along with the request.
+func AllowCredentials() CORSOption {
+	return func(ch *cors) error {
+		ch.allowCredentials = true
+		return nil
+	}
+}
+
+func (ch *cors) isOriginAllowed(origin string) bool {
+	if origin == "" {
+		return false
+	}
+
+	if ch.allowedOriginValidator != nil {
+		return ch.allowedOriginValidator(origin)
+	}
+
+	if len(ch.allowedOrigins) == 0 {
+		return true
+	}
+
+	for _, allowedOrigin := range ch.allowedOrigins {
+		if allowedOrigin == origin || allowedOrigin == corsOriginMatchAll {
+			return true
+		}
+	}
+
+	return false
+}
+
+func (ch *cors) isMatch(needle string, haystack []string) bool {
+	for _, v := range haystack {
+		if v == needle {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/vendor/github.com/gorilla/handlers/doc.go b/vendor/github.com/gorilla/handlers/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..944e5a8ae998234832642dbc50ba799a77618d28
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/doc.go
@@ -0,0 +1,9 @@
+/*
+Package handlers is a collection of handlers (aka "HTTP middleware") for use
+with Go's net/http package (or any framework supporting http.Handler).
+
+The package includes handlers for logging in standardised formats, compressing
+HTTP responses, validating content types and other useful tools for manipulating
+requests and responses.
+*/
+package handlers
diff --git a/vendor/github.com/gorilla/handlers/go.mod b/vendor/github.com/gorilla/handlers/go.mod
new file mode 100644
index 0000000000000000000000000000000000000000..58e6a85ec3a606da3924fbdb66362b90dd237a5b
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/go.mod
@@ -0,0 +1,5 @@
+module github.com/gorilla/handlers
+
+go 1.14
+
+require github.com/felixge/httpsnoop v1.0.1
diff --git a/vendor/github.com/gorilla/handlers/go.sum b/vendor/github.com/gorilla/handlers/go.sum
new file mode 100644
index 0000000000000000000000000000000000000000..8c2645804e7876c28cc3dd5d15d8dec4621ffd55
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/go.sum
@@ -0,0 +1,2 @@
+github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
+github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
diff --git a/vendor/github.com/gorilla/handlers/handlers.go b/vendor/github.com/gorilla/handlers/handlers.go
new file mode 100644
index 0000000000000000000000000000000000000000..0509482ad7a3eff1e66c016f4e5ab2a49ae594e2
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/handlers.go
@@ -0,0 +1,147 @@
+// Copyright 2013 The Gorilla 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 handlers
+
+import (
+	"bufio"
+	"fmt"
+	"net"
+	"net/http"
+	"sort"
+	"strings"
+)
+
+// MethodHandler is an http.Handler that dispatches to a handler whose key in the
+// MethodHandler's map matches the name of the HTTP request's method, eg: GET
+//
+// If the request's method is OPTIONS and OPTIONS is not a key in the map then
+// the handler responds with a status of 200 and sets the Allow header to a
+// comma-separated list of available methods.
+//
+// If the request's method doesn't match any of its keys the handler responds
+// with a status of HTTP 405 "Method Not Allowed" and sets the Allow header to a
+// comma-separated list of available methods.
+type MethodHandler map[string]http.Handler
+
+func (h MethodHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	if handler, ok := h[req.Method]; ok {
+		handler.ServeHTTP(w, req)
+	} else {
+		allow := []string{}
+		for k := range h {
+			allow = append(allow, k)
+		}
+		sort.Strings(allow)
+		w.Header().Set("Allow", strings.Join(allow, ", "))
+		if req.Method == "OPTIONS" {
+			w.WriteHeader(http.StatusOK)
+		} else {
+			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+		}
+	}
+}
+
+// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP
+// status code and body size
+type responseLogger struct {
+	w      http.ResponseWriter
+	status int
+	size   int
+}
+
+func (l *responseLogger) Write(b []byte) (int, error) {
+	size, err := l.w.Write(b)
+	l.size += size
+	return size, err
+}
+
+func (l *responseLogger) WriteHeader(s int) {
+	l.w.WriteHeader(s)
+	l.status = s
+}
+
+func (l *responseLogger) Status() int {
+	return l.status
+}
+
+func (l *responseLogger) Size() int {
+	return l.size
+}
+
+func (l *responseLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	conn, rw, err := l.w.(http.Hijacker).Hijack()
+	if err == nil && l.status == 0 {
+		// The status will be StatusSwitchingProtocols if there was no error and
+		// WriteHeader has not been called yet
+		l.status = http.StatusSwitchingProtocols
+	}
+	return conn, rw, err
+}
+
+// isContentType validates the Content-Type header matches the supplied
+// contentType. That is, its type and subtype match.
+func isContentType(h http.Header, contentType string) bool {
+	ct := h.Get("Content-Type")
+	if i := strings.IndexRune(ct, ';'); i != -1 {
+		ct = ct[0:i]
+	}
+	return ct == contentType
+}
+
+// ContentTypeHandler wraps and returns a http.Handler, validating the request
+// content type is compatible with the contentTypes list. It writes a HTTP 415
+// error if that fails.
+//
+// Only PUT, POST, and PATCH requests are considered.
+func ContentTypeHandler(h http.Handler, contentTypes ...string) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if !(r.Method == "PUT" || r.Method == "POST" || r.Method == "PATCH") {
+			h.ServeHTTP(w, r)
+			return
+		}
+
+		for _, ct := range contentTypes {
+			if isContentType(r.Header, ct) {
+				h.ServeHTTP(w, r)
+				return
+			}
+		}
+		http.Error(w, fmt.Sprintf("Unsupported content type %q; expected one of %q", r.Header.Get("Content-Type"), contentTypes), http.StatusUnsupportedMediaType)
+	})
+}
+
+const (
+	// HTTPMethodOverrideHeader is a commonly used
+	// http header to override a request method.
+	HTTPMethodOverrideHeader = "X-HTTP-Method-Override"
+	// HTTPMethodOverrideFormKey is a commonly used
+	// HTML form key to override a request method.
+	HTTPMethodOverrideFormKey = "_method"
+)
+
+// HTTPMethodOverrideHandler wraps and returns a http.Handler which checks for
+// the X-HTTP-Method-Override header or the _method form key, and overrides (if
+// valid) request.Method with its value.
+//
+// This is especially useful for HTTP clients that don't support many http verbs.
+// It isn't secure to override e.g a GET to a POST, so only POST requests are
+// considered.  Likewise, the override method can only be a "write" method: PUT,
+// PATCH or DELETE.
+//
+// Form method takes precedence over header method.
+func HTTPMethodOverrideHandler(h http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.Method == "POST" {
+			om := r.FormValue(HTTPMethodOverrideFormKey)
+			if om == "" {
+				om = r.Header.Get(HTTPMethodOverrideHeader)
+			}
+			if om == "PUT" || om == "PATCH" || om == "DELETE" {
+				r.Method = om
+			}
+		}
+		h.ServeHTTP(w, r)
+	})
+}
diff --git a/vendor/github.com/gorilla/handlers/logging.go b/vendor/github.com/gorilla/handlers/logging.go
new file mode 100644
index 0000000000000000000000000000000000000000..228465eba003d57e72653a7832fe4547523cb941
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/logging.go
@@ -0,0 +1,244 @@
+// Copyright 2013 The Gorilla 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 handlers
+
+import (
+	"io"
+	"net"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+	"unicode/utf8"
+
+	"github.com/felixge/httpsnoop"
+)
+
+// Logging
+
+// LogFormatterParams is the structure any formatter will be handed when time to log comes
+type LogFormatterParams struct {
+	Request    *http.Request
+	URL        url.URL
+	TimeStamp  time.Time
+	StatusCode int
+	Size       int
+}
+
+// LogFormatter gives the signature of the formatter function passed to CustomLoggingHandler
+type LogFormatter func(writer io.Writer, params LogFormatterParams)
+
+// loggingHandler is the http.Handler implementation for LoggingHandlerTo and its
+// friends
+
+type loggingHandler struct {
+	writer    io.Writer
+	handler   http.Handler
+	formatter LogFormatter
+}
+
+func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	t := time.Now()
+	logger, w := makeLogger(w)
+	url := *req.URL
+
+	h.handler.ServeHTTP(w, req)
+	if req.MultipartForm != nil {
+		req.MultipartForm.RemoveAll()
+	}
+
+	params := LogFormatterParams{
+		Request:    req,
+		URL:        url,
+		TimeStamp:  t,
+		StatusCode: logger.Status(),
+		Size:       logger.Size(),
+	}
+
+	h.formatter(h.writer, params)
+}
+
+func makeLogger(w http.ResponseWriter) (*responseLogger, http.ResponseWriter) {
+	logger := &responseLogger{w: w, status: http.StatusOK}
+	return logger, httpsnoop.Wrap(w, httpsnoop.Hooks{
+		Write: func(httpsnoop.WriteFunc) httpsnoop.WriteFunc {
+			return logger.Write
+		},
+		WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
+			return logger.WriteHeader
+		},
+	})
+}
+
+const lowerhex = "0123456789abcdef"
+
+func appendQuoted(buf []byte, s string) []byte {
+	var runeTmp [utf8.UTFMax]byte
+	for width := 0; len(s) > 0; s = s[width:] {
+		r := rune(s[0])
+		width = 1
+		if r >= utf8.RuneSelf {
+			r, width = utf8.DecodeRuneInString(s)
+		}
+		if width == 1 && r == utf8.RuneError {
+			buf = append(buf, `\x`...)
+			buf = append(buf, lowerhex[s[0]>>4])
+			buf = append(buf, lowerhex[s[0]&0xF])
+			continue
+		}
+		if r == rune('"') || r == '\\' { // always backslashed
+			buf = append(buf, '\\')
+			buf = append(buf, byte(r))
+			continue
+		}
+		if strconv.IsPrint(r) {
+			n := utf8.EncodeRune(runeTmp[:], r)
+			buf = append(buf, runeTmp[:n]...)
+			continue
+		}
+		switch r {
+		case '\a':
+			buf = append(buf, `\a`...)
+		case '\b':
+			buf = append(buf, `\b`...)
+		case '\f':
+			buf = append(buf, `\f`...)
+		case '\n':
+			buf = append(buf, `\n`...)
+		case '\r':
+			buf = append(buf, `\r`...)
+		case '\t':
+			buf = append(buf, `\t`...)
+		case '\v':
+			buf = append(buf, `\v`...)
+		default:
+			switch {
+			case r < ' ':
+				buf = append(buf, `\x`...)
+				buf = append(buf, lowerhex[s[0]>>4])
+				buf = append(buf, lowerhex[s[0]&0xF])
+			case r > utf8.MaxRune:
+				r = 0xFFFD
+				fallthrough
+			case r < 0x10000:
+				buf = append(buf, `\u`...)
+				for s := 12; s >= 0; s -= 4 {
+					buf = append(buf, lowerhex[r>>uint(s)&0xF])
+				}
+			default:
+				buf = append(buf, `\U`...)
+				for s := 28; s >= 0; s -= 4 {
+					buf = append(buf, lowerhex[r>>uint(s)&0xF])
+				}
+			}
+		}
+	}
+	return buf
+}
+
+// buildCommonLogLine builds a log entry for req in Apache Common Log Format.
+// ts is the timestamp with which the entry should be logged.
+// status and size are used to provide the response HTTP status and size.
+func buildCommonLogLine(req *http.Request, url url.URL, ts time.Time, status int, size int) []byte {
+	username := "-"
+	if url.User != nil {
+		if name := url.User.Username(); name != "" {
+			username = name
+		}
+	}
+
+	host, _, err := net.SplitHostPort(req.RemoteAddr)
+	if err != nil {
+		host = req.RemoteAddr
+	}
+
+	uri := req.RequestURI
+
+	// Requests using the CONNECT method over HTTP/2.0 must use
+	// the authority field (aka r.Host) to identify the target.
+	// Refer: https://httpwg.github.io/specs/rfc7540.html#CONNECT
+	if req.ProtoMajor == 2 && req.Method == "CONNECT" {
+		uri = req.Host
+	}
+	if uri == "" {
+		uri = url.RequestURI()
+	}
+
+	buf := make([]byte, 0, 3*(len(host)+len(username)+len(req.Method)+len(uri)+len(req.Proto)+50)/2)
+	buf = append(buf, host...)
+	buf = append(buf, " - "...)
+	buf = append(buf, username...)
+	buf = append(buf, " ["...)
+	buf = append(buf, ts.Format("02/Jan/2006:15:04:05 -0700")...)
+	buf = append(buf, `] "`...)
+	buf = append(buf, req.Method...)
+	buf = append(buf, " "...)
+	buf = appendQuoted(buf, uri)
+	buf = append(buf, " "...)
+	buf = append(buf, req.Proto...)
+	buf = append(buf, `" `...)
+	buf = append(buf, strconv.Itoa(status)...)
+	buf = append(buf, " "...)
+	buf = append(buf, strconv.Itoa(size)...)
+	return buf
+}
+
+// writeLog writes a log entry for req to w in Apache Common Log Format.
+// ts is the timestamp with which the entry should be logged.
+// status and size are used to provide the response HTTP status and size.
+func writeLog(writer io.Writer, params LogFormatterParams) {
+	buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
+	buf = append(buf, '\n')
+	writer.Write(buf)
+}
+
+// writeCombinedLog writes a log entry for req to w in Apache Combined Log Format.
+// ts is the timestamp with which the entry should be logged.
+// status and size are used to provide the response HTTP status and size.
+func writeCombinedLog(writer io.Writer, params LogFormatterParams) {
+	buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
+	buf = append(buf, ` "`...)
+	buf = appendQuoted(buf, params.Request.Referer())
+	buf = append(buf, `" "`...)
+	buf = appendQuoted(buf, params.Request.UserAgent())
+	buf = append(buf, '"', '\n')
+	writer.Write(buf)
+}
+
+// CombinedLoggingHandler return a http.Handler that wraps h and logs requests to out in
+// Apache Combined Log Format.
+//
+// See http://httpd.apache.org/docs/2.2/logs.html#combined for a description of this format.
+//
+// LoggingHandler always sets the ident field of the log to -
+func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler {
+	return loggingHandler{out, h, writeCombinedLog}
+}
+
+// LoggingHandler return a http.Handler that wraps h and logs requests to out in
+// Apache Common Log Format (CLF).
+//
+// See http://httpd.apache.org/docs/2.2/logs.html#common for a description of this format.
+//
+// LoggingHandler always sets the ident field of the log to -
+//
+// Example:
+//
+//  r := mux.NewRouter()
+//  r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+//  	w.Write([]byte("This is a catch-all route"))
+//  })
+//  loggedRouter := handlers.LoggingHandler(os.Stdout, r)
+//  http.ListenAndServe(":1123", loggedRouter)
+//
+func LoggingHandler(out io.Writer, h http.Handler) http.Handler {
+	return loggingHandler{out, h, writeLog}
+}
+
+// CustomLoggingHandler provides a way to supply a custom log formatter
+// while taking advantage of the mechanisms in this package
+func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler {
+	return loggingHandler{out, h, f}
+}
diff --git a/vendor/github.com/gorilla/handlers/proxy_headers.go b/vendor/github.com/gorilla/handlers/proxy_headers.go
new file mode 100644
index 0000000000000000000000000000000000000000..ed939dcef5d21fc82067f9d9ffc05a80dfcc67a0
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/proxy_headers.go
@@ -0,0 +1,120 @@
+package handlers
+
+import (
+	"net/http"
+	"regexp"
+	"strings"
+)
+
+var (
+	// De-facto standard header keys.
+	xForwardedFor    = http.CanonicalHeaderKey("X-Forwarded-For")
+	xForwardedHost   = http.CanonicalHeaderKey("X-Forwarded-Host")
+	xForwardedProto  = http.CanonicalHeaderKey("X-Forwarded-Proto")
+	xForwardedScheme = http.CanonicalHeaderKey("X-Forwarded-Scheme")
+	xRealIP          = http.CanonicalHeaderKey("X-Real-IP")
+)
+
+var (
+	// RFC7239 defines a new "Forwarded: " header designed to replace the
+	// existing use of X-Forwarded-* headers.
+	// e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43
+	forwarded = http.CanonicalHeaderKey("Forwarded")
+	// Allows for a sub-match of the first value after 'for=' to the next
+	// comma, semi-colon or space. The match is case-insensitive.
+	forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)`)
+	// Allows for a sub-match for the first instance of scheme (http|https)
+	// prefixed by 'proto='. The match is case-insensitive.
+	protoRegex = regexp.MustCompile(`(?i)(?:proto=)(https|http)`)
+)
+
+// ProxyHeaders inspects common reverse proxy headers and sets the corresponding
+// fields in the HTTP request struct. These are X-Forwarded-For and X-Real-IP
+// for the remote (client) IP address, X-Forwarded-Proto or X-Forwarded-Scheme
+// for the scheme (http|https), X-Forwarded-Host for the host and the RFC7239
+// Forwarded header, which may include both client IPs and schemes.
+//
+// NOTE: This middleware should only be used when behind a reverse
+// proxy like nginx, HAProxy or Apache. Reverse proxies that don't (or are
+// configured not to) strip these headers from client requests, or where these
+// headers are accepted "as is" from a remote client (e.g. when Go is not behind
+// a proxy), can manifest as a vulnerability if your application uses these
+// headers for validating the 'trustworthiness' of a request.
+func ProxyHeaders(h http.Handler) http.Handler {
+	fn := func(w http.ResponseWriter, r *http.Request) {
+		// Set the remote IP with the value passed from the proxy.
+		if fwd := getIP(r); fwd != "" {
+			r.RemoteAddr = fwd
+		}
+
+		// Set the scheme (proto) with the value passed from the proxy.
+		if scheme := getScheme(r); scheme != "" {
+			r.URL.Scheme = scheme
+		}
+		// Set the host with the value passed by the proxy
+		if r.Header.Get(xForwardedHost) != "" {
+			r.Host = r.Header.Get(xForwardedHost)
+		}
+		// Call the next handler in the chain.
+		h.ServeHTTP(w, r)
+	}
+
+	return http.HandlerFunc(fn)
+}
+
+// getIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239
+// Forwarded headers (in that order).
+func getIP(r *http.Request) string {
+	var addr string
+
+	if fwd := r.Header.Get(xForwardedFor); fwd != "" {
+		// Only grab the first (client) address. Note that '192.168.0.1,
+		// 10.1.1.1' is a valid key for X-Forwarded-For where addresses after
+		// the first may represent forwarding proxies earlier in the chain.
+		s := strings.Index(fwd, ", ")
+		if s == -1 {
+			s = len(fwd)
+		}
+		addr = fwd[:s]
+	} else if fwd := r.Header.Get(xRealIP); fwd != "" {
+		// X-Real-IP should only contain one IP address (the client making the
+		// request).
+		addr = fwd
+	} else if fwd := r.Header.Get(forwarded); fwd != "" {
+		// match should contain at least two elements if the protocol was
+		// specified in the Forwarded header. The first element will always be
+		// the 'for=' capture, which we ignore. In the case of multiple IP
+		// addresses (for=8.8.8.8, 8.8.4.4,172.16.1.20 is valid) we only
+		// extract the first, which should be the client IP.
+		if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 {
+			// IPv6 addresses in Forwarded headers are quoted-strings. We strip
+			// these quotes.
+			addr = strings.Trim(match[1], `"`)
+		}
+	}
+
+	return addr
+}
+
+// getScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239
+// Forwarded headers (in that order).
+func getScheme(r *http.Request) string {
+	var scheme string
+
+	// Retrieve the scheme from X-Forwarded-Proto.
+	if proto := r.Header.Get(xForwardedProto); proto != "" {
+		scheme = strings.ToLower(proto)
+	} else if proto = r.Header.Get(xForwardedScheme); proto != "" {
+		scheme = strings.ToLower(proto)
+	} else if proto = r.Header.Get(forwarded); proto != "" {
+		// match should contain at least two elements if the protocol was
+		// specified in the Forwarded header. The first element will always be
+		// the 'proto=' capture, which we ignore. In the case of multiple proto
+		// parameters (invalid) we only extract the first.
+		if match := protoRegex.FindStringSubmatch(proto); len(match) > 1 {
+			scheme = strings.ToLower(match[1])
+		}
+	}
+
+	return scheme
+}
diff --git a/vendor/github.com/gorilla/handlers/recovery.go b/vendor/github.com/gorilla/handlers/recovery.go
new file mode 100644
index 0000000000000000000000000000000000000000..4c4c1d9c6cefe36e43983714f1aed002dc4673ba
--- /dev/null
+++ b/vendor/github.com/gorilla/handlers/recovery.go
@@ -0,0 +1,96 @@
+package handlers
+
+import (
+	"log"
+	"net/http"
+	"runtime/debug"
+)
+
+// RecoveryHandlerLogger is an interface used by the recovering handler to print logs.
+type RecoveryHandlerLogger interface {
+	Println(...interface{})
+}
+
+type recoveryHandler struct {
+	handler    http.Handler
+	logger     RecoveryHandlerLogger
+	printStack bool
+}
+
+// RecoveryOption provides a functional approach to define
+// configuration for a handler; such as setting the logging
+// whether or not to print stack traces on panic.
+type RecoveryOption func(http.Handler)
+
+func parseRecoveryOptions(h http.Handler, opts ...RecoveryOption) http.Handler {
+	for _, option := range opts {
+		option(h)
+	}
+
+	return h
+}
+
+// RecoveryHandler is HTTP middleware that recovers from a panic,
+// logs the panic, writes http.StatusInternalServerError, and
+// continues to the next handler.
+//
+// Example:
+//
+//  r := mux.NewRouter()
+//  r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+//  	panic("Unexpected error!")
+//  })
+//
+//  http.ListenAndServe(":1123", handlers.RecoveryHandler()(r))
+func RecoveryHandler(opts ...RecoveryOption) func(h http.Handler) http.Handler {
+	return func(h http.Handler) http.Handler {
+		r := &recoveryHandler{handler: h}
+		return parseRecoveryOptions(r, opts...)
+	}
+}
+
+// RecoveryLogger is a functional option to override
+// the default logger
+func RecoveryLogger(logger RecoveryHandlerLogger) RecoveryOption {
+	return func(h http.Handler) {
+		r := h.(*recoveryHandler)
+		r.logger = logger
+	}
+}
+
+// PrintRecoveryStack is a functional option to enable
+// or disable printing stack traces on panic.
+func PrintRecoveryStack(print bool) RecoveryOption {
+	return func(h http.Handler) {
+		r := h.(*recoveryHandler)
+		r.printStack = print
+	}
+}
+
+func (h recoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	defer func() {
+		if err := recover(); err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+			h.log(err)
+		}
+	}()
+
+	h.handler.ServeHTTP(w, req)
+}
+
+func (h recoveryHandler) log(v ...interface{}) {
+	if h.logger != nil {
+		h.logger.Println(v...)
+	} else {
+		log.Println(v...)
+	}
+
+	if h.printStack {
+		stack := string(debug.Stack())
+		if h.logger != nil {
+			h.logger.Println(stack)
+		} else {
+			log.Println(stack)
+		}
+	}
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index af80e8145e7b2fe7bff2f60eb078ffe478d631bc..e9d2d3abb6f0f722d69decf96b62e49d13a168ef 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -17,6 +17,8 @@ github.com/dustin/go-humanize
 # github.com/elazarl/go-bindata-assetfs v1.0.1
 ## explicit
 github.com/elazarl/go-bindata-assetfs
+# github.com/felixge/httpsnoop v1.0.1
+github.com/felixge/httpsnoop
 # github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
 github.com/ghodss/yaml
 # github.com/gogo/protobuf v1.2.1
@@ -48,6 +50,9 @@ github.com/google/btree
 github.com/google/subcommands
 # github.com/google/uuid v1.0.0
 github.com/google/uuid
+# github.com/gorilla/handlers v1.5.1
+## explicit
+github.com/gorilla/handlers
 # github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c
 github.com/gorilla/websocket
 # github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4