diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index ec971fb1c..71a11cca1 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -12,8 +12,11 @@ "io" "net" "net/http" + "net/http/httputil" "net/netip" + "net/url" pathpkg "path" + "strings" "time" "tailscale.com/ipn" @@ -156,13 +159,52 @@ func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) { return } if v := h.Proxy(); v != "" { - io.WriteString(w, "TODO(bradfitz): proxy") + // TODO(bradfitz): this is a lot of setup per HTTP request. We should + // build the whole http.Handler with all the muxing and child handlers + // only on start/config change. But this works for now (2022-11-09). + u, err := url.Parse(expandProxyArg(v)) + if err != nil { + http.Error(w, "bad proxy config", http.StatusInternalServerError) + return + } + rp := httputil.NewSingleHostReverseProxy(u) + rp.Transport = &http.Transport{ + DialContext: b.dialer.SystemDial, + } + rp.ServeHTTP(w, r) return } http.Error(w, "empty handler", 500) } +// expandProxyArg returns a URL from s, where s can be of form: +// +// * port number ("8080") +// * host:port ("localhost:8080") +// * full URL ("http://localhost:8080", in which case it's returned unchanged) +func expandProxyArg(s string) string { + if s == "" { + return "" + } + if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") { + return s + } + if allNumeric(s) { + return "http://127.0.0.1:" + s + } + return "http://" + s +} + +func allNumeric(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] < '0' || s[i] > '9' { + return false + } + } + return s != "" +} + func (b *LocalBackend) webServerConfig(sniName string, port uint16) (c ipn.WebServerConfigView, ok bool) { key := ipn.HostPort(fmt.Sprintf("%s:%v", sniName, port))