diff --git a/README.md b/README.md index fdf69259f9e01e0a4dc52ee9e635f3e3819b51da..b7dc446f32b5270075bc96213d17d7742a207cf7 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,13 @@ a single attribute *key*. `/api/close` (*CloseRequest*) Forget the key for a given user. + +# Dovecot integration + +The final consumer for user encryption keys is the Dovecot +service. The *dovecot-keylookupd* daemon can read the user public and +private keys from LDAP, and serve the *unencrypted* keys to Dovecot +using its [dict proxy +protocol](https://wiki2.dovecot.org/AuthDatabase/Dict). + +TODO: explain the lookup protocol. diff --git a/backend/ldap.go b/backend/ldap.go index 03c2b755ea19ac057ea565231a2b3ac9401d7f91..aa10edd5c23b083f9d1c0e3bff44734f67451cdd 100644 --- a/backend/ldap.go +++ b/backend/ldap.go @@ -19,10 +19,12 @@ type LDAPQueryConfig struct { // a query. SearchBase string `yaml:"search_base"` SearchFilter string `yaml:"search_filter"` - Scope string `yaml:"scope"` + ScopeStr string `yaml:"scope"` + scope int // Attr is the LDAP attribute holding the encrypted user key. - Attr string `yaml:"attr"` + PublicKeyAttr string `yaml:"attr"` + PrivateKeyAttr string `yaml:"attr"` } // Valid returns an error if the configuration is invalid. @@ -33,32 +35,36 @@ func (c *LDAPQueryConfig) Valid() error { if c.SearchFilter == "" { return errors.New("empty search_filter") } - if c.Scope != "one" && c.Scope != "sub" { - return errors.New("unknown scope") + c.scope = ldap.ScopeWholeSubtree + if c.ScopeStr != "" { + s, err := ldaputil.ParseScope(c.ScopeStr) + if err != nil { + return err + } + c.scope = s } - if c.Attr == "" { - return errors.New("empty attr") + if c.PublicKeyAttr == "" { + return errors.New("empty public_key_attr") + } + if c.PrivateKeyAttr == "" { + return errors.New("empty public_key_attr") } return nil } -func (c *LDAPQueryConfig) searchRequest(username string) *ldap.SearchRequest { +func (c *LDAPQueryConfig) searchRequest(username string, attrs ...string) *ldap.SearchRequest { u := ldap.EscapeFilter(username) base := strings.Replace(c.SearchBase, "%s", u, -1) filter := strings.Replace(c.SearchFilter, "%s", u, -1) - scope := ldap.ScopeWholeSubtree - if c.Scope == "one" { - scope = ldap.ScopeSingleLevel - } return ldap.NewSearchRequest( base, - scope, + c.scope, ldap.NeverDerefAliases, 0, 0, false, filter, - []string{c.Attr}, + attrs, nil, ) } @@ -117,8 +123,8 @@ func NewLDAPBackend(config *LDAPConfig) (*ldapBackend, error) { }, nil } -func (b *ldapBackend) GetKeys(ctx context.Context, username string) [][]byte { - result, err := b.pool.Search(ctx, b.config.Query.searchRequest(username)) +func (b *ldapBackend) GetPrivateKeys(ctx context.Context, username string) [][]byte { + result, err := b.pool.Search(ctx, b.config.Query.searchRequest(username, b.config.Query.PrivateKeyAttr)) if err != nil { log.Printf("LDAP error: %v", err) return nil @@ -126,8 +132,30 @@ func (b *ldapBackend) GetKeys(ctx context.Context, username string) [][]byte { var out [][]byte for _, ent := range result.Entries { - k := []byte(ent.GetAttributeValue(b.config.Query.Attr)) - out = append(out, k) + for _, val := range ent.GetAttributeValues(b.config.Query.PrivateKeyAttr) { + out = append(out, []byte(val)) + } } return out } + +func (b *ldapBackend) GetPublicKey(ctx context.Context, username string) []byte { + result, err := b.pool.Search(ctx, b.config.Query.searchRequest(username, b.config.Query.PublicKeyAttr)) + if err != nil { + log.Printf("LDAP error: %v", err) + return nil + } + if len(result.Entries) == 0 { + return nil + } + if len(result.Entries) > 1 { + log.Printf("public key query for %s returned too many results (%d)", username, len(result.Entries)) + return nil + } + + s := result.Entries[0].GetAttributeValue(b.config.Query.PublicKeyAttr) + if s == "" { + return nil + } + return []byte(s) +} diff --git a/cmd/dovecot-keylookupd/main.go b/cmd/dovecot-keylookupd/main.go new file mode 100644 index 0000000000000000000000000000000000000000..8416d52ee12a718b42e65e1dff7025ee7356abd7 --- /dev/null +++ b/cmd/dovecot-keylookupd/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "flag" + "io/ioutil" + "log" + "os" + "os/signal" + "syscall" + "time" + + "git.autistici.org/ai3/go-common/unix" + "github.com/coreos/go-systemd/daemon" + "gopkg.in/yaml.v2" + + "git.autistici.org/id/keystore/dovecot" +) + +var ( + configFile = flag.String("config", "/etc/keystore/dovecot.yml", "path of config file") + socketPath = flag.String("socket", "/run/dovecot-keystored/socket", "`path` to the UNIX socket to listen on") + systemdSocketActivation = flag.Bool("systemd-socket", false, "use SystemD socket activation") + requestTimeout = flag.Duration("timeout", 5*time.Second, "timeout for incoming requests") +) + +// Read YAML config. +func loadConfig() (*dovecot.Config, error) { + data, err := ioutil.ReadFile(*configFile) + if err != nil { + return nil, err + } + var config dovecot.Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, err + } + return &config, nil +} + +func main() { + log.SetFlags(0) + flag.Parse() + + syscall.Umask(007) + + config, err := loadConfig() + if err != nil { + log.Fatal(err) + } + + ddp, err := dovecot.NewKeyLookupProxy(config) + if err != nil { + log.Fatal(err) + } + + srv := unix.NewLineServer(dovecot.NewDictProxyServer(ddp)) + srv.RequestTimeout = *requestTimeout + + var sockSrv *unix.SocketServer + if *systemdSocketActivation { + sockSrv, err = unix.NewSystemdSocketServer(srv) + } else { + sockSrv, err = unix.NewUNIXSocketServer(*socketPath, srv) + } + if err != nil { + log.Fatalf("error: %v", err) + } + + done := make(chan struct{}) + sigCh := make(chan os.Signal, 1) + go func() { + <-sigCh + log.Printf("terminating") + sockSrv.Close() + close(done) + }() + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + log.Printf("starting") + daemon.SdNotify(false, "READY=1") + if err := sockSrv.Serve(); err != nil { + log.Fatal(err) + } + + <-done +} diff --git a/dovecot/dict.go b/dovecot/dict.go new file mode 100644 index 0000000000000000000000000000000000000000..8278e3c4a657069153d25984ae478552fdd7953c --- /dev/null +++ b/dovecot/dict.go @@ -0,0 +1,71 @@ +package dovecot + +import ( + "bytes" + "context" + "encoding/json" + "errors" + + "git.autistici.org/ai3/go-common/unix" +) + +var ( + failResponse = []byte{'F', '\n'} + noMatchResponse = []byte{'N', '\n'} +) + +// DictDatabase is an interface to a key/value store by way of the Lookup +// method. +type DictDatabase interface { + Lookup(context.Context, string) (interface{}, bool) +} + +// DictProxyServer exposes a Database using the Dovecot dict proxy +// protocol (see https://wiki2.dovecot.org/AuthDatabase/Dict). +// +// It implements the unix.LineHandler interface from the +// ai3/go-common/unix package. +type DictProxyServer struct { + db DictDatabase +} + +// NewDictProxyServer creates a new DictProxyServer. +func NewDictProxyServer(db DictDatabase) *DictProxyServer { + return &DictProxyServer{db: db} +} + +// ServeLine handles a single command. +func (p *DictProxyServer) ServeLine(ctx context.Context, lw unix.LineResponseWriter, line []byte) error { + if len(line) < 1 { + return errors.New("line too short") + } + + switch line[0] { + case 'H': + return p.handleHello(ctx, lw, line[1:]) + case 'L': + return p.handleLookup(ctx, lw, line[1:]) + default: + return lw.WriteLine(failResponse) + } +} + +func (p *DictProxyServer) handleHello(ctx context.Context, lw unix.LineResponseWriter, arg []byte) error { + // TODO: parse the hello line and extract useful information. + return nil +} + +func (p *DictProxyServer) handleLookup(ctx context.Context, lw unix.LineResponseWriter, arg []byte) error { + obj, ok := p.db.Lookup(ctx, string(arg)) + if !ok { + return lw.WriteLine(noMatchResponse) + } + + var buf bytes.Buffer + buf.Write([]byte{'O'}) + if err := json.NewEncoder(&buf).Encode(obj); err != nil { + return err + } + buf.Write([]byte{'\n'}) + return lw.WriteLine(buf.Bytes()) +} diff --git a/dovecot/keyproxy.go b/dovecot/keyproxy.go new file mode 100644 index 0000000000000000000000000000000000000000..9be2a1039f1789547d42f02a49aa37995df807f9 --- /dev/null +++ b/dovecot/keyproxy.go @@ -0,0 +1,124 @@ +package dovecot + +import ( + "context" + "encoding/base64" + "errors" + "strings" + + "git.autistici.org/ai3/go-common/clientutil" + + "git.autistici.org/id/keystore/backend" + "git.autistici.org/id/keystore/client" + "git.autistici.org/id/keystore/userenckey" +) + +// Config for the dovecot-keystore daemon. +type Config struct { + Shard string `yaml:"shard"` + LDAPConfig *backend.LDAPConfig `yaml:"ldap"` + Keystore *clientutil.BackendConfig `yaml:"keystore"` +} + +// Database represents the interface to the underlying backend for +// encrypted user keys. +type Database interface { + GetPublicKey(context.Context, string) []byte + GetPrivateKeys(context.Context, string) [][]byte +} + +func (c *Config) check() error { + if c.LDAPConfig == nil { + return errors.New("missing backend config") + } + return c.LDAPConfig.Valid() +} + +type userdbResponse struct { + PublicKey string `json:"mail_crypt_global_public_key"` +} + +type passdbResponse struct { + PrivateKey string `json:"mail_crypt_global_private_key"` +} + +var passwordSep = "/" + +// KeyLookupProxy interfaces Dovecot with the user encryption key database. +type KeyLookupProxy struct { + config *Config + keystore client.Client + db Database +} + +func NewKeyLookupProxy(config *Config) (*KeyLookupProxy, error) { + if err := config.check(); err != nil { + return nil, err + } + + ksc, err := client.New(config.Keystore) + if err != nil { + return nil, err + } + + // There is only one supported backend type, ldap. + ldap, err := backend.NewLDAPBackend(config.LDAPConfig) + if err != nil { + return nil, err + } + + return &KeyLookupProxy{ + config: config, + keystore: ksc, + db: ldap, + }, nil +} + +// Lookup a key using the dovecot dict proxy interface. +// +// We can be sent a userdb lookup, or a passdb lookup, and we can tell +// them apart from the structure of the key: +// +// If it contains passwordSep, then it's a passdb lookup and the key +// consists of 'username' and 'password'. Otherwise, it's a userdb +// lookup and the key is simply the username. +func (s *KeyLookupProxy) Lookup(ctx context.Context, key string) (interface{}, bool) { + if strings.Contains(key, passwordSep) { + kparts := strings.SplitN(key, passwordSep, 2) + return s.lookupPassdb(ctx, kparts[0], kparts[1]) + } + return s.lookupUserdb(ctx, key) +} + +func (s *KeyLookupProxy) lookupUserdb(ctx context.Context, username string) (interface{}, bool) { + pub := s.db.GetPublicKey(ctx, username) + if pub == nil { + return nil, false + } + return &userdbResponse{PublicKey: b64encode(pub)}, true +} + +func (s *KeyLookupProxy) lookupPassdb(ctx context.Context, username, password string) (interface{}, bool) { + // If the password is a SSO token, try to fetch the + // unencrypted key from the keystore daemon. + priv, err := s.keystore.Get(ctx, s.config.Shard, username, password) + if err == nil { + return &passdbResponse{PrivateKey: b64encode(priv)}, true + } + + // Otherwise, fetch encrypted keys from the db and attempt to + // decrypt them. + encKeys := s.db.GetPrivateKeys(ctx, username) + if len(encKeys) == 0 { + return nil, false + } + priv, err = userenckey.Decrypt(encKeys, []byte(password)) + if err != nil { + return nil, false + } + return &passdbResponse{PrivateKey: b64encode(priv)}, true +} + +func b64encode(b []byte) string { + return base64.StdEncoding.EncodeToString(b) +} diff --git a/server/keystore.go b/server/keystore.go index 8daf03124c7bdf43e14b0e9a62a80a635b9bdbc8..9cc820b36f1142e57776e95626afa6668cdb2354 100644 --- a/server/keystore.go +++ b/server/keystore.go @@ -11,19 +11,19 @@ import ( "git.autistici.org/id/go-sso" "git.autistici.org/id/keystore/backend" + "git.autistici.org/id/keystore/userenckey" ) var ( - ErrNoKeys = errors.New("no keys available") - ErrBadPassword = errors.New("could not decrypt key with password") - ErrBadUser = errors.New("username does not match authentication token") - ErrInvalidTTL = errors.New("invalid ttl") + ErrNoKeys = errors.New("no keys available") + ErrBadUser = errors.New("username does not match authentication token") + ErrInvalidTTL = errors.New("invalid ttl") ) // Database represents the interface to the underlying backend for // encrypted user keys. type Database interface { - GetKeys(context.Context, string) [][]byte + GetPrivateKeys(context.Context, string) [][]byte } type userKey struct { @@ -54,7 +54,7 @@ func (c *Config) check() error { if c.LDAPConfig == nil { return errors.New("missing backend config") } - return nil + return c.LDAPConfig.Valid() } // KeyStore holds decrypted secrets for users in memory for a short @@ -133,21 +133,16 @@ func (s *KeyStore) Open(ctx context.Context, username, password string, ttlSecon return ErrInvalidTTL } - encKeys := s.db.GetKeys(ctx, username) + encKeys := s.db.GetPrivateKeys(ctx, username) if len(encKeys) == 0 { return ErrNoKeys } - var pkey []byte - var err error - for _, key := range encKeys { - pkey, err = decrypt(key, []byte(password)) - if err == nil { - break - } - } + // Naive and inefficient way of decrypting multiple keys: it + // will recompute the kdf every time, which is expensive. + pkey, err := userenckey.Decrypt(encKeys, []byte(password)) if err != nil { - return ErrBadPassword + return err } s.mx.Lock() diff --git a/server/decrypt.go b/userenckey/decrypt.go similarity index 57% rename from server/decrypt.go rename to userenckey/decrypt.go index 0aac0b1aed2ee074c1dc67f3d5bde69936bdd37e..3f59eca698e0ac41e97478f3530272f6c80e9068 100644 --- a/server/decrypt.go +++ b/userenckey/decrypt.go @@ -1,4 +1,4 @@ -package server +package userenckey import ( "errors" @@ -7,6 +7,8 @@ import ( "golang.org/x/crypto/scrypt" ) +var ErrBadPassword = errors.New("could not decrypt key with password") + const ( scryptN = 32768 scryptR = 8 @@ -15,7 +17,20 @@ const ( saltLen = 32 ) -func decrypt(data, pw []byte) ([]byte, error) { +// Decrypt one out of multiple keys with the specified password. The +// keys share the same cleartext, but have been encrypted with +// different passwords. +func Decrypt(encKeys [][]byte, pw []byte) ([]byte, error) { + for _, key := range encKeys { + dec, err := decryptData(key, pw) + if err != nil { + return dec, err + } + } + return nil, ErrBadPassword +} + +func decryptData(data, pw []byte) ([]byte, error) { // The KDF salt is prepended to the encrypted key. if len(data) < saltLen { return nil, errors.New("short data") diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/parse.go b/vendor/git.autistici.org/ai3/go-common/ldap/parse.go new file mode 100644 index 0000000000000000000000000000000000000000..22a285c6c6d4f82694106852293f93f97a0b1f91 --- /dev/null +++ b/vendor/git.autistici.org/ai3/go-common/ldap/parse.go @@ -0,0 +1,20 @@ +package ldaputil + +import ( + "fmt" + + "gopkg.in/ldap.v2" +) + +func ParseScope(s string) (int, error) { + switch s { + case "base": + return ldap.ScopeBaseObject, nil + case "one": + return ldap.ScopeSingleLevel, nil + case "sub": + return ldap.ScopeWholeSubtree, nil + default: + return 0, fmt.Errorf("unknown LDAP scope '%s'", s) + } +} 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 6d8093e93dccd5d333133633ab8958092a171355..c77d06177cb1b13ed8c7b71bc3facf07a015adc4 100644 --- a/vendor/git.autistici.org/ai3/go-common/ldap/pool.go +++ b/vendor/git.autistici.org/ai3/go-common/ldap/pool.go @@ -40,10 +40,12 @@ func (p *ConnectionPool) connect(ctx context.Context) (*ldap.Conn, error) { conn := ldap.NewConn(c, false) conn.Start() - conn.SetTimeout(time.Until(deadline)) - if _, err = conn.SimpleBind(ldap.NewSimpleBindRequest(p.bindDN, p.bindPw, nil)); err != nil { - conn.Close() - return nil, err + if p.bindDN != "" { + conn.SetTimeout(time.Until(deadline)) + if _, err = conn.SimpleBind(ldap.NewSimpleBindRequest(p.bindDN, p.bindPw, nil)); err != nil { + conn.Close() + return nil, err + } } return conn, err diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/search.go b/vendor/git.autistici.org/ai3/go-common/ldap/search.go index 872f6fec3fdb2a8516f7e3da55dddeb972b358f5..db29ba092ea0cb1e1be3db3a753c75c7e1188f3f 100644 --- a/vendor/git.autistici.org/ai3/go-common/ldap/search.go +++ b/vendor/git.autistici.org/ai3/go-common/ldap/search.go @@ -10,16 +10,24 @@ import ( "git.autistici.org/ai3/go-common/clientutil" ) -// Treat all errors as potential network-level issues, except for a -// whitelist of LDAP protocol level errors that we know are benign. +// 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 { - ldapErr, ok := err.(*ldap.Error) - if !ok { - return true - } - switch ldapErr.ResultCode { - case ldap.ErrorNetwork: - return true + 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 } @@ -32,6 +40,7 @@ func (p *ConnectionPool) Search(ctx context.Context, searchRequest *ldap.SearchR 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) } @@ -44,7 +53,7 @@ func (p *ConnectionPool) Search(ctx context.Context, searchRequest *ldap.SearchR result, err = conn.Search(searchRequest) if err != nil && isTemporaryLDAPError(err) { - p.Release(conn, nil) + p.Release(conn, err) return clientutil.TempError(err) } p.Release(conn, err) diff --git a/vendor/git.autistici.org/ai3/go-common/unix/server.go b/vendor/git.autistici.org/ai3/go-common/unix/server.go new file mode 100644 index 0000000000000000000000000000000000000000..19fd813900e22eaa541a181d529c5b88bc03b05a --- /dev/null +++ b/vendor/git.autistici.org/ai3/go-common/unix/server.go @@ -0,0 +1,231 @@ +package unix + +import ( + "bufio" + "context" + "errors" + "io" + "log" + "net" + "net/textproto" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/coreos/go-systemd/activation" + "github.com/theckman/go-flock" +) + +// Handler for UNIX socket server connections. +type Handler interface { + ServeConnection(c net.Conn) +} + +// SocketServer accepts connections on a UNIX socket, speaking the +// line-based wire protocol, and dispatches incoming requests to the +// wrapped Server. +type SocketServer struct { + l net.Listener + lock *flock.Flock + closing atomic.Value + wg sync.WaitGroup + handler Handler +} + +func newServer(l net.Listener, lock *flock.Flock, h Handler) *SocketServer { + s := &SocketServer{ + l: l, + lock: lock, + handler: h, + } + s.closing.Store(false) + return s +} + +// NewUNIXSocketServer returns a new SocketServer listening on the given path. +func NewUNIXSocketServer(socketPath string, h Handler) (*SocketServer, error) { + // The simplest workflow is: create a new socket, remove it on + // exit. However, if the program crashes, the socket might + // stick around and prevent the next execution from starting + // successfully. We could remove it before starting, but that + // would be dangerous if another instance was listening on + // that socket. So we wrap socket access with a file lock. + lock := flock.NewFlock(socketPath + ".lock") + locked, err := lock.TryLock() + if err != nil { + return nil, err + } + if !locked { + return nil, errors.New("socket is locked by another process") + } + + addr, err := net.ResolveUnixAddr("unix", socketPath) + if err != nil { + return nil, err + } + + // Always try to unlink the socket before creating it. + os.Remove(socketPath) + + l, err := net.ListenUnix("unix", addr) + if err != nil { + return nil, err + } + + return newServer(l, lock, h), nil +} + +// NewSystemdSocketServer uses systemd socket activation, receiving +// the open socket as a file descriptor on exec. +func NewSystemdSocketServer(h Handler) (*SocketServer, error) { + listeners, err := activation.Listeners(false) + if err != nil { + return nil, err + } + // Our server loop implies a single listener, so find + // the first one passed by systemd and ignore all others. + // TODO: listen on all fds. + for _, l := range listeners { + if l != nil { + return newServer(l, nil, h), nil + } + } + return nil, errors.New("no available sockets found") +} + +// Close the socket listener and release all associated resources. +// Waits for active connections to terminate before returning. +func (s *SocketServer) Close() { + s.closing.Store(true) + s.l.Close() + s.wg.Wait() + if s.lock != nil { + s.lock.Unlock() + } +} + +func (s *SocketServer) isClosing() bool { + return s.closing.Load().(bool) +} + +// Serve connections. +func (s *SocketServer) Serve() error { + for { + conn, err := s.l.Accept() + if err != nil { + if s.isClosing() { + return nil + } + return err + } + s.wg.Add(1) + go func() { + s.handler.ServeConnection(conn) + conn.Close() + s.wg.Done() + }() + } +} + +// LineHandler is the handler for LineServer. +type LineHandler interface { + ServeLine(context.Context, LineResponseWriter, []byte) error +} + +// ErrCloseConnection must be returned by a LineHandler when we want +// to cleanly terminate the connection without raising an error. +var ErrCloseConnection = errors.New("close") + +// LineResponseWriter writes a single-line response to the underlying +// connection. +type LineResponseWriter interface { + // WriteLine writes a response (which must include the + // line terminator). + WriteLine([]byte) error + + // WriteLineCRLF writes a response and adds a line terminator. + WriteLineCRLF([]byte) error +} + +// LineServer implements a line-based text protocol. It satisfies the +// Handler interface. +type LineServer struct { + handler LineHandler + + IdleTimeout time.Duration + WriteTimeout time.Duration + RequestTimeout time.Duration +} + +var ( + defaultIdleTimeout = 600 * time.Second + defaultWriteTimeout = 10 * time.Second + defaultRequestTimeout = 30 * time.Second +) + +// NewLineServer returns a new LineServer with the given handler and +// default I/O timeouts. +func NewLineServer(h LineHandler) *LineServer { + return &LineServer{ + handler: h, + IdleTimeout: defaultIdleTimeout, + WriteTimeout: defaultWriteTimeout, + RequestTimeout: defaultRequestTimeout, + } +} + +var crlf = []byte{'\r', '\n'} + +type lrWriter struct { + *bufio.Writer +} + +func (w *lrWriter) WriteLine(data []byte) error { + if _, err := w.Writer.Write(data); err != nil { + return err + } + return w.Writer.Flush() +} + +func (w *lrWriter) WriteLineCRLF(data []byte) error { + if _, err := w.Writer.Write(data); err != nil { + return err + } + if _, err := w.Writer.Write(crlf); err != nil { + return err + } + return w.Writer.Flush() +} + +func (l *LineServer) ServeConnection(nc net.Conn) { + c := textproto.NewConn(nc) + rw := &lrWriter{bufio.NewWriter(nc)} + for { + nc.SetReadDeadline(time.Now().Add(l.IdleTimeout)) + line, err := c.ReadLineBytes() + if err == io.EOF { + break + } else if err != nil { + log.Printf("client error: %v", err) + break + } + + // Create a context for the request and call the + // handler with it. Set a write deadline on the + // connection to allow the full RequestTimeout time to + // generate the response. + nc.SetWriteDeadline(time.Now().Add(l.RequestTimeout + l.WriteTimeout)) + ctx, cancel := context.WithTimeout(context.Background(), l.RequestTimeout) + err = l.handler.ServeLine(ctx, rw, line) + cancel() + + // Close the connection on error, or on empty response. + if err != nil { + if err != ErrCloseConnection { + log.Printf("request error: %v", err) + } + break + } + } +} diff --git a/vendor/github.com/coreos/go-systemd/LICENSE b/vendor/github.com/coreos/go-systemd/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..37ec93a14fdcd0d6e525d97c0cfa6b314eaa98d8 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"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: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +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 +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. diff --git a/vendor/github.com/coreos/go-systemd/activation/files.go b/vendor/github.com/coreos/go-systemd/activation/files.go new file mode 100644 index 0000000000000000000000000000000000000000..c8e85fcd58894494b84693a9840e9c820558fce6 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/activation/files.go @@ -0,0 +1,52 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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. + +// Package activation implements primitives for systemd socket activation. +package activation + +import ( + "os" + "strconv" + "syscall" +) + +// based on: https://gist.github.com/alberts/4640792 +const ( + listenFdsStart = 3 +) + +func Files(unsetEnv bool) []*os.File { + if unsetEnv { + defer os.Unsetenv("LISTEN_PID") + defer os.Unsetenv("LISTEN_FDS") + } + + pid, err := strconv.Atoi(os.Getenv("LISTEN_PID")) + if err != nil || pid != os.Getpid() { + return nil + } + + nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS")) + if err != nil || nfds == 0 { + return nil + } + + files := make([]*os.File, 0, nfds) + for fd := listenFdsStart; fd < listenFdsStart+nfds; fd++ { + syscall.CloseOnExec(fd) + files = append(files, os.NewFile(uintptr(fd), "LISTEN_FD_"+strconv.Itoa(fd))) + } + + return files +} diff --git a/vendor/github.com/coreos/go-systemd/activation/listeners.go b/vendor/github.com/coreos/go-systemd/activation/listeners.go new file mode 100644 index 0000000000000000000000000000000000000000..fd5dfc709c77e00796ade8fb89ca089eeb7bbc9f --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/activation/listeners.go @@ -0,0 +1,60 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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. + +package activation + +import ( + "crypto/tls" + "net" +) + +// Listeners returns a slice containing a net.Listener for each matching socket type +// passed to this process. +// +// The order of the file descriptors is preserved in the returned slice. +// Nil values are used to fill any gaps. For example if systemd were to return file descriptors +// corresponding with "udp, tcp, tcp", then the slice would contain {nil, net.Listener, net.Listener} +func Listeners(unsetEnv bool) ([]net.Listener, error) { + files := Files(unsetEnv) + listeners := make([]net.Listener, len(files)) + + for i, f := range files { + if pc, err := net.FileListener(f); err == nil { + listeners[i] = pc + } + } + return listeners, nil +} + +// TLSListeners returns a slice containing a net.listener for each matching TCP socket type +// passed to this process. +// It uses default Listeners func and forces TCP sockets handlers to use TLS based on tlsConfig. +func TLSListeners(unsetEnv bool, tlsConfig *tls.Config) ([]net.Listener, error) { + listeners, err := Listeners(unsetEnv) + + if listeners == nil || err != nil { + return nil, err + } + + if tlsConfig != nil && err == nil { + for i, l := range listeners { + // Activate TLS only for TCP sockets + if l.Addr().Network() == "tcp" { + listeners[i] = tls.NewListener(l, tlsConfig) + } + } + } + + return listeners, err +} diff --git a/vendor/github.com/coreos/go-systemd/activation/packetconns.go b/vendor/github.com/coreos/go-systemd/activation/packetconns.go new file mode 100644 index 0000000000000000000000000000000000000000..48b2ca029df208f20fd465f510a3c9a68e6eed75 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/activation/packetconns.go @@ -0,0 +1,37 @@ +// Copyright 2015 CoreOS, Inc. +// +// 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. + +package activation + +import ( + "net" +) + +// PacketConns returns a slice containing a net.PacketConn for each matching socket type +// passed to this process. +// +// The order of the file descriptors is preserved in the returned slice. +// Nil values are used to fill any gaps. For example if systemd were to return file descriptors +// corresponding with "udp, tcp, udp", then the slice would contain {net.PacketConn, nil, net.PacketConn} +func PacketConns(unsetEnv bool) ([]net.PacketConn, error) { + files := Files(unsetEnv) + conns := make([]net.PacketConn, len(files)) + + for i, f := range files { + if pc, err := net.FilePacketConn(f); err == nil { + conns[i] = pc + } + } + return conns, nil +} diff --git a/vendor/github.com/coreos/go-systemd/daemon/sdnotify.go b/vendor/github.com/coreos/go-systemd/daemon/sdnotify.go new file mode 100644 index 0000000000000000000000000000000000000000..ba6d41d85baae74d59958304bef2f3b8cc27fb00 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/daemon/sdnotify.go @@ -0,0 +1,63 @@ +// Copyright 2014 Docker, Inc. +// +// 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. +// + +// Code forked from Docker project +package daemon + +import ( + "net" + "os" +) + +// SdNotify sends a message to the init daemon. It is common to ignore the error. +// If `unsetEnvironment` is true, the environment variable `NOTIFY_SOCKET` +// will be unconditionally unset. +// +// It returns one of the following: +// (false, nil) - notification not supported (i.e. NOTIFY_SOCKET is unset) +// (false, err) - notification supported, but failure happened (e.g. error connecting to NOTIFY_SOCKET or while sending data) +// (true, nil) - notification supported, data has been sent +func SdNotify(unsetEnvironment bool, state string) (sent bool, err error) { + socketAddr := &net.UnixAddr{ + Name: os.Getenv("NOTIFY_SOCKET"), + Net: "unixgram", + } + + // NOTIFY_SOCKET not set + if socketAddr.Name == "" { + return false, nil + } + + if unsetEnvironment { + err = os.Unsetenv("NOTIFY_SOCKET") + } + if err != nil { + return false, err + } + + conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) + // Error connecting to NOTIFY_SOCKET + if err != nil { + return false, err + } + defer conn.Close() + + _, err = conn.Write([]byte(state)) + // Error sending the message + if err != nil { + return false, err + } + return true, nil +} diff --git a/vendor/github.com/coreos/go-systemd/daemon/watchdog.go b/vendor/github.com/coreos/go-systemd/daemon/watchdog.go new file mode 100644 index 0000000000000000000000000000000000000000..35a92e6e67dbf6d5900f47af95774bdf74b41a54 --- /dev/null +++ b/vendor/github.com/coreos/go-systemd/daemon/watchdog.go @@ -0,0 +1,72 @@ +// Copyright 2016 CoreOS, Inc. +// +// 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. + +package daemon + +import ( + "fmt" + "os" + "strconv" + "time" +) + +// SdWatchdogEnabled return watchdog information for a service. +// Process should send daemon.SdNotify("WATCHDOG=1") every time / 2. +// If `unsetEnvironment` is true, the environment variables `WATCHDOG_USEC` +// and `WATCHDOG_PID` will be unconditionally unset. +// +// It returns one of the following: +// (0, nil) - watchdog isn't enabled or we aren't the watched PID. +// (0, err) - an error happened (e.g. error converting time). +// (time, nil) - watchdog is enabled and we can send ping. +// time is delay before inactive service will be killed. +func SdWatchdogEnabled(unsetEnvironment bool) (time.Duration, error) { + wusec := os.Getenv("WATCHDOG_USEC") + wpid := os.Getenv("WATCHDOG_PID") + if unsetEnvironment { + wusecErr := os.Unsetenv("WATCHDOG_USEC") + wpidErr := os.Unsetenv("WATCHDOG_PID") + if wusecErr != nil { + return 0, wusecErr + } + if wpidErr != nil { + return 0, wpidErr + } + } + + if wusec == "" { + return 0, nil + } + s, err := strconv.Atoi(wusec) + if err != nil { + return 0, fmt.Errorf("error converting WATCHDOG_USEC: %s", err) + } + if s <= 0 { + return 0, fmt.Errorf("error WATCHDOG_USEC must be a positive number") + } + interval := time.Duration(s) * time.Microsecond + + if wpid == "" { + return interval, nil + } + p, err := strconv.Atoi(wpid) + if err != nil { + return 0, fmt.Errorf("error converting WATCHDOG_PID: %s", err) + } + if os.Getpid() != p { + return 0, nil + } + + return interval, nil +} diff --git a/vendor/github.com/theckman/go-flock/LICENSE b/vendor/github.com/theckman/go-flock/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..aff7d358e246cb73066a17451c6017b713d5871a --- /dev/null +++ b/vendor/github.com/theckman/go-flock/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2015, Tim Heckman +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of linode-netint nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/theckman/go-flock/README.md b/vendor/github.com/theckman/go-flock/README.md new file mode 100644 index 0000000000000000000000000000000000000000..82069971770bc80a663795e1e19bf7fcb56f5a56 --- /dev/null +++ b/vendor/github.com/theckman/go-flock/README.md @@ -0,0 +1,36 @@ +# go-flock +[](https://travis-ci.org/theckman/go-flock) +[](https://godoc.org/github.com/theckman/go-flock) +[](https://github.com/theckman/go-flock/blob/master/LICENSE) + +`flock` implements a thread-safe sync.Locker interface for file locking. It also +includes a non-blocking TryLock() function to allow locking without blocking execution. + +## License +`flock` is released under the BSD 3-Clause License. See the `LICENSE` file for more details. + +## Intsallation +``` +go get -u github.com/theckman/go-flock +``` + +## Usage +```Go +import "github.com/theckman/go-flock" + +fileLock := flock.NewFlock("/var/lock/go-lock.lock") + +locked, err := fileLock.TryLock() + +if err != nil { + // handle locking error +} + +if locked { + // do work + fileLock.Unlock() +} +``` + +For more detailed usage information take a look at the package API docs on +[GoDoc](https://godoc.org/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 new file mode 100644 index 0000000000000000000000000000000000000000..2582077ffc67ecde4d84f37c59f6594dccedd99d --- /dev/null +++ b/vendor/github.com/theckman/go-flock/flock.go @@ -0,0 +1,61 @@ +// Copyright 2015 Tim Heckman. All rights reserved. +// Use of this source code is governed by the BSD 3-Clause +// license that can be found in the LICENSE file. + +// Package flock implements a thread-safe sync.Locker interface for file locking. +// It also includes a non-blocking TryLock() function to allow locking +// without blocking execution. +// +// Package flock is released under the BSD 3-Clause License. See the LICENSE file +// for more details. +package flock + +import ( + "os" + "sync" +) + +// Flock is the struct type to handle file locking. All fields are unexported, +// with access to some of the fields provided by getter methods (Path() and Locked()). +type Flock struct { + path string + m sync.RWMutex + fh *os.File + l bool +} + +// NewFlock is a function to return a new instance of *Flock. The only parameter +// it takes is the path to the desired lockfile. +func NewFlock(path string) *Flock { + return &Flock{path: path} +} + +// Path is a function to return the path as provided in NewFlock(). +func (f *Flock) Path() string { + return f.path +} + +// Locked is a function to return the current lock state (locked: true, unlocked: false). +func (f *Flock) Locked() bool { + f.m.RLock() + defer f.m.RUnlock() + return f.l +} + +func (f *Flock) String() string { + return f.path +} + +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 + fh, err := os.OpenFile(f.path, os.O_CREATE|os.O_TRUNC|os.O_RDWR, os.FileMode(0600)) + + if err != nil { + return err + } + + // set the filehandle on the struct + f.fh = fh + return nil +} diff --git a/vendor/github.com/theckman/go-flock/flock_unix.go b/vendor/github.com/theckman/go-flock/flock_unix.go new file mode 100644 index 0000000000000000000000000000000000000000..8c9a64f0732fed55a5300f6314bb48abebf13a5e --- /dev/null +++ b/vendor/github.com/theckman/go-flock/flock_unix.go @@ -0,0 +1,104 @@ +// Copyright 2015 Tim Heckman. All rights reserved. +// Use of this source code is governed by the BSD 3-Clause +// license that can be found in the LICENSE file. + +// +build !windows + +package flock + +import ( + "syscall" +) + +// Lock is a blocking call to try and take the file lock. It will wait until it +// is able to obtain the exclusive file lock. It's recommended that TryLock() be +// used over this function. This function may block the ability to query the +// current Locked() status due to a RW-mutex lock. +// +// If we are already locked, this function short-circuits and returns immediately +// assuming it can take the mutex lock. +func (f *Flock) Lock() error { + f.m.Lock() + defer f.m.Unlock() + + if f.l { + return nil + } + + if f.fh == nil { + if err := f.setFh(); err != nil { + return err + } + } + + if err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_EX); err != nil { + return err + } + + f.l = true + return nil +} + +// Unlock is a function to unlock the file. This file takes a RW-mutex lock, so +// while it is running the Locked() function will be blocked. +// +// This function short-circuits if we are unlocked already. If not, it calls +// syscall.LOCK_UN on the file and closes the file descriptor It does not remove +// the file from disk. It's up to your application to do. +func (f *Flock) Unlock() error { + f.m.Lock() + defer f.m.Unlock() + + // if we aren't locked or if the lockfile instance is nil + // just return a nil error because we are unlocked + if !f.l || f.fh == nil { + return nil + } + + // mark the file as unlocked + if err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_UN); err != nil { + return err + } + + f.fh.Close() + + f.l = false + f.fh = nil + + return nil +} + +// TryLock is the preferred function for taking a file lock. This function does +// take a RW-mutex lock before it tries to lock the file, so there is the +// possibility that this function may block for a short time if another goroutine +// is trying to take any action. +// +// The actual file lock is non-blocking. If we are unable to get the exclusive +// file lock, the function will return false instead of waiting for the lock. +// If we get the lock, we also set the *Flock instance as being locked. +func (f *Flock) TryLock() (bool, error) { + f.m.Lock() + defer f.m.Unlock() + + if f.l { + return true, nil + } + + if f.fh == nil { + if err := f.setFh(); err != nil { + return false, err + } + } + + err := syscall.Flock(int(f.fh.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + + switch err { + case syscall.EWOULDBLOCK: + return false, nil + case nil: + f.l = true + return true, nil + } + + return false, err +} diff --git a/vendor/github.com/theckman/go-flock/flock_winapi.go b/vendor/github.com/theckman/go-flock/flock_winapi.go new file mode 100644 index 0000000000000000000000000000000000000000..e4a25769a005054019801929dee4df1766f12dce --- /dev/null +++ b/vendor/github.com/theckman/go-flock/flock_winapi.go @@ -0,0 +1,92 @@ +// Copyright 2015 Tim Heckman. All rights reserved. +// Use of this source code is governed by the BSD 3-Clause +// license that can be found in the LICENSE file. + +// +build windows + +package flock + +import ( + "syscall" + "unsafe" +) + +var ( + kernel32, _ = syscall.LoadLibrary("kernel32.dll") + procLockFileEx, _ = syscall.GetProcAddress(kernel32, "LockFileEx") + procUnlockFileEx, _ = syscall.GetProcAddress(kernel32, "UnlockFileEx") +) + +const ( + LOCKFILE_FAIL_IMMEDIATELY = 0x00000001 + LOCKFILE_EXCLUSIVE_LOCK = 0x00000002 +) + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return nil + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +func lockFileEx(handle syscall.Handle, flags uint32, reserved uint32, numberOfBytesToLockLow uint32, numberOfBytesToLockHigh uint32, offset *syscall.Overlapped) (success bool, err error) { + r1, _, e1 := syscall.Syscall6( + uintptr(procLockFileEx), + 6, + uintptr(handle), + uintptr(flags), + uintptr(reserved), + uintptr(numberOfBytesToLockLow), + uintptr(numberOfBytesToLockHigh), + uintptr(unsafe.Pointer(offset))) + + success = r1 == 1 + if !success { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func unlockFileEx(handle syscall.Handle, reserved uint32, numberOfBytesToLockLow uint32, numberOfBytesToLockHigh uint32, offset *syscall.Overlapped) (success bool, err error) { + r1, _, e1 := syscall.Syscall6( + uintptr(procUnlockFileEx), + 5, + uintptr(handle), + uintptr(reserved), + uintptr(numberOfBytesToLockLow), + uintptr(numberOfBytesToLockHigh), + uintptr(unsafe.Pointer(offset)), + 0) + + success = r1 == 1 + if !success { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} diff --git a/vendor/github.com/theckman/go-flock/flock_windows.go b/vendor/github.com/theckman/go-flock/flock_windows.go new file mode 100644 index 0000000000000000000000000000000000000000..c3a04e9614291fcd3deb8deba50b4a7f2f36fe1a --- /dev/null +++ b/vendor/github.com/theckman/go-flock/flock_windows.go @@ -0,0 +1,100 @@ +// Copyright 2015 Tim Heckman. All rights reserved. +// Use of this source code is governed by the BSD 3-Clause +// license that can be found in the LICENSE file. + +package flock + +import ( + "syscall" +) + +// Lock is a blocking call to try and take the file lock. It will wait until it +// is able to obtain the exclusive file lock. It's recommended that TryLock() be +// used over this function. This function may block the ability to query the +// current Locked() status due to a RW-mutex lock. +// +// If we are already locked, this function short-circuits and returns immediately +// assuming it can take the mutex lock. +func (f *Flock) Lock() error { + f.m.Lock() + defer f.m.Unlock() + + if f.l { + return nil + } + + if f.fh == nil { + if err := f.setFh(); err != nil { + return err + } + } + + if _, err := lockFileEx(syscall.Handle(f.fh.Fd()), LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &syscall.Overlapped{}); err != nil { + return err + } + + f.l = true + return nil +} + +// Unlock is a function to unlock the file. This file takes a RW-mutex lock, so +// while it is running the Locked() function will be blocked. +// +// This function short-circuits if we are unlocked already. If not, it calls +// syscall.LOCK_UN on the file and closes the file descriptor It does not remove +// the file from disk. It's up to your application to do. +func (f *Flock) Unlock() error { + f.m.Lock() + defer f.m.Unlock() + + // if we aren't locked or if the lockfile instance is nil + // just return a nil error because we are unlocked + if !f.l || f.fh == nil { + return nil + } + + // mark the file as unlocked + if _, err := unlockFileEx(syscall.Handle(f.fh.Fd()), 0, 1, 0, &syscall.Overlapped{}); err != nil { + return err + } + + f.fh.Close() + + f.l = false + f.fh = nil + + return nil +} + +// TryLock is the preferred function for taking a file lock. This function does +// take a RW-mutex lock before it tries to lock the file, so there is the +// possibility that this function may block for a short time if another goroutine +// is trying to take any action. +// +// The actual file lock is non-blocking. If we are unable to get the exclusive +// file lock, the function will return false instead of waiting for the lock. +// If we get the lock, we also set the *Flock instance as being locked. +func (f *Flock) TryLock() (bool, error) { + f.m.Lock() + defer f.m.Unlock() + + if f.l { + return true, nil + } + + if f.fh == nil { + if err := f.setFh(); err != nil { + return false, err + } + } + + _, err := lockFileEx(syscall.Handle(f.fh.Fd()), LOCKFILE_EXCLUSIVE_LOCK|LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &syscall.Overlapped{}) + + switch err { + case nil: + f.l = true + return true, nil + } + + return false, err +} diff --git a/vendor/vendor.json b/vendor/vendor.json index a40d3404e1821fba6e2c0edf39f2ef4619e13307..95789792e84a7e53a7ad9644e8af74d8cbf5a200 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -5,26 +5,32 @@ { "checksumSHA1": "raJx5BjBbVQG0ylGSjPpi+JvqjU=", "path": "git.autistici.org/ai3/go-common", - "revision": "0cc062297e2c27f9a1abcb1a00172d1e0281f8cb", - "revisionTime": "2017-12-14T08:46:15Z" + "revision": "a65293114a1adbb45d047a8f9014a307ec0d9051", + "revisionTime": "2018-01-12T09:10:27Z" }, { "checksumSHA1": "o+rWKVQIDy79ZwrItwa5/whAL6g=", "path": "git.autistici.org/ai3/go-common/clientutil", - "revision": "9b20acad90c411c48f7ddc837a35ef3d0d6f98d4", - "revisionTime": "2017-12-17T20:32:41Z" + "revision": "a65293114a1adbb45d047a8f9014a307ec0d9051", + "revisionTime": "2018-01-12T09:10:27Z" }, { - "checksumSHA1": "mEnXMNziH82HFtGngHU19VHTVHs=", + "checksumSHA1": "iHObDrZa0HlyzdelqAaGfKNzpiM=", "path": "git.autistici.org/ai3/go-common/ldap", - "revision": "0cc062297e2c27f9a1abcb1a00172d1e0281f8cb", - "revisionTime": "2017-12-14T08:46:15Z" + "revision": "a65293114a1adbb45d047a8f9014a307ec0d9051", + "revisionTime": "2018-01-12T09:10:27Z" }, { "checksumSHA1": "nlGRxexjZUxnHc/z/+ZqV/Xq51w=", "path": "git.autistici.org/ai3/go-common/serverutil", - "revision": "aa88011352b67032c19b0c14eaa06b3417176753", - "revisionTime": "2017-12-19T17:20:40Z" + "revision": "a65293114a1adbb45d047a8f9014a307ec0d9051", + "revisionTime": "2018-01-12T09:10:27Z" + }, + { + "checksumSHA1": "T2vf4xzKRqoIjfXlofMgudKA8rA=", + "path": "git.autistici.org/ai3/go-common/unix", + "revision": "a65293114a1adbb45d047a8f9014a307ec0d9051", + "revisionTime": "2018-01-12T09:10:27Z" }, { "checksumSHA1": "DFjm2ZJpUwioPApa3htGXLEFWl8=", @@ -44,6 +50,18 @@ "revision": "309aa717adbf351e92864cbedf9cca0b769a4b5a", "revisionTime": "2017-10-07T11:45:50Z" }, + { + "checksumSHA1": "RBwpnMpfQt7Jo7YWrRph0Vwe+f0=", + "path": "github.com/coreos/go-systemd/activation", + "revision": "d2196463941895ee908e13531a23a39feb9e1243", + "revisionTime": "2017-07-31T11:19:25Z" + }, + { + "checksumSHA1": "+Zz+leZHHC9C0rx8DoRuffSRPso=", + "path": "github.com/coreos/go-systemd/daemon", + "revision": "d2196463941895ee908e13531a23a39feb9e1243", + "revisionTime": "2017-07-31T11:19:25Z" + }, { "checksumSHA1": "yqF125xVSkmfLpIVGrLlfE05IUk=", "path": "github.com/golang/protobuf/proto", @@ -128,6 +146,12 @@ "revision": "a6e9df898b1336106c743392c48ee0b71f5c4efa", "revisionTime": "2017-10-17T21:40:25Z" }, + { + "checksumSHA1": "cDiE2qLTJ2kmiC7k1KS+AbP87X8=", + "path": "github.com/theckman/go-flock", + "revision": "6de226b0d5f040ed85b88c82c381709b98277f3d", + "revisionTime": "2017-05-22T02:22:41Z" + }, { "checksumSHA1": "X6Q8nYb+KXh+64AKHwWOOcyijHQ=", "origin": "git.autistici.org/id/go-sso/vendor/golang.org/x/crypto/ed25519",