package main import ( "embed" "flag" "io/fs" "log" "net" "net/http" "os" "strings" "time" ) //go:embed build/* var buildAssets embed.FS func corsMiddleware(allowedOrigins []string) func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin == "" { next.ServeHTTP(w, r) return } allowed := false if len(allowedOrigins) == 0 { allowed = true } else { for _, o := range allowedOrigins { if o == "*" || o == origin { allowed = true break } } } if allowed { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") } if r.Method == "OPTIONS" { if allowed { w.WriteHeader(http.StatusOK) } else { w.WriteHeader(http.StatusForbidden) } return } if !allowed && len(allowedOrigins) > 0 { log.Printf("Blocked CORS request from origin: %s", origin) http.Error(w, "CORS Origin Not Allowed", http.StatusForbidden) return } next.ServeHTTP(w, r) } } } func main() { frontendPath := flag.String("frontend", "", "Path to custom frontend build directory (overrides embedded assets)") host := flag.String("host", "127.0.0.1", "Host to bind the server to") port := flag.String("port", "", "Port to listen on (overrides PORT env var)") allowedOriginsStr := flag.String("allowed-origins", os.Getenv("ALLOWED_ORIGINS"), "Comma-separated list of allowed CORS origins") flag.Parse() var allowedOrigins []string if *allowedOriginsStr != "" { origins := strings.Split(*allowedOriginsStr, ",") for _, o := range origins { allowedOrigins = append(allowedOrigins, strings.TrimSpace(o)) } } if hostEnv := os.Getenv("HOST"); hostEnv != "" { *host = hostEnv } if *port == "" { *port = os.Getenv("PORT") if *port == "" { *port = "8080" } } // Middleware chains cors := corsMiddleware(allowedOrigins) http.HandleFunc("/api/ping", cors(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if _, err := w.Write([]byte(`{"status":"ok"}`)); err != nil { log.Printf("Error writing response: %v", err) } })) // Static Assets var staticFS fs.FS if *frontendPath != "" { log.Printf("Using custom frontend from: %s\n", *frontendPath) staticFS = os.DirFS(*frontendPath) } else { sub, err := fs.Sub(buildAssets, "build") if err != nil { log.Fatal(err) } staticFS = sub } fileServer := http.FileServer(http.FS(staticFS)) // SPA Handler http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/") if path == "" { path = "index.html" } _, err := staticFS.Open(path) if err != nil { // If file doesn't exist, serve index.html for SPA routing r.URL.Path = "/" } fileServer.ServeHTTP(w, r) }) addr := net.JoinHostPort(*host, *port) log.Printf("Linking Tool server starting on %s...\n", addr) server := &http.Server{ Addr: addr, Handler: nil, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatal(err) } }