package api import ( "crypto/rand" "database/sql" "embed" "errors" "log" "sync" "time" "vppn/hub/api/db" "vppn/m" "git.crumpington.com/lib/go/idgen" "git.crumpington.com/lib/go/sqliteutil" "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/nacl/box" "golang.org/x/crypto/nacl/sign" ) //go:embed migrations var migrations embed.FS type API struct { db *sql.DB lock sync.Mutex peerIntents map[string]PeerCreateArgs } func New(dbPath string) (*API, error) { sqlDB, err := sql.Open("sqlite3", dbPath+"?_journal=WAL") if err != nil { return nil, err } if err := sqliteutil.Migrate(sqlDB, migrations); err != nil { return nil, err } a := &API{ db: sqlDB, peerIntents: map[string]PeerCreateArgs{}, } return a, a.ensurePassword() } func (a *API) ensurePassword() error { _, err := db.Config_Get(a.db, 1) if err == nil { return nil } if !errors.Is(err, sql.ErrNoRows) { return err } pwd := idgen.NewToken() log.Printf("Setting password: %s", pwd) hashed, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) if err != nil { return err } conf := &Config{ ConfigID: 1, VPNNetwork: []byte{10, 1, 1, 0}, Password: hashed, } return db.Config_Insert(a.db, conf) } func (a *API) Config_Get() *Config { conf, err := db.Config_Get(a.db, 1) if err != nil { panic(err) } return conf } func (a *API) Config_Update(conf *Config) error { return db.Config_Update(a.db, conf) } func (a *API) Config_UpdatePassword(pwdHash []byte) error { return db.Config_UpdatePassword(a.db, pwdHash) } func (a *API) Session_Delete(sessionID string) error { return db.Session_Delete(a.db, sessionID) } func (a *API) Session_Get(sessionID string) (*Session, error) { if sessionID == "" { return a.session_CreatePub() } session, err := db.Session_Get(a.db, sessionID) if err != nil { return a.session_CreatePub() } if timeSince(session.LastSeenAt) > 86400*21 { return a.session_CreatePub() } if timeSince(session.LastSeenAt) > 86400*7 { session.LastSeenAt = time.Now().Unix() if err := db.Session_UpdateLastSeenAt(a.db, session.SessionID); err != nil { log.Printf("Failed to update session: %v", err) } } return session, nil } func (a *API) session_CreatePub() (*Session, error) { s := &Session{ SessionID: idgen.NewToken(), CSRF: idgen.NewToken(), SignedIn: false, CreatedAt: time.Now().Unix(), LastSeenAt: time.Now().Unix(), } err := db.Session_Insert(a.db, s) return s, err } func (a *API) Session_DeleteBefore(timestamp int64) error { return db.Session_DeleteBefore(a.db, timestamp) } func (a *API) Session_SignIn(s *Session, pwd string) error { conf := a.Config_Get() if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil { return ErrNotAuthorized } return db.Session_SetSignedIn(a.db, s.SessionID) } type PeerCreateArgs struct { Name string IP []byte Port uint16 Mediator bool } // Create the intention to add a peer. The returned code is used to complete // the peer creation. The code is valid for 5 minutes. func (a *API) Peer_CreateIntent(args PeerCreateArgs) string { a.lock.Lock() defer a.lock.Unlock() code := idgen.NewToken() a.peerIntents[code] = args go func() { time.Sleep(5 * time.Minute) a.lock.Lock() defer a.lock.Unlock() delete(a.peerIntents, code) }() return code } func (a *API) Peer_Create(creationCode string) (*m.PeerConfig, error) { a.lock.Lock() defer a.lock.Unlock() args, ok := a.peerIntents[creationCode] if !ok { return nil, ErrNotAuthorized } delete(a.peerIntents, creationCode) encPubKey, encPrivKey, err := box.GenerateKey(rand.Reader) if err != nil { return nil, err } signPubKey, signPrivKey, err := sign.GenerateKey(rand.Reader) if err != nil { return nil, err } // Get peer IP. peerIP := byte(0) for i := byte(1); i < 255; i++ { exists, err := db.Peer_Exists(a.db, i) if err != nil { return nil, err } if !exists { peerIP = i break } } if peerIP == 0 { return nil, ErrNoIPAvailable } peer := &Peer{ PeerIP: peerIP, APIKey: idgen.NewToken(), Name: args.Name, IP: args.IP, Port: args.Port, Mediator: args.Mediator, EncPubKey: encPubKey[:], SignPubKey: signPubKey[:], } if err := db.Peer_Insert(a.db, peer); err != nil { return nil, err } conf := a.Config_Get() return &m.PeerConfig{ PeerIP: peer.PeerIP, HubAddress: conf.HubAddress, APIKey: peer.APIKey, Network: conf.VPNNetwork, IP: peer.IP, Port: peer.Port, Mediator: peer.Mediator, EncPubKey: encPubKey[:], EncPrivKey: encPrivKey[:], SignPubKey: signPubKey[:], SignPrivKey: signPrivKey[:], }, nil } func (a *API) Peer_Update(p *Peer) error { return db.Peer_Update(a.db, p) } func (a *API) Peer_Delete(ip byte) error { return db.Peer_Delete(a.db, ip) } func (a *API) Peer_List() ([]*Peer, error) { return db.Peer_ListAll(a.db) } func (a *API) Peer_Get(ip byte) (*Peer, error) { return db.Peer_Get(a.db, ip) } func (a *API) Peer_GetByAPIKey(key string) (*Peer, error) { return db.Peer_GetByAPIKey(a.db, key) }