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) AccessByUserPswd(ctx context.Context, username, password, repons, reponame string, perm common.Perm) (ok bool, err error) Signers() ([]ssh.Signer, error) } type IStateMut interface { IState Engine() *xorm.Engine MoveNS(old, new string) error MoveRepo(old, new string) error CreateNS(ns string) error DeleteNS(ns string) error UpsertRepo(ns, name string, isPrivate bool) 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 ACLSetPerm(id int64, perm common.Perm) error UserPasswordUpsert(ns, password string) error UserDelete(ns string) 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_id IN (SELECT id FROM namespace WHERE name = ?) 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 }