package main import ( "context" "html/template" "log" "net/http" "os" "os/signal" "strings" "syscall" "time" ) var templates = make(map[string]*template.Template) // contextKey is used to pass per-request log extras from handlers to the logging middleware. type contextKey struct{} type logContext struct{ extra string } func main() { port := os.Getenv("PORT") if port == "" { port = "8080" } // Parse templates once at startup. index, err := template.ParseFiles("templates/base.html", "templates/index.html") if err != nil { log.Fatalf("failed to parse index template: %v", err) } templates["index"] = index waitlist, err := template.ParseFiles("templates/partials/waitlist_confirmation.html") if err != nil { log.Fatalf("failed to parse waitlist template: %v", err) } templates["waitlist"] = waitlist mux := http.NewServeMux() // Serve static assets fs := http.FileServer(http.Dir("static")) mux.Handle("/static/", http.StripPrefix("/static/", fs)) // Page routes mux.HandleFunc("/", handleIndex) mux.HandleFunc("/healthz", handleHealthz) // HTMX partial routes mux.HandleFunc("/partials/waitlist", handleWaitlist) // Wrap with logging (outermost) and security headers (innermost). handler := loggingMiddleware(securityHeaders(mux)) srv := &http.Server{ Addr: ":" + port, Handler: handler, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } // Graceful shutdown on SIGINT / SIGTERM. go func() { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) <-sig log.Println("shutting down…") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Printf("shutdown error: %v", err) } }() log.Printf("Arcline Project server starting on :%s", port) if err := srv.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("server failed: %v", err) } log.Println("server stopped") } // securityHeaders adds basic security headers to every response. func securityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()") next.ServeHTTP(w, r) }) } // responseWriter wraps http.ResponseWriter to capture the status code and bytes written. type responseWriter struct { http.ResponseWriter status int bytes int } func (rw *responseWriter) WriteHeader(code int) { rw.status = code rw.ResponseWriter.WriteHeader(code) } func (rw *responseWriter) Write(b []byte) (int, error) { if rw.status == 0 { rw.status = http.StatusOK } n, err := rw.ResponseWriter.Write(b) rw.bytes += n return n, err } // loggingMiddleware logs every request with IP, method, path, status, size, duration, referer, and user-agent. func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lc := &logContext{} ctx := context.WithValue(r.Context(), contextKey{}, lc) r = r.WithContext(ctx) start := time.Now() rw := &responseWriter{ResponseWriter: w, status: 0} next.ServeHTTP(rw, r) ip := r.RemoteAddr if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { ip = fwd } ref := r.Referer() if ref == "" { ref = "-" } ua := r.UserAgent() if ua == "" { ua = "-" } duration := time.Since(start) if lc.extra != "" { log.Printf("%s %s %s → %d (%dB) %v ref:%q ua:%q | %s", ip, r.Method, r.URL.Path, rw.status, rw.bytes, duration, ref, ua, lc.extra) } else { log.Printf("%s %s %s → %d (%dB) %v ref:%q ua:%q", ip, r.Method, r.URL.Path, rw.status, rw.bytes, duration, ref, ua) } }) } func handleHealthz(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } func handleIndex(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") data := map[string]interface{}{ "Title": "Arcline Project", } if err := templates["index"].ExecuteTemplate(w, "base", data); err != nil { log.Printf("render error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } func handleWaitlist(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { http.Error(w, "Bad Request", http.StatusBadRequest) return } email := strings.TrimSpace(r.FormValue("email")) if email == "" || !strings.Contains(email, "@") { http.Error(w, "Invalid email address", http.StatusBadRequest) return } // Signal the logging middleware that this was a waitlist signup. if lc, ok := r.Context().Value(contextKey{}).(*logContext); ok { lc.extra = "signup" } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := templates["waitlist"].Execute(w, map[string]string{"Email": email}); err != nil { log.Printf("render error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }