k_card/component3/proxy.go

182 lines
4.8 KiB
Go

package main
import (
"fmt"
"io"
"log"
"net"
"net/http"
"strings"
"time"
)
// hopByHop headers that must not be forwarded by a proxy (RFC 7230 §6.1).
var hopByHopHeaders = map[string]bool{
"connection": true,
"keep-alive": true,
"proxy-authenticate": true,
"proxy-authorization": true,
"te": true,
"trailers": true,
"transfer-encoding": true,
"upgrade": true,
"proxy-connection": true, // non-standard but common
}
// Proxy is the HTTP/HTTPS proxy handler.
//
// For plain HTTP requests to gated hosts:
// browser → Proxy → (session token from phone) → endpoint directly → browser
//
// For HTTPS CONNECT to gated hosts:
// 407 — HTTPS interception is not supported; use plain HTTP for gated endpoints.
//
// For non-gated hosts:
// browser → Proxy → internet (transparent, no auth)
type Proxy struct {
phone *PhoneClient
gated *GatedHosts
verbose bool
}
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
p.handleConnect(w, r)
} else {
p.handleHTTP(w, r)
}
}
// handleHTTP handles plain HTTP proxy requests.
// For gated hosts: acquires a session token from the phone, adds it as
// Authorization: Bearer, then calls the endpoint directly.
func (p *Proxy) handleHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Host == "" {
http.Error(w, "not a proxy request", http.StatusBadRequest)
return
}
host := r.URL.Hostname()
port := r.URL.Port()
if port == "" {
port = "80"
}
isGated := p.gated.IsGated(host, port)
p.logf("HTTP %s %s (gated=%v)", r.Method, r.URL, isGated)
// Build outgoing request. RequestURI must be empty for http.Client/RoundTrip.
out := r.Clone(r.Context())
out.RequestURI = ""
stripHopByHop(out.Header)
if isGated {
token, err := p.phone.EnsureSession()
if err != nil {
http.Error(w, "auth: "+err.Error(), http.StatusUnauthorized)
return
}
out.Header.Set("Authorization", "Bearer "+token)
}
resp, err := http.DefaultTransport.RoundTrip(out)
if err != nil {
http.Error(w, "upstream: "+err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
copyHeaders(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
// handleConnect handles HTTPS CONNECT tunnels.
// For gated hosts: does TLS MITM so Authorization can be injected into each
// inner HTTP request before it is forwarded to the actual server.
// For non-gated hosts: transparent byte-level tunnel.
func (p *Proxy) handleConnect(w http.ResponseWriter, r *http.Request) {
host, portStr, err := net.SplitHostPort(r.Host)
if err != nil {
// No port — default to 443.
host = r.Host
portStr = "443"
}
if portStr == "" {
portStr = "443"
}
target := net.JoinHostPort(host, portStr)
isGated := p.gated.IsGated(host, portStr)
p.logf("CONNECT %s (gated=%v)", target, isGated)
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "hijack not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
log.Printf("hijack: %v", err)
return
}
if isGated {
fmt.Fprintf(clientConn, "HTTP/1.1 407 Proxy Authentication Required\r\nContent-Type: text/plain\r\nProxy-Authenticate: Bearer realm=\"chromecard\"\r\n\r\nHTTPS tunnels to gated hosts are not supported. Use plain HTTP.\r\n")
p.logf("CONNECT %s: gated HTTPS not supported, returned 407", target)
clientConn.Close()
} else {
p.handleDirectConnect(clientConn, target)
}
}
// handleDirectConnect tunnels bytes transparently — no auth, no inspection.
func (p *Proxy) handleDirectConnect(clientConn net.Conn, target string) {
defer clientConn.Close()
upConn, err := net.DialTimeout("tcp", target, 10*time.Second)
if err != nil {
fmt.Fprintf(clientConn, "HTTP/1.1 502 Bad Gateway\r\n\r\n")
return
}
defer upConn.Close()
fmt.Fprintf(clientConn, "HTTP/1.1 200 Connection Established\r\n\r\n")
pipe(clientConn, upConn)
}
// pipe copies bytes bidirectionally between two connections until either closes.
func pipe(a, b net.Conn) {
done := make(chan struct{}, 2)
go func() { io.Copy(a, b); done <- struct{}{} }()
go func() { io.Copy(b, a); done <- struct{}{} }()
<-done
}
// stripHopByHop removes hop-by-hop headers and any headers named in Connection.
func stripHopByHop(h http.Header) {
if conn := h.Get("Connection"); conn != "" {
for _, name := range strings.Split(conn, ",") {
h.Del(strings.TrimSpace(name))
}
}
for name := range hopByHopHeaders {
h.Del(name)
}
}
// copyHeaders copies non-hop-by-hop headers from src to dst.
func copyHeaders(dst, src http.Header) {
for k, vs := range src {
if !hopByHopHeaders[strings.ToLower(k)] {
for _, v := range vs {
dst.Add(k, v)
}
}
}
}
func (p *Proxy) logf(format string, args ...any) {
if p.verbose {
log.Printf(format, args...)
}
}