package knotserver import ( "context" "fmt" "net/http" "github.com/bluesky-social/indigo/xrpc" "github.com/urfave/cli/v3" "tangled.org/core/api/tangled" "tangled.org/core/hook" "tangled.org/core/idresolver" "tangled.org/core/jetstream" "tangled.org/core/knotserver/config" knotdb "tangled.org/core/knotserver/db" "tangled.org/core/knotserver/sandbox" "tangled.org/core/log" "tangled.org/core/notifier" "tangled.org/core/rbac" ) func Command() *cli.Command { return &cli.Command{ Name: "server", Usage: "run a knot server", Action: Run, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "secure-mode", Usage: "isolate git subprocesses to their own repository directory", }, }, Description: ` Environment variables: KNOT_SERVER_LISTEN_ADDR (default: 0.0.0.0:5555) KNOT_SERVER_INTERNAL_LISTEN_ADDR (default: 127.0.0.1:5444) KNOT_SERVER_DB_PATH (default: knotserver.db) KNOT_SERVER_HOSTNAME (required) KNOT_SERVER_JETSTREAM_ENDPOINT (default: wss://jetstream1.us-west.bsky.network/subscribe) KNOT_SERVER_OWNER (required) KNOT_SERVER_LOG_DIDS (default: true) KNOT_SERVER_DEV (default: false) KNOT_SERVER_SECURE_MODE (default: false) KNOT_REPO_SCAN_PATH (default: /home/git) KNOT_REPO_README (comma-separated list) KNOT_REPO_MAIN_BRANCH (default: main) KNOT_GIT_USER_NAME (default: Tangled) KNOT_GIT_USER_EMAIL (default: noreply@tangled.sh) APPVIEW_ENDPOINT (default: https://tangled.sh) `, } } func Run(ctx context.Context, cmd *cli.Command) error { logger := log.FromContext(ctx) logger = log.SubLogger(logger, cmd.Name) ctx = log.IntoContext(ctx, logger) c, err := config.Load(ctx) if err != nil { return fmt.Errorf("failed to load config: %w", err) } // CLI flag overrides env var. if cmd.Bool("secure-mode") { c.Server.SecureMode = true } if !c.Server.SecureMode { err = hook.Setup(hook.Config( hook.WithScanPath(c.Repo.ScanPath), hook.WithInternalApi(c.Server.InternalListenAddr), )) if err != nil { return fmt.Errorf("failed to setup hooks: %w", err) } logger.Info("successfully finished setting up hooks") } if c.Server.Dev { logger.Info("running in dev mode, signature verification is disabled") } db, err := knotdb.Setup(ctx, c.Server.DBPath) if err != nil { return fmt.Errorf("failed to load db: %w", err) } e, err := rbac.NewEnforcer(c.Server.DBPath) if err != nil { return fmt.Errorf("failed to setup rbac enforcer: %w", err) } e.E.EnableAutoSave(true) jc, err := jetstream.NewJetstreamClient(c.Server.JetstreamEndpoint, "knotserver", []string{ tangled.PublicKeyNSID, tangled.RepoNSID, }, nil, log.SubLogger(logger, "jetstream"), db, true, c.Server.LogDids) if err != nil { logger.Error("failed to setup jetstream", "error", err) } notifier := notifier.New() resolver := idresolver.DefaultResolver(c.Server.PlcUrl) if err := BackfillKnotMembers(ctx, db, e, c.Server.Owner, logger); err != nil { logger.Warn("knot member backfill failed, continuing", "err", err) } if err := BackfillCollaborators(ctx, db, e, logger, false); err != nil { logger.Warn("collaborator backfill failed, continuing", "err", err) } // probe and initialise the sandbox backend. var sb sandbox.Backend if c.Server.SecureMode { var warn string sb, warn = sandbox.New(func(repoPath string) (uint32, uint32, error) { return sandbox.LookupUIDForRepoPath(c.Repo.ScanPath, repoPath) }) if warn != "" { return fmt.Errorf("secure-mode: %s", warn) } logger.Info("secure-mode: activated", "backend", sb.Name()) // refuse to start if any repos have not yet been isolation-migrated. unmigrated, countErr := db.CountUnmigratedRepos() if countErr != nil { return fmt.Errorf("secure-mode: checking unmigrated repos: %w", countErr) } if unmigrated > 0 { return fmt.Errorf( "secure-mode: %d repo(s) have not been isolation-migrated; "+ "run 'knot migrate-isolation' first", unmigrated, ) } } else { sb = &sandbox.NoopBackend{} } go func() { migrated := migrateReposOnStartup(ctx, c, db, e, ¬ifier, log.SubLogger(logger, "migrate")) if err := BackfillCollaborators(ctx, db, e, logger, migrated); err != nil { logger.Warn("collaborator backfill failed, continuing", "err", err) } }() mux, err := Setup(ctx, c, db, e, jc, ¬ifier, resolver, sb) if err != nil { return fmt.Errorf("failed to setup server: %w", err) } imux := Internal(ctx, c, db, e, ¬ifier, resolver) logger.Info("starting internal server", "address", c.Server.InternalListenAddr) go http.ListenAndServe(c.Server.InternalListenAddr, imux) // TODO(boltless): too lazy here. should clear this up go func() { input := &tangled.SyncRequestCrawl_Input{ Hostname: c.Server.Hostname, } for _, knotmirror := range c.KnotMirrors { xrpcc := xrpc.Client{Host: knotmirror} if err := tangled.SyncRequestCrawl(ctx, &xrpcc, input); err != nil { logger.Error("error requesting crawl", "err", err) } else { logger.Info("crawl requested successfully") } } }() logger.Info("starting main server", "address", c.Server.ListenAddr) logger.Error("server error", "error", http.ListenAndServe(c.Server.ListenAddr, mux)) return nil }