diff --git a/database/boltdb.go b/database/boltdb.go new file mode 100644 index 0000000..505ec19 --- /dev/null +++ b/database/boltdb.go @@ -0,0 +1,209 @@ +package database + +import ( + "errors" + "time" + + "github.com/Xe/uuid" + "github.com/asdine/storm" + "github.com/brandur/simplebox" + "golang.org/x/net/context" +) + +// Database errors +var ( + ErrNotImplemented = errors.New("database: not implemented") + ErrInvalidKind = errors.New("database: invalid route kind") + ErrNoSuchToken = errors.New("database: no such token") + ErrCantDecryptCert = errors.New("database: can't decrypt cert") + ErrUnknownCryptMethod = errors.New("database: unknown encryption method") +) + +// BoltDBStorage is a backend that uses https://github.com/boltdb/bolt to store +// route data. +type BoltDBStorage struct { + db *storm.DB + sb *simplebox.SimpleBox +} + +// NewBoltStorage creates a new Storage instance backed by BoltDB + Storm. +func NewBoltStorage(path string, key *[32]byte) (Storage, error) { + db, err := storm.Open(path) + if err != nil { + return nil, err + } + + b := &BoltDBStorage{ + db: db, + sb: simplebox.NewFromSecretKey(key), + } + + return Storage(b), nil +} + +// interface compliance +var ( + _ Storage = &BoltDBStorage{} +) + +// GetRoute gets a single route out of the database. +func (b *BoltDBStorage) GetRoute(ctx context.Context, host string) (Route, error) { + r := Route{} + err := b.db.One("Hostname", host, &r) + return r, err +} + +// GetAllRoutes gets all routes out of the database. +func (b *BoltDBStorage) GetAllRoutes(ctx context.Context) ([]Route, error) { + rs := []Route{} + err := b.db.All(&rs) + return rs, err +} + +// PutRoute creates a new route in the database. +func (b *BoltDBStorage) PutRoute(ctx context.Context, domain, kind string) (Route, error) { + switch kind { + case "tcp", "http": + // empty case, do nothing + default: + return Route{}, ErrInvalidKind + } + + r := Route{ + ID: uuid.New(), + Creator: ctx.Value("creator").(string), + Hostname: domain, + } + + err := b.db.Save(&r) + if err != nil { + return Route{}, err + } + + return r, nil +} + +// DeleteRoute removes a route from the database. +func (b *BoltDBStorage) DeleteRoute(ctx context.Context, id string) error { + r := Route{} + err := b.db.One("ID", id, &r) + if err != nil { + return err + } + + return b.db.DeleteStruct(&r) +} + +// GetToken fetches a token from the database. This is mainly used in validation +// of tokens. +func (b *BoltDBStorage) GetToken(ctx context.Context, token string) (Token, error) { + t := Token{} + err := b.db.One("Body", token, &t) + return t, err +} + +// GetTokensForOwner fetches all of the tokens owned by a given owner. +func (b *BoltDBStorage) GetTokensForOwner(ctx context.Context, owner string) ([]Token, error) { + ts := []Token{} + err := b.db.Find("Owner", owner, &ts) + return ts, err +} + +// PutToken adds a new token to the database. +func (b *BoltDBStorage) PutToken(ctx context.Context, token, owner string, scopes []string) (Token, error) { + t := Token{ + ID: uuid.New(), + Body: token, + Owner: owner, + Scopes: scopes, + + CreatedAt: time.Now(), + Active: true, + } + + err := b.db.Save(&t) + if err != nil { + return Token{}, err + } + + return t, nil +} + +// DeleteToken removes a token from the database. +func (b *BoltDBStorage) DeleteToken(ctx context.Context, id string) error { + t := Token{} + err := b.db.One("ID", id, &t) + if err != nil { + return err + } + + return b.db.DeleteStruct(&t) +} + +// DeactivateToken de-activates a token in the database. This should be used +// instead of deletion in many cases. +func (b *BoltDBStorage) DeactivateToken(ctx context.Context, id string) error { + t := Token{} + err := b.db.One("ID", id, &t) + if err != nil { + return err + } + + t.Active = false + + return b.db.Save(&t) +} + +// GetCert fetches a TLS certificate from the database. +func (b *BoltDBStorage) GetCert(ctx context.Context, key string) ([]byte, error) { + cc := CachedCert{} + err := b.db.One("Key", key, &cc) + if err != nil { + return nil, err + } + + var body []byte + + switch cc.CryptoLevel { + case CryptoLevelNone: + body = cc.Body + case CryptoLevelSecretbox: + if b.sb == nil { + return nil, ErrCantDecryptCert + } + + body, err = b.sb.Decrypt(cc.Body) + if err != nil { + return nil, err + } + } + + return body, nil +} + +// PutCert adds a new TLS certificate to the database. +func (b *BoltDBStorage) PutCert(ctx context.Context, key string, data []byte) error { + cc := CachedCert{ + Key: key, + CryptoLevel: CryptoLevelNone, + Body: data, + } + + if b.sb != nil { + cc.CryptoLevel = CryptoLevelSecretbox + cc.Body = b.sb.Encrypt(data) + } + + return b.db.Save(&cc) +} + +// DeleteCert removes a certificate from the database. +func (b *BoltDBStorage) DeleteCert(ct context.Context, key string) error { + cc := CachedCert{} + err := b.db.One("Key", key, &cc) + if err != nil { + return err + } + + return b.db.DeleteStruct(&cc) +} diff --git a/database/certcache.go b/database/certcache.go index 3570042..699d8d4 100644 --- a/database/certcache.go +++ b/database/certcache.go @@ -32,7 +32,7 @@ const ( // CachedCert is an individual cached certificate in the database. type CachedCert struct { - Key string `gorethink:"id"` + Key string `gorethink:"id" storm:"id"` CryptoLevel CryptoLevel `gorethink:"cryptoLevel"` // PEM-encoded bytes with the above crypto level as a filter. Body []byte `gorethink:"body"` diff --git a/database/db.go b/database/db.go index e8cc51a..b95aa36 100644 --- a/database/db.go +++ b/database/db.go @@ -1,10 +1,7 @@ package database import ( - "crypto/rsa" - "git.xeserv.us/xena/route/routerpc" - "git.xeserv.us/xena/route/utils" r "github.com/GoRethink/gorethink" ) @@ -35,24 +32,20 @@ var tables = []string{ "routes", } -// Route is a single route object serialized to rethinkdb. +// Route is a single HTTP route. type Route struct { - ID string `gorethink:"id,omitempty"` - Hostname string `gorethink:"hostname"` - OnionHostname string `gorethink:"onionhostname"` - Token string `gorethink:"token"` - OnionKey []byte `gorethink:"onionKey"` // PEM-encoded + ID string `gorethink:"id,omitempty" storm:"id"` + Creator string + Hostname string `gorethink:"hostname" storm:"index"` + + Token string `gorethink:"token" storm:"-"` // deprecated } // SaveRoute adds the route to the database. func (db *DB) SaveRoute(resp *routerpc.AddHostResponse) error { - bytes := utils.RSAPrivateKeyToPem(resp.PrivKey.(*rsa.PrivateKey)) - rt := &Route{ - Hostname: resp.Hostname, - OnionHostname: resp.OnionHostname, - Token: resp.Token, - OnionKey: bytes, + Hostname: resp.Hostname, + Token: resp.Token, } // TODO: check if OnionHostname or Hostname actually exists in diff --git a/database/storage.go b/database/storage.go new file mode 100644 index 0000000..bfd407a --- /dev/null +++ b/database/storage.go @@ -0,0 +1,50 @@ +package database + +import ( + "golang.org/x/crypto/acme/autocert" + "golang.org/x/net/context" +) + +// Storage is the parent interface for the database backends of route. +type Storage interface { + // routes + GetRoute(ctx context.Context, host string) (Route, error) + GetAllRoutes(ctx context.Context) ([]Route, error) + PutRoute(ctx context.Context, domain, kind string) (Route, error) + DeleteRoute(ctx context.Context, id string) error + + // tokens + GetToken(ctx context.Context, token string) (Token, error) + GetTokensForOwner(ctx context.Context, owner string) ([]Token, error) + PutToken(ctx context.Context, token, owner string, scopes []string) (Token, error) + DeleteToken(ctx context.Context, id string) error + DeactivateToken(ctx context.Context, id string) error + + // certificates + GetCert(ctx context.Context, key string) ([]byte, error) + PutCert(ctx context.Context, key string, data []byte) error + DeleteCert(ctx context.Context, key string) error +} + +type storageManager struct { + Storage +} + +func (s *storageManager) Get(ctx context.Context, key string) ([]byte, error) { + return s.GetCert(ctx, key) +} + +func (s *storageManager) Put(ctx context.Context, key string, data []byte) error { + return s.PutCert(ctx, key, data) +} + +func (s *storageManager) Delete(ctx context.Context, key string) error { + return s.DeleteCert(ctx, key) +} + +// Cache creates an autocert.Cache from a Storage instance. +func Cache(s Storage) autocert.Cache { + return autocert.Cache(&storageManager{ + Storage: s, + }) +} diff --git a/database/token.go b/database/token.go new file mode 100644 index 0000000..f923517 --- /dev/null +++ b/database/token.go @@ -0,0 +1,14 @@ +package database + +import "time" + +// Token is a single authorization token. +type Token struct { + ID string `storm:"id"` + Body string `storm:"unique"` + Owner string `storm:"index"` + Scopes []string + + CreatedAt time.Time `json:"created_at"` + Active bool `json:"active"` +}