// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package ipnlocal

import (
	"context"
	"crypto/sha256"
	"crypto/tls"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/httputil"
	"net/netip"
	"net/url"
	"os"
	"path"
	"slices"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"golang.org/x/net/http2"
	"tailscale.com/ipn"
	"tailscale.com/logtail/backoff"
	"tailscale.com/net/netutil"
	"tailscale.com/syncs"
	"tailscale.com/tailcfg"
	"tailscale.com/types/lazy"
	"tailscale.com/types/logger"
	"tailscale.com/util/ctxkey"
	"tailscale.com/util/mak"
	"tailscale.com/version"
)

const (
	contentTypeHeader   = "Content-Type"
	grpcBaseContentType = "application/grpc"
)

// ErrETagMismatch signals that the given
// If-Match header does not match with the
// current etag of a resource.
var ErrETagMismatch = errors.New("etag mismatch")

var serveHTTPContextKey ctxkey.Key[*serveHTTPContext]

type serveHTTPContext struct {
	SrcAddr  netip.AddrPort
	DestPort uint16
}

// localListener is the state of host-level net.Listen for a specific (Tailscale IP, port)
// combination. If there are two TailscaleIPs (v4 and v6) and three ports being served,
// then there will be six of these active and looping in their Run method.
//
// This is not used in userspace-networking mode.
//
// localListener is used by tailscale serve (TCP only), the built-in web client and tailfs.
// Most serve traffic and peer traffic for the web client are intercepted by netstack.
// This listener exists purely for connections from the machine itself, as that goes via the kernel,
// so we need to be in the kernel's listening/routing tables.
type localListener struct {
	b      *LocalBackend
	ap     netip.AddrPort
	ctx    context.Context    // valid while listener is desired
	cancel context.CancelFunc // for ctx, to close listener
	logf   logger.Logf
	bo     *backoff.Backoff // for retrying failed Listen calls

	handler       func(net.Conn) error            // handler for inbound connections
	closeListener syncs.AtomicValue[func() error] // Listener's Close method, if any
}

func (b *LocalBackend) newServeListener(ctx context.Context, ap netip.AddrPort, logf logger.Logf) *localListener {
	ctx, cancel := context.WithCancel(ctx)
	return &localListener{
		b:      b,
		ap:     ap,
		ctx:    ctx,
		cancel: cancel,
		logf:   logf,

		handler: func(conn net.Conn) error {
			srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
			handler := b.tcpHandlerForServe(ap.Port(), srcAddr)
			if handler == nil {
				b.logf("[unexpected] local-serve: no handler for %v to port %v", srcAddr, ap.Port())
				conn.Close()
				return nil
			}
			return handler(conn)
		},
		bo: backoff.NewBackoff("serve-listener", logf, 30*time.Second),
	}

}

// Close cancels the context and closes the listener, if any.
func (s *localListener) Close() error {
	s.cancel()
	if close, ok := s.closeListener.LoadOk(); ok {
		s.closeListener.Store(nil)
		close()
	}
	return nil
}

