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