From 989b5d38631d3ef7004256d8e17d314219024bba Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Fri, 17 Aug 2018 07:24:02 +0100
Subject: [PATCH] Update clientutil.Backend interface

---
 client/client.go                              |   6 +-
 .../ai3/go-common/clientutil/backend.go       | 111 ++-----
 .../ai3/go-common/clientutil/balancer.go      | 271 ++++++++++++++++++
 .../ai3/go-common/clientutil/cpu.prof         | Bin 0 -> 4566 bytes
 .../ai3/go-common/clientutil/dns.go           |  98 +++++++
 .../ai3/go-common/clientutil/doc.go           |  37 +++
 .../ai3/go-common/clientutil/error.go         |  35 +++
 .../ai3/go-common/clientutil/json.go          |  45 ---
 .../ai3/go-common/clientutil/retry.go         |  92 ------
 .../ai3/go-common/clientutil/track.go         | 123 ++++++++
 .../ai3/go-common/clientutil/transport.go     | 175 +++--------
 .../ai3/go-common/ldap/parse.go               |   2 +
 .../ai3/go-common/ldap/pool.go                |  76 +++++
 .../ai3/go-common/ldap/search.go              |  63 ----
 .../ai3/go-common/serverutil/http.go          | 103 +++++--
 .../ai3/go-common/serverutil/json.go          |  12 +-
 .../ai3/go-common/serverutil/proxy_headers.go |  78 +++++
 vendor/git.autistici.org/id/go-sso/README.md  |  20 ++
 vendor/github.com/cenkalti/backoff/README.md  |   2 +-
 vendor/github.com/cenkalti/backoff/context.go |   3 +-
 vendor/github.com/cenkalti/backoff/retry.go   |   1 -
 vendor/github.com/cenkalti/backoff/ticker.go  |   2 -
 vendor/github.com/cenkalti/backoff/tries.go   |   4 +-
 .../miscreant/miscreant/go/block/block.go     |   2 +-
 .../miscreant/miscreant/go/pmac/pmac.go       |  14 +-
 .../prometheus/common/model/time.go           |   3 +
 vendor/github.com/theckman/go-flock/README.md |   6 +-
 vendor/github.com/theckman/go-flock/flock.go  |  21 ++
 vendor/golang.org/x/net/context/context.go    |   2 +
 vendor/gopkg.in/yaml.v2/LICENSE               | 208 +-------------
 vendor/vendor.json                            | 126 ++++----
 31 files changed, 992 insertions(+), 749 deletions(-)
 create mode 100644 vendor/git.autistici.org/ai3/go-common/clientutil/balancer.go
 create mode 100644 vendor/git.autistici.org/ai3/go-common/clientutil/cpu.prof
 create mode 100644 vendor/git.autistici.org/ai3/go-common/clientutil/dns.go
 create mode 100644 vendor/git.autistici.org/ai3/go-common/clientutil/doc.go
 create mode 100644 vendor/git.autistici.org/ai3/go-common/clientutil/error.go
 delete mode 100644 vendor/git.autistici.org/ai3/go-common/clientutil/json.go
 delete mode 100644 vendor/git.autistici.org/ai3/go-common/clientutil/retry.go
 create mode 100644 vendor/git.autistici.org/ai3/go-common/clientutil/track.go
 delete mode 100644 vendor/git.autistici.org/ai3/go-common/ldap/search.go
 create mode 100644 vendor/git.autistici.org/ai3/go-common/serverutil/proxy_headers.go

diff --git a/client/client.go b/client/client.go
index 16787db7..307f3eeb 100644
--- a/client/client.go
+++ b/client/client.go
@@ -36,7 +36,7 @@ func (c *ksClient) Open(ctx context.Context, shard, username, password string, t
 		TTL:      ttl,
 	}
 	var resp keystore.OpenResponse
-	return clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/open", &req, &resp)
+	return c.be.Call(ctx, shard, "/api/open", &req, &resp)
 }
 
 func (c *ksClient) Get(ctx context.Context, shard, username, ssoTicket string) ([]byte, error) {
@@ -45,7 +45,7 @@ func (c *ksClient) Get(ctx context.Context, shard, username, ssoTicket string) (
 		SSOTicket: ssoTicket,
 	}
 	var resp keystore.GetResponse
-	err := clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/get_key", &req, &resp)
+	err := c.be.Call(ctx, shard, "/api/get_key", &req, &resp)
 	return resp.Key, err
 }
 
@@ -54,5 +54,5 @@ func (c *ksClient) Close(ctx context.Context, shard, username string) error {
 		Username: username,
 	}
 	var resp keystore.CloseResponse
-	return clientutil.DoJSONHTTPRequest(ctx, c.be.Client(shard), c.be.URL(shard)+"/api/close", &req, &resp)
+	return c.be.Call(ctx, shard, "/api/close", &req, &resp)
 }
diff --git a/vendor/git.autistici.org/ai3/go-common/clientutil/backend.go b/vendor/git.autistici.org/ai3/go-common/clientutil/backend.go
index 1f49a7e5..6580d0eb 100644
--- a/vendor/git.autistici.org/ai3/go-common/clientutil/backend.go
+++ b/vendor/git.autistici.org/ai3/go-common/clientutil/backend.go
@@ -1,15 +1,10 @@
 package clientutil
 
 import (
-	"crypto/tls"
-	"fmt"
-	"net/http"
-	"net/url"
-	"sync"
-	"time"
+	"context"
 )
 
-// BackendConfig specifies the configuration to access a service.
+// BackendConfig specifies the configuration of a service backend.
 //
 // Services with multiple backends can be replicated or partitioned,
 // depending on a configuration switch, making it a deployment-time