// Run starts a net.Listen for the localListener's address and port.
// If unable to listen, it retries with exponential backoff.
// Listen is retried until the context is canceled.
func (s *localListener) Run() {
	for {
		ip := s.ap.Addr()
		ipStr := ip.String()

		var lc net.ListenConfig
		if initListenConfig != nil {
			// On macOS, this sets the lc.Control hook to
			// setsockopt the interface index to bind to. This is
			// required by the network sandbox to allow binding to
			// a specific interface. Without this hook, the system
			// chooses a default interface to bind to.
			if err := initListenConfig(&lc, ip, s.b.prevIfState, s.b.dialer.TUNName()); err != nil {
				s.logf("localListener failed to init listen config %v, backing off: %v", s.ap, err)
				s.bo.BackOff(s.ctx, err)
				continue
			}
			// On macOS (AppStore or macsys) and if we're binding to a privileged port,
			if version.IsSandboxedMacOS() && s.ap.Port() < 1024 {
				// On macOS, we need to bind to ""/all-interfaces due to
				// the network sandbox. Ideally we would only bind to the
				// Tailscale interface, but macOS errors out if we try to
				// to listen on privileged ports binding only to a specific
				// interface. (#6364)
				ipStr = ""
			}
		}

		tcp4or6 := "tcp4"
		if ip.Is6() {
			tcp4or6 = "tcp6"
		}

		ln, err := lc.Listen(s.ctx, tcp4or6, net.JoinHostPort(ipStr, fmt.Sprint(s.ap.Port())))
		if err != nil {
			if s.shouldWarnAboutListenError(err) {
				s.logf("localListener failed to listen on %v, backing off: %v", s.ap, err)
			}
			s.bo.BackOff(s.ctx, err)
			continue
		}
		s.closeListener.Store(ln.Close)

		s.logf("listening on %v", s.ap)
		err = s.handleListenersAccept(ln)
		if s.ctx.Err() != nil {
			// context canceled, we're done
			return
		}
		if err != nil {
			s.logf("localListener accept error, retrying: %v", err)
		}
	}
}

func (s *localListener) shouldWarnAboutListenError(err error) bool {
	if !s.b.sys.NetMon.Get().InterfaceState().HasIP(s.ap.Addr()) {
		// Machine likely doesn't have IPv6 enabled (or the IP is still being
		// assigned). No need to warn. Notably, WSL2 (Issue 6303).
		return false
	}
	// TODO(bradfitz): check errors.Is(err, syscall.EADDRNOTAVAIL) etc? Let's
	// see what happens in practice.
	return true
}

// handleListenersAccept accepts connections for the Listener. It calls the
// handler in a new goroutine for each accepted connection. This is used to
// handle local "tailscale serve" and web client traffic originating from the
// machine itself.
func (s *localListener) handleListenersAccept(ln net.Listener) error {
	for {
		conn, err := ln.Accept()
		if err != nil {
			return err
		}
		go s.handler(conn)
	}
}

// updateServeTCPPortNetMapAddrListenersLocked starts a net.Listen for configured
// Serve ports on all the node's addresses.
// Existing Listeners are closed if port no longer in incoming ports list.
//
// b.mu must be held.
func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint16) {
	// close existing listeners where port
	// is no longer in incoming ports list
	for ap, sl := range b.serveListeners {
		if !slices.Contains(ports, ap.Port()) {
			b.logf("closing listener %v", ap)
			sl.Close()
			delete(b.serveListeners, ap)
		}
	}

	nm := b.netMap
	if nm == nil {
		b.logf("netMap is nil")
		return
	}
	if !nm.SelfNode.Valid() {
		b.logf("netMap SelfNode is nil")
		return
	}

	addrs := nm.GetAddresses()
	for i := range addrs.Len() {
		a := addrs.At(i)
		for _, p := range ports {
			addrPort := netip.AddrPortFrom(a.Addr(), p)
			if _, ok := b.serveListeners[addrPort]; ok {
				continue // already listening
			}

			sl := b.newServeListener(context.Background(), addrPort, b.logf)
			mak.Set(&b.serveListeners, addrPort, sl)

			go sl.Run()
		}
	}
}

// SetServeConfig establishes or replaces the current serve config.
// ETag is an optional parameter to enforce Optimistic Concurrency Control.
// If it is an empty string, then the config will be overwritten.
func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig, etag string) error {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.setServeConfigLocked(config, etag)
}

