File size: 3,345 Bytes
6a7089a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | // 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)
}
}
}
|