~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
392 lines · 10.1 KB Raw History
package request

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/bluesky-social/indigo/atproto/atcrypto"
	_ "modernc.org/sqlite"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/hostos"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/fsx"
	pdssvc "github.com/johnandersen777/socialweb-computer-kcp/internal/services/pds"
)

const (
	IdentityFile = "identity.json"

	IdentitySchema = 1

	ExposureIdentityDir = "exposure-identity"

	IdentityLockFile = "lock"
)

var (
	ErrIdentityInUse = errors.New("request: the identity is in use by another command")

	ErrIdentityPartial = errors.New("request: the identity store is partial")
)

type IdentityInvocation struct {
	Name string `json:"name"`

	StateDir string `json:"stateDir,omitempty"`
}

type Identity struct {
	Schema int `json:"schema"`

	Name string `json:"name"`

	CreatedAt time.Time `json:"createdAt"`

	UpdatedAt time.Time `json:"updatedAt,omitempty"`

	Mode string `json:"mode,omitempty"`

	DID string `json:"did,omitempty"`

	Handle string `json:"handle,omitempty"`

	Association string `json:"association,omitempty"`

	ApprovedAssociations []string `json:"approvedAssociations,omitempty"`

	PDSPublicURL string `json:"pdsPublicURL,omitempty"`

	PLCDirectoryURL string `json:"plcDirectoryUrl,omitempty"`

	PDSStateDir string `json:"pdsStateDir,omitempty"`

	RotationKeyDID string `json:"rotationKeyDID,omitempty"`

	StateRoot string `json:"stateRoot,omitempty"`

	LastInvocation *IdentityInvocation `json:"lastInvocation,omitempty"`
}

type Account struct {
	DID string

	Handle string
}

type IdentityStore struct {
	stateRoot string

	name string

	dir string

	pds string

	exposure string

	lockPath string

	lock *os.File
}

func IdentityRoot(stateRoot string) string {
	return filepath.Join(stateRoot, "identities")
}

func IdentityDir(stateRoot, name string) string {
	return filepath.Join(IdentityRoot(stateRoot), name)
}

func CheckIdentityName(name string) error {
	if err := CheckName(name); err != nil {
		return fmt.Errorf("identity: %w", err)
	}
	return nil
}

func OpenIdentity(stateRoot, name string) (*IdentityStore, error) {
	if err := CheckIdentityName(name); err != nil {
		return nil, err
	}
	stateRoot, err := filepath.Abs(strings.TrimSpace(stateRoot))
	if err != nil {
		return nil, fmt.Errorf("request: the state root: %w", err)
	}
	root := IdentityRoot(stateRoot)
	if err := secureDir(root, 0o700); err != nil {
		return nil, err
	}
	dir := filepath.Join(root, name)
	if err := secureDir(dir, 0o700); err != nil {
		return nil, err
	}
	store := &IdentityStore{stateRoot: stateRoot, name: name, dir: dir, pds: filepath.Join(dir, "pds"),
		exposure: filepath.Join(dir, ExposureIdentityDir), lockPath: filepath.Join(dir, IdentityLockFile)}
	if err := store.lockDir(); err != nil {
		return nil, err
	}
	return store, nil
}

func secureDir(path string, perm os.FileMode) error {
	if info, err := os.Lstat(path); err == nil {
		if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
			return fmt.Errorf("request: %s is not a real directory", path)
		}
		return os.Chmod(path, perm)
	} else if !errors.Is(err, os.ErrNotExist) {
		return err
	}
	if err := os.MkdirAll(path, perm); err != nil {
		return err
	}
	return os.Chmod(path, perm)
}

func (s *IdentityStore) lockDir() error {
	info, err := os.Lstat(s.lockPath)
	if err == nil && (info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular()) {
		return fmt.Errorf("request: the identity lock %s is not a regular file", s.lockPath)
	}
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	f, err := os.OpenFile(s.lockPath, os.O_RDWR|os.O_CREATE, 0o600)
	if err != nil {
		return fmt.Errorf("request: the identity lock: %w", err)
	}
	if err := f.Chmod(0o600); err != nil {
		_ = f.Close()
		return err
	}
	if err := hostos.Lock(f, hostos.LockExclusive|hostos.LockNonBlocking); err != nil {
		_ = f.Close()
		return fmt.Errorf("%w: %s", ErrIdentityInUse, s.name)
	}
	s.lock = f
	return nil
}

func (s *IdentityStore) Close() {
	if s == nil || s.lock == nil {
		return
	}
	_ = hostos.Unlock(s.lock)
	_ = s.lock.Close()
	s.lock = nil
}

func (s *IdentityStore) Name() string { return s.name }

func (s *IdentityStore) Dir() string { return s.dir }

func (s *IdentityStore) PDSDir() string { return s.pds }

func (s *IdentityStore) Path() string { return filepath.Join(s.dir, IdentityFile) }

func (s *IdentityStore) Load() (*Identity, error) {
	if info, err := os.Lstat(s.Path()); err == nil && info.Mode()&os.ModeSymlink != 0 {
		return nil, fmt.Errorf("request: the identity %s is a symlink", s.Path())
	}
	var id Identity
	err := fsx.ReadJSON(s.Path(), &id)
	if errors.Is(err, os.ErrNotExist) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("request: the identity %s: %w", s.name, err)
	}
	if id.Schema != IdentitySchema || id.Name != s.name {
		return nil, fmt.Errorf("request: the identity %s has schema %d or another name", s.name, id.Schema)
	}
	if id.StateRoot != "" && id.StateRoot != s.stateRoot {
		return nil, fmt.Errorf("request: the identity %s belongs to state root %s, not %s", s.name, id.StateRoot, s.stateRoot)
	}
	if id.PDSStateDir != "" && id.PDSStateDir != s.pds {
		return nil, fmt.Errorf("request: the identity %s names the PDS state dir %s, not %s", s.name, id.PDSStateDir, s.pds)
	}
	return &id, nil
}

