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)
		}
	}
}