Adds persistent storage using modernc.org/sqlite (pure Go). Login attempts are deduplicated by (username, password, ip) with counts. Sessions and session logs are tracked with UUID IDs. Includes embedded SQL migrations, configurable retention with background pruning, and an in-memory store for tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
139 lines
3.4 KiB
Go
139 lines
3.4 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
)
|
|
|
|
type Config struct {
|
|
SSH SSHConfig `toml:"ssh"`
|
|
Auth AuthConfig `toml:"auth"`
|
|
Storage StorageConfig `toml:"storage"`
|
|
LogLevel string `toml:"log_level"`
|
|
LogFormat string `toml:"log_format"` // "text" (default) or "json"
|
|
}
|
|
|
|
type StorageConfig struct {
|
|
DBPath string `toml:"db_path"`
|
|
RetentionDays int `toml:"retention_days"`
|
|
RetentionInterval string `toml:"retention_interval"`
|
|
|
|
// Parsed duration, not from TOML directly.
|
|
RetentionIntervalDuration time.Duration `toml:"-"`
|
|
}
|
|
|
|
type SSHConfig struct {
|
|
ListenAddr string `toml:"listen_addr"`
|
|
HostKeyPath string `toml:"host_key_path"`
|
|
MaxConnections int `toml:"max_connections"`
|
|
}
|
|
|
|
type AuthConfig struct {
|
|
AcceptAfter int `toml:"accept_after"`
|
|
CredentialTTL string `toml:"credential_ttl"`
|
|
StaticCredentials []Credential `toml:"static_credentials"`
|
|
|
|
// Parsed duration, not from TOML directly.
|
|
CredentialTTLDuration time.Duration `toml:"-"`
|
|
}
|
|
|
|
type Credential struct {
|
|
Username string `toml:"username"`
|
|
Password string `toml:"password"`
|
|
}
|
|
|
|
func Load(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading config: %w", err)
|
|
}
|
|
|
|
cfg := &Config{}
|
|
if err := toml.Unmarshal(data, cfg); err != nil {
|
|
return nil, fmt.Errorf("parsing config: %w", err)
|
|
}
|
|
|
|
applyDefaults(cfg)
|
|
|
|
if err := validate(cfg); err != nil {
|
|
return nil, fmt.Errorf("validating config: %w", err)
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func applyDefaults(cfg *Config) {
|
|
if cfg.SSH.ListenAddr == "" {
|
|
cfg.SSH.ListenAddr = ":2222"
|
|
}
|
|
if cfg.SSH.HostKeyPath == "" {
|
|
cfg.SSH.HostKeyPath = "oubliette_host_key"
|
|
}
|
|
if cfg.SSH.MaxConnections == 0 {
|
|
cfg.SSH.MaxConnections = 500
|
|
}
|
|
if cfg.Auth.AcceptAfter == 0 {
|
|
cfg.Auth.AcceptAfter = 10
|
|
}
|
|
if cfg.Auth.CredentialTTL == "" {
|
|
cfg.Auth.CredentialTTL = "24h"
|
|
}
|
|
if cfg.LogLevel == "" {
|
|
cfg.LogLevel = "info"
|
|
}
|
|
if cfg.LogFormat == "" {
|
|
cfg.LogFormat = "text"
|
|
}
|
|
if cfg.Storage.DBPath == "" {
|
|
cfg.Storage.DBPath = "oubliette.db"
|
|
}
|
|
if cfg.Storage.RetentionDays == 0 {
|
|
cfg.Storage.RetentionDays = 90
|
|
}
|
|
if cfg.Storage.RetentionInterval == "" {
|
|
cfg.Storage.RetentionInterval = "1h"
|
|
}
|
|
}
|
|
|
|
func validate(cfg *Config) error {
|
|
d, err := time.ParseDuration(cfg.Auth.CredentialTTL)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid credential_ttl %q: %w", cfg.Auth.CredentialTTL, err)
|
|
}
|
|
if d <= 0 {
|
|
return fmt.Errorf("credential_ttl must be positive, got %s", d)
|
|
}
|
|
cfg.Auth.CredentialTTLDuration = d
|
|
|
|
if cfg.Auth.AcceptAfter < 1 {
|
|
return fmt.Errorf("accept_after must be at least 1, got %d", cfg.Auth.AcceptAfter)
|
|
}
|
|
|
|
ri, err := time.ParseDuration(cfg.Storage.RetentionInterval)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid retention_interval %q: %w", cfg.Storage.RetentionInterval, err)
|
|
}
|
|
if ri <= 0 {
|
|
return fmt.Errorf("retention_interval must be positive, got %s", ri)
|
|
}
|
|
cfg.Storage.RetentionIntervalDuration = ri
|
|
|
|
if cfg.Storage.RetentionDays < 1 {
|
|
return fmt.Errorf("retention_days must be at least 1, got %d", cfg.Storage.RetentionDays)
|
|
}
|
|
|
|
for i, cred := range cfg.Auth.StaticCredentials {
|
|
if cred.Username == "" {
|
|
return fmt.Errorf("static_credentials[%d]: username must not be empty", i)
|
|
}
|
|
if cred.Password == "" {
|
|
return fmt.Errorf("static_credentials[%d]: password must not be empty", i)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|