package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "commilitia.net/cdrop/internal/apns" "commilitia.net/cdrop/internal/calls" "commilitia.net/cdrop/internal/clipboard" "commilitia.net/cdrop/internal/config" "commilitia.net/cdrop/internal/db" "commilitia.net/cdrop/internal/httpapi" "commilitia.net/cdrop/internal/hub" "commilitia.net/cdrop/internal/jwtauth" "commilitia.net/cdrop/internal/push" "commilitia.net/cdrop/internal/relay" "commilitia.net/cdrop/internal/transfer" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) slog.SetDefault(logger) cfg, err := config.Load() if err != nil { slog.Error("config load failed", "err", err) os.Exit(1) } slog.Info("cdropd booting", "auth_mode", cfg.AuthMode, "listen", cfg.Listen, "db_path", cfg.DBPath, ) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() dbConn, err := db.Open(cfg.DBPath) if err != nil { slog.Error("db open failed", "err", err) os.Exit(1) } defer dbConn.Close() if err := db.Bootstrap(ctx, dbConn); err != nil { slog.Error("db bootstrap failed", "err", err) os.Exit(1) } queries := db.New(dbConn) auth := jwtauth.New(cfg, queries) h := hub.New(queries) defer h.Close() relayMgr := relay.NewManager(relay.DefaultMaxSessions, relay.DefaultSessionCap) pushSender := push.NewSender(queries, cfg.VAPIDPublicKey, cfg.VAPIDPrivateKey, cfg.VAPIDSubjectOrDefault()) if pushSender.Enabled() { slog.Info("web push enabled (VAPID configured)") } else { slog.Info("web push disabled (VAPID keys unset)") } apnsSender := apns.NewSender(queries, cfg.APNSKeyPEM(), cfg.APNSKeyID, cfg.APNSTeamID, cfg.APNSTopic, cfg.APNSEnv) if apnsSender.Enabled() { slog.Info("apns enabled", "topic", cfg.APNSTopic, "env", cfg.APNSEnv) } else { slog.Info("apns disabled (APNS keys unset)") } transfers := transfer.NewService(queries, h, relayMgr, pushSender, apnsSender) var cfTurn *calls.Provider if cfg.CFTurnKeyID != "" && cfg.CFTurnAPIToken != "" { cfTurn = calls.NewProvider(cfg.CFTurnKeyID, cfg.CFTurnAPIToken, calls.DefaultTTL) slog.Info("cloudflare realtime TURN enabled") } else { slog.Info("cloudflare TURN not configured; falling back to STUN-only") } clip := clipboard.New( queries, h, cfg.ClipboardMaxBytes, time.Duration(cfg.ClipboardDebounceSec)*time.Second, time.Duration(cfg.ClipboardTTLSec)*time.Second, ) go transfer.RunSweeper(ctx, transfers) go relayMgr.RunReaper(ctx) go jwtauth.RunDeviceSweeper(ctx, queries, time.Duration(cfg.DeviceTTLHours)*time.Hour) go httpapi.RunLoginRequestReaper(ctx, queries) if cfg.ClipboardTTLSec > 0 { go clipboard.RunSweeper(ctx, clip) } srv := &http.Server{ Addr: cfg.Listen, Handler: httpapi.New(cfg, dbConn, queries, auth, h, transfers, relayMgr, cfTurn, clip, pushSender, apnsSender).Handler(), ReadHeaderTimeout: 5 * time.Second, } go func() { slog.Info("http server listening", "addr", cfg.Listen) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error("http server error", "err", err) cancel() } }() <-ctx.Done() slog.Info("shutdown initiated") shutCtx, shutCancel := context.WithTimeout(context.Background(), 5*time.Second) defer shutCancel() if err := srv.Shutdown(shutCtx); err != nil { slog.Error("http shutdown error", "err", err) } slog.Info("cdropd stopped") }