func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string) error {
	prefs := b.pm.CurrentPrefs()
	if config.IsFunnelOn() && prefs.ShieldsUp() {
		return errors.New("Unable to turn on Funnel while shields-up is enabled")
	}
	if b.isConfigLocked_Locked() {
		return errors.New("can't reconfigure tailscaled when using a config file; config file is locked")
	}

	nm := b.netMap
	if nm == nil {
		return errors.New("netMap is nil")
	}
	if !nm.SelfNode.Valid() {
		return errors.New("netMap SelfNode is nil")
	}

	// If etag is present, check that it has
	// not changed from the last config.
	prevConfig := b.serveConfig
	if etag != "" {
		// Note that we marshal b.serveConfig
		// and not use b.lastServeConfJSON as that might
		// be a Go nil value, which produces a different
		// checksum from a JSON "null" value.
		prevBytes, err := json.Marshal(prevConfig)
		if err != nil {
			return fmt.Errorf("error encoding previous config: %w", err)
		}
		sum := sha256.Sum256(prevBytes)
		previousEtag := hex.EncodeToString(sum[:])
		if etag != previousEtag {
			return ErrETagMismatch
		}
	}

	var bs []byte
	if config != nil {
		j, err := json.Marshal(config)
		if err != nil {
			return fmt.Errorf("encoding serve config: %w", err)
		}
		bs = j
	}

	profileID := b.pm.CurrentProfile().ID
	confKey := ipn.ServeConfigKey(profileID)
	if err := b.store.WriteState(confKey, bs); err != nil {
		return fmt.Errorf("writing ServeConfig to StateStore: %w", err)
	}

	b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())

	// clean up and close all previously open foreground sessions
	// if the current ServeConfig has overwritten them.
	if prevConfig.Valid() {
		has := func(string) bool { return false }
		if b.serveConfig.Valid() {
			has = b.serveConfig.Foreground().Has
		}
		prevConfig.Foreground().Range(func(k string, v ipn.ServeConfigView) (cont bool) {
			if !has(k) {
				for _, sess := range b.notifyWatchers {
					if sess.sessionID == k {
						close(sess.ch)
					}
				}
			}
			return true
		})
	}

	return nil
}

// ServeConfig provides a view of the current serve mappings.
// If serving is not configured, the returned view is not Valid.
func (b *LocalBackend) ServeConfig() ipn.ServeConfigView {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.serveConfig
}

// DeleteForegroundSession deletes a ServeConfig's foreground session
// in the LocalBackend if it exists. It also ensures check, delete, and
// set operations happen within the same mutex lock to avoid any races.
func (b *LocalBackend) DeleteForegroundSession(sessionID string) error {
	b.mu.Lock()
	defer b.mu.Unlock()
	if !b.serveConfig.Valid() || !b.serveConfig.Foreground().Has(sessionID) {
		return nil
	}
	sc := b.serveConfig.AsStruct()
	delete(sc.Foreground, sessionID)
	return b.setServeConfigLocked(sc, "")
}

// HandleIngressTCPConn handles a TCP connection initiated by the ingressPeer
// proxied to the local node over the PeerAPI.
// Target represents the destination HostPort of the conn.
// srcAddr represents the source AddrPort and not that of the ingressPeer.
// getConnOrReset is a callback to get the connection, or reset if the connection
// is no longer available.
// sendRST is a callback to send a TCP RST to the ingressPeer indicating that
// the connection was not accepted.
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
	b.mu.Lock()
	sc := b.serveConfig
	b.mu.Unlock()

	// TODO(maisem,bradfitz): make this not alloc for every conn.
	logf := logger.WithPrefix(b.logf, "handleIngress: ")

	if !sc.Valid() {
		logf("got ingress conn w/o serveConfig; rejecting")
		sendRST()
		return
	}

	if !sc.HasFunnelForTarget(target) {
		logf("got ingress conn for unconfigured %q; rejecting", target)
		sendRST()
		return
	}

	_, port, err := net.SplitHostPort(string(target))
	if err != nil {
		logf("got ingress conn for bad target %q; rejecting", target)
		sendRST()
		return
	}
	port16, err := strconv.ParseUint(port, 10, 16)
	if err != nil {
		logf("got ingress conn for bad target %q; rejecting", target)
		sendRST()
		return
	}
	dport := uint16(port16)
	if b.getTCPHandlerForFunnelFlow != nil {
		handler := b.getTCPHandlerForFunnelFlow(srcAddr, dport)
		if handler != nil {
			c, ok := getConnOrReset()
			if !ok {
				logf("getConn didn't complete from %v to port %v", srcAddr, dport)
				return
			}
			handler(c)
			return
		}
	}
	// TODO(bradfitz): pass ingressPeer etc in context to tcpHandlerForServe,
	// extend serveHTTPContext or similar.
	handler := b.tcpHandlerForServe(dport, srcAddr)
	if handler == nil {
		logf("[unexpected] no matching ingress serve handler for %v to port %v", srcAddr, dport)
		sendRST()
		return
	}
	c, ok := getConnOrReset()
	if !ok {
		logf("getConn didn't complete from %v to port %v", srcAddr, dport)
		return
	}
	handler(c)
}

