182 lines
4.8 KiB
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...)
|
|
}
|
|
}
|