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 }