// tcpHandlerForServe returns a handler for a TCP connection to be served via
// the ipn.ServeConfig.
func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort) (handler func(net.Conn) error) {
	b.mu.Lock()
	sc := b.serveConfig
	b.mu.Unlock()

	if !sc.Valid() {
		return nil
	}

	tcph, ok := sc.FindTCP(dport)
	if !ok {
		return nil
	}

	if tcph.HTTPS() || tcph.HTTP() {
		hs := &http.Server{
			Handler: http.HandlerFunc(b.serveWebHandler),
			BaseContext: func(_ net.Listener) context.Context {
				return serveHTTPContextKey.WithValue(context.Background(), &serveHTTPContext{
					SrcAddr:  srcAddr,
					DestPort: dport,
				})
			},
		}
		if tcph.HTTPS() {
			hs.TLSConfig = &tls.Config{
				GetCertificate: b.getTLSServeCertForPort(dport),
			}
			return func(c net.Conn) error {
				return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
			}
		}

		return func(c net.Conn) error {
			return hs.Serve(netutil.NewOneConnListener(c, nil))
		}
	}

	if backDst := tcph.TCPForward(); backDst != "" {
		return func(conn net.Conn) error {
			defer conn.Close()
			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
			backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
			cancel()
			if err != nil {
				b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
				return nil
			}
			defer backConn.Close()
			if sni := tcph.TerminateTLS(); sni != "" {
				conn = tls.Server(conn, &tls.Config{
					GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
						ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
						defer cancel()
						pair, err := b.GetCertPEM(ctx, sni)
						if err != nil {
							return nil, err
						}
						cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
						if err != nil {
							return nil, err
						}
						return &cert, nil
					},
				})
			}

			// TODO(bradfitz): do the RegisterIPPortIdentity and
			// UnregisterIPPortIdentity stuff that netstack does
			errc := make(chan error, 1)
			go func() {
				_, err := io.Copy(backConn, conn)
				errc <- err
			}()
			go func() {
				_, err := io.Copy(conn, backConn)
				errc <- err
			}()
			return <-errc
		}
	}

	return nil
}

func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView, at string, ok bool) {
	var z ipn.HTTPHandlerView // zero value

	hostname := r.Host
	if r.TLS == nil {
		tcd := "." + b.Status().CurrentTailnet.MagicDNSSuffix
		if host, _, err := net.SplitHostPort(hostname); err == nil {
			hostname = host
		}
		if !strings.HasSuffix(hostname, tcd) {
			hostname += tcd
		}
	} else {
		hostname = r.TLS.ServerName
	}

	sctx, ok := serveHTTPContextKey.ValueOk(r.Context())
	if !ok {
		b.logf("[unexpected] localbackend: no serveHTTPContext in request")
		return z, "", false
	}
	wsc, ok := b.webServerConfig(hostname, sctx.DestPort)
	if !ok {
		return z, "", false
	}

	if h, ok := wsc.Handlers().GetOk(r.URL.Path); ok {
		return h, r.URL.Path, true
	}
	pth := path.Clean(r.URL.Path)
	for {
		withSlash := pth + "/"
		if h, ok := wsc.Handlers().GetOk(withSlash); ok {
			return h, withSlash, true
		}
		if h, ok := wsc.Handlers().GetOk(pth); ok {
			return h, pth, true
		}
		if pth == "/" {
			return z, "", false
		}
		pth = path.Dir(pth)
	}
}

