Files
glchat/internal/config/config.go
T
grendervill 16218ee045 feat(auth): сервис аккаунтов и аутентификации Фазы 1
- регистрация: политика пароля (≥10 + локальный словарь 10k утечек, SecLists MIT),
  username и email без учёта регистра, шифрование email с привязкой к аккаунту,
  blind index для поиска, автовступление в главный сервер с ролью @user
- вход: Argon2id+pepper, TOTP при включённом 2FA, блокировка после серии неудач,
  события безопасности; единый ответ на неверный пароль и неизвестный аккаунт
- сессии: opaque-токены (в БД только SHA-256), ротация токена, logout/logout-all,
  отзыв остальных сессий при смене пароля, step-up с окном 10 минут
- 2FA: настройка секрета с QR-URL, подтверждение кодом, 8 резервных кодов
  (хранятся хэшами), обязательность для инстанс-админов, запрет отключения
- тесты на реальной SQLite: регистрация/вход/дубликаты, слабые и утёкшие пароли,
  ротация и logout-all, TOTP и резервные коды, step-up, смена пароля,
  выключенная регистрация, отсутствие секретов в логах
2026-09-19 21:29:07 +03:00

179 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"fmt"
"os"
"strconv"
"strings"
"time"
)
const (
DefaultListenAddr = "127.0.0.1:8080"
DefaultDataDir = "/opt/glchat/data"
DefaultLogLevel = "info"
)
type Config struct {
Domain string
FilesDomain string
InstanceName string
ListenAddr string
DataDir string
WebRoot string
DatabasePath string
SQLiteCache string
SQLiteMmap string
LogLevel string
LogFormat string
ReadPoolSize int
MaxUploadSize int64
TLSEnabled bool
// Секреты инстанса (генерируются установщиком, AGENT.md §10.3).
SessionPepper string
MasterKey string
TOTPEncryptionKey string
SessionTTLHours int
Argon2MemoryKiB int
Argon2Iterations int
Argon2Parallelism int
Version string
Commit string
BuildDate string
ShutdownTTL time.Duration
Maintenance time.Duration
}
func Load() (Config, error) {
cfg := Config{
Domain: env("DOMAIN", "localhost"),
FilesDomain: env("FILES_DOMAIN", ""),
InstanceName: env("INSTANCE_NAME", "glchat"),
ListenAddr: env("LISTEN_ADDR", DefaultListenAddr),
DataDir: env("DATA_DIR", DefaultDataDir),
WebRoot: env("WEB_ROOT", "/app/web"),
DatabasePath: env("DATABASE_PATH", ""),
SQLiteCache: env("SQLITE_CACHE_SIZE", "-8000"),
SQLiteMmap: env("SQLITE_MMAP_SIZE", "134217728"),
LogLevel: strings.ToLower(env("LOG_LEVEL", DefaultLogLevel)),
LogFormat: strings.ToLower(env("LOG_FORMAT", "json")),
ReadPoolSize: envInt("SQLITE_READ_POOL", 4),
MaxUploadSize: envInt64("MAX_UPLOAD_SIZE", 26214400),
TLSEnabled: envBool("TLS_ENABLED", true),
SessionPepper: env("SESSION_PEPPER", ""),
MasterKey: env("MASTER_KEY", ""),
TOTPEncryptionKey: env("TOTP_ENCRYPTION_KEY", ""),
SessionTTLHours: envInt("SESSION_TTL_HOURS", 720),
Argon2MemoryKiB: envInt("ARGON2_MEMORY_KIB", 19456),
Argon2Iterations: envInt("ARGON2_ITERATIONS", 2),
Argon2Parallelism: envInt("ARGON2_PARALLELISM", 1),
Version: env("APP_VERSION", "dev"),
Commit: env("APP_COMMIT", "none"),
BuildDate: env("APP_BUILD_DATE", "unknown"),
ShutdownTTL: time.Duration(envInt("SHUTDOWN_TIMEOUT_SECONDS", 20)) * time.Second,
Maintenance: time.Duration(envInt("DB_MAINTENANCE_INTERVAL_SECONDS", 21600)) * time.Second,
}
if cfg.FilesDomain == "" {
cfg.FilesDomain = "files." + cfg.Domain
}
if cfg.DatabasePath == "" {
cfg.DatabasePath = cfg.DataDir + "/db/glchat.db"
}
if cfg.ReadPoolSize < 1 {
return Config{}, fmt.Errorf("SQLITE_READ_POOL must be >= 1, got %d", cfg.ReadPoolSize)
}
if _, err := parseLogLevel(cfg.LogLevel); err != nil {
return Config{}, err
}
if cfg.LogFormat != "json" && cfg.LogFormat != "text" {
return Config{}, fmt.Errorf("LOG_FORMAT must be json or text, got %q", cfg.LogFormat)
}
return cfg, nil
}
// Scheme выбирается установщиком: при установке без TLS (--skip-tls,
// например стенд без домена) клиент должен получать http/ws, а не https/wss.
func (c Config) Scheme() string {
if c.TLSEnabled {
return "https"
}
return "http"
}
func (c Config) BaseURL() string { return c.Scheme() + "://" + c.Domain }
func (c Config) FilesURL() string { return c.Scheme() + "://" + c.FilesDomain }
func (c Config) GatewayURL() string {
scheme := "ws"
if c.TLSEnabled {
scheme = "wss"
}
return scheme + "://" + c.Domain + "/gateway"
}
func (c Config) LogLevelNumeric() int {
level, err := parseLogLevel(c.LogLevel)
if err != nil {
return 0
}
return level
}
func parseLogLevel(level string) (int, error) {
switch level {
case "debug":
return -4, nil
case "info":
return 0, nil
case "warn", "warning":
return 4, nil
case "error":
return 8, nil
default:
return 0, fmt.Errorf("unknown LOG_LEVEL %q", level)
}
}
func env(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok && v != "" {
return v
}
return fallback
}
func envInt(key string, fallback int) int {
v, ok := os.LookupEnv(key)
if !ok || v == "" {
return fallback
}
n, err := strconv.Atoi(v)
if err != nil {
return fallback
}
return n
}
func envBool(key string, fallback bool) bool {
v, ok := os.LookupEnv(key)
if !ok || v == "" {
return fallback
}
parsed, err := strconv.ParseBool(v)
if err != nil {
return fallback
}
return parsed
}
func envInt64(key string, fallback int64) int64 {
v, ok := os.LookupEnv(key)
if !ok || v == "" {
return fallback
}
n, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return fallback
}
return n
}