func (s *IdentityStore) Save(id *Identity) error {
	if id == nil {
		return errors.New("request: a nil identity")
	}
	if id.Schema == 0 {
		id.Schema = IdentitySchema
	}
	id.Name = s.name
	id.StateRoot = s.stateRoot
	id.PDSStateDir = s.pds
	id.UpdatedAt = time.Now().UTC()
	if id.CreatedAt.IsZero() {
		id.CreatedAt = id.UpdatedAt
	}
	if err := fsx.WriteJSON(s.Path(), id, 0o600); err != nil {
		return fmt.Errorf("request: writing the identity %s: %w", s.name, err)
	}
	return nil
}

func (s *IdentityStore) EnsureDirs() error {
	for _, dir := range []string{s.pds, s.exposure} {
		if err := secureDir(dir, 0o700); err != nil {
			return err
		}
	}
	return nil
}

func (s *IdentityStore) Validate(did string, wantOAuth bool) (string, error) {
	rotation, err := pdssvc.ValidateStateDir(s.pds, did, wantOAuth)
	if err != nil {
		return "", err
	}
	accounts, err := ReadAccounts(s.pds)
	if err != nil {
		return "", err
	}
	if len(accounts) != 1 {
		return "", fmt.Errorf("request: the identity %s PDS holds %d accounts; one identity serves one account", s.name, len(accounts))
	}
	if accounts[0].DID != did {
		return "", fmt.Errorf("request: the identity %s PDS holds %s, not %s", s.name, accounts[0].DID, did)
	}
	return rotation, nil
}

func (s *IdentityStore) LinkExposure(instanceState string) error {
	if err := secureDir(instanceState, 0o700); err != nil {
		return err
	}
	if err := secureDir(s.exposure, 0o700); err != nil {
		return err
	}
	link := filepath.Join(instanceState, ExposureIdentityDir)
	rel, err := filepath.Rel(instanceState, s.exposure)
	if err != nil {
		return err
	}
	info, err := os.Lstat(link)
	if errors.Is(err, os.ErrNotExist) {
		if err := os.Symlink(rel, link); err != nil {
			return fmt.Errorf("request: linking the identity exposure dir: %w", err)
		}
		return nil
	}
	if err != nil {
		return err
	}
	if info.Mode()&os.ModeSymlink == 0 {
		return fmt.Errorf("request: %s is a real directory, not the identity exposure link", link)
	}
	have, err := os.Readlink(link)
	if err != nil {
		return err
	}
	if filepath.Clean(have) != filepath.Clean(rel) {
		return fmt.Errorf("request: %s points at %s, not the identity exposure dir", link, have)
	}
	return nil
}

func (s *IdentityStore) PDSLockHeld() (bool, error) {
	path := filepath.Join(s.pds, "lock")
	if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
		return false, nil
	} else if err != nil {
		return false, err
	}
	f, err := os.OpenFile(path, os.O_RDWR, 0)
	if err != nil {
		return false, err
	}
	defer f.Close()
	if err := hostos.Lock(f, hostos.LockExclusive|hostos.LockNonBlocking); err != nil {
		return true, nil
	}
	_ = hostos.Unlock(f)
	return false, nil
}

func (s *IdentityStore) ReadRotationKeyDID() (string, error) {
	path := filepath.Join(s.pds, pdssvc.RotationKeyFile)
	raw, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	key, err := atcrypto.ParsePrivateMultibase(strings.TrimSpace(string(raw)))
	if err != nil {
		return "", err
	}
	public, err := key.PublicKey()
	if err != nil {
		return "", err
	}
	return public.DIDKey(), nil
}

func ReadAccounts(pdsDir string) ([]Account, error) {
	path := filepath.Join(pdsDir, "accounts.db")
	info, err := os.Lstat(path)
	if errors.Is(err, os.ErrNotExist) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
		return nil, fmt.Errorf("request: %s is not a regular file", path)
	}
	db, err := sql.Open("sqlite", "file:"+path+"?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)")
	if err != nil {
		return nil, fmt.Errorf("request: opening the identity accounts: %w", err)
	}
	defer db.Close()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	rows, err := db.QueryContext(ctx, `SELECT did, handle FROM pds_accounts ORDER BY did`)
	if err != nil {
		return nil, fmt.Errorf("request: reading the identity accounts: %w", err)
	}
	defer rows.Close()
	var out []Account
	for rows.Next() {
		var did, handle string
		if err := rows.Scan(&did, &handle); err != nil {
			return nil, err
		}
		out = append(out, Account{DID: did, Handle: handle})
	}
	return out, rows.Err()
}

func InstanceStateDir(stateRoot, name string) string {
	return InstanceStateDirIn(Dir(stateRoot, name), name)
}

func InstanceStateDirIn(dir, name string) string {
	return filepath.Join(dir, "dev", name)
}

func LinkIdentityExposure(stateRoot, name, instanceState string) error {
	if err := CheckIdentityName(name); err != nil {
		return err
	}
	dir := IdentityDir(stateRoot, name)
	info, err := os.Lstat(dir)
	if err != nil {
		return fmt.Errorf("request: the identity %s: %w", name, err)
	}
	if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
		return fmt.Errorf("request: the identity %s is not a real directory", name)
	}
	s := &IdentityStore{stateRoot: stateRoot, name: name, dir: dir,
		pds: filepath.Join(dir, "pds"), exposure: filepath.Join(dir, ExposureIdentityDir)}
	return s.LinkExposure(instanceState)
}