@@ -18,102 +13,30 @@ import (
 // 'shard' parameter on their APIs.
 type BackendConfig struct {
 	URL       string           `yaml:"url"`
-	Sharded   bool             `yaml:"sharded"`
 	TLSConfig *TLSClientConfig `yaml:"tls_config"`
+	Sharded   bool             `yaml:"sharded"`
+	Debug     bool             `yaml:"debug"`
 }
 
 // Backend is a runtime class that provides http Clients for use with
 // a specific service backend. If the service can't be partitioned,
-// pass an empty string to the Client method.
+// pass an empty string to the Call method.
 type Backend interface {
-	// URL for the service for a specific shard.
-	URL(string) string
+	// Call a remote method. The sharding behavior is the following:
+	//
+	// Services that support sharding (partitioning) should always
+	// include the shard ID in their Call() requests. Users can
+	// then configure backends to be sharded or not in their
+	// Config. When invoking Call with a shard ID on a non-sharded
+	// service, the shard ID is simply ignored. Invoking Call
+	// *without* a shard ID on a sharded service is an error.
+	Call(context.Context, string, string, interface{}, interface{}) error
 
-	// Client that can be used to make a request to the service.
-	Client(string) *http.Client
+	// Close all resources associated with the backend.
+	Close()
 }
 
 // NewBackend returns a new Backend with the given config.
 func NewBackend(config *BackendConfig) (Backend, error) {
-	u, err := url.Parse(config.URL)
-	if err != nil {
-		return nil, err
-	}
-
-	var tlsConfig *tls.Config
-	if config.TLSConfig != nil {
-		tlsConfig, err = config.TLSConfig.TLSConfig()
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	if config.Sharded {
-		return &replicatedClient{
-			u: u,
-			c: newHTTPClient(u, tlsConfig),
-		}, nil
-	}
-	return &shardedClient{
-		baseURL:   u,
-		tlsConfig: tlsConfig,
-		urls:      make(map[string]*url.URL),
-		shards:    make(map[string]*http.Client),
-	}, nil
-}
-
-type replicatedClient struct {
-	c *http.Client
-	u *url.URL
-}
-
-func (r *replicatedClient) Client(_ string) *http.Client { return r.c }
-func (r *replicatedClient) URL(_ string) string          { return r.u.String() }
-
-type shardedClient struct {
-	baseURL   *url.URL
-	tlsConfig *tls.Config
-	mx        sync.Mutex
-	urls      map[string]*url.URL
-	shards    map[string]*http.Client
-}
-
-func (s *shardedClient) getShardURL(shard string) *url.URL {
-	if shard == "" {
-		return s.baseURL
-	}
-	u, ok := s.urls[shard]
-	if !ok {
-		var tmp = *s.baseURL
-		tmp.Host = fmt.Sprintf("%s.%s", shard, tmp.Host)
-		u = &tmp
-		s.urls[shard] = u
-	}
-	return u
-}
-
-func (s *shardedClient) URL(shard string) string {
-	s.mx.Lock()
-	defer s.mx.Unlock()
-	return s.getShardURL(shard).String()
-}
-
-func (s *shardedClient) Client(shard string) *http.Client {
-	s.mx.Lock()
-	defer s.mx.Unlock()
-
-	client, ok := s.shards[shard]
-	if !ok {
-		u := s.getShardURL(shard)
-		client = newHTTPClient(u, s.tlsConfig)
-		s.shards[shard] = client
-	}
-	return client
-}
-
-func newHTTPClient(u *url.URL, tlsConfig *tls.Config) *http.Client {
-	return &http.Client{
-		Transport: NewTransport([]string{u.Host}, tlsConfig, nil),
-		Timeout:   30 * time.Second,
-	}
+	return newBalancedBackend(config, defaultResolver)
 }
diff --git a/vendor/git.autistici.org/ai3/go-common/clientutil/balancer.go b/vendor/git.autistici.org/ai3/go-common/clientutil/balancer.go
new file mode 100644
index 00000000..9d9b7dd6
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/clientutil/balancer.go
@@ -0,0 +1,271 @@
+package clientutil
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"log"
+	"math/rand"
+	"net/http"
+	"net/url"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/cenkalti/backoff"
+)
+
+// Our own narrow logger interface.
+type logger interface {
+	Printf(string, ...interface{})
+}
+
+// A nilLogger is used when Config.Debug is false.
+type nilLogger struct{}
+
+func (l nilLogger) Printf(_ string, _ ...interface{}) {}
+
+// Parameters that define the exponential backoff algorithm used.
+var (
+	ExponentialBackOffInitialInterval = 100 * time.Millisecond
+	ExponentialBackOffMultiplier      = 1.4142
+)
+
+// newExponentialBackOff creates a backoff.ExponentialBackOff object
+// with our own default values.
+func newExponentialBackOff() *backoff.ExponentialBackOff {
+	b := backoff.NewExponentialBackOff()
+	b.InitialInterval = ExponentialBackOffInitialInterval
+	b.Multiplier = ExponentialBackOffMultiplier
+
+	// Set MaxElapsedTime to 0 because we expect the overall
+	// timeout to be dictated by the request Context.
+	b.MaxElapsedTime = 0
+
+	return b
+}
+
+// Balancer for HTTP connections. It will round-robin across available
+// backends, trying to avoid ones that are erroring out, until one
+// succeeds or returns a permanent error.
+//
+// This object should not be used for load balancing of individual
+// HTTP requests: it doesn't do anything smart beyond trying to avoid
+// broken targets. It's meant to provide a *reliable* connection to a
+// set of equivalent services for HA purposes.
+type balancedBackend struct {
+	*backendTracker
+	*transportCache
+	baseURI  *url.URL
+	sharded  bool
+	resolver resolver
+	log      logger
+}
+
+func newBalancedBackend(config *BackendConfig, resolver resolver) (*balancedBackend, error) {
+	u, err := url.Parse(config.URL)
+	if err != nil {
+		return nil, err
+	}
+
+	var tlsConfig *tls.Config
+	if config.TLSConfig != nil {
+		tlsConfig, err = config.TLSConfig.TLSConfig()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	var logger logger = &nilLogger{}
+	if config.Debug {
+		logger = log.New(os.Stderr, fmt.Sprintf("backend %s: ", u.Host), 0)
+	}
+	return &balancedBackend{
+		backendTracker: newBackendTracker(u.Host, resolver, logger),
+		transportCache: newTransportCache(tlsConfig),
+		sharded:        config.Sharded,
+		baseURI:        u,
+		resolver:       resolver,
+		log:            logger,
+	}, nil
+}
+
+// Call the backend. Makes an HTTP POST request to the specified uri,
+// with a JSON-encoded request body. It will attempt to decode the
+// response body as JSON.
+func (b *balancedBackend) Call(ctx context.Context, shard, path string, req, resp interface{}) error {
+	data, err := json.Marshal(req)
+	if err != nil {
+		return err
+	}
+
+	var tg targetGenerator = b.backendTracker
+	if b.sharded && shard != "" {
+		tg = newShardedGenerator(shard, b.baseURI.Host, b.resolver)
+	}
+	seq := newSequence(tg)
+	b.log.Printf("%016x: initialized", seq.ID())
+
+	var httpResp *http.Response
+	err = backoff.Retry(func() error {
+		req, rerr := b.newJSONRequest(path, shard, data)
+		if rerr != nil {
+			return rerr
+		}
+		httpResp, rerr = b.do(ctx, seq, req)
+		return rerr
+	}, backoff.WithContext(newExponentialBackOff(), ctx))
+	if err != nil {
+		return err
+	}
+	defer httpResp.Body.Close() // nolint
+
+	if httpResp.Header.Get("Content-Type") != "application/json" {
+		return errors.New("not a JSON response")
+	}
+
+	if resp == nil {
+		return nil
+	}
+	return json.NewDecoder(httpResp.Body).Decode(resp)
+}
+
+// Return the URI to be used for the request. This is used both in the
+// Host HTTP header and as the TLS server name used to pick a server
+// certificate (if using TLS).
+func (b *balancedBackend) getURIForRequest(shard, path string) string {
+	u := *b.baseURI
+	if b.sharded && shard != "" {
+		u.Host = fmt.Sprintf("%s.%s", shard, u.Host)
+	}
+	u.Path = appendPath(u.Path, path)
+	return u.String()
+}
+
+// Build a http.Request object.
+func (b *balancedBackend) newJSONRequest(path, shard string, data []byte) (*http.Request, error) {
+	req, err := http.NewRequest("POST", b.getURIForRequest(shard, path), bytes.NewReader(data))
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Content-Length", strconv.FormatInt(int64(len(data)), 10))
+	return req, nil
+}
+
+// Select a new target from the given sequence and send the request to
+// it. Wrap HTTP errors in a RemoteError object.
+func (b *balancedBackend) do(ctx context.Context, seq *sequence, req *http.Request) (resp *http.Response, err error) {
+	target, terr := seq.Next()
+	if terr != nil {
+		return
+	}
+
+	b.log.Printf("sequence %016x: connecting to %s", seq.ID(), target)
+	client := &http.Client{
+		Transport: b.transportCache.getTransport(target),
+	}
+	resp, err = client.Do(req.WithContext(ctx))
+	if err == nil && resp.StatusCode != 200 {
+		err = remoteErrorFromResponse(resp)
+		if !isStatusTemporary(resp.StatusCode) {
+			err = backoff.Permanent(err)
+		}
+		resp.Body.Close() // nolint
+		resp = nil
+	}
+
+	seq.Done(target, err)
+	return
+}
+
+var errNoTargets = errors.New("no available backends")
+
+type targetGenerator interface {
+	getTargets() []string
+	setStatus(string, bool)
+}
+
+// A replicatedSequence repeatedly iterates over available backends in order of
+// preference. Once in a while it refreshes its list of available
+// targets.
+type sequence struct {
+	id      uint64
+	tg      targetGenerator
+	targets []string
+	pos     int
+}
+
+func newSequence(tg targetGenerator) *sequence {
+	return &sequence{
+		id:      rand.Uint64(),
+		tg:      tg,
+		targets: tg.getTargets(),
+	}
+}
+
+func (s *sequence) ID() uint64 { return s.id }
+
+func (s *sequence) reloadTargets() {
+	targets := s.tg.getTargets()
+	if len(targets) > 0 {
+		s.targets = targets
+		s.pos = 0
+	}
+}
+
+// Next returns the next target.
+func (s *sequence) Next() (t string, err error) {
+	if s.pos >= len(s.targets) {
+		s.reloadTargets()
+		if len(s.targets) == 0 {
+			err = errNoTargets
+			return
+		}
+	}
+	t = s.targets[s.pos]
+	s.pos++
+	return
+}
+
+func (s *sequence) Done(t string, err error) {
+	s.tg.setStatus(t, err == nil)
+}
+
+// A shardedGenerator returns a single sharded target to a sequence.
+type shardedGenerator struct {
+	id    uint64
+	addrs []string
+}
+
+func newShardedGenerator(shard, base string, resolver resolver) *shardedGenerator {
+	return &shardedGenerator{
+		id:    rand.Uint64(),
+		addrs: resolver.ResolveIP(fmt.Sprintf("%s.%s", shard, base)),
+	}
+}
+
+func (g *shardedGenerator) getTargets() []string       { return g.addrs }
+func (g *shardedGenerator) setStatus(_ string, _ bool) {}
+
+// Concatenate two URI paths.
+func appendPath(a, b string) string {
+	if strings.HasSuffix(a, "/") && strings.HasPrefix(b, "/") {
+		return a + b[1:]
+	}
+	return a + b
+}
+
+// Some HTTP status codes are treated are temporary errors.
+func isStatusTemporary(code int) bool {
+	switch code {
+	case http.StatusTooManyRequests, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
+		return true
+	default:
+		return false
+	}
+}
diff --git a/vendor/git.autistici.org/ai3/go-common/clientutil/cpu.prof b/vendor/git.autistici.org/ai3/go-common/clientutil/cpu.prof
new file mode 100644
index 0000000000000000000000000000000000000000..89fe0a7d104f6c1ca9466d218abbfe393d431402
GIT binary patch
literal 4566
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

literal 0
HcmV?d00001

diff --git a/vendor/git.autistici.org/ai3/go-common/clientutil/dns.go b/vendor/git.autistici.org/ai3/go-common/clientutil/dns.go
new file mode 100644
index 00000000..ed30f873
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/clientutil/dns.go
@@ -0,0 +1,98 @@
+package clientutil
+
+import (
+	"log"
+	"net"
+	"sync"
+	"time"
+
+	"golang.org/x/sync/singleflight"
+)
+
+type resolver interface {
+	ResolveIP(string) []string
+}
+
+type dnsResolver struct{}
+
+func (r *dnsResolver) ResolveIP(hostport string) []string {
+	var resolved []string
+	host, port, err := net.SplitHostPort(hostport)
+	if err != nil {
+		log.Printf("error parsing %s: %v", hostport, err)
+		return nil
+	}
+	hostIPs, err := net.LookupIP(host)
+	if err != nil {
+		log.Printf("error resolving %s: %v", host, err)
+		return nil
+	}
+	for _, ip := range hostIPs {
+		resolved = append(resolved, net.JoinHostPort(ip.String(), port))
+	}
+	return resolved
+}
+
+var defaultResolver = newDNSCache(&dnsResolver{})
+
+type cacheDatum struct {
+	addrs    []string
+	deadline time.Time
+}
+
+type dnsCache struct {
+	resolver resolver
+	sf       singleflight.Group
+	mx       sync.RWMutex
+	cache    map[string]cacheDatum
+}
+
+func newDNSCache(resolver resolver) *dnsCache {
+	return &dnsCache{
+		resolver: resolver,
+		cache:    make(map[string]cacheDatum),
+	}
+}
+
+func (c *dnsCache) get(host string) ([]string, bool) {
+	d, ok := c.cache[host]
+	if !ok {
+		return nil, false
+	}
+	return d.addrs, d.deadline.After(time.Now())
+}
+
+func (c *dnsCache) update(host string) []string {
+	v, _, _ := c.sf.Do(host, func() (interface{}, error) {
+		addrs := c.resolver.ResolveIP(host)
+		// By uncommenting this, we stop caching negative results.
+		// if len(addrs) == 0 {
+		// 	return nil, nil
+		// }
+		c.mx.Lock()
+		c.cache[host] = cacheDatum{
+			addrs:    addrs,
+			deadline: time.Now().Add(60 * time.Second),
+		}
+		c.mx.Unlock()
+		return addrs, nil
+	})
+	return v.([]string)
+}
+
+func (c *dnsCache) ResolveIP(host string) []string {
+	c.mx.RLock()
+	addrs, ok := c.get(host)
+	c.mx.RUnlock()
+
+	if ok {
+		return addrs
+	}
+
+	if len(addrs) > 0 {
+		go c.update(host)
+		return addrs
+	}
+
+	return c.update(host)
+}
diff --git a/vendor/git.autistici.org/ai3/go-common/clientutil/doc.go b/vendor/git.autistici.org/ai3/go-common/clientutil/doc.go
new file mode 100644
index 00000000..421915b6
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/clientutil/doc.go
@@ -0,0 +1,37 @@
+// Package clientutil implements a very simple style of JSON RPC.
+//
+// Requests and responses are both encoded in JSON, and they should
+// have the "application/json" Content-Type.
+//
+// HTTP response statuses other than 200 indicate an error: in this
+// case, the response body may contain (in plain text) further details
+// about the error. Some HTTP status codes are considered temporary
+// errors (incl. 429 for throttling). The client will retry requests,
+// if targets are available, until the context expires - so it's quite
+// important to remember to set a timeout on the context given to the
+// Call() function!
+//
+// The client handles both replicated services and sharded
+// (partitioned) services. Users of this package that want to support
+// sharded deployments are supposed to pass a shard ID to every
+// Call(). At the deployment stage, sharding can be enabled via the
+// configuration.
+//
+// For replicated services, the client will expect the provided
+// hostname to resolve to one or more IP addresses, in which case it
+// will pick a random IP address on every new request, while
+// remembering which addresses have had errors and trying to avoid
+// them. It will however send an occasional request to the failed
+// targets, to see if they've come back.
+//
+// For sharded services, the client makes simple HTTP requests to the
+// specific target identified by the shard. It does this by prepending
+// the shard ID to the backend hostname (so a request to "example.com"
+// with shard ID "1" becomes a request to "1.example.com").
+//
+// The difference with other JSON-RPC implementations is that we use a
+// different URI for every method, and we force the usage of
+// request/response types. This makes it easy for projects to
+// eventually migrate to GRPC.
+//
+package clientutil
diff --git a/vendor/git.autistici.org/ai3/go-common/clientutil/error.go b/vendor/git.autistici.org/ai3/go-common/clientutil/error.go
new file mode 100644
index 00000000..f011e162
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/clientutil/error.go
@@ -0,0 +1,35 @@
+package clientutil
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+)
+
+// RemoteError represents a HTTP error from the server. The status
+// code and response body can be retrieved with the StatusCode() and
+// Body() methods.
+type RemoteError struct {
+	statusCode int
+	body       string
+}
+
+func remoteErrorFromResponse(resp *http.Response) *RemoteError {
+	// Optimistically read the response body, ignoring errors.
+	var body string
+	if data, err := ioutil.ReadAll(resp.Body); err == nil {
+		body = string(data)
+	}
+	return &RemoteError{statusCode: resp.StatusCode, body: body}
+}
+
+// Error implements the error interface.
+func (e *RemoteError) Error() string {
+	return fmt.Sprintf("%d - %s", e.statusCode, e.body)
+}
+
+// StatusCode returns the HTTP status code.
+func (e *RemoteError) StatusCode() int { return e.statusCode }
+
+// Body returns the response body.
+func (e *RemoteError) Body() string { return e.body }
diff --git a/vendor/git.autistici.org/ai3/go-common/clientutil/json.go b/vendor/git.autistici.org/ai3/go-common/clientutil/json.go
deleted file mode 100644
index 5fc1ab2e..00000000
--- a/vendor/git.autistici.org/ai3/go-common/clientutil/json.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package clientutil
-
-import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"net/http"
-)
-
-// DoJSONHTTPRequest makes an HTTP POST request to the specified uri,
-// with a JSON-encoded request body. It will attempt to decode the
-// response body as JSON.
-func DoJSONHTTPRequest(ctx context.Context, client *http.Client, uri string, req, resp interface{}) error {
-	data, err := json.Marshal(req)
-	if err != nil {
-		return err
-	}
-
-	httpReq, err := http.NewRequest("POST", uri, bytes.NewReader(data))
-	if err != nil {
-		return err
-	}
-	httpReq.Header.Set("Content-Type", "application/json")
-	httpReq = httpReq.WithContext(ctx)
-
-	httpResp, err := RetryHTTPDo(client, httpReq, NewExponentialBackOff())
-	if err != nil {
-		return err
-	}
-	defer httpResp.Body.Close()
-
-	if httpResp.StatusCode != 200 {
-		return fmt.Errorf("HTTP status %d", httpResp.StatusCode)
-	}
-	if httpResp.Header.Get("Content-Type") != "application/json" {
-		return errors.New("not a JSON response")
-	}
-
-	if resp == nil {
-		return nil
-	}
-	return json.NewDecoder(httpResp.Body).Decode(resp)
-}
diff --git a/vendor/git.autistici.org/ai3/go-common/clientutil/retry.go b/vendor/git.autistici.org/ai3/go-common/clientutil/retry.go
deleted file mode 100644
index 3ca7b51a..00000000
--- a/vendor/git.autistici.org/ai3/go-common/clientutil/retry.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package clientutil
-
-import (
-	"errors"
-	"net/http"
-	"time"
-
-	"github.com/cenkalti/backoff"
-)
-
-// NewExponentialBackOff creates a backoff.ExponentialBackOff object
-// with our own default values.
-func NewExponentialBackOff() *backoff.ExponentialBackOff {
-	b := backoff.NewExponentialBackOff()
-	b.InitialInterval = 100 * time.Millisecond
-	//b.Multiplier = 1.4142
-	return b
-}
-
-// A temporary (retriable) error is something that has a Temporary method.
-type tempError interface {
-	Temporary() bool
-}
-
-type tempErrorWrapper struct {
-	error
-}
-
-func (t tempErrorWrapper) Temporary() bool { return true }
-
-// TempError makes a temporary (retriable) error out of a normal error.
-func TempError(err error) error {
-	return tempErrorWrapper{err}
-}
-
-// Retry operation op until it succeeds according to the backoff
-// policy b.
-//
-// Note that this function reverses the error semantics of
-// backoff.Operation: all errors are permanent unless explicitly
-// marked as temporary (i.e. they have a Temporary() method that
-// returns true). This is to better align with the errors returned by
-// the net package.
-func Retry(op backoff.Operation, b backoff.BackOff) error {
-	innerOp := func() error {
-		err := op()
-		if err == nil {
-			return err
-		}
-		if tmpErr, ok := err.(tempError); ok && tmpErr.Temporary() {
-			return err
-		}
-		return backoff.Permanent(err)
-	}
-	return backoff.Retry(innerOp, b)
-}
-
-var errHTTPBackOff = TempError(errors.New("temporary http error"))
-
-func isStatusTemporary(code int) bool {
-	switch code {
-	case http.StatusTooManyRequests, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
-		return true
-	default:
-		return false
-	}
-}
-
-// RetryHTTPDo retries an HTTP request until it succeeds, according to
-// the backoff policy b. It will retry on temporary network errors and
-// upon receiving specific temporary HTTP errors. It will use the
-// context associated with the HTTP request object.
-func RetryHTTPDo(client *http.Client, req *http.Request, b backoff.BackOff) (*http.Response, error) {
-	var resp *http.Response
-	op := func() error {
-		// Clear up previous response if set.
-		if resp != nil {
-			resp.Body.Close()
-		}
-
-		var err error
-		resp, err = client.Do(req)
-		if err == nil && isStatusTemporary(resp.StatusCode) {
-			resp.Body.Close()
-			return errHTTPBackOff
-		}
-		return err
-	}
-
-	err := Retry(op, backoff.WithContext(b, req.Context()))
-	return resp, err
-}
diff --git a/vendor/git.autistici.org/ai3/go-common/clientutil/track.go b/vendor/git.autistici.org/ai3/go-common/clientutil/track.go
new file mode 100644
index 00000000..2db20bbb
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/clientutil/track.go
@@ -0,0 +1,123 @@
+package clientutil
+
+import (
+	"math/rand"
+	"sync"
+	"time"
+)
+
+// The backendTracker tracks the state of the targets associated with
+// a backend, and periodically checks DNS for updates.
+type backendTracker struct {
+	log      logger
+	addr     string
+	resolver resolver
+	stopCh   chan struct{}
+
+	mx       sync.Mutex
+	resolved []string
+	failed   map[string]time.Time
+}
+
+func newBackendTracker(addr string, resolver resolver, logger logger) *backendTracker {
+	// Resolve the targets once before returning.
+	b := &backendTracker{
+		addr:     addr,
+		resolver: resolver,
+		resolved: resolver.ResolveIP(addr),
+		failed:   make(map[string]time.Time),
+		stopCh:   make(chan struct{}),
+		log:      logger,
+	}
+	go b.updateProc()
+	return b
+}
+
+func (b *backendTracker) Close() {
+	close(b.stopCh)
+}
+
+// Return the full list of targets in reverse preference order.
+func (b *backendTracker) getTargets() []string {
+	b.mx.Lock()
+	defer b.mx.Unlock()
+
+	var good, bad []string
+	for _, t := range b.resolved {
+		if _, ok := b.failed[t]; ok {
+			bad = append(bad, t)
+		} else {
+			good = append(good, t)
+		}
+	}
+
+	good = shuffle(good)
+	bad = shuffle(bad)
+
+	return append(good, bad...)
+}
+
+func (b *backendTracker) setStatus(addr string, ok bool) {
+	b.mx.Lock()
+
+	_, isFailed := b.failed[addr]
+	if isFailed && ok {
+		b.log.Printf("target %s now ok", addr)
+		delete(b.failed, addr)
+	} else if !isFailed && !ok {
+		b.log.Printf("target %s failed", addr)
+		b.failed[addr] = time.Now()
+	}
+
+	b.mx.Unlock()
+}
+
+var (
+	backendUpdateInterval       = 60 * time.Second
+	backendFailureRetryInterval = 60 * time.Second
+)
+
+func (b *backendTracker) expireFailedTargets() {
+	b.mx.Lock()
+	now := time.Now()
+	for k, v := range b.failed {
+		if now.Sub(v) > backendFailureRetryInterval {
+			delete(b.failed, k)
+		}
+	}
+	b.mx.Unlock()
+}
+
+func (b *backendTracker) updateProc() {
+	tick := time.NewTicker(backendUpdateInterval)
+	defer tick.Stop()
+	for {
+		select {
+		case <-b.stopCh:
+			return
+		case <-tick.C:
+			b.expireFailedTargets()
+			resolved := b.resolver.ResolveIP(b.addr)
+			if len(resolved) > 0 {
+				b.mx.Lock()
+				b.resolved = resolved
+				b.mx.Unlock()
+			}
+		}
+	}
+}
+
+var shuffleSrc = rand.NewSource(time.Now().UnixNano())
+
+// Re-order elements of a slice randomly.
+func shuffle(values []string) []string {
+	if len(values) < 2 {
+		return values
+	}
+	rnd := rand.New(shuffleSrc)
+	for i := len(values) - 1; i > 0; i-- {
+		j := rnd.Intn(i + 1)
+		values[i], values[j] = values[j], values[i]
+	}
+	return values
+}
diff --git a/vendor/git.autistici.org/ai3/go-common/clientutil/transport.go b/vendor/git.autistici.org/ai3/go-common/clientutil/transport.go
index e4f98e3f..843a760b 100644
--- a/vendor/git.autistici.org/ai3/go-common/clientutil/transport.go
+++ b/vendor/git.autistici.org/ai3/go-common/clientutil/transport.go
@@ -3,170 +3,63 @@ package clientutil
 import (
 	"context"
 	"crypto/tls"
-	"errors"
-	"log"
 	"net"
 	"net/http"
 	"sync"
 	"time"
 )
 
