package platform import ( "fmt" "net" "net/http" ) // StartLocalAPIServer runs the /api reverse proxy on a loopback HTTP listener and // returns its base URL (e.g. http://127.0.0.1:54321). // // Why: on Windows, Wails' custom-scheme AssetServer buffers a handler's response // until the handler RETURNS, so a long-lived SSE (/api/hub/events, whose handler // never returns) never reaches the WebView — the device list never loads and the // status sticks on "connecting". A real loopback HTTP origin is served by // WebView2's normal network stack, which streams response bodies incrementally. // // The page reaches this via the injected api_base; since its origin differs from // the listener, CORS is added. No credentials/cookies are used (auth is a Bearer // header), so a permissive policy is safe. macOS (WKWebView) streams the custom // scheme fine and doesn't use this. func StartLocalAPIServer(apiBase string) (string, error) { proxy, err := NewAPIProxy(apiBase) if err != nil { return "", err } ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return "", fmt.Errorf("local api: listen: %w", err) } srv := &http.Server{Handler: corsWrap(proxy)} go func() { _ = srv.Serve(ln) }() return "http://" + ln.Addr().String(), nil } // corsWrap adds permissive CORS so the WebView page (a different origin than this // loopback listener) can fetch /api, and answers the preflight. Includes the // Private Network Access ack since the page origin is also loopback. func corsWrap(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin == "" { origin = "*" } h := w.Header() h.Set("Access-Control-Allow-Origin", origin) h.Add("Vary", "Origin") h.Add("Vary", "Access-Control-Request-Headers") h.Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") // Echo the requested headers so every client header is allowed without // maintaining a list — the app sends Authorization, X-Device-Name, // X-Device-Type, and conditional headers like If-None-Match (clipboard // caching). Missing one fails the preflight as a "Failed to fetch". if reqHeaders := r.Header.Get("Access-Control-Request-Headers"); reqHeaders != "" { h.Set("Access-Control-Allow-Headers", reqHeaders) } else { h.Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Device-Name, X-Device-Type, X-Dev-User, If-None-Match") } h.Set("Access-Control-Allow-Private-Network", "true") h.Set("Access-Control-Max-Age", "600") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) }