wip
This commit is contained in:
260
hub/api/api.go
Normal file
260
hub/api/api.go
Normal file
@@ -0,0 +1,260 @@
|
||||
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)
|
||||
}
|
||||
3
hub/api/db/generate.go
Normal file
3
hub/api/db/generate.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package db
|
||||
|
||||
//go:generate sqlgen sqlite tables.defs generated.go
|
||||
0
hub/api/db/generated
Normal file
0
hub/api/db/generated
Normal file
480
hub/api/db/generated.go
Normal file
480
hub/api/db/generated.go
Normal file
@@ -0,0 +1,480 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"iter"
|
||||
)
|
||||
|
||||
type TX interface {
|
||||
Exec(query string, args ...any) (sql.Result, error)
|
||||
Query(query string, args ...any) (*sql.Rows, error)
|
||||
QueryRow(query string, args ...any) *sql.Row
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Table: config
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type Config struct {
|
||||
ConfigID int64
|
||||
HubAddress string
|
||||
VPNNetwork []byte
|
||||
Password []byte
|
||||
}
|
||||
|
||||
const Config_SelectQuery = "SELECT ConfigID,HubAddress,VPNNetwork,Password FROM config"
|
||||
|
||||
func Config_Insert(
|
||||
tx TX,
|
||||
row *Config,
|
||||
) (err error) {
|
||||
Config_Sanitize(row)
|
||||
if err = Config_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO config(ConfigID,HubAddress,VPNNetwork,Password) VALUES(?,?,?,?)", row.ConfigID, row.HubAddress, row.VPNNetwork, row.Password)
|
||||
return err
|
||||
}
|
||||
|
||||
func Config_Update(
|
||||
tx TX,
|
||||
row *Config,
|
||||
) (err error) {
|
||||
Config_Sanitize(row)
|
||||
if err = Config_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := tx.Exec("UPDATE config SET HubAddress=?,VPNNetwork=? WHERE ConfigID=?", row.HubAddress, row.VPNNetwork, row.ConfigID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows updated")
|
||||
}
|
||||
}
|
||||
|
||||
func Config_UpdateFull(
|
||||
tx TX,
|
||||
row *Config,
|
||||
) (err error) {
|
||||
Config_Sanitize(row)
|
||||
if err = Config_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := tx.Exec("UPDATE config SET HubAddress=?,VPNNetwork=?,Password=? WHERE ConfigID=?", row.HubAddress, row.VPNNetwork, row.Password, row.ConfigID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows updated")
|
||||
}
|
||||
}
|
||||
|
||||
func Config_Delete(
|
||||
tx TX,
|
||||
ConfigID int64,
|
||||
) (err error) {
|
||||
result, err := tx.Exec("DELETE FROM config WHERE ConfigID=?", ConfigID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func Config_Get(
|
||||
tx TX,
|
||||
ConfigID int64,
|
||||
) (
|
||||
row *Config,
|
||||
err error,
|
||||
) {
|
||||
row = &Config{}
|
||||
r := tx.QueryRow("SELECT ConfigID,HubAddress,VPNNetwork,Password FROM config WHERE ConfigID=?", ConfigID)
|
||||
err = r.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password)
|
||||
return
|
||||
}
|
||||
|
||||
func Config_GetWhere(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
row *Config,
|
||||
err error,
|
||||
) {
|
||||
row = &Config{}
|
||||
r := tx.QueryRow(query, args...)
|
||||
err = r.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password)
|
||||
return
|
||||
}
|
||||
|
||||
func Config_Iterate(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) iter.Seq2[*Config, error] {
|
||||
rows, err := tx.Query(query, args...)
|
||||
if err != nil {
|
||||
return func(yield func(*Config, error) bool) {
|
||||
yield(nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
return func(yield func(*Config, error) bool) {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
row := &Config{}
|
||||
err := rows.Scan(&row.ConfigID, &row.HubAddress, &row.VPNNetwork, &row.Password)
|
||||
if !yield(row, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Config_List(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
l []*Config,
|
||||
err error,
|
||||
) {
|
||||
for row, err := range Config_Iterate(tx, query, args...) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, row)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Table: sessions
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type Session struct {
|
||||
SessionID string
|
||||
CSRF string
|
||||
SignedIn bool
|
||||
CreatedAt int64
|
||||
LastSeenAt int64
|
||||
}
|
||||
|
||||
const Session_SelectQuery = "SELECT SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt FROM sessions"
|
||||
|
||||
func Session_Insert(
|
||||
tx TX,
|
||||
row *Session,
|
||||
) (err error) {
|
||||
Session_Sanitize(row)
|
||||
if err = Session_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO sessions(SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt) VALUES(?,?,?,?,?)", row.SessionID, row.CSRF, row.SignedIn, row.CreatedAt, row.LastSeenAt)
|
||||
return err
|
||||
}
|
||||
|
||||
func Session_Delete(
|
||||
tx TX,
|
||||
SessionID string,
|
||||
) (err error) {
|
||||
result, err := tx.Exec("DELETE FROM sessions WHERE SessionID=?", SessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func Session_Get(
|
||||
tx TX,
|
||||
SessionID string,
|
||||
) (
|
||||
row *Session,
|
||||
err error,
|
||||
) {
|
||||
row = &Session{}
|
||||
r := tx.QueryRow("SELECT SessionID,CSRF,SignedIn,CreatedAt,LastSeenAt FROM sessions WHERE SessionID=?", SessionID)
|
||||
err = r.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
|
||||
return
|
||||
}
|
||||
|
||||
func Session_GetWhere(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
row *Session,
|
||||
err error,
|
||||
) {
|
||||
row = &Session{}
|
||||
r := tx.QueryRow(query, args...)
|
||||
err = r.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
|
||||
return
|
||||
}
|
||||
|
||||
func Session_Iterate(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) iter.Seq2[*Session, error] {
|
||||
rows, err := tx.Query(query, args...)
|
||||
if err != nil {
|
||||
return func(yield func(*Session, error) bool) {
|
||||
yield(nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
return func(yield func(*Session, error) bool) {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
row := &Session{}
|
||||
err := rows.Scan(&row.SessionID, &row.CSRF, &row.SignedIn, &row.CreatedAt, &row.LastSeenAt)
|
||||
if !yield(row, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Session_List(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
l []*Session,
|
||||
err error,
|
||||
) {
|
||||
for row, err := range Session_Iterate(tx, query, args...) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, row)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Table: peers
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type Peer struct {
|
||||
PeerIP byte
|
||||
APIKey string
|
||||
Name string
|
||||
IP []byte
|
||||
Port uint16
|
||||
Mediator bool
|
||||
EncPubKey []byte
|
||||
SignPubKey []byte
|
||||
}
|
||||
|
||||
const Peer_SelectQuery = "SELECT PeerIP,APIKey,Name,IP,Port,Mediator,EncPubKey,SignPubKey FROM peers"
|
||||
|
||||
func Peer_Insert(
|
||||
tx TX,
|
||||
row *Peer,
|
||||
) (err error) {
|
||||
Peer_Sanitize(row)
|
||||
if err = Peer_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO peers(PeerIP,APIKey,Name,IP,Port,Mediator,EncPubKey,SignPubKey) VALUES(?,?,?,?,?,?,?,?)", row.PeerIP, row.APIKey, row.Name, row.IP, row.Port, row.Mediator, row.EncPubKey, row.SignPubKey)
|
||||
return err
|
||||
}
|
||||
|
||||
func Peer_Update(
|
||||
tx TX,
|
||||
row *Peer,
|
||||
) (err error) {
|
||||
Peer_Sanitize(row)
|
||||
if err = Peer_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := tx.Exec("UPDATE peers SET Name=?,IP=?,Port=?,Mediator=? WHERE PeerIP=?", row.Name, row.IP, row.Port, row.Mediator, row.PeerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows updated")
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_UpdateFull(
|
||||
tx TX,
|
||||
row *Peer,
|
||||
) (err error) {
|
||||
Peer_Sanitize(row)
|
||||
if err = Peer_Validate(row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := tx.Exec("UPDATE peers SET APIKey=?,Name=?,IP=?,Port=?,Mediator=?,EncPubKey=?,SignPubKey=? WHERE PeerIP=?", row.APIKey, row.Name, row.IP, row.Port, row.Mediator, row.EncPubKey, row.SignPubKey, row.PeerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows updated")
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_Delete(
|
||||
tx TX,
|
||||
PeerIP byte,
|
||||
) (err error) {
|
||||
result, err := tx.Exec("DELETE FROM peers WHERE PeerIP=?", PeerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
switch n {
|
||||
case 0:
|
||||
return sql.ErrNoRows
|
||||
case 1:
|
||||
return nil
|
||||
default:
|
||||
panic("multiple rows deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_Get(
|
||||
tx TX,
|
||||
PeerIP byte,
|
||||
) (
|
||||
row *Peer,
|
||||
err error,
|
||||
) {
|
||||
row = &Peer{}
|
||||
r := tx.QueryRow("SELECT PeerIP,APIKey,Name,IP,Port,Mediator,EncPubKey,SignPubKey FROM peers WHERE PeerIP=?", PeerIP)
|
||||
err = r.Scan(&row.PeerIP, &row.APIKey, &row.Name, &row.IP, &row.Port, &row.Mediator, &row.EncPubKey, &row.SignPubKey)
|
||||
return
|
||||
}
|
||||
|
||||
func Peer_GetWhere(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
row *Peer,
|
||||
err error,
|
||||
) {
|
||||
row = &Peer{}
|
||||
r := tx.QueryRow(query, args...)
|
||||
err = r.Scan(&row.PeerIP, &row.APIKey, &row.Name, &row.IP, &row.Port, &row.Mediator, &row.EncPubKey, &row.SignPubKey)
|
||||
return
|
||||
}
|
||||
|
||||
func Peer_Iterate(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) iter.Seq2[*Peer, error] {
|
||||
rows, err := tx.Query(query, args...)
|
||||
if err != nil {
|
||||
return func(yield func(*Peer, error) bool) {
|
||||
yield(nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
return func(yield func(*Peer, error) bool) {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
row := &Peer{}
|
||||
err := rows.Scan(&row.PeerIP, &row.APIKey, &row.Name, &row.IP, &row.Port, &row.Mediator, &row.EncPubKey, &row.SignPubKey)
|
||||
if !yield(row, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_List(
|
||||
tx TX,
|
||||
query string,
|
||||
args ...any,
|
||||
) (
|
||||
l []*Peer,
|
||||
err error,
|
||||
) {
|
||||
for row, err := range Peer_Iterate(tx, query, args...) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, row)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
69
hub/api/db/sanitize-validate.go
Normal file
69
hub/api/db/sanitize-validate.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidIP = errors.New("invalid IP")
|
||||
ErrInvalidPort = errors.New("invalid port")
|
||||
)
|
||||
|
||||
func Config_Sanitize(c *Config) {
|
||||
if u, err := url.Parse(c.HubAddress); err == nil {
|
||||
c.HubAddress = u.String()
|
||||
}
|
||||
|
||||
if addr, ok := netip.AddrFromSlice(c.VPNNetwork); ok {
|
||||
c.VPNNetwork = addr.AsSlice()
|
||||
}
|
||||
}
|
||||
|
||||
func Config_Validate(c *Config) error {
|
||||
if _, err := url.Parse(c.HubAddress); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr, ok := netip.AddrFromSlice(c.VPNNetwork)
|
||||
if !ok || !addr.Is4() || addr.As4()[3] != 0 || addr.As4()[0] == 0 {
|
||||
return ErrInvalidIP
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Session_Sanitize(s *Session) {
|
||||
}
|
||||
|
||||
func Session_Validate(s *Session) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Peer_Sanitize(p *Peer) {
|
||||
p.Name = strings.TrimSpace(p.Name)
|
||||
if len(p.IP) != 0 {
|
||||
addr, ok := netip.AddrFromSlice(p.IP)
|
||||
if ok && addr.Is4() {
|
||||
p.IP = addr.AsSlice()
|
||||
}
|
||||
}
|
||||
if p.Port == 0 {
|
||||
p.Port = 515
|
||||
}
|
||||
}
|
||||
|
||||
func Peer_Validate(p *Peer) error {
|
||||
if len(p.IP) > 0 {
|
||||
_, ok := netip.AddrFromSlice(p.IP)
|
||||
if !ok {
|
||||
return ErrInvalidIP
|
||||
}
|
||||
}
|
||||
if p.Port == 0 {
|
||||
return ErrInvalidPort
|
||||
}
|
||||
return nil
|
||||
}
|
||||
25
hub/api/db/tables.defs
Normal file
25
hub/api/db/tables.defs
Normal file
@@ -0,0 +1,25 @@
|
||||
TABLE config OF Config (
|
||||
ConfigID int64 PK,
|
||||
HubAddress string,
|
||||
VPNNetwork []byte,
|
||||
Password []byte NoUpdate
|
||||
);
|
||||
|
||||
TABLE sessions OF Session NoUpdate (
|
||||
SessionID string PK,
|
||||
CSRF string,
|
||||
SignedIn bool,
|
||||
CreatedAt int64,
|
||||
LastSeenAt int64
|
||||
);
|
||||
|
||||
TABLE peers OF Peer (
|
||||
PeerIP byte PK,
|
||||
APIKey string NoUpdate,
|
||||
Name string,
|
||||
IP []byte,
|
||||
Port uint16,
|
||||
Mediator bool,
|
||||
EncPubKey []byte NoUpdate,
|
||||
SignPubKey []byte NoUpdate
|
||||
);
|
||||
51
hub/api/db/written.go
Normal file
51
hub/api/db/written.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package db
|
||||
|
||||
import "vppn/fasttime"
|
||||
|
||||
func Session_UpdateLastSeenAt(
|
||||
tx TX,
|
||||
id string,
|
||||
) (err error) {
|
||||
_, err = tx.Exec("UPDATE sessions SET LastSeenAt=? WHERE SessionID=?", fasttime.Now(), id)
|
||||
return err
|
||||
}
|
||||
|
||||
func Session_SetSignedIn(
|
||||
tx TX,
|
||||
id string,
|
||||
) (err error) {
|
||||
_, err = tx.Exec("UPDATE sessions SET SignedIn=1 WHERE SessionID=?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func Session_DeleteBefore(
|
||||
tx TX,
|
||||
timestamp int64,
|
||||
) (err error) {
|
||||
_, err = tx.Exec("DELETE FROM sessions WHERE LastSeenAt<?", timestamp)
|
||||
return err
|
||||
}
|
||||
|
||||
func Config_UpdatePassword(
|
||||
tx TX,
|
||||
pwdHash []byte,
|
||||
) (err error) {
|
||||
_, err = tx.Exec("UPDATE config SET Password=? WHERE ConfigID=1", pwdHash)
|
||||
return err
|
||||
}
|
||||
|
||||
func Peer_ListAll(tx TX) ([]*Peer, error) {
|
||||
return Peer_List(tx, Peer_SelectQuery)
|
||||
}
|
||||
|
||||
func Peer_GetByAPIKey(tx TX, apiKey string) (*Peer, error) {
|
||||
return Peer_GetWhere(
|
||||
tx,
|
||||
Peer_SelectQuery+` WHERE APIKey=?`,
|
||||
apiKey)
|
||||
}
|
||||
|
||||
func Peer_Exists(tx TX, ip byte) (exists bool, err error) {
|
||||
err = tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM peers WHERE PeerIP=?)`, ip).Scan(&exists)
|
||||
return
|
||||
}
|
||||
13
hub/api/errors.go
Normal file
13
hub/api/errors.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"vppn/hub/api/db"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotAuthorized = errors.New("not authorized")
|
||||
ErrNoIPAvailable = errors.New("no IP address available")
|
||||
ErrInvalidIP = db.ErrInvalidIP
|
||||
ErrInvalidPort = db.ErrInvalidPort
|
||||
)
|
||||
27
hub/api/migrations/2024-11-30-init.sql
Normal file
27
hub/api/migrations/2024-11-30-init.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
CREATE TABLE config (
|
||||
ConfigID INTEGER NOT NULL PRIMARY KEY, -- Always 1.
|
||||
HubAddress TEXT NOT NULL, -- https://for.example.com
|
||||
VPNNetwork BLOB NOT NULL, -- Network (/24), example 10.51.50.0
|
||||
Password BLOB NOT NULL -- bcrypt password for web interface
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE sessions (
|
||||
SessionID TEXT NOT NULL PRIMARY KEY,
|
||||
CSRF TEXT NOT NULL,
|
||||
SignedIn INTEGER NOT NULL,
|
||||
CreatedAt INTEGER NOT NULL,
|
||||
LastSeenAt INTEGER NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE INDEX sessions_last_seen_index ON sessions(LastSeenAt);
|
||||
|
||||
CREATE TABLE peers (
|
||||
PeerIP INTEGER NOT NULL PRIMARY KEY, -- Final byte.
|
||||
APIKey TEXT NOT NULL UNIQUE,
|
||||
Name TEXT NOT NULL UNIQUE, -- For humans.
|
||||
IP BLOB NOT NULL,
|
||||
Port INTEGER NOT NULL,
|
||||
Mediator INTEGER NOT NULL DEFAULT 0, -- Boolean if peer will forward packets. Must also have public address.
|
||||
EncPubKey BLOB NOT NULL,
|
||||
SignPubKey BLOB NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
1
hub/api/session.go
Normal file
1
hub/api/session.go
Normal file
@@ -0,0 +1 @@
|
||||
package api
|
||||
7
hub/api/time.go
Normal file
7
hub/api/time.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package api
|
||||
|
||||
import "time"
|
||||
|
||||
func timeSince(ts int64) int64 {
|
||||
return time.Now().Unix() - ts
|
||||
}
|
||||
7
hub/api/types.go
Normal file
7
hub/api/types.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package api
|
||||
|
||||
import "vppn/hub/api/db"
|
||||
|
||||
type Config = db.Config
|
||||
type Session = db.Session
|
||||
type Peer = db.Peer
|
||||
Reference in New Issue
Block a user