Skip to content
Snippets Groups Projects
Commit ac17981a authored by ale's avatar ale
Browse files

Add dovecot-keylookupd

The dovecot-keylookupd is a dict-proxy dovecot lookup daemon, which
can read public and private keys from the LDAP backend and use them
for Dovecot userdb / passdb lookups.
parent cc414896
No related branches found
No related tags found
No related merge requests found
Showing
with 1238 additions and 49 deletions
......@@ -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.
......@@ -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.PublicKeyAttr == "" {
return errors.New("empty public_key_attr")
}
if c.Attr == "" {
return errors.New("empty 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)
}
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
}
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())
}
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)
}
......@@ -11,11 +11,11 @@ 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")
)
......@@ -23,7 +23,7 @@ var (
// 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()
......
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")
......
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)
}
}
......@@ -40,11 +40,13 @@ func (p *ConnectionPool) connect(ctx context.Context) (*ldap.Conn, error) {
conn := ldap.NewConn(c, false)
conn.Start()
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
}
......
......@@ -10,19 +10,27 @@ 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.
func isTemporaryLDAPError(err error) bool {
ldapErr, ok := err.(*ldap.Error)
if !ok {
return true
// Interface matched by net.Error.
type hasTemporary interface {
Temporary() bool
}
switch ldapErr.ResultCode {
// 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
......@@ -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)
......
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
}
}
}
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.
// 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
}
// 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
}
// 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
}
// 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
}
// 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
}
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.
# go-flock
[![TravisCI Build Status](https://img.shields.io/travis/theckman/go-flock/master.svg?style=flat)](https://travis-ci.org/theckman/go-flock)
[![GoDoc](https://img.shields.io/badge/godoc-go--flock-blue.svg?style=flat)](https://godoc.org/github.com/theckman/go-flock)
[![License](https://img.shields.io/badge/license-BSD_3--Clause-brightgreen.svg?style=flat)](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).
// 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
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment