package server import ( "database/sql" "fmt" "net" "net/http" "os" "path/filepath" "strings" "time" _ "github.com/mattn/go-sqlite3" ) type Server struct { db *sql.DB dbPath string cfg *Config blobsDir string mux *http.ServeMux limiter *rateLimiter startedAt time.Time } // Version and BuildCommit are assigned through -ldflags during release builds. var ( Version = "dev" BuildCommit = "unknown" ) func (s *Server) auditLog(eventType, userID, deviceID, ip, msg string) { s.db.Exec("INSERT INTO server_audit_log (event_type, user_id, device_id, ip, message, created_at) VALUES (?, ?, ?, ?, ?, ?)", eventType, userID, deviceID, ip, msg, time.Now().UTC().Format(time.RFC3339)) } func NewServer(dbPath, dataDir string, cfg *Config) (*Server, error) { if cfg == nil { cfg = DefaultConfig() } if err := cfg.normalize(); err != nil { return nil, fmt.Errorf("config: %w", err) } db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=rwc", dbPath)) if err != nil { return nil, fmt.Errorf("open db: %w", err) } db.SetMaxOpenConns(1) for _, pragma := range []string{ "PRAGMA foreign_keys = ON", "PRAGMA busy_timeout = 5000", "PRAGMA journal_mode = WAL", "PRAGMA synchronous = NORMAL", } { if _, err := db.Exec(pragma); err != nil { db.Close() return nil, fmt.Errorf("sqlite %s: %w", pragma, err) } } for _, stmt := range strings.Split(serverSchema, ";") { stmt = strings.TrimSpace(stmt) if stmt == "" { continue } if _, err := db.Exec(stmt); err != nil { db.Close() return nil, fmt.Errorf("schema: %w", err) } } if err := migrateServerSchema(db); err != nil { db.Close() return nil, fmt.Errorf("migrate schema: %w", err) } blobsDir := filepath.Join(dataDir, "blobs") if err := os.MkdirAll(blobsDir, 0750); err != nil { db.Close() return nil, err } s := &Server{ db: db, dbPath: dbPath, cfg: cfg, blobsDir: blobsDir, limiter: newRateLimiter(nil), startedAt: time.Now().UTC(), } s.mux = http.NewServeMux() return s, nil } func (s *Server) SetupRoutes() { s.routes() } func (s *Server) locale() string { return "ru" } func (s *Server) Close() error { return s.db.Close() } // Handler is the only HTTP entrypoint. Additional request security middleware // is composed here so tests and production use the same path. func (s *Server) Handler() http.Handler { return s.mux } // HTTPServer creates a conservatively configured server suitable for running // behind nginx or Caddy. It intentionally does not enable TLS itself. func (s *Server) HTTPServer(addr string) *http.Server { return &http.Server{ Addr: addr, Handler: s.Handler(), ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 120 * time.Second, MaxHeaderBytes: 16 << 10, } } // ListenAndServe exists for callers that do not need to manage lifecycle. The // command entrypoint uses HTTPServer plus graceful shutdown instead. func (s *Server) ListenAndServe(addr string) error { return s.HTTPServer(addr).ListenAndServe() } func (s *Server) clientIP(r *http.Request) string { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { host = r.RemoteAddr } return s.clientIPFromPeer(host, r.Header.Get("X-Forwarded-For")) }