-var errAllBackendsFailed = errors.New("all backends failed")
-
-type dnsResolver struct{}
-
-func (r *dnsResolver) ResolveIPs(hosts []string) []string {
-	var resolved []string
-	for _, hostport := range hosts {
-		host, port, err := net.SplitHostPort(hostport)
-		if err != nil {
-			log.Printf("error parsing %s: %v", hostport, err)
-			continue
-		}
-		hostIPs, err := net.LookupIP(host)
-		if err != nil {
-			log.Printf("error resolving %s: %v", host, err)
-			continue
-		}
-		for _, ip := range hostIPs {
-			resolved = append(resolved, net.JoinHostPort(ip.String(), port))
-		}
-	}
-	return resolved
-}
-
-var defaultResolver = &dnsResolver{}
-
-type resolver interface {
-	ResolveIPs([]string) []string
-}
-
-// Balancer for HTTP connections. It will round-robin across available
-// backends, trying to avoid ones that are erroring out, until one
-// succeeds or they all fail.
+// The transportCache is just a cache of http transports, each
+// connecting to a specific address.
 //
-// This object should not be used for load balancing of individual
-// HTTP requests: once a new connection is established, requests will
-// be sent over it until it errors out. It's meant to provide a
-// *reliable* connection to a set of equivalent backends for HA
-// purposes.
-type balancer struct {
-	hosts    []string
-	resolver resolver
-	stop     chan bool
+// We use this to control the HTTP Host header and the TLS ServerName
+// independently of the target address.
+type transportCache struct {
+	tlsConfig *tls.Config
 
-	// List of currently valid (or untested) backends, and ones
-	// that errored out at least once.
-	mx    sync.Mutex
-	addrs []string
-	ok    map[string]bool
+	mx         sync.RWMutex
+	transports map[string]http.RoundTripper
 }
 
-var backendUpdateInterval = 60 * time.Second
+func newTransportCache(tlsConfig *tls.Config) *transportCache {
+	return &transportCache{
+		tlsConfig:  tlsConfig,
+		transports: make(map[string]http.RoundTripper),
+	}
+}
 
-// Periodically update the list of available backends.
-func (b *balancer) updateProc() {
-	tick := time.NewTicker(backendUpdateInterval)
-	for {
-		select {
-		case <-b.stop:
-			return
-		case <-tick.C:
-			resolved := b.resolver.ResolveIPs(b.hosts)
-			if len(resolved) > 0 {
-				b.mx.Lock()
-				b.addrs = resolved
-				b.mx.Unlock()
-			}
-		}
+func (m *transportCache) newTransport(addr string) http.RoundTripper {
+	return &http.Transport{
+		TLSClientConfig: m.tlsConfig,
+		DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
+			return netDialContext(ctx, network, addr)
+		},
 	}
 }
 
-// Returns a list of all available backends, split into "good ones"
-// (no errors seen since last successful connection) and "bad ones".
-func (b *balancer) getBackends() ([]string, []string) {
-	b.mx.Lock()
-	defer b.mx.Unlock()
+func (m *transportCache) getTransport(addr string) http.RoundTripper {
+	m.mx.RLock()
+	t, ok := m.transports[addr]
+	m.mx.RUnlock()
 
-	var good, bad []string
-	for _, addr := range b.addrs {
-		if ok := b.ok[addr]; ok {
-			good = append(good, addr)
-		} else {
-			bad = append(bad, addr)
+	if !ok {
+		m.mx.Lock()
+		if t, ok = m.transports[addr]; !ok {
+			t = m.newTransport(addr)
+			m.transports[addr] = t
 		}
+		m.mx.Unlock()
 	}
-	return good, bad
-}
 
-func (b *balancer) notify(addr string, ok bool) {
-	b.mx.Lock()
-	b.ok[addr] = ok
-	b.mx.Unlock()
+	return t
 }
 
+// Go < 1.9 does not have net.DialContext, reimplement it in terms of
+// net.DialTimeout.
 func netDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
-	timeout := 30 * time.Second
-	// Go < 1.9 does not have net.DialContext, reimplement it in
-	// terms of net.DialTimeout.
+	timeout := 60 * time.Second // some arbitrary max timeout
 	if deadline, ok := ctx.Deadline(); ok {
 		timeout = time.Until(deadline)
 	}
 	return net.DialTimeout(network, addr, timeout)
 }
-
-func (b *balancer) dial(ctx context.Context, network, addr string) (net.Conn, error) {
-	// Start by attempting a connection on 'good' targets.
-	good, bad := b.getBackends()
-
-	for _, addr := range good {
-		// Go < 1.9 does not have DialContext, deal with it
-		conn, err := netDialContext(ctx, network, addr)
-		if err == nil {
-			return conn, nil
-		} else if err == context.Canceled {
-			// A timeout might be bad, set the error bit
-			// on the connection.
-			b.notify(addr, false)
-			return nil, err
-		}
-		b.notify(addr, false)
-	}
-
-	for _, addr := range bad {
-		conn, err := netDialContext(ctx, network, addr)
-		if err == nil {
-			b.notify(addr, true)
-			return conn, nil
-		} else if err == context.Canceled {
-			return nil, err
-		}
-	}
-
-	return nil, errAllBackendsFailed
-}
-
-// NewTransport returns a suitably configured http.RoundTripper that
-// talks to a specific backend service. It performs discovery of
-// available backends via DNS (using A or AAAA record lookups), tries
-// to route traffic away from faulty backends.
-//
-// It will periodically attempt to rediscover new backends.
-func NewTransport(backends []string, tlsConf *tls.Config, resolver resolver) http.RoundTripper {
-	if resolver == nil {
-		resolver = defaultResolver
-	}
-	addrs := resolver.ResolveIPs(backends)
-	b := &balancer{
-		hosts:    backends,
-		resolver: resolver,
-		addrs:    addrs,
-		ok:       make(map[string]bool),
-	}
-	go b.updateProc()
-
-	return &http.Transport{
-		DialContext:     b.dial,
-		TLSClientConfig: tlsConf,
-	}
-}
diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/parse.go b/vendor/git.autistici.org/ai3/go-common/ldap/parse.go
index 22a285c6..2aec0b26 100644
--- a/vendor/git.autistici.org/ai3/go-common/ldap/parse.go
+++ b/vendor/git.autistici.org/ai3/go-common/ldap/parse.go
@@ -6,6 +6,8 @@ import (
 	"gopkg.in/ldap.v2"
 )
 
+// ParseScope parses a string representation of an LDAP scope into the
+// proper enum value.
 func ParseScope(s string) (int, error) {
 	switch s {
 	case "base":
diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/pool.go b/vendor/git.autistici.org/ai3/go-common/ldap/pool.go
index c77d0617..560d639b 100644
--- a/vendor/git.autistici.org/ai3/go-common/ldap/pool.go
+++ b/vendor/git.autistici.org/ai3/go-common/ldap/pool.go
@@ -7,6 +7,8 @@ import (
 	"net/url"
 	"time"
 
+	"git.autistici.org/ai3/go-common/clientutil"
+	"github.com/cenkalti/backoff"
 	"gopkg.in/ldap.v2"
 )
 
@@ -125,3 +127,77 @@ func NewConnectionPool(uri, bindDN, bindPw string, cacheSize int) (*ConnectionPo
 		bindPw:  bindPw,
 	}, nil
 }
+
+func (p *ConnectionPool) doRequest(ctx context.Context, fn func(*ldap.Conn) error) error {
+	return clientutil.Retry(func() error {
+		conn, err := p.Get(ctx)
+		if err != nil {
+			// Here conn is nil, so we don't need to Release it.
+			if isTemporaryLDAPError(err) {
+				return clientutil.TempError(err)
+			}
+			return err
+		}
+
+		if deadline, ok := ctx.Deadline(); ok {
+			conn.SetTimeout(time.Until(deadline))
+		}
+
+		err = fn(conn)
+		if err != nil && isTemporaryLDAPError(err) {
+			p.Release(conn, err)
+			return clientutil.TempError(err)
+		}
+		p.Release(conn, err)
+		return err
+	}, backoff.WithContext(clientutil.NewExponentialBackOff(), ctx))
+}
+
+// Search performs the given search request. It will retry the request
+// on temporary errors.
+func (p *ConnectionPool) Search(ctx context.Context, searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
+	var result *ldap.SearchResult
+	err := p.doRequest(ctx, func(conn *ldap.Conn) error {
+		var err error
+		result, err = conn.Search(searchRequest)
+		return err
+	})
+	return result, err
+}
+
+// Modify issues a ModifyRequest to the LDAP server.
+func (p *ConnectionPool) Modify(ctx context.Context, modifyRequest *ldap.ModifyRequest) error {
+	return p.doRequest(ctx, func(conn *ldap.Conn) error {
+		return conn.Modify(modifyRequest)
+	})
+}
+
+// Add issues an AddRequest to the LDAP server.
+func (p *ConnectionPool) Add(ctx context.Context, addRequest *ldap.AddRequest) error {
+	return p.doRequest(ctx, func(conn *ldap.Conn) error {
+		return conn.Add(addRequest)
+	})
+}
+
+// Interface matched by net.Error.
+type hasTemporary interface {
+	Temporary() bool
+}
+
+// Treat network errors as temporary. Other errors are permanent by
+// default.
+func isTemporaryLDAPError(err error) bool {
+	switch v := err.(type) {
+	case *ldap.Error:
+		switch v.ResultCode {
+		case ldap.ErrorNetwork:
+			return true
+		default:
+			return false
+		}
+	case hasTemporary:
+		return v.Temporary()
+	default:
+		return false
+	}
+}
diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/search.go b/vendor/git.autistici.org/ai3/go-common/ldap/search.go
deleted file mode 100644
index db29ba09..00000000
--- a/vendor/git.autistici.org/ai3/go-common/ldap/search.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package ldaputil
-
-import (
-	"context"
-	"time"
-
-	"github.com/cenkalti/backoff"
-	"gopkg.in/ldap.v2"
-
-	"git.autistici.org/ai3/go-common/clientutil"
-)
-
-// Interface matched by net.Error.
-type hasTemporary interface {
-	Temporary() bool
-}
-
-// Treat network errors as temporary. Other errors are permanent by
-// default.
-func isTemporaryLDAPError(err error) bool {
-	switch v := err.(type) {
-	case *ldap.Error:
-		switch v.ResultCode {
-		case ldap.ErrorNetwork:
-			return true
-		default:
-			return false
-		}
-	case hasTemporary:
-		return v.Temporary()
-	default:
-		return false
-	}
-}
-
-// Search performs the given search request. It will retry the request
-// on temporary errors.
-func (p *ConnectionPool) Search(ctx context.Context, searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
-	var result *ldap.SearchResult
-	err := clientutil.Retry(func() error {
-		conn, err := p.Get(ctx)
-		if err != nil {
-			// Here conn is nil, so we don't need to Release it.
-			if isTemporaryLDAPError(err) {
-				return clientutil.TempError(err)
-			}
-			return err
-		}
-
-		if deadline, ok := ctx.Deadline(); ok {
-			conn.SetTimeout(time.Until(deadline))
-		}
-
-		result, err = conn.Search(searchRequest)
-		if err != nil && isTemporaryLDAPError(err) {
-			p.Release(conn, err)
-			return clientutil.TempError(err)
-		}
-		p.Release(conn, err)
-		return err
-	}, backoff.WithContext(clientutil.NewExponentialBackOff(), ctx))
-	return result, err
-}
diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/http.go b/vendor/git.autistici.org/ai3/go-common/serverutil/http.go
index 7797ae66..32329492 100644
--- a/vendor/git.autistici.org/ai3/go-common/serverutil/http.go
+++ b/vendor/git.autistici.org/ai3/go-common/serverutil/http.go
@@ -3,13 +3,17 @@ package serverutil
 import (
 	"context"
 	"crypto/tls"
+	"io"
 	"log"
+	"net"
 	"net/http"
+	"net/http/pprof"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"
 
+	"github.com/coreos/go-systemd/daemon"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 )
@@ -20,43 +24,73 @@ var gracefulShutdownTimeout = 3 * time.Second
 type ServerConfig struct {
 	TLS                 *TLSServerConfig `yaml:"tls"`
 	MaxInflightRequests int              `yaml:"max_inflight_requests"`
+	TrustedForwarders   []string         `yaml:"trusted_forwarders"`
 }
 
-// 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) {
+func (config *ServerConfig) buildHTTPServer(h http.Handler) (*http.Server, error) {
 	var tlsConfig *tls.Config
-	if serverConfig != nil {
-		if serverConfig.TLS != nil {
-			tlsConfig, err = serverConfig.TLS.TLSConfig()
+	var err error
+	if config != nil {
+		if config.TLS != nil {
+			tlsConfig, err = config.TLS.TLSConfig()
 			if err != nil {
-				return err
+				return nil, err
 			}
-			h, err = serverConfig.TLS.TLSAuthWrapper(h)
+			h, err = config.TLS.TLSAuthWrapper(h)
 			if err != nil {
-				return err
+				return nil, err
 			}
 		}
 
-		if serverConfig.MaxInflightRequests > 0 {
-			h = newLoadSheddingWrapper(serverConfig.MaxInflightRequests, h)
+		// If TrustedForwarders is defined, rewrite the request
+		// headers using X-Forwarded-Proto and X-Real-IP.
+		if len(config.TrustedForwarders) > 0 {
+			h, err = newProxyHeaders(h, config.TrustedForwarders)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		// If MaxInflightRequests is set, enable the load
+		// shedding wrapper.
+		if config.MaxInflightRequests > 0 {
+			h = newLoadSheddingWrapper(config.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),
+	return &http.Server{
+		Handler:      defaultHandler(h),
 		ReadTimeout:  30 * time.Second,
 		WriteTimeout: 30 * time.Second,
 		IdleTimeout:  600 * time.Second,
 		TLSConfig:    tlsConfig,
+	}, nil
+}
+
+// Serve HTTP(S) content on the specified address. If config.TLS 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, config *ServerConfig, addr string) error {
+	// Create the HTTP server.
+	srv, err := config.buildHTTPServer(h)
+	if err != nil {
+		return err
+	}
+
+	// Create the net.Listener first, so we can detect
+	// initialization-time errors safely.
+	l, err := net.Listen("tcp", addr)
+	if err != nil {
+		return err
+	}
+	if srv.TLSConfig != nil {
+		l = tls.NewListener(l, srv.TLSConfig)
 	}
 
 	// Install a signal handler for gentle process termination.
@@ -78,13 +112,13 @@ func Serve(h http.Handler, serverConfig *ServerConfig, addr string) (err error)
 
 		close(done)
 	}()
+
 	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
 
-	if tlsConfig != nil {
-		err = srv.ListenAndServeTLS("", "")
-	} else {
-		err = srv.ListenAndServe()
-	}
+	// Notify systemd that we are ready to serve.
+	daemon.SdNotify(false, "READY=1")
+
+	err = srv.Serve(l)
 	if err != http.ErrServerClosed {
 		return err
 	}
@@ -93,12 +127,27 @@ func Serve(h http.Handler, serverConfig *ServerConfig, addr string) (err error)
 	return nil
 }
 
-func instrumentHandler(h http.Handler) http.Handler {
+func defaultHandler(h http.Handler) http.Handler {
 	root := http.NewServeMux()
+
+	// Add an endpoint for HTTP health checking probes.
+	root.Handle("/health", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		io.WriteString(w, "OK")
+	}))
+
+	// Add an endpoint to serve Prometheus metrics.
 	root.Handle("/metrics", promhttp.Handler())
-	root.Handle("/", h)
-	return promhttp.InstrumentHandlerInFlight(inFlightRequests,
-		promhttp.InstrumentHandlerCounter(totalRequests, root))
+
+	// Add the net/http/pprof debug handlers.
+	root.Handle("/debug/pprof/", pprof.Handler(""))
+
+	// Forward everything else to the main handler, adding
+	// Prometheus instrumentation (requests to /metrics and
+	// /health are not included).
+	root.Handle("/", promhttp.InstrumentHandlerInFlight(inFlightRequests,
+		promhttp.InstrumentHandlerCounter(totalRequests, h)))
+
+	return root
 }
 
 // HTTP-related metrics.
diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/json.go b/vendor/git.autistici.org/ai3/go-common/serverutil/json.go
index b307932e..746ed201 100644
--- a/vendor/git.autistici.org/ai3/go-common/serverutil/json.go
+++ b/vendor/git.autistici.org/ai3/go-common/serverutil/json.go
@@ -2,6 +2,7 @@ package serverutil
 
 import (
 	"encoding/json"
+	"log"
 	"net/http"
 )
 
@@ -28,10 +29,19 @@ func DecodeJSONRequest(w http.ResponseWriter, r *http.Request, obj interface{})
 
 // EncodeJSONResponse writes an application/json response to w.
 func EncodeJSONResponse(w http.ResponseWriter, obj interface{}) {
+	data, err := json.Marshal(obj)
+	if err != nil {
+		log.Printf("JSON serialization error: %v", err)
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
 	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)
+	if _, err = w.Write(data); err != nil {
+		log.Printf("error writing response: %v", err)
+	}
 }
diff --git a/vendor/git.autistici.org/ai3/go-common/serverutil/proxy_headers.go b/vendor/git.autistici.org/ai3/go-common/serverutil/proxy_headers.go
new file mode 100644
index 00000000..00480b93
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/serverutil/proxy_headers.go
@@ -0,0 +1,78 @@
+package serverutil
+
+import (
+	"fmt"
+	"net"
+	"net/http"
+
+	"github.com/gorilla/handlers"
+)
+
+type proxyHeaders struct {
+	wrap, phWrap http.Handler
+	forwarders   []net.IPNet
+}
+
+func newProxyHeaders(h http.Handler, trustedForwarders []string) (http.Handler, error) {
+	f, err := parseIPNetList(trustedForwarders)
+	if err != nil {
+		return nil, err
+	}
+	return &proxyHeaders{
+		wrap:       h,
+		phWrap:     handlers.ProxyHeaders(h),
+		forwarders: f,
+	}, nil
+}
+
+func (p *proxyHeaders) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	host, _, err := net.SplitHostPort(r.RemoteAddr)
+	if err != nil {
+		host = r.RemoteAddr
+	}
+	ip := net.ParseIP(host)
+	if ip != nil && matchIPNetList(ip, p.forwarders) {
+		p.phWrap.ServeHTTP(w, r)
+		return
+	}
+	p.wrap.ServeHTTP(w, r)
+}
+
+func fullMask(ip net.IP) net.IPMask {
+	if ip.To4() == nil {
+		return net.CIDRMask(128, 128)
+	}
+	return net.CIDRMask(32, 32)
+}
+
+// ParseIPNetList turns a comma-separated list of IP addresses or CIDR
+// networks into a net.IPNet slice.
+func parseIPNetList(iplist []string) ([]net.IPNet, error) {
+	var nets []net.IPNet
+	for _, s := range iplist {
+		if s == "" {
+			continue
+		}
+		_, ipnet, err := net.ParseCIDR(s)
+		if err != nil {
+			ip := net.ParseIP(s)
+			if ip == nil {
+				return nil, fmt.Errorf("could not parse '%s'", s)
+			}
+			ipnet = &net.IPNet{IP: ip, Mask: fullMask(ip)}
+		}
+		nets = append(nets, *ipnet)
+	}
+	return nets, nil
+}
+
+// MatchIPNetList returns true if the given IP address matches one of
+// the specified networks.
+func matchIPNetList(ip net.IP, nets []net.IPNet) bool {
+	for _, n := range nets {
+		if n.Contains(ip) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/vendor/git.autistici.org/id/go-sso/README.md b/vendor/git.autistici.org/id/go-sso/README.md
index 023f892b..9bd4afb1 100644
--- a/vendor/git.autistici.org/id/go-sso/README.md
+++ b/vendor/git.autistici.org/id/go-sso/README.md
@@ -121,3 +121,23 @@ parameters:
 Note that annoyingly *cur_svc* and *cur_nonce* are redundant, as they
 are already contained within *cur_tkt*, but the SSO ticket API won't
 allow us to decode the ticket without verifying it at the same time.
+
+
+# Implementation notes
+
+The single-sign-on functionality works using HTTP cookies and
+redirects between the protected service and the SSO server implemented
+in this package. This part works without any Javascript, it's just
+plain old HTTP (the browser must accept cookies though). SSO cookies
+have a builtin (signed) expiration timestamp, and are set to be
+automatically deleted on browser exit.
+
+Logout, on the other hand, is more complex: in order to get the
+browser to delete the cookies from the signed-in services, we use
+XMLHttpRequests from the logout page, and expect the service logout
+endpoints to support authenticated CORS. If Javascript is not
+available, however, we try to clear the cookies using image requests,
+but this may not work depending on the browser (Safari), or the
+presence of privacy-protecting extensions meant to block third-party
+cookies. In this case a message is displayed asking the user to quit
+the browser, but this isn't really a satisfying solution.
diff --git a/vendor/github.com/cenkalti/backoff/README.md b/vendor/github.com/cenkalti/backoff/README.md
index 13b347fb..55ebc98f 100644
--- a/vendor/github.com/cenkalti/backoff/README.md
+++ b/vendor/github.com/cenkalti/backoff/README.md
@@ -24,7 +24,7 @@ See https://godoc.org/github.com/cenkalti/backoff#pkg-examples
 [coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master
 [coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master
 
-[google-http-java-client]: https://github.com/google/google-http-java-client
+[google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java
 [exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff
 
 [advanced example]: https://godoc.org/github.com/cenkalti/backoff#example_
diff --git a/vendor/github.com/cenkalti/backoff/context.go b/vendor/github.com/cenkalti/backoff/context.go
index 5d157092..d7005522 100644
--- a/vendor/github.com/cenkalti/backoff/context.go
+++ b/vendor/github.com/cenkalti/backoff/context.go
@@ -1,9 +1,8 @@
 package backoff
 
 import (
+	"context"
 	"time"
-
-	"golang.org/x/net/context"
 )
 
 // BackOffContext is a backoff policy that stops retrying after the context
diff --git a/vendor/github.com/cenkalti/backoff/retry.go b/vendor/github.com/cenkalti/backoff/retry.go
index 5dbd825b..e65cc700 100644
--- a/vendor/github.com/cenkalti/backoff/retry.go
+++ b/vendor/github.com/cenkalti/backoff/retry.go
@@ -15,7 +15,6 @@ type Notify func(error, time.Duration)
 
 // Retry the operation o until it does not return error or BackOff stops.
 // o is guaranteed to be run at least once.
-// It is the caller's responsibility to reset b after Retry returns.
 //
 // If o returns a *PermanentError, the operation is not retried, and the
 // wrapped error is returned.
diff --git a/vendor/github.com/cenkalti/backoff/ticker.go b/vendor/github.com/cenkalti/backoff/ticker.go
index e742512f..e41084b0 100644
--- a/vendor/github.com/cenkalti/backoff/ticker.go
+++ b/vendor/github.com/cenkalti/backoff/ticker.go
@@ -1,7 +1,6 @@
 package backoff
 
 import (
-	"runtime"
 	"sync"
 	"time"
 )
@@ -34,7 +33,6 @@ func NewTicker(b BackOff) *Ticker {
 	}
 	t.b.Reset()
 	go t.run()
-	runtime.SetFinalizer(t, (*Ticker).Stop)
 	return t
 }
 
diff --git a/vendor/github.com/cenkalti/backoff/tries.go b/vendor/github.com/cenkalti/backoff/tries.go
index d2da7308..cfeefd9b 100644
--- a/vendor/github.com/cenkalti/backoff/tries.go
+++ b/vendor/github.com/cenkalti/backoff/tries.go
@@ -3,13 +3,13 @@ package backoff
 import "time"
 
 /*
-WithMaxTries creates a wrapper around another BackOff, which will
+WithMaxRetries creates a wrapper around another BackOff, which will
 return Stop if NextBackOff() has been called too many times since
 the last time Reset() was called
 
 Note: Implementation is not thread-safe.
 */
-func WithMaxTries(b BackOff, max uint64) BackOff {
+func WithMaxRetries(b BackOff, max uint64) BackOff {
 	return &backOffTries{delegate: b, maxTries: max}
 }
 
diff --git a/vendor/github.com/miscreant/miscreant/go/block/block.go b/vendor/github.com/miscreant/miscreant/go/block/block.go
index be1f22cd..9c16ed81 100644
--- a/vendor/github.com/miscreant/miscreant/go/block/block.go
+++ b/vendor/github.com/miscreant/miscreant/go/block/block.go
@@ -15,7 +15,7 @@ const (
 	R = 0x87
 )
 
-// Block is a 128-byte array used by certain block ciphers (i.e. AES)
+// Block is a 128-bit array used by certain block ciphers (i.e. AES)
 type Block [Size]byte
 
 // Clear zeroes out the contents of the block
diff --git a/vendor/github.com/miscreant/miscreant/go/pmac/pmac.go b/vendor/github.com/miscreant/miscreant/go/pmac/pmac.go
index 080574ca..65a74343 100644
--- a/vendor/github.com/miscreant/miscreant/go/pmac/pmac.go
+++ b/vendor/github.com/miscreant/miscreant/go/pmac/pmac.go
@@ -7,6 +7,7 @@ import (
 	"crypto/cipher"
 	"crypto/subtle"
 	"hash"
+	"math/bits"
 
 	"github.com/miscreant/miscreant/go/block"
 )
@@ -183,7 +184,7 @@ func (d *pmac) BlockSize() int { return block.Size }
 
 // Update the internal tag state based on the buf contents
 func (d *pmac) processBuffer() {
-	xor(d.offset[:], d.l[ctz(d.ctr+1)][:])
+	xor(d.offset[:], d.l[bits.TrailingZeros(d.ctr+1)][:])
 	xor(d.buf[:], d.offset[:])
 	d.ctr++
 
@@ -192,17 +193,6 @@ func (d *pmac) processBuffer() {
 	d.pos = 0
 }
 
-// TODO: use math/bits TrailingZeros() when it becomes available
-// See: https://github.com/golang/go/issues/18616
-func ctz(n uint) uint {
-	var c uint
-	for n&1 == 0 {
-		c++
-		n >>= 1
-	}
-	return c
-}
-
 // XOR the contents of b into a in-place
 func xor(a, b []byte) {
 	for i, v := range b {
diff --git a/vendor/github.com/prometheus/common/model/time.go b/vendor/github.com/prometheus/common/model/time.go
index 7e87f1ac..74ed5a9f 100644
--- a/vendor/github.com/prometheus/common/model/time.go
+++ b/vendor/github.com/prometheus/common/model/time.go
@@ -214,6 +214,9 @@ func (d Duration) String() string {
 		ms   = int64(time.Duration(d) / time.Millisecond)
 		unit = "ms"
 	)
+	if ms == 0 {
+		return "0s"
+	}
 	factors := map[string]int64{
 		"y":  1000 * 60 * 60 * 24 * 365,
 		"w":  1000 * 60 * 60 * 24 * 7,
diff --git a/vendor/github.com/theckman/go-flock/README.md b/vendor/github.com/theckman/go-flock/README.md
index 82069971..38c794c8 100644
--- a/vendor/github.com/theckman/go-flock/README.md
+++ b/vendor/github.com/theckman/go-flock/README.md
@@ -9,7 +9,11 @@ includes a non-blocking TryLock() function to allow locking without blocking exe
 ## License
 `flock` is released under the BSD 3-Clause License. See the `LICENSE` file for more details.
 
-## Intsallation
+## Go Compatibility
+This package makes use of the `context` package that was introduced in Go 1.7. As such, this
+package has an implicit dependency on Go 1.7+.
+
+## Installation
 ```
 go get -u github.com/theckman/go-flock
 ```
diff --git a/vendor/github.com/theckman/go-flock/flock.go b/vendor/github.com/theckman/go-flock/flock.go
index 2582077f..ac40ec6c 100644
--- a/vendor/github.com/theckman/go-flock/flock.go
+++ b/vendor/github.com/theckman/go-flock/flock.go
@@ -11,8 +11,10 @@
 package flock
 
 import (
+	"context"
 	"os"
 	"sync"
+	"time"
 )
 
 // Flock is the struct type to handle file locking. All fields are unexported,
@@ -46,6 +48,25 @@ func (f *Flock) String() string {
 	return f.path
 }
 
+// TryLockContext repeatedly tries locking until one of the conditions is met:
+// TryLock succeeds, TryLock fails with error, or Context Done channel is closed.
+func (f *Flock) TryLockContext(ctx context.Context, retryDelay time.Duration) (bool, error) {
+	if ctx.Err() != nil {
+		return false, ctx.Err()
+	}
+	for {
+		if ok, err := f.TryLock(); ok || err != nil {
+			return ok, err
+		}
+		select {
+		case <-ctx.Done():
+			return false, ctx.Err()
+		case <-time.After(retryDelay):
+			// try again
+		}
+	}
+}
+
 func (f *Flock) setFh() error {
 	// open a new os.File instance
 	// create it if it doesn't exist, truncate it if it does exist, open the file read-write
diff --git a/vendor/golang.org/x/net/context/context.go b/vendor/golang.org/x/net/context/context.go
index d3681ab4..a3c021d3 100644
--- a/vendor/golang.org/x/net/context/context.go
+++ b/vendor/golang.org/x/net/context/context.go
@@ -5,6 +5,8 @@
 // Package context defines the Context type, which carries deadlines,
 // cancelation signals, and other request-scoped values across API boundaries
 // and between processes.
+// As of Go 1.7 this package is available in the standard library under the
+// name context.  https://golang.org/pkg/context.
 //
 // Incoming requests to a server should create a Context, and outgoing calls to
 // servers should accept a Context. The chain of function calls between must
diff --git a/vendor/gopkg.in/yaml.v2/LICENSE b/vendor/gopkg.in/yaml.v2/LICENSE
index 8dada3ed..866d74a7 100644
--- a/vendor/gopkg.in/yaml.v2/LICENSE
+++ b/vendor/gopkg.in/yaml.v2/LICENSE
@@ -1,201 +1,13 @@
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
+Copyright 2011-2016 Canonical Ltd.
 
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
 
-   1. Definitions.
+    http://www.apache.org/licenses/LICENSE-2.0
 
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "{}"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright {yyyy} {name of copyright owner}
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 5ddd7c90..928b89d3 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -5,44 +5,44 @@
 		{
 			"checksumSHA1": "raJx5BjBbVQG0ylGSjPpi+JvqjU=",
 			"path": "git.autistici.org/ai3/go-common",
-			"revision": "a65293114a1adbb45d047a8f9014a307ec0d9051",
-			"revisionTime": "2018-01-12T09:10:27Z"
+			"revision": "193e29e61c81e6bb5548bfe89bba05836d06b61f",
+			"revisionTime": "2018-08-16T21:47:58Z"
 		},
 		{
-			"checksumSHA1": "o+rWKVQIDy79ZwrItwa5/whAL6g=",
+			"checksumSHA1": "49MChcx9D+/+pCyl/F469TcQcK4=",
 			"path": "git.autistici.org/ai3/go-common/clientutil",
-			"revision": "a65293114a1adbb45d047a8f9014a307ec0d9051",
-			"revisionTime": "2018-01-12T09:10:27Z"
+			"revision": "193e29e61c81e6bb5548bfe89bba05836d06b61f",
+			"revisionTime": "2018-08-16T21:47:58Z"
 		},
 		{
-			"checksumSHA1": "iHObDrZa0HlyzdelqAaGfKNzpiM=",
+			"checksumSHA1": "udgeRdy83f6tJmhoKXFxCfwOIZw=",
 			"path": "git.autistici.org/ai3/go-common/ldap",
-			"revision": "a65293114a1adbb45d047a8f9014a307ec0d9051",
-			"revisionTime": "2018-01-12T09:10:27Z"
+			"revision": "193e29e61c81e6bb5548bfe89bba05836d06b61f",
+			"revisionTime": "2018-08-16T21:47:58Z"
 		},
 		{
-			"checksumSHA1": "nlGRxexjZUxnHc/z/+ZqV/Xq51w=",
+			"checksumSHA1": "7VBLbwaK1m/jwsk8sLsh4iD9T/s=",
 			"path": "git.autistici.org/ai3/go-common/serverutil",
-			"revision": "a65293114a1adbb45d047a8f9014a307ec0d9051",
-			"revisionTime": "2018-01-12T09:10:27Z"
+			"revision": "193e29e61c81e6bb5548bfe89bba05836d06b61f",
+			"revisionTime": "2018-08-16T21:47:58Z"
 		},
 		{
 			"checksumSHA1": "T2vf4xzKRqoIjfXlofMgudKA8rA=",
 			"path": "git.autistici.org/ai3/go-common/unix",
-			"revision": "a65293114a1adbb45d047a8f9014a307ec0d9051",
-			"revisionTime": "2018-01-12T09:10:27Z"
+			"revision": "193e29e61c81e6bb5548bfe89bba05836d06b61f",
+			"revisionTime": "2018-08-16T21:47:58Z"
 		},
 		{
 			"checksumSHA1": "OY/kamdVCtqr00dhc+STq7ApbmA=",
 			"path": "git.autistici.org/ai3/go-common/userenckey",
-			"revision": "9c87d6c357e641c4ad4af0c06db87e0b04f1e6f0",
-			"revisionTime": "2018-06-28T09:11:42Z"
+			"revision": "193e29e61c81e6bb5548bfe89bba05836d06b61f",
+			"revisionTime": "2018-08-16T21:47:58Z"
 		},
 		{
-			"checksumSHA1": "zvdsYaPEZrgcsRJy1bOo6YF5rVQ=",
+			"checksumSHA1": "QHzKyLU57jphYBzuhl4ELbCwVX0=",
 			"path": "git.autistici.org/id/go-sso",
-			"revision": "10356d2430081e5f6dc60a68d576ef5b476f83ea",
-			"revisionTime": "2018-02-16T18:29:55Z"
+			"revision": "dc62a1d65832c8a3f73e7a0cffb78ad0b4d3d8fc",
+			"revisionTime": "2018-02-18T15:46:43Z"
 		},
 		{
 			"checksumSHA1": "spyv5/YFBjYyZLZa1U2LBfDR8PM=",
@@ -51,28 +51,28 @@
 			"revisionTime": "2016-08-04T10:47:26Z"
 		},
 		{
-			"checksumSHA1": "hTThB1Cw2ue02RD5Oig4eu1Dkzk=",
+			"checksumSHA1": "EAUmmJ4ccZbyuyf8Fnf+KU+DH3w=",
 			"path": "github.com/cenkalti/backoff",
-			"revision": "309aa717adbf351e92864cbedf9cca0b769a4b5a",
-			"revisionTime": "2017-10-07T11:45:50Z"
+			"revision": "b7325b0f3f1097c6546ea5e83c4a23267e58ad71",
+			"revisionTime": "2018-08-01T15:21:24Z"
 		},
 		{
 			"checksumSHA1": "RBwpnMpfQt7Jo7YWrRph0Vwe+f0=",
 			"path": "github.com/coreos/go-systemd/activation",
-			"revision": "d2196463941895ee908e13531a23a39feb9e1243",
-			"revisionTime": "2017-07-31T11:19:25Z"
+			"revision": "1f9909e51b2dab2487c26d64c8f2e7e580e4c9f5",
+			"revisionTime": "2017-03-24T09:58:19Z"
 		},
 		{
 			"checksumSHA1": "+Zz+leZHHC9C0rx8DoRuffSRPso=",
 			"path": "github.com/coreos/go-systemd/daemon",
-			"revision": "d2196463941895ee908e13531a23a39feb9e1243",
-			"revisionTime": "2017-07-31T11:19:25Z"
+			"revision": "1f9909e51b2dab2487c26d64c8f2e7e580e4c9f5",
+			"revisionTime": "2017-03-24T09:58:19Z"
 		},
 		{
 			"checksumSHA1": "yqF125xVSkmfLpIVGrLlfE05IUk=",
 			"path": "github.com/golang/protobuf/proto",
-			"revision": "17ce1425424ab154092bbb43af630bd647f3bb0d",
-			"revisionTime": "2017-09-02T00:04:52Z"
+			"revision": "1e59b77b52bf8e4b449a57e6f79f21226d571845",
+			"revisionTime": "2017-11-13T18:07:20Z"
 		},
 		{
 			"checksumSHA1": "D5EG5yMsbtHqKbzBVbxFMKEMDNY=",
@@ -93,58 +93,58 @@
 			"revisionTime": "2018-06-23T00:14:59Z"
 		},
 		{
-			"checksumSHA1": "KnQFNiyIyIH+V8EbySzLhySrF9s=",
+			"checksumSHA1": "LAN1Gs68UZhRbEUy25LfzDEjRT0=",
 			"path": "github.com/miscreant/miscreant/go/block",
-			"revision": "834b150f9163806592d075e9e235296ba5c64425",
-			"revisionTime": "2017-12-04T00:48:37Z"
+			"revision": "71d82ae9dd614c1ec09d4491e01d5d0ea762c398",
+			"revisionTime": "2018-06-23T00:14:59Z"
 		},
 		{
 			"checksumSHA1": "Q1pe4LrL8CwfA/r+xYSl0/3AzPQ=",
 			"path": "github.com/miscreant/miscreant/go/cmac",
-			"revision": "834b150f9163806592d075e9e235296ba5c64425",
-			"revisionTime": "2017-12-04T00:48:37Z"
+			"revision": "71d82ae9dd614c1ec09d4491e01d5d0ea762c398",
+			"revisionTime": "2018-06-23T00:14:59Z"
 		},
 		{
-			"checksumSHA1": "bnZ6mFgtuRnZcqNwiILP/as94aE=",
+			"checksumSHA1": "Xl/ZDNMfyC5t/HF2Ms+c519lGRk=",
 			"path": "github.com/miscreant/miscreant/go/pmac",
-			"revision": "834b150f9163806592d075e9e235296ba5c64425",
-			"revisionTime": "2017-12-04T00:48:37Z"
+			"revision": "71d82ae9dd614c1ec09d4491e01d5d0ea762c398",
+			"revisionTime": "2018-06-23T00:14:59Z"
 		},
 		{
 			"checksumSHA1": "hu0MsbTdFzZxNRyAxe2HmTFFFak=",
 			"path": "github.com/prometheus/client_golang/prometheus",
-			"revision": "5cec1d0429b02e4323e042eb04dafdb079ddf568",
-			"revisionTime": "2017-10-05T11:29:15Z"
+			"revision": "661e31bf844dfca9aeba15f27ea8aa0d485ad212",
+			"revisionTime": "2017-12-01T12:22:22Z"
 		},
 		{
 			"checksumSHA1": "wsAkYlRRUNx+OAuUOIqdjO7dICM=",
 			"path": "github.com/prometheus/client_golang/prometheus/promhttp",
-			"revision": "5cec1d0429b02e4323e042eb04dafdb079ddf568",
-			"revisionTime": "2017-10-05T11:29:15Z"
+			"revision": "661e31bf844dfca9aeba15f27ea8aa0d485ad212",
+			"revisionTime": "2017-12-01T12:22:22Z"
 		},
 		{
 			"checksumSHA1": "DvwvOlPNAgRntBzt3b3OSRMS2N4=",
 			"path": "github.com/prometheus/client_model/go",
-			"revision": "6f3806018612930941127f2a7c6c453ba2c527d2",
-			"revisionTime": "2017-02-16T18:52:47Z"
+			"revision": "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c",
+			"revisionTime": "2017-11-17T10:05:41Z"
 		},
 		{
 			"checksumSHA1": "xfnn0THnqNwjwimeTClsxahYrIo=",
 			"path": "github.com/prometheus/common/expfmt",
-			"revision": "e3fb1a1acd7605367a2b378bc2e2f893c05174b7",
-			"revisionTime": "2017-11-04T09:59:07Z"
+			"revision": "2e54d0b93cba2fd133edc32211dcc32c06ef72ca",
+			"revisionTime": "2017-11-17T16:30:51Z"
 		},
 		{
 			"checksumSHA1": "GWlM3d2vPYyNATtTFgftS10/A9w=",
 			"path": "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg",
-			"revision": "e3fb1a1acd7605367a2b378bc2e2f893c05174b7",
-			"revisionTime": "2017-11-04T09:59:07Z"
+			"revision": "2e54d0b93cba2fd133edc32211dcc32c06ef72ca",
+			"revisionTime": "2017-11-17T16:30:51Z"
 		},
 		{
-			"checksumSHA1": "3VoqH7TFfzA6Ds0zFzIbKCUvBmw=",
+			"checksumSHA1": "YU+/K48IMawQnToO4ETE6a+hhj4=",
 			"path": "github.com/prometheus/common/model",
-			"revision": "e3fb1a1acd7605367a2b378bc2e2f893c05174b7",
-			"revisionTime": "2017-11-04T09:59:07Z"
+			"revision": "2e54d0b93cba2fd133edc32211dcc32c06ef72ca",
+			"revisionTime": "2017-11-17T16:30:51Z"
 		},
 		{
 			"checksumSHA1": "pW1yt1G1J9jnQMCxr1TDI7LQr3s=",
@@ -159,10 +159,10 @@
 			"revisionTime": "2017-10-17T21:40:25Z"
 		},
 		{
-			"checksumSHA1": "cDiE2qLTJ2kmiC7k1KS+AbP87X8=",
+			"checksumSHA1": "o4pEl1nZL7+e3cjayH05qcwxprI=",
 			"path": "github.com/theckman/go-flock",
-			"revision": "6de226b0d5f040ed85b88c82c381709b98277f3d",
-			"revisionTime": "2017-05-22T02:22:41Z"
+			"revision": "d0cbbf0727cd7369d6deda1da29490dc132d45b4",
+			"revisionTime": "2017-10-30T20:34:48Z"
 		},
 		{
 			"checksumSHA1": "FwW3Vv4jW0Nv7V2SZC7x/Huj5M4=",
@@ -180,15 +180,15 @@
 			"checksumSHA1": "X6Q8nYb+KXh+64AKHwWOOcyijHQ=",
 			"origin": "git.autistici.org/id/go-sso/vendor/golang.org/x/crypto/ed25519",
 			"path": "golang.org/x/crypto/ed25519",
-			"revision": "2f1d893daf6ea55c4c3a704d14cf3c0996e1fec5",
-			"revisionTime": "2017-12-14T07:43:49Z"
+			"revision": "dc62a1d65832c8a3f73e7a0cffb78ad0b4d3d8fc",
+			"revisionTime": "2018-02-18T15:46:43Z"
 		},
 		{
 			"checksumSHA1": "LXFcVx8I587SnWmKycSDEq9yvK8=",
 			"origin": "git.autistici.org/id/go-sso/vendor/golang.org/x/crypto/ed25519/internal/edwards25519",
 			"path": "golang.org/x/crypto/ed25519/internal/edwards25519",
-			"revision": "2f1d893daf6ea55c4c3a704d14cf3c0996e1fec5",
-			"revisionTime": "2017-12-14T07:43:49Z"
+			"revision": "dc62a1d65832c8a3f73e7a0cffb78ad0b4d3d8fc",
+			"revisionTime": "2018-02-18T15:46:43Z"
 		},
 		{
 			"checksumSHA1": "1MGpGDQqnUoRpv7VEcQrXOBydXE=",
@@ -203,16 +203,16 @@
 			"revisionTime": "2018-06-20T09:14:27Z"
 		},
 		{
-			"checksumSHA1": "dr5+PfIRzXeN+l1VG+s0lea9qz8=",
+			"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
 			"path": "golang.org/x/net/context",
-			"revision": "66aacef3dd8a676686c7ae3716979581e8b03c47",
-			"revisionTime": "2016-09-14T00:11:54Z"
+			"revision": "c39426892332e1bb5ec0a434a079bf82f5d30c54",
+			"revisionTime": "2018-07-15T06:54:04Z"
 		},
 		{
 			"checksumSHA1": "REkmyB368pIiip76LiqMLspgCRk=",
 			"path": "golang.org/x/sys/cpu",
-			"revision": "c4afb3effaa53fd9a06ca61262dc7ce8df4c081b",
-			"revisionTime": "2018-06-26T06:41:42Z"
+			"revision": "7138fd3d9dc8335c567ca206f4333fb75eb05d56",
+			"revisionTime": "2018-06-27T13:57:12Z"
 		},
 		{
 			"checksumSHA1": "xsaHqy6/sonLV6xIxTNh4FfkWbU=",
@@ -227,10 +227,10 @@
 			"revisionTime": "2017-11-23T04:56:18Z"
 		},
 		{
-			"checksumSHA1": "RDJpJQwkF012L6m/2BJizyOksNw=",
+			"checksumSHA1": "o20lmjzBQyKD5LfLZ3OhUoMkLds=",
 			"path": "gopkg.in/yaml.v2",
-			"revision": "eb3733d160e74a9c7e442f435eb3bea458e1d19f",
-			"revisionTime": "2017-08-12T16:00:11Z"
+			"revision": "25c4ec802a7d637f88d584ab26798e94ad14c13b",
+			"revisionTime": "2017-07-21T12:20:51Z"
 		}
 	],
 	"rootPath": "git.autistici.org/id/keystore"
-- 
GitLab