diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/http.go b/vendor/git.autistici.org/ai3/go-common/serverutil/http.go
new file mode 100644
index 0000000000000000000000000000000000000000..b1d4b94968edc27e116836d981440d24ebeae60e
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/serverutil/http.go
@@ -0,0 +1,118 @@
+package serverutil
+
+import (
+	"context"
+	"crypto/tls"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+var gracefulShutdownTimeout = 3 * time.Second
+
+// ServerConfig stores common HTTP/HTTPS server configuration parameters.
+type ServerConfig struct {
+	TLS                 *TLSServerConfig `yaml:"tls"`
+	MaxInflightRequests int              `yaml:"max_inflight_requests"`
+}
+
+// Serve HTTP(S) content on the specified address. If serverConfig is
+// not nil, enable HTTPS and TLS authentication.
+//
+// This function will return an error if there are problems creating
+// the listener, otherwise it will handle graceful termination on
+// SIGINT or SIGTERM and return nil.
+func Serve(h http.Handler, serverConfig *ServerConfig, addr string) (err error) {
+	var tlsConfig *tls.Config
+	if serverConfig != nil {
+		if serverConfig.TLS != nil {
+			tlsConfig, err = serverConfig.TLS.TLSConfig()
+			if err != nil {
+				return err
+			}
+			h, err = serverConfig.TLS.TLSAuthWrapper(h)
+			if err != nil {
+				return err
+			}
+		}
+
+		if serverConfig.MaxInflightRequests > 0 {
+			h = newLoadSheddingWrapper(serverConfig.MaxInflightRequests, h)
+		}
+	}
+
+	// These are not meant to be external-facing servers, so we
+	// can be generous with the timeouts to keep the number of
+	// reconnections low.
+	srv := &http.Server{
+		Addr:         addr,
+		Handler:      instrumentHandler(h),
+		ReadTimeout:  30 * time.Second,
+		WriteTimeout: 30 * time.Second,
+		IdleTimeout:  600 * time.Second,
+		TLSConfig:    tlsConfig,
+	}
+
+	// Install a signal handler for gentle process termination.
+	done := make(chan struct{})
+	sigCh := make(chan os.Signal, 1)
+	go func() {
+		<-sigCh
+		log.Printf("exiting")
+
+		// Gracefully terminate for 3 seconds max, then shut
+		// down remaining clients.
+		ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout)
+		defer cancel()
+		if err := srv.Shutdown(ctx); err == context.Canceled {
+			if err := srv.Close(); err != nil {
+				log.Printf("error terminating server: %v", err)
+			}
+		}
+
+		close(done)
+	}()
+	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+
+	if err := srv.ListenAndServe(); err != http.ErrServerClosed {
+		return err
+	}
+
+	<-done
+	return nil
+}
+
+func instrumentHandler(h http.Handler) http.Handler {
+	root := http.NewServeMux()
+	root.Handle("/metrics", promhttp.Handler())
+	root.Handle("/", h)
+	return promhttp.InstrumentHandlerInFlight(inFlightRequests,
+		promhttp.InstrumentHandlerCounter(totalRequests, root))
+}
+
+// HTTP-related metrics.
+var (
+	totalRequests = prometheus.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "total_requests",
+			Help: "Total number of requests.",
+		},
+		[]string{"code"},
+	)
+	inFlightRequests = prometheus.NewGauge(
+		prometheus.GaugeOpts{
+			Name: "inflight_requests",
+			Help: "Number of in-flight requests.",
+		},
+	)
+)
+
+func init() {
+	prometheus.MustRegister(totalRequests, inFlightRequests)
+}
diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/json.go b/vendor/git.autistici.org/ai3/go-common/serverutil/json.go
new file mode 100644
index 0000000000000000000000000000000000000000..b307932eb878197d49c945e51e5f42adb240747e
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/serverutil/json.go
@@ -0,0 +1,37 @@
+package serverutil
+
+import (
+	"encoding/json"
+	"net/http"
+)
+
+// DecodeJSONRequest decodes a JSON object from an incoming HTTP POST
+// request and return true when successful. In case of errors, it will
+// write an error response to w and return false.
+func DecodeJSONRequest(w http.ResponseWriter, r *http.Request, obj interface{}) bool {
+	if r.Method != "POST" {
+		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+		return false
+	}
+	if r.Header.Get("Content-Type") != "application/json" {
+		http.Error(w, "Need JSON request", http.StatusBadRequest)
+		return false
+	}
+
+	if err := json.NewDecoder(r.Body).Decode(obj); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return false
+	}
+
+	return true
+}
+
+// EncodeJSONResponse writes an application/json response to w.
+func EncodeJSONResponse(w http.ResponseWriter, obj interface{}) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Pragma", "no-cache")
+	w.Header().Set("Cache-Control", "no-store")
+	w.Header().Set("Expires", "-1")
+	w.Header().Set("X-Content-Type-Options", "nosniff")
+	_ = json.NewEncoder(w).Encode(obj)
+}
diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/load_shedding.go b/vendor/git.autistici.org/ai3/go-common/serverutil/load_shedding.go
new file mode 100644
index 0000000000000000000000000000000000000000..beb2ae00d04b988e000392a2931f951d4772dfc8
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/serverutil/load_shedding.go
@@ -0,0 +1,51 @@
+package serverutil
+
+import (
+	"net/http"
+	"sync/atomic"
+
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+type loadSheddingWrapper struct {
+	limit, inflight int32
+	h               http.Handler
+}
+
+func newLoadSheddingWrapper(limit int, h http.Handler) *loadSheddingWrapper {
+	return &loadSheddingWrapper{limit: int32(limit), h: h}
+}
+
+func (l *loadSheddingWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	inflight := atomic.AddInt32(&l.inflight, 1)
+	defer atomic.AddInt32(&l.inflight, -1)
+
+	if inflight > l.limit {
+		throttledRequests.Inc()
+		w.Header().Set("Connection", "close")
+		http.Error(w, "Throttled", http.StatusTooManyRequests)
+		return
+	}
+
+	allowedRequests.Inc()
+	l.h.ServeHTTP(w, r)
+}
+
+var (
+	throttledRequests = prometheus.NewCounter(
+		prometheus.CounterOpts{
+			Name: "ls_throttled_requests",
+			Help: "Requests throttled by the load shedding wrapper.",
+		},
+	)
+	allowedRequests = prometheus.NewCounter(
+		prometheus.CounterOpts{
+			Name: "ls_allowed_requests",
+			Help: "Requests allowed by the load shedding wrapper.",
+		},
+	)
+)
+
+func init() {
+	prometheus.MustRegister(throttledRequests, allowedRequests)
+}
diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/tls.go b/vendor/git.autistici.org/ai3/go-common/serverutil/tls.go
new file mode 100644
index 0000000000000000000000000000000000000000..926488f4c6e566a828021c775faff3529c8bc168
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/serverutil/tls.go
@@ -0,0 +1,119 @@
+package serverutil
+
+import (
+	"crypto/tls"
+	"net/http"
+	"regexp"
+
+	common "git.autistici.org/ai3/go-common"
+)
+
+// TLSAuthACL describes a single access control entry. Path and
+// CommonName are anchored regular expressions (they must match the
+// entire string).
+type TLSAuthACL struct {
+	Path       string `yaml:"path"`
+	CommonName string `yaml:"cn"`
+
+	pathRx, cnRx *regexp.Regexp
+}
+
+func (p *TLSAuthACL) compile() error {
+	var err error
+	p.pathRx, err = regexp.Compile("^" + p.Path + "$")
+	if err != nil {
+		return err
+	}
+	p.cnRx, err = regexp.Compile("^" + p.CommonName + "$")
+	return err
+}
+
+func (p *TLSAuthACL) match(req *http.Request) bool {
+	if !p.pathRx.MatchString(req.URL.Path) {
+		return false
+	}
+	for _, cert := range req.TLS.PeerCertificates {
+		if p.cnRx.MatchString(cert.Subject.CommonName) {
+			return true
+		}
+	}
+	return false
+}
+
+// TLSAuthConfig stores access control lists for TLS authentication. Access
+// control lists are matched against the request path and the
+// CommonName component of the peer certificate subject.
+type TLSAuthConfig struct {
+	Allow []*TLSAuthACL `yaml:"allow"`
+}
+
+func (c *TLSAuthConfig) match(req *http.Request) bool {
+	// Fail *OPEN* if unconfigured.
+	if c == nil || len(c.Allow) == 0 {
+		return true
+	}
+	for _, acl := range c.Allow {
+		if acl.match(req) {
+			return true
+		}
+	}
+	return false
+}
+
+// TLSServerConfig configures a TLS server with client authentication
+// and authorization based on the client X509 certificate.
+type TLSServerConfig struct {
+	Cert string         `yaml:"cert"`
+	Key  string         `yaml:"key"`
+	CA   string         `yaml:"ca"`
+	Auth *TLSAuthConfig `yaml:"acl"`
+}
+
+// TLSConfig returns a tls.Config created with the current configuration.
+func (c *TLSServerConfig) TLSConfig() (*tls.Config, error) {
+	cert, err := tls.LoadX509KeyPair(c.Cert, c.Key)
+	if err != nil {
+		return nil, err
+	}
+
+	cas, err := common.LoadCA(c.CA)
+	if err != nil {
+		return nil, err
+	}
+
+	// Set some TLS-level parameters (cipher-related), assuming
+	// we're using EC keys.
+	tlsConf := &tls.Config{
+		Certificates:             []tls.Certificate{cert},
+		ClientAuth:               tls.RequireAndVerifyClientCert,
+		ClientCAs:                cas,
+		CipherSuites:             []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384},
+		MinVersion:               tls.VersionTLS12,
+		PreferServerCipherSuites: true,
+	}
+	tlsConf.BuildNameToCertificate()
+
+	return tlsConf, nil
+}
+
+// TLSAuthWrapper protects a root HTTP handler with TLS authentication.
+func (c *TLSServerConfig) TLSAuthWrapper(h http.Handler) (http.Handler, error) {
+	// Compile regexps.
+	if c.Auth != nil {
+		for _, acl := range c.Auth.Allow {
+			if err := acl.compile(); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	// Build the wrapper function to check client certificates
+	// identities (looking at the CN part of the X509 subject).
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if c.Auth.match(r) {
+			h.ServeHTTP(w, r)
+			return
+		}
+		http.Error(w, "Unauthorized", http.StatusUnauthorized)
+	}), nil
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index be247eafb0c1f311d181daa8f1d47e6477cc98e7..248c6cb957d05515cdf8df0ec071d3a4c7218557 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -14,6 +14,12 @@
 			"revision": "245211ebe9f881b575461958274bada3b8e20b7b",
 			"revisionTime": "2017-11-23T18:34:19Z"
 		},
+		{
+			"checksumSHA1": "3bComZxAfgnoTG4UDlyFgLyeykc=",
+			"path": "git.autistici.org/ai3/go-common/serverutil",
+			"revision": "96dc550223598dd5d984bb5fc222323ef239bed7",
+			"revisionTime": "2017-12-09T10:27:16Z"
+		},
 		{
 			"checksumSHA1": "hJvRJwSx9aZUKF26o/gOmgUJSsE=",
 			"path": "git.autistici.org/id/auth",