WitNote / internal /proxy /proxy.go
AUXteam's picture
Upload folder using huggingface_hub
6a7089a verified
// Package proxy provides a shared HTTP reverse-proxy helper used by
// strategies and the dashboard fallback routes. It consolidates the
// previously duplicated proxyHTTP / proxyRequest functions into one
// place with a shared http.Client and WebSocket upgrade support.
package proxy
import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/pinchtab/pinchtab/internal/handlers"
"github.com/pinchtab/pinchtab/internal/web"
)
// DefaultClient is the shared HTTP client for proxy requests.
// A 60-second timeout accommodates lazy Chrome initialization (8-20s)
// and tab navigation (up to 60s for NavigateTimeout in bridge config).
var DefaultClient = &http.Client{Timeout: 60 * time.Second}
type Options struct {
Client *http.Client
AllowedURL func(*url.URL) bool
RewriteRequest func(*http.Request)
}
var hopByHopHeaders = map[string]struct{}{
"connection": {},
"keep-alive": {},
"proxy-authenticate": {},
"proxy-authorization": {},
"te": {},
"trailers": {},
"transfer-encoding": {},
"upgrade": {},
"host": {},
}
func Forward(w http.ResponseWriter, r *http.Request, targetURL *url.URL, opts Options) {
if targetURL == nil {
web.Error(w, 502, fmt.Errorf("proxy error: missing target URL"))
return
}
if opts.AllowedURL != nil && !opts.AllowedURL(targetURL) {
web.Error(w, 400, fmt.Errorf("invalid proxy target"))
return
}
proxyReq := r.Clone(r.Context())
proxyReq.URL = targetURL
proxyReq.Host = targetURL.Host
proxyReq.Header = r.Header.Clone()
if opts.RewriteRequest != nil {
opts.RewriteRequest(proxyReq)
}
if isWebSocketUpgrade(proxyReq) {
handlers.ProxyWebSocket(w, proxyReq, targetURL.String())
return
}
client := opts.Client
if client == nil {
client = DefaultClient
}
outReq, err := http.NewRequestWithContext(r.Context(), r.Method, targetURL.String(), r.Body)
if err != nil {
web.Error(w, 502, fmt.Errorf("proxy error: %w", err))
return
}
copyHeaders(outReq.Header, proxyReq.Header)
resp, err := client.Do(outReq)
if err != nil {
web.Error(w, 502, fmt.Errorf("instance unreachable: %w", err))
return
}
defer func() { _ = resp.Body.Close() }()
copyHeaders(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
buf := make([]byte, 32*1024)
for {
n, readErr := resp.Body.Read(buf)
if n > 0 {
_, _ = w.Write(buf[:n])
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
if readErr != nil {
break
}
}
}
// HTTP forwards an HTTP request to targetURL, streaming the response
// back to w. If the request is a WebSocket upgrade, it delegates to
// handlers.ProxyWebSocket instead.
func HTTP(w http.ResponseWriter, r *http.Request, targetURL string) {
parsed, err := url.Parse(targetURL)
if err != nil {
web.Error(w, 502, fmt.Errorf("proxy error: %w", err))
return
}
if parsed.RawQuery == "" {
parsed.RawQuery = r.URL.RawQuery
}
Forward(w, r, parsed, Options{})
}
func isWebSocketUpgrade(r *http.Request) bool {
for _, v := range r.Header["Upgrade"] {
if strings.EqualFold(v, "websocket") {
return true
}
}
return false
}
func copyHeaders(dst, src http.Header) {
for k, vv := range src {
if _, skip := hopByHopHeaders[strings.ToLower(k)]; skip {
continue
}
for _, v := range vv {
dst.Add(k, v)
}
}
}