diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 5457d2fad..450ee197c 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -564,7 +564,8 @@ func runDebugComponentLogs(ctx context.Context, args []string) error { func runDevStoreSet(ctx context.Context, args []string) error { // TODO(bradfitz): remove this temporary (2022-11-09) hack once // profile stuff and serving CLI commands are more fleshed out. - if len(args) >= 1 && strings.HasPrefix(args[0], "_serve/") { + isServe := len(args) >= 1 && strings.HasPrefix(args[0], "_serve/") + if isServe { st, err := localClient.StatusWithoutPeers(ctx) if err != nil { return err @@ -584,6 +585,11 @@ func runDevStoreSet(ctx context.Context, args []string) error { if err != nil { return err } + if isServe { + if err := json.Unmarshal(valb, new(ipn.ServeConfig)); err != nil { + return fmt.Errorf("invalid JSON: %w", err) + } + } val = string(valb) } return localClient.SetDevStoreKeyValue(ctx, key, val) diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 3a2374a40..48df748b4 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -118,6 +118,7 @@ func (src *HTTPHandler) Clone() *HTTPHandler { var _HTTPHandlerCloneNeedsRegeneration = HTTPHandler(struct { Path string Proxy string + Text string }{}) // Clone makes a deep copy of WebServerConfig. diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index 56444edbc..634158341 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -285,11 +285,13 @@ func (v *HTTPHandlerView) UnmarshalJSON(b []byte) error { func (v HTTPHandlerView) Path() string { return v.ж.Path } func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy } +func (v HTTPHandlerView) Text() string { return v.ж.Text } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _HTTPHandlerViewNeedsRegeneration = HTTPHandler(struct { Path string Proxy string + Text string }{}) // View returns a readonly view of WebServerConfig. diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 3e09a650f..66e15e12b 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -6,7 +6,6 @@ import ( "context" - "crypto/tls" "encoding/base64" "encoding/json" "errors" @@ -4086,41 +4085,3 @@ func (b *LocalBackend) SetDevStateStore(key, value string) error { func (b *LocalBackend) ShouldInterceptTCPPort(port uint16) bool { return b.shouldInterceptTCPPortAtomic.Load()(port) } - -var runDevWebServer = envknob.RegisterBool("TS_DEV_WEBSERVER") - -func (b *LocalBackend) HandleInterceptedTCPConn(c net.Conn) { - if !runDevWebServer() { - b.logf("localbackend: closing TCP conn from %v to %v", c.RemoteAddr(), c.LocalAddr()) - c.Close() - return - } - - // TODO(bradfitz): look up how; sniff SNI if ambiguous - hs := &http.Server{ - TLSConfig: &tls.Config{ - GetCertificate: b.getTLSServeCert, - }, - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, "

hello world

this is tailscaled") - }), - } - hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "") -} - -func (b *LocalBackend) getTLSServeCert(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { - if hi == nil || hi.ServerName == "" { - return nil, errors.New("no SNI ServerName") - } - 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 -} diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go new file mode 100644 index 000000000..ff1cb1741 --- /dev/null +++ b/ipn/ipnlocal/serve.go @@ -0,0 +1,107 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ipnlocal + +import ( + "context" + "crypto/tls" + "errors" + "io" + "net" + "net/http" + pathpkg "path" + "time" + + "tailscale.com/envknob" + "tailscale.com/ipn" + "tailscale.com/net/netutil" +) + +var runDevWebServer = envknob.RegisterBool("TS_DEV_WEBSERVER") + +func (b *LocalBackend) HandleInterceptedTCPConn(c net.Conn) { + if !runDevWebServer() { + b.logf("localbackend: closing TCP conn from %v to %v", c.RemoteAddr(), c.LocalAddr()) + c.Close() + return + } + + // TODO(bradfitz): look up how; sniff SNI if ambiguous + hs := &http.Server{ + TLSConfig: &tls.Config{ + GetCertificate: b.getTLSServeCert, + }, + Handler: http.HandlerFunc(b.serveWebHandler), + } + hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "") +} + +func (b *LocalBackend) getServeHandler(r *http.Request) (_ *ipn.HTTPHandler, ok bool) { + if r.TLS == nil { + return nil, false + } + + sni := r.TLS.ServerName + port := "443" // TODO(bradfitz): fix + key := ipn.HostPort(net.JoinHostPort(sni, port)) + + b.mu.Lock() + defer b.mu.Unlock() + + wsc, ok := b.serveConfig.Web[key] + if !ok { + return nil, false + } + path := r.URL.Path + for { + if h, ok := wsc.Handlers[path]; ok { + return h, true + } + if path == "/" { + return nil, false + } + path = pathpkg.Dir(path) + } +} + +func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) { + h, 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 != "" { + io.WriteString(w, "TODO(bradfitz): serve file") + return + } + if v := h.Proxy; v != "" { + io.WriteString(w, "TODO(bradfitz): proxy") + return + } + + http.Error(w, "empty handler", 500) +} + +func (b *LocalBackend) getTLSServeCert(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { + if hi == nil || hi.ServerName == "" { + return nil, errors.New("no SNI ServerName") + } + 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 +} diff --git a/ipn/store.go b/ipn/store.go index dff2b2ee4..85cf0a8e5 100644 --- a/ipn/store.go +++ b/ipn/store.go @@ -121,6 +121,8 @@ type HTTPHandler struct { Path string `json:",omitempty"` // absolute path to directory or file to serve Proxy string `json:",omitempty"` // http://localhost:3000/, localhost:3030, 3030 + Text string `json:",omitempty"` // plaintext to serve (primarily for testing) + // TODO(bradfitz): bool to not enumerate directories? TTL on mapping for // temporary ones? Error codes? Redirects? }