An open source supporter broker powered by high-fives. high-five.atprotofans.com/
at main 178 lines 5.7 kB view raw
1package main 2 3import ( 4 "context" 5 "flag" 6 "log" 7 "net/http" 8 "os" 9 "os/signal" 10 "strings" 11 "syscall" 12 "time" 13 14 "github.com/go-chi/chi/v5" 15 "github.com/go-chi/chi/v5/middleware" 16 17 "github.com/ngerakines/high-five-app/internal/handlers" 18 "github.com/ngerakines/high-five-app/internal/broker" 19 "github.com/ngerakines/high-five-app/internal/oauth" 20 "github.com/ngerakines/high-five-app/internal/storage" 21 "github.com/ngerakines/high-five-app/internal/websocket" 22) 23 24// getEnv returns the environment variable value or a default if not set. 25func getEnv(key, defaultValue string) string { 26 if value := os.Getenv(key); value != "" { 27 return value 28 } 29 return defaultValue 30} 31 32// securityHeaders adds security-related HTTP headers to all responses. 33func securityHeaders(next http.Handler) http.Handler { 34 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 // Prevent MIME type sniffing 36 w.Header().Set("X-Content-Type-Options", "nosniff") 37 // Prevent clickjacking 38 w.Header().Set("X-Frame-Options", "DENY") 39 // Control referrer information 40 w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") 41 // Permissions policy - restrict unnecessary APIs 42 w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()") 43 44 next.ServeHTTP(w, r) 45 }) 46} 47 48func main() { 49 // Command line flags with environment variable fallbacks 50 addr := flag.String("addr", getEnv("LISTEN_ADDR", ":8080"), "HTTP server address") 51 redisURL := flag.String("redis", getEnv("REDIS_URL", "redis://localhost:6379"), "Redis URL") 52 baseURL := flag.String("base-url", getEnv("BASE_URL", "http://localhost:8080"), "Base URL for OAuth callbacks") 53 templateDir := flag.String("templates", getEnv("TEMPLATE_DIR", "templates"), "Template directory") 54 flag.Parse() 55 56 // Read broker credentials from environment variables 57 brokerID := os.Getenv("BROKER_ID") 58 brokerAppPassword := os.Getenv("BROKER_APP_PASSWORD") 59 dryRun := os.Getenv("DRY_RUN") == "true" 60 61 if brokerID == "" { 62 log.Fatal("BROKER_ID environment variable is required") 63 } 64 if brokerAppPassword == "" { 65 log.Fatal("BROKER_APP_PASSWORD environment variable is required") 66 } 67 if dryRun { 68 log.Println("════════════════════════════════════════════════════════════════") 69 log.Println("DRY-RUN MODE ENABLED") 70 log.Println("No records will be created in any PDS") 71 log.Println("All record creation requests will be logged instead") 72 log.Println("════════════════════════════════════════════════════════════════") 73 } 74 75 // Initialize Redis store 76 store, err := storage.NewStore(*redisURL) 77 if err != nil { 78 log.Fatalf("Failed to connect to Redis: %v", err) 79 } 80 defer store.Close() 81 82 // Initialize broker service and authenticate 83 brokerService := broker.NewService(store, brokerID, brokerAppPassword, dryRun) 84 85 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 86 if err := brokerService.Authenticate(ctx); err != nil { 87 cancel() 88 log.Fatalf("Failed to authenticate broker: %v", err) 89 } 90 cancel() 91 92 brokerDID := brokerService.GetDID() 93 log.Printf("Broker authenticated: %s (DID: %s)", brokerID, brokerDID) 94 95 // Initialize OAuth service 96 oauthConfig := &oauth.Config{ 97 ClientID: *baseURL + "/oauth-client-metadata.json", 98 RedirectURI: *baseURL + "/oauth/callback", 99 Scopes: []string{ 100 "atproto", 101 "repo:com.atprotofans.high-five.support", 102 "repo:com.atprotofans.high-five.supportProof", 103 "repo:com.atprotofans.high-five.highFive", 104 "repo:com.atprotofans.high-five.highFiveProof", 105 "repo:app.bsky.feed.post", 106 "repo:app.bsky.actor.status", 107 }, 108 } 109 oauthService := oauth.NewService(oauthConfig, store, dryRun) 110 111 // Initialize WebSocket hub 112 hub := websocket.NewHub() 113 go hub.Run() 114 115 // Initialize handlers 116 highFiveHandler := handlers.NewHighFiveHandler(store, oauthService, hub, brokerService, *baseURL) 117 118 // Determine if we should use secure cookies (HTTPS) 119 secureMode := strings.HasPrefix(*baseURL, "https://") 120 121 httpHandler, err := handlers.NewHTTPHandler(store, oauthService, hub, highFiveHandler, *templateDir, secureMode) 122 if err != nil { 123 log.Fatalf("Failed to initialize HTTP handler: %v", err) 124 } 125 126 // Set up router 127 r := chi.NewRouter() 128 129 // Middleware 130 r.Use(middleware.Logger) 131 r.Use(middleware.Recoverer) 132 r.Use(middleware.Compress(5)) 133 r.Use(middleware.Timeout(60 * time.Second)) 134 r.Use(securityHeaders) 135 136 // Mount routes 137 r.Mount("/", httpHandler.Routes()) 138 139 // Serve static files 140 r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) 141 142 // Create server 143 server := &http.Server{ 144 Addr: *addr, 145 Handler: r, 146 ReadTimeout: 15 * time.Second, 147 WriteTimeout: 15 * time.Second, 148 IdleTimeout: 60 * time.Second, 149 } 150 151 // Start server in goroutine 152 go func() { 153 log.Printf("Starting server on %s", *addr) 154 log.Printf("Base URL: %s", *baseURL) 155 log.Printf("Broker DID: %s", brokerDID) 156 157 if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 158 log.Fatalf("Server error: %v", err) 159 } 160 }() 161 162 // Wait for interrupt signal 163 quit := make(chan os.Signal, 1) 164 signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 165 <-quit 166 167 log.Println("Shutting down server...") 168 169 // Give outstanding requests 30 seconds to complete 170 shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) 171 defer shutdownCancel() 172 173 if err := server.Shutdown(shutdownCtx); err != nil { 174 log.Fatalf("Server shutdown error: %v", err) 175 } 176 177 log.Println("Server stopped") 178}