Commit c7002dac authored by ale's avatar ale

Add vendor deps and Debian pkg metadata

parent e6fbab0e
This diff is collapsed.
usermetadb (0.2) unstable; urgency=medium
* Initial Release.
-- Autistici/Inventati <debian@autistici.org> Sun, 26 Nov 2017 21:04:22 +0000
Source: usermetadb
Section: admin
Priority: optional
Maintainer: Autistici/Inventati <debian@autistici.org>
Build-Depends: debhelper (>=9), golang-go, dh-systemd, dh-golang
Standards-Version: 3.9.6
Package: user-meta-server
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, auth-server
Description: User metadata server.
Stores anonymized user login patterns and device information.
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: usermetadb
Source: <https://git.autistici.org/id/usermetadb>
Files: *
Copyright: 2017 Autistici/Inventati <info@autistici.org>
License: GPL-3.0+
License: GPL-3.0+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
.
On Debian systems, the complete text of the GNU General
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
#!/bin/sh
set -e
case "$1" in
configure)
addgroup --system --quiet user-meta-server
adduser --system --no-create-home --home /run/user-meta-server \
--disabled-password --disabled-login \
--quiet --ingroup user-meta-server user-meta-server
;;
esac
#DEBHELPER#
exit 0
#!/usr/bin/make -f
export DH_GOPKG = git.autistici.org/id/usermetadb
export DH_GOLANG_EXCLUDES = vendor
%:
dh $@ --with systemd --with golang --buildsystem golang
override_dh_install:
rm -fr $(CURDIR)/debian/user-meta-server/usr/share/gocode
dh_install
override_dh_systemd_enable:
dh_systemd_enable --no-enable
override_dh_systemd_start:
dh_systemd_start --no-start
[Unit]
Description=SSO Server
After=network.target
[Service]
User=user-meta-server
Group=user-meta-server
EnvironmentFile=-/etc/default/user-meta-server
ExecStart=/usr/bin/user-meta-server --addr $ADDR
Restart=always
# Hardening
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectHome=yes
ProtectSystem=full
ReadOnlyDirectories=/
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
package clientutil
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
)
func DoJSONHTTPRequest(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")
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)
}
package clientutil
import (
"errors"
"net"
"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
}
// Retry operation op until it succeeds according to the backoff policy b.
func Retry(op backoff.Operation, b backoff.BackOff) error {
innerOp := func() error {
err := op()
if err == nil {
return err
}
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
return err
}
return backoff.Permanent(err)
}
return backoff.Retry(innerOp, b)
}
var errHTTPBackOff = 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.
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, b)
return resp, err
}
package clientutil
import (
"crypto/tls"
common "git.autistici.org/ai3/go-common"
)
// TLSClientConfig defines the TLS parameters for a client connection
// that should use a client X509 certificate for authentication.
type TLSClientConfig struct {
Cert string `yaml:"cert"`
Key string `yaml:"key"`
CA string `yaml:"ca"`
}
// TLSConfig returns a tls.Config object with the current configuration.
func (c *TLSClientConfig) TLSConfig() (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(c.Cert, c.Key)
if err != nil {
return nil, err
}
tlsConf := &tls.Config{
Certificates: []tls.Certificate{cert},
}
if c.CA != "" {
cas, err := common.LoadCA(c.CA)
if err != nil {
return nil, err
}
tlsConf.RootCAs = cas
}
tlsConf.BuildNameToCertificate()
return tlsConf, nil
}
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.
//
// 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
// List of currently valid (or untested) backends, and ones
// that errored out at least once.
mx sync.Mutex
addrs []string
ok map[string]bool
}
var backendUpdateInterval = 60 * time.Second
// 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()
}
}
}
}
// 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()
var good, bad []string
for _, addr := range b.addrs {
if ok := b.ok[addr]; ok {
good = append(good, addr)
} else {
bad = append(bad, addr)
}
}
return good, bad
}
func (b *balancer) notify(addr string, ok bool) {
b.mx.Lock()
b.ok[addr] = ok
b.mx.Unlock()
}
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.
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 {
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,
}
}
package common
import (
"crypto/x509"
"io/ioutil"
)
// LoadCA loads a file containing CA certificates into a x509.CertPool.
func LoadCA(path string) (*x509.CertPool, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
cas := x509.NewCertPool()
cas.AppendCertsFromPEM(data)
return cas, nil
}
package serverutil
import (
"context"
"crypto/tls"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
// 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 *TLSServerConfig, addr string) (err error) {
var tlsConfig *tls.Config
if serverConfig != nil {
tlsConfig, err = serverConfig.TLSConfig()
if err != nil {
return err
}
h, err = serverConfig.TLSAuthWrapper(h)
if err != nil {
return err
}
}
srv := &http.Server{
Addr: addr,
Handler: h,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
TLSConfig: tlsConfig,
}
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(), 3*time.Second)
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
}
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)
}
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
}
}
}