224 lines
4.7 KiB
Go
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 }
|