// proxyHandlerForBackend creates a new HTTP reverse proxy for a particular backend that
// we serve requests for. `backend` is a HTTPHandler.Proxy string (url, hostport or just port).
func (b *LocalBackend) proxyHandlerForBackend(backend string) (http.Handler, error) {
	targetURL, insecure := expandProxyArg(backend)
	u, err := url.Parse(targetURL)
	if err != nil {
		return nil, fmt.Errorf("invalid url %s: %w", targetURL, err)
	}
	p := &reverseProxy{
		logf:     b.logf,
		url:      u,
		insecure: insecure,
		backend:  backend,
		lb:       b,
	}
	return p, nil
}

// reverseProxy is a proxy that forwards a request to a backend host
// (preconfigured via ipn.ServeConfig). If the host is configured with
// http+insecure prefix, connection between proxy and backend will be over
// insecure TLS. If the backend host has a http prefix and the incoming request
// has application/grpc content type header, the connection will be over h2c.
// Otherwise standard Go http transport will be used.
type reverseProxy struct {
	logf logger.Logf
	url  *url.URL
	// insecure tracks whether the connection to an https backend should be
	// insecure (i.e because we cannot verify its CA).
	insecure      bool
	backend       string
	lb            *LocalBackend
	httpTransport lazy.SyncValue[*http.Transport]  // transport for non-h2c backends
	h2cTransport  lazy.SyncValue[*http2.Transport] // transport for h2c backends
	// closed tracks whether proxy is closed/currently closing.
	closed atomic.Bool
}

// close ensures that any open backend connections get closed.
func (rp *reverseProxy) close() {
	rp.closed.Store(true)
	if h2cT := rp.h2cTransport.Get(func() *http2.Transport {
		return nil
	}); h2cT != nil {
		h2cT.CloseIdleConnections()
	}
	if httpTransport := rp.httpTransport.Get(func() *http.Transport {
		return nil
	}); httpTransport != nil {
		httpTransport.CloseIdleConnections()
	}
}

func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if closed := rp.closed.Load(); closed {
		rp.logf("received a request for a proxy that's being closed or has been closed")
		http.Error(w, "proxy is closed", http.StatusServiceUnavailable)
		return
	}
	p := &httputil.ReverseProxy{Rewrite: func(r *httputil.ProxyRequest) {
		oldOutPath := r.Out.URL.Path
		r.SetURL(rp.url)

		// If mount point matches the request path exactly, the outbound
		// request URL was set to empty string in serveWebHandler which
		// would have resulted in the outbound path set to <proxy path>
		// + '/' in SetURL. In that case, if the proxy path was set, we
		// want to send the request to the <proxy path> (without the
		// '/') .
		if oldOutPath == "" && rp.url.Path != "" {
			r.Out.URL.Path = rp.url.Path
			r.Out.URL.RawPath = rp.url.RawPath
		}

		r.Out.Host = r.In.Host
		addProxyForwardedHeaders(r)
		rp.lb.addTailscaleIdentityHeaders(r)
	}}

	// There is no way to autodetect h2c as per RFC 9113
	// https://datatracker.ietf.org/doc/html/rfc9113#name-starting-http-2.
	// However, we assume that http:// proxy prefix in combination with the
	// protoccol being HTTP/2 is sufficient to detect h2c for our needs. Only use this for
	// gRPC to fix a known problem of plaintext gRPC backends
	if rp.shouldProxyViaH2C(r) {
		rp.logf("received a proxy request for plaintext gRPC")
		p.Transport = rp.getH2CTransport()
	} else {
		p.Transport = rp.getTransport()
	}
	p.ServeHTTP(w, r)
}

// getTransport returns the Transport used for regular (non-GRPC) requests
// to the backend. The Transport gets created lazily, at most once.
func (rp *reverseProxy) getTransport() *http.Transport {
	return rp.httpTransport.Get(func() *http.Transport {
		return &http.Transport{
			DialContext: rp.lb.dialer.SystemDial,
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: rp.insecure,
			},
			// Values for the following parameters have been copied from http.DefaultTransport.
			ForceAttemptHTTP2:     true,
			MaxIdleConns:          100,
			IdleConnTimeout:       90 * time.Second,
			TLSHandshakeTimeout:   10 * time.Second,
			ExpectContinueTimeout: 1 * time.Second,
		}
	})
}

// getH2CTransport returns the Transport used for GRPC requests to the backend.
// The Transport gets created lazily, at most once.
func (rp *reverseProxy) getH2CTransport() *http2.Transport {
	return rp.h2cTransport.Get(func() *http2.Transport {
		return &http2.Transport{
			AllowHTTP: true,
			DialTLSContext: func(ctx context.Context, network string, addr string, _ *tls.Config) (net.Conn, error) {
				return rp.lb.dialer.SystemDial(ctx, "tcp", rp.url.Host)
			},
		}
	})
}

// This is not a generally reliable way how to determine whether a request is
// for a h2c server, but sufficient for our particular use case.
func (rp *reverseProxy) shouldProxyViaH2C(r *http.Request) bool {
	contentType := r.Header.Get(contentTypeHeader)
	return r.ProtoMajor == 2 && strings.HasPrefix(rp.backend, "http://") && isGRPCContentType(contentType)
}

// isGRPC accepts an HTTP request's content type header value and determines
// whether this is gRPC content. grpc-go considers a value that equals
// application/grpc or has a prefix of application/grpc+ or application/grpc; a
// valid grpc content type header.
// https://github.com/grpc/grpc-go/blob/v1.60.0-dev/internal/grpcutil/method.go#L41-L78
func isGRPCContentType(contentType string) bool {
	s, ok := strings.CutPrefix(contentType, grpcBaseContentType)
	return ok && (len(s) == 0 || s[0] == '+' || s[0] == ';')
}

func addProxyForwardedHeaders(r *httputil.ProxyRequest) {
	r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
	if r.In.TLS != nil {
		r.Out.Header.Set("X-Forwarded-Proto", "https")
	}
	if c, ok := serveHTTPContextKey.ValueOk(r.Out.Context()); ok {
		r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
	}
}

func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) {
	// Clear any incoming values squatting in the headers.
	r.Out.Header.Del("Tailscale-User-Login")
	r.Out.Header.Del("Tailscale-User-Name")
	r.Out.Header.Del("Tailscale-User-Profile-Pic")
	r.Out.Header.Del("Tailscale-Headers-Info")

	c, ok := serveHTTPContextKey.ValueOk(r.Out.Context())
	if !ok {
		return
	}
	node, user, ok := b.WhoIs(c.SrcAddr)
	if !ok {
		return // traffic from outside of Tailnet (funneled)
	}
	if node.IsTagged() {
		// 2023-06-14: Not setting identity headers for tagged nodes.
		// Only currently set for nodes with user identities.
		return
	}
	r.Out.Header.Set("Tailscale-User-Login", user.LoginName)
	r.Out.Header.Set("Tailscale-User-Name", user.DisplayName)
	r.Out.Header.Set("Tailscale-User-Profile-Pic", user.ProfilePicURL)
	r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers")
}

// serveWebHandler is an http.HandlerFunc that maps incoming requests to the
// correct *http.
func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
	h, mountPoint, ok := b.getServeHandler(r)
	if !ok {
		http.NotFound(w, r)
		return
	}
	if s := h.Text(); s != "" {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		io.WriteString(w, s)
		return
	}
	if v := h.Path(); v != "" {
		b.serveFileOrDirectory(w, r, v, mountPoint)
		return
	}
	if v := h.Proxy(); v != "" {
		p, ok := b.serveProxyHandlers.Load(v)
		if !ok {
			http.Error(w, "unknown proxy destination", http.StatusInternalServerError)
			return
		}
		h := p.(http.Handler)
		// Trim the mount point from the URL path before proxying. (#6571)
		if r.URL.Path != "/" {
			h = http.StripPrefix(strings.TrimSuffix(mountPoint, "/"), h)
		}
		h.ServeHTTP(w, r)
		return
	}

	http.Error(w, "empty handler", 500)
}

func (b *LocalBackend) serveFileOrDirectory(w http.ResponseWriter, r *http.Request, fileOrDir, mountPoint string) {
	fi, err := os.Stat(fileOrDir)
	if err != nil {
		if os.IsNotExist(err) {
			http.NotFound(w, r)
			return
		}
		b.logf("error calling stat on %s: %v", fileOrDir, err)
		http.Error(w, "an error occurred reading the file or directory", 500)
		return
	}
	if fi.Mode().IsRegular() {
		if mountPoint != r.URL.Path {
			http.NotFound(w, r)
			return
		}
		f, err := os.Open(fileOrDir)
		if err != nil {
			b.logf("error opening %s: %v", fileOrDir, err)
			http.Error(w, "an error occurred reading the file or directory", 500)
			return
		}
		defer f.Close()
		http.ServeContent(w, r, path.Base(mountPoint), fi.ModTime(), f)
		return
	}
	if !fi.IsDir() {
		http.Error(w, "not a file or directory", 500)
		return
	}
	if len(r.URL.Path) < len(mountPoint) && r.URL.Path+"/" == mountPoint {
		http.Redirect(w, r, mountPoint, http.StatusFound)
		return
	}

	var fs http.Handler = http.FileServer(http.Dir(fileOrDir))
	if mountPoint != "/" {
		fs = http.StripPrefix(strings.TrimSuffix(mountPoint, "/"), fs)
	}
	fs.ServeHTTP(&fixLocationHeaderResponseWriter{
		ResponseWriter: w,
		mountPoint:     mountPoint,
	}, r)
}

// fixLocationHeaderResponseWriter is an http.ResponseWriter wrapper that, upon
// flushing HTTP headers, prefixes any Location header with the mount point.
type fixLocationHeaderResponseWriter struct {
	http.ResponseWriter
	mountPoint string
	fixOnce    sync.Once // guards call to fix
}

func (w *fixLocationHeaderResponseWriter) fix() {
	h := w.ResponseWriter.Header()
	if v := h.Get("Location"); v != "" {
		h.Set("Location", w.mountPoint+v)
	}
}

func (w *fixLocationHeaderResponseWriter) WriteHeader(code int) {
	w.fixOnce.Do(w.fix)
	w.ResponseWriter.WriteHeader(code)
}

func (w *fixLocationHeaderResponseWriter) Write(p []byte) (int, error) {
	w.fixOnce.Do(w.fix)
	return w.ResponseWriter.Write(p)
}

// 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)
// * insecure TLS ("https+insecure://127.0.0.1:4430")
func expandProxyArg(s string) (targetURL string, insecureSkipVerify bool) {
	if s == "" {
		return "", false
	}
	if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
		return s, false
	}
	if rest, ok := strings.CutPrefix(s, "https+insecure://"); ok {
		return "https://" + rest, true
	}
	if allNumeric(s) {
		return "http://127.0.0.1:" + s, false
	}
	return "http://" + s, false
}

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(hostname string, port uint16) (c ipn.WebServerConfigView, ok bool) {
	key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port))

	b.mu.Lock()
	defer b.mu.Unlock()

	if !b.serveConfig.Valid() {
		return c, false
	}
	return b.serveConfig.FindWeb(key)
}

func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
	return func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
		if hi == nil || hi.ServerName == "" {
			return nil, errors.New("no SNI ServerName")
		}
		_, ok := b.webServerConfig(hi.ServerName, port)
		if !ok {
			return nil, errors.New("no webserver configured for name/port")
		}

		ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
		defer cancel()
		pair, err := b.GetCertPEM(ctx, hi.ServerName)
		if err != nil {
			return nil, err
		}
		cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
		if err != nil {
			return nil, err
		}
		return &cert, nil
	}
}
