package platform import ( "fmt" "net/http" "net/http/httputil" "net/url" "strings" ) // NewAPIProxy returns an http.Handler suitable for Wails' AssetServer.Handler. // It reverse-proxies every /api/* request to the remote cdrop backend so the // embedded web UI keeps using same-origin relative URLs — no CORS, no API-base // rewrite, no backend change. // // Why a proxy instead of cross-origin fetch: the WebView serves the bundle from // a custom scheme, so a relative /api request never reaches the backend, and a // cross-origin fetch to https://drop.commilitia.net would need server-side CORS // the backend doesn't send. Proxying keeps the request same-origin end to end. // // SSE (/api/hub/events) streams correctly: FlushInterval=-1 flushes each chunk, // and the darwin WebView ResponseWriter forwards every Write to the // WKURLSchemeTask immediately (didReceiveData per write), so frames arrive live. // // apiBase is the backend origin, e.g. https://drop.commilitia.net. Requests // that are not under /api/ get a 404 — desktop navigation is client-side, so the // asset FS already answers "/" and deep-link misses don't happen in normal use. func NewAPIProxy(apiBase string) (http.Handler, error) { target, err := url.Parse(apiBase) if err != nil { return nil, fmt.Errorf("proxy: parse api base %q: %w", apiBase, err) } if target.Scheme == "" || target.Host == "" { return nil, fmt.Errorf("proxy: api base %q missing scheme or host", apiBase) } proxy := httputil.NewSingleHostReverseProxy(target) base := proxy.Director proxy.Director = func(r *http.Request) { base(r) // The backend is vhosted behind Caddy; the Host header drives TLS SNI // and request routing, so it must be the backend host, not the WebView's // synthetic origin. r.Host = target.Host } proxy.FlushInterval = -1 // immediate flush — SSE frames / streamed relay bodies return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/api/") { proxy.ServeHTTP(w, r) return } http.NotFound(w, r) }), nil }