Files
simplegit/state/db.go
T
2026-07-17 05:55:31 -04:00

224 lines
4.7 KiB
Go

package state
import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"database/sql"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"simplegit/common"
"simplegit/gitcmd"
"strings"
"sync"
_ "github.com/lib/pq"
"golang.org/x/crypto/ssh"
"modernc.org/sqlite"
"xorm.io/xorm"
)
const sqliteDriver = "sqlite3"
type IState interface {
Open(owner, name string) (*gitcmd.Repository, error)
RepoPath(owner, name string) (string, error)
KnownSSHKey(key ssh.PublicKey) (bool, error)
AccessBySSHKey(ctx context.Context, key ssh.PublicKey, repons, reponame string, perm common.Perm) (ok bool, err error)
AccessByPAT(ctx context.Context, pat, repons, reponame string, perm common.Perm) (ok bool, err error)
Signers() ([]ssh.Signer, error)
}
type IStateMut interface {
IState
MoveNS(old, new string) error
MoveRepo(old, new string) error
CreateRepo(ns, name string) error
ExistRepo(ns, name string) error
DeleteRepo(ns, name string) error
ACLUpsertSSHKeyOnNS(ns string, key ssh.PublicKey, perm common.Perm) (aclID int64, err error)
ACLUpsertPATOnNS(ns string, pat string, perm common.Perm) (aclID int64, err error)
ACLUpsertSSHKeyOnRepo(reponame string, key ssh.PublicKey, perm common.Perm) (aclID int64, err error)
ACLUpsertPATOnRepo(reponame string, pat string, perm common.Perm) (aclID int64, err error)
ACLDelete(id int64) error
}
var (
_ IState = (*LocalState)(nil)
_ IStateMut = (*LocalState)(nil)
)
type LocalState struct {
engine *xorm.Engine
cfg *common.Config
signerMu sync.Mutex
signers []ssh.Signer
signerDone bool
}
func init() {
sql.Register(sqliteDriver, &sqlite.Driver{})
}
func Open(cfg *common.Config) (*LocalState, error) {
engine, err := InitEngine(cfg.Driver(), cfg.DSN())
if err != nil {
return nil, fmt.Errorf("state: init engine: %w", err)
}
if cfg.Driver() == sqliteDriver {
engine.SetMaxOpenConns(1)
}
log.Printf("state: RepoRoot: %s", cfg.RepoRoot())
log.Printf("state: DB engine %s opened, %s", cfg.Driver(), cfg.DSN())
return &LocalState{
engine: engine,
cfg: cfg,
}, nil
}
func (s *LocalState) Signers() ([]ssh.Signer, error) {
s.signerMu.Lock()
defer s.signerMu.Unlock()
if s.signerDone {
return s.signers, nil
}
s.signerDone = true
ed25519signer, err := s.loadOrCreateHostKey("ed25519")
if err != nil {
return nil, err
}
rsasigner, err := s.loadOrCreateHostKey("rsa")
if err != nil {
return nil, err
}
ecdsasigner, err := s.loadOrCreateHostKey("ecdsa")
if err != nil {
return nil, err
}
s.signers = append(s.signers, ed25519signer, rsasigner, ecdsasigner)
return s.signers, err
}
func (s *LocalState) loadOrCreateHostKey(ty string) (ssh.Signer, error) {
var der []byte
switch ty {
case "ed25519":
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
der, err = x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
case "rsa":
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
der, err = x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
case "ecdsa":
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
der, err = x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return nil, err
}
default:
return nil, errors.New("loadOrCreateHostKey")
}
hostKeyPath := filepath.Join(s.cfg.Root, fmt.Sprintf("host_key_%v.pem", ty))
if data, err := os.ReadFile(hostKeyPath); err == nil {
return ssh.ParsePrivateKey(data)
} else if !os.IsNotExist(err) {
return nil, err
}
data := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der})
if err := os.MkdirAll(filepath.Dir(hostKeyPath), 0o700); err != nil {
return nil, err
}
if err := os.WriteFile(hostKeyPath, data, 0o600); err != nil {
return nil, err
}
return ssh.ParsePrivateKey(data)
}
func fingerprint(key ssh.PublicKey) string {
sum := sha256.Sum256(key.Marshal())
return "SHA256:" + hex.EncodeToString(sum[:])
}
func (s *LocalState) findRepo(owner, name string) (*Repo, error) {
var r Repo
has, err := s.engine.Where("namespace = ? AND name = ?", owner, strings.TrimSuffix(name, ".git")).Get(&r)
if err != nil {
return nil, fmt.Errorf("lookup repo: %w", err)
}
if !has {
return nil, ErrRepoNotFound
}
return &r, nil
}
func (s *LocalState) RepoPath(owner, name string) (string, error) {
return s.cfg.RepoPath(owner, name)
}
func (s *LocalState) Open(owner, name string) (*gitcmd.Repository, error) {
path, err := s.RepoPath(owner, name)
if err != nil {
return nil, err
}
return gitcmd.OpenRepository(context.Background(), path)
}
func (s *LocalState) Engine() *xorm.Engine { return s.engine }