mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 11:05:45 +00:00
cmd/tailscale: add background mode to serve/funnel wip (#9202)
> **Note** > Behind the `TAILSCALE_FUNNEL_DEV` flag * Expose additional listeners through flags * Add a --bg flag to run in the background * --set-path to set a path for a specific target (assumes running in background) See the parent issue for more context. Updates #8489 Signed-off-by: Tyler Smalley <tyler@tailscale.com>
This commit is contained in:
parent
5ee349e075
commit
70a9854b39
@ -27,7 +27,7 @@
|
||||
// implementation of the tailscale funnel command.
|
||||
// See https://github.com/tailscale/tailscale/issues/7844
|
||||
if envknob.UseWIPCode() {
|
||||
return newServeDevCommand(se, "funnel")
|
||||
return newServeDevCommand(se, funnel)
|
||||
}
|
||||
return newFunnelCommand(se)
|
||||
}
|
||||
|
@ -39,7 +39,7 @@
|
||||
// implementation of the tailscale funnel command.
|
||||
// See https://github.com/tailscale/tailscale/issues/7844
|
||||
if envknob.UseWIPCode() {
|
||||
return newServeDevCommand(se, "serve")
|
||||
return newServeDevCommand(se, serve)
|
||||
}
|
||||
return newServeCommand(se)
|
||||
}
|
||||
@ -158,9 +158,18 @@ type localServeClient interface {
|
||||
//
|
||||
// It also contains the flags, as registered with newServeCommand.
|
||||
type serveEnv struct {
|
||||
// flags
|
||||
// v1 flags
|
||||
json bool // output JSON (status only for now)
|
||||
|
||||
// v2 specific flags
|
||||
bg bool // background mode
|
||||
setPath string // serve path
|
||||
https string // HTTP port
|
||||
http string // HTTP port
|
||||
tcp string // TCP port
|
||||
tlsTerminatedTcp string // a TLS terminated TCP port
|
||||
subcmd serveMode // subcommand
|
||||
|
||||
lc localServeClient // localClient interface, specific to serve
|
||||
|
||||
// optional stuff for tests:
|
||||
|
@ -9,61 +9,110 @@
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
type execFunc func(ctx context.Context, args []string) error
|
||||
|
||||
type commandInfo struct {
|
||||
Name string
|
||||
ShortHelp string
|
||||
LongHelp string
|
||||
}
|
||||
|
||||
var infoMap = map[string]commandInfo{
|
||||
"serve": {
|
||||
var serveHelpCommon = strings.TrimSpace(`
|
||||
<target> can be a port number (e.g., 3000), a partial URL (e.g., localhost:3000), or a
|
||||
full URL including a path (e.g., http://localhost:3000/foo, https+insecure://localhost:3000/foo).
|
||||
|
||||
EXAMPLES
|
||||
- Mount a local web server at 127.0.0.1:3000 in the foreground:
|
||||
$ tailscale %s localhost:3000
|
||||
|
||||
- Mount a local web server at 127.0.0.1:3000 in the background:
|
||||
$ tailscale %s --bg localhost:3000
|
||||
|
||||
For more examples and use cases visit our docs site https://tailscale.com/kb/1247/funnel-serve-use-cases
|
||||
`)
|
||||
|
||||
type serveMode int
|
||||
|
||||
const (
|
||||
serve serveMode = iota
|
||||
funnel
|
||||
)
|
||||
|
||||
var infoMap = map[serveMode]commandInfo{
|
||||
serve: {
|
||||
Name: "serve",
|
||||
ShortHelp: "Serve content and local servers on your tailnet",
|
||||
LongHelp: strings.Join([]string{
|
||||
"Serve lets you share a local server securely within your tailnet.",
|
||||
`To share a local server on the internet, use "tailscale funnel"`,
|
||||
"Serve enables you to share a local server securely within your tailnet.\n",
|
||||
"To share a local server on the internet, use `tailscale funnel`\n\n",
|
||||
}, "\n"),
|
||||
},
|
||||
"funnel": {
|
||||
funnel: {
|
||||
Name: "funnel",
|
||||
ShortHelp: "Serve content and local servers on the internet",
|
||||
LongHelp: strings.Join([]string{
|
||||
"Funnel lets you share a local server on the internet using Tailscale.",
|
||||
`To share only within your tailnet, use "tailscale serve"`,
|
||||
"Funnel enables you to share a local server on the internet using Tailscale.\n",
|
||||
"To share only within your tailnet, use `tailscale serve`\n\n",
|
||||
}, "\n"),
|
||||
},
|
||||
}
|
||||
|
||||
func buildShortUsage(subcmd string) string {
|
||||
return strings.Join([]string{
|
||||
subcmd + " [flags] <target> [off]",
|
||||
subcmd + " status [--json]",
|
||||
subcmd + " reset",
|
||||
}, "\n ")
|
||||
}
|
||||
|
||||
// newServeDevCommand returns a new "serve" subcommand using e as its environment.
|
||||
func newServeDevCommand(e *serveEnv, subcmd string) *ffcli.Command {
|
||||
if subcmd != "serve" && subcmd != "funnel" {
|
||||
func newServeDevCommand(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
if subcmd != serve && subcmd != funnel {
|
||||
log.Fatalf("newServeDevCommand called with unknown subcmd %q", subcmd)
|
||||
}
|
||||
|
||||
info := infoMap[subcmd]
|
||||
|
||||
return &ffcli.Command{
|
||||
Name: subcmd,
|
||||
Name: info.Name,
|
||||
ShortHelp: info.ShortHelp,
|
||||
ShortUsage: strings.Join([]string{
|
||||
fmt.Sprintf("%s <target>", subcmd),
|
||||
fmt.Sprintf("%s status [--json]", subcmd),
|
||||
fmt.Sprintf("%s reset", subcmd),
|
||||
fmt.Sprintf("%s <target>", info.Name),
|
||||
fmt.Sprintf("%s status [--json]", info.Name),
|
||||
fmt.Sprintf("%s reset", info.Name),
|
||||
}, "\n "),
|
||||
LongHelp: info.LongHelp,
|
||||
Exec: e.runServeDev(subcmd == "funnel"),
|
||||
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), subcmd, subcmd),
|
||||
Exec: e.runServeCombined(subcmd),
|
||||
|
||||
FlagSet: e.newFlags("serve-set", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.bg, "bg", false, "run the command in the background")
|
||||
fs.StringVar(&e.setPath, "set-path", "", "set a path for a specific target and run in the background")
|
||||
fs.StringVar(&e.https, "https", "", "default; HTTPS listener")
|
||||
fs.StringVar(&e.http, "http", "", "HTTP listener")
|
||||
fs.StringVar(&e.tcp, "tcp", "", "TCP listener")
|
||||
fs.StringVar(&e.tlsTerminatedTcp, "tls-terminated-tcp", "", "TLS terminated TCP listener")
|
||||
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
Subcommands: []*ffcli.Command{
|
||||
// TODO(tyler+marwan-at-work) Implement set, unset, and logs subcommands
|
||||
{
|
||||
Name: "status",
|
||||
Exec: e.runServeStatus,
|
||||
@ -84,52 +133,97 @@ func newServeDevCommand(e *serveEnv, subcmd string) *ffcli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
// runServeDev is the entry point for the "tailscale {serve,funnel}" commands.
|
||||
func (e *serveEnv) runServeDev(funnel bool) execFunc {
|
||||
// runServeCombined is the entry point for the "tailscale {serve,funnel}" commands.
|
||||
func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
e.subcmd = subcmd
|
||||
|
||||
return func(ctx context.Context, args []string) error {
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
defer cancel()
|
||||
if len(args) != 1 {
|
||||
if len(args) == 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
var source string
|
||||
port64, err := strconv.ParseUint(args[0], 10, 16)
|
||||
if err == nil {
|
||||
source = fmt.Sprintf("http://127.0.0.1:%d", port64)
|
||||
} else {
|
||||
source, err = expandProxyTarget(args[0])
|
||||
}
|
||||
|
||||
funnel := subcmd == funnel
|
||||
|
||||
err := checkLegacyServeInvocation(subcmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
fmt.Fprintf(os.Stderr, "error: the CLI for serve and funnel has changed.\n")
|
||||
fmt.Fprintf(os.Stderr, "Please see https://tailscale.com/kb/1242/tailscale-serve for more information.\n\n")
|
||||
|
||||
return errHelp
|
||||
}
|
||||
|
||||
if len(args) > 2 {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments (%d)\n\n", len(args))
|
||||
return errHelp
|
||||
}
|
||||
|
||||
turnOff := "off" == args[len(args)-1]
|
||||
|
||||
// support passing in a port number as the target
|
||||
// TODO(tylersmalley) move to expandProxyTarget when we remove the legacy serve invocation
|
||||
target := args[0]
|
||||
port, err := strconv.ParseUint(args[0], 10, 16)
|
||||
if err == nil {
|
||||
target = fmt.Sprintf("http://127.0.0.1:%d", port)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
|
||||
if funnel {
|
||||
// verify node has funnel capabilities
|
||||
if err := e.verifyFunnelEnabled(ctx, st, 443); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// default mount point to "/"
|
||||
mount := e.setPath
|
||||
if mount == "" {
|
||||
mount = "/"
|
||||
}
|
||||
|
||||
if e.bg || turnOff || e.setPath != "" {
|
||||
srvType, srvPort, err := srvTypeAndPortFromFlags(e)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n\n", err)
|
||||
return errHelp
|
||||
}
|
||||
|
||||
if turnOff {
|
||||
err := e.unsetServe(ctx, srvType, srvPort, mount)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n\n", err)
|
||||
return errHelp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err = e.setServe(ctx, st, srvType, srvPort, mount, target, funnel)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n\n", err)
|
||||
return errHelp
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
hp := ipn.HostPort(dnsName + ":443") // TODO(marwan-at-work): support the 2 other ports
|
||||
|
||||
// In the streaming case, the process stays running in the
|
||||
// foreground and prints out connections to the HostPort.
|
||||
//
|
||||
// The local backend handles updating the ServeConfig as
|
||||
// necessary, then restores it to its original state once
|
||||
// the process's context is closed or the client turns off
|
||||
// Tailscale.
|
||||
// TODO(tyler+marwan-at-work) support flag to run in the background
|
||||
// TODO(marwan-at-work): combine this with the above setServe code.
|
||||
// Foreground and background should be the same, we just pass
|
||||
// a foreground config instead of the top level background one.
|
||||
return e.streamServe(ctx, ipn.ServeStreamRequest{
|
||||
Funnel: funnel,
|
||||
HostPort: hp,
|
||||
Source: source,
|
||||
MountPoint: "/", // TODO(marwan-at-work): support multiple mount points
|
||||
Source: target,
|
||||
MountPoint: mount,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -188,3 +282,431 @@ func setHandler(sc *ipn.ServeConfig, req ipn.ServeStreamRequest, sessionID strin
|
||||
})
|
||||
mak.Set(&fconf.AllowFunnel, req.HostPort, true)
|
||||
}
|
||||
|
||||
func (e *serveEnv) setServe(ctx context.Context, st *ipnstate.Status, srvType string, srvPort uint16, mount string, target string, allowFunnel bool) error {
|
||||
if srvType == "https" {
|
||||
// Running serve with https requires that the tailnet has enabled
|
||||
// https cert provisioning. Send users through an interactive flow
|
||||
// to enable this if not already done.
|
||||
//
|
||||
// TODO(sonia,tailscale/corp#10577): The interactive feature flow
|
||||
// is behind a control flag. If the tailnet doesn't have the flag
|
||||
// on, enableFeatureInteractive will error. For now, we hide that
|
||||
// error and maintain the previous behavior (prior to 2023-08-15)
|
||||
// of letting them edit the serve config before enabling certs.
|
||||
e.enableFeatureInteractive(ctx, "serve", func(caps []string) bool {
|
||||
return slices.Contains(caps, tailcfg.CapabilityHTTPS)
|
||||
})
|
||||
}
|
||||
|
||||
// get serve config
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dnsName, err := e.getSelfDNSName(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// nil if no config
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
|
||||
// update serve config based on the type
|
||||
switch srvType {
|
||||
case "https", "http":
|
||||
mount, err := cleanMountPoint(mount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clean the mount point: %w", err)
|
||||
}
|
||||
useTLS := srvType == "https"
|
||||
err = e.applyWebServe(sc, dnsName, srvPort, useTLS, mount, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed apply web serve: %w", err)
|
||||
}
|
||||
case "tcp", "tls-terminated-tcp":
|
||||
err = e.applyTCPServe(sc, dnsName, srvType, srvPort, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply TCP serve: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid type %q", srvType)
|
||||
}
|
||||
|
||||
// update the serve config based on if funnel is enabled
|
||||
e.applyFunnel(sc, dnsName, srvPort, allowFunnel)
|
||||
|
||||
// persist the serve config changes
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// notify the user of the change
|
||||
m, err := e.messageForPort(ctx, sc, st, dnsName, srvPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, m)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) messageForPort(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvPort uint16) (string, error) {
|
||||
var output strings.Builder
|
||||
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
|
||||
if sc.AllowFunnel[hp] == true {
|
||||
output.WriteString("Available on the internet:\n")
|
||||
} else {
|
||||
output.WriteString("Available within your tailnet:\n")
|
||||
}
|
||||
|
||||
scheme := "https"
|
||||
if sc.IsServingHTTP(srvPort) {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
portPart := ":" + fmt.Sprint(srvPort)
|
||||
if scheme == "http" && srvPort == 80 ||
|
||||
scheme == "https" && srvPort == 443 {
|
||||
portPart = ""
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s\n\n", scheme, dnsName, portPart))
|
||||
|
||||
srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) {
|
||||
switch {
|
||||
case h.Path != "":
|
||||
return "path", h.Path
|
||||
case h.Proxy != "":
|
||||
return "proxy", h.Proxy
|
||||
case h.Text != "":
|
||||
return "text", "\"" + elipticallyTruncate(h.Text, 20) + "\""
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
if sc.Web[hp] != nil {
|
||||
var mounts []string
|
||||
|
||||
for k := range sc.Web[hp].Handlers {
|
||||
mounts = append(mounts, k)
|
||||
}
|
||||
sort.Slice(mounts, func(i, j int) bool {
|
||||
return len(mounts[i]) < len(mounts[j])
|
||||
})
|
||||
maxLen := len(mounts[len(mounts)-1])
|
||||
|
||||
for _, m := range mounts {
|
||||
h := sc.Web[hp].Handlers[m]
|
||||
t, d := srvTypeAndDesc(h)
|
||||
output.WriteString(fmt.Sprintf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d))
|
||||
}
|
||||
} else if sc.TCP[srvPort] != nil {
|
||||
h := sc.TCP[srvPort]
|
||||
|
||||
tlsStatus := "TLS over TCP"
|
||||
if h.TerminateTLS != "" {
|
||||
tlsStatus = "TLS terminated"
|
||||
}
|
||||
|
||||
output.WriteString(fmt.Sprintf("|-- tcp://%s (%s)\n", hp, tlsStatus))
|
||||
for _, a := range st.TailscaleIPs {
|
||||
ipp := net.JoinHostPort(a.String(), strconv.Itoa(int(srvPort)))
|
||||
output.WriteString(fmt.Sprintf("|-- tcp://%s\n", ipp))
|
||||
}
|
||||
output.WriteString(fmt.Sprintf("|--> tcp://%s\n", h.TCPForward))
|
||||
}
|
||||
|
||||
output.WriteString("\nServe started and running in the background.\n")
|
||||
output.WriteString(fmt.Sprintf("To disable the proxy, run: tailscale %s off", infoMap[e.subcmd].Name))
|
||||
|
||||
return output.String(), nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, useTLS bool, mount, target string) error {
|
||||
h := new(ipn.HTTPHandler)
|
||||
|
||||
// TODO: use strings.Cut as the prefix OR use strings.HasPrefix
|
||||
ts, _, _ := strings.Cut(target, ":")
|
||||
switch {
|
||||
case ts == "text":
|
||||
text := strings.TrimPrefix(target, "text:")
|
||||
if text == "" {
|
||||
return errors.New("unable to serve; text cannot be an empty string")
|
||||
}
|
||||
h.Text = text
|
||||
case isProxyTarget(target):
|
||||
t, err := expandProxyTarget(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Proxy = t
|
||||
default: // assume path
|
||||
if version.IsSandboxedMacOS() {
|
||||
// don't allow path serving for now on macOS (2022-11-15)
|
||||
return errors.New("path serving is not supported if sandboxed on macOS")
|
||||
}
|
||||
if !filepath.IsAbs(target) {
|
||||
return errors.New("path must be absolute")
|
||||
}
|
||||
target = filepath.Clean(target)
|
||||
fi, err := os.Stat(target)
|
||||
if err != nil {
|
||||
return errors.New("invalid path")
|
||||
}
|
||||
|
||||
// TODO: need to understand this further
|
||||
if fi.IsDir() && !strings.HasSuffix(mount, "/") {
|
||||
// dir mount points must end in /
|
||||
// for relative file links to work
|
||||
mount += "/"
|
||||
}
|
||||
h.Path = target
|
||||
}
|
||||
|
||||
// TODO: validation needs to check nested foreground configs
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
return errors.New("cannot serve web; already serving TCP")
|
||||
}
|
||||
|
||||
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
|
||||
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
if _, ok := sc.Web[hp]; !ok {
|
||||
mak.Set(&sc.Web, hp, new(ipn.WebServerConfig))
|
||||
}
|
||||
mak.Set(&sc.Web[hp].Handlers, mount, h)
|
||||
|
||||
// TODO: handle multiple web handlers from foreground mode
|
||||
for k, v := range sc.Web[hp].Handlers {
|
||||
if v == h {
|
||||
continue
|
||||
}
|
||||
// If the new mount point ends in / and another mount point
|
||||
// shares the same prefix, remove the other handler.
|
||||
// (e.g. /foo/ overwrites /foo)
|
||||
// The opposite example is also handled.
|
||||
m1 := strings.TrimSuffix(mount, "/")
|
||||
m2 := strings.TrimSuffix(k, "/")
|
||||
if m1 == m2 {
|
||||
delete(sc.Web[hp].Handlers, k)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType string, srcPort uint16, target string) error {
|
||||
var terminateTLS bool
|
||||
switch srcType {
|
||||
case "tcp":
|
||||
terminateTLS = false
|
||||
case "tls-terminated-tcp":
|
||||
terminateTLS = true
|
||||
default:
|
||||
return fmt.Errorf("invalid TCP target %q", target)
|
||||
}
|
||||
|
||||
dstURL, err := url.Parse(target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid TCP target %q: %v", target, err)
|
||||
}
|
||||
host, dstPortStr, err := net.SplitHostPort(dstURL.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid TCP target %q: %v", target, err)
|
||||
}
|
||||
|
||||
switch host {
|
||||
case "localhost", "127.0.0.1":
|
||||
// ok
|
||||
default:
|
||||
return fmt.Errorf("invalid TCP target %q, must be one of localhost or 127.0.0.1", target)
|
||||
}
|
||||
|
||||
if p, err := strconv.ParseUint(dstPortStr, 10, 16); p == 0 || err != nil {
|
||||
return fmt.Errorf("invalid port %q", dstPortStr)
|
||||
}
|
||||
|
||||
fwdAddr := "127.0.0.1:" + dstPortStr
|
||||
|
||||
// TODO: needs to account for multiple configs from foreground mode
|
||||
if sc.IsServingWeb(srcPort) {
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||
}
|
||||
|
||||
mak.Set(&sc.TCP, srcPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
|
||||
|
||||
if terminateTLS {
|
||||
sc.TCP[srcPort].TerminateTLS = dnsName
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) applyFunnel(sc *ipn.ServeConfig, dnsName string, srvPort uint16, allowFunnel bool) {
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
|
||||
// TODO: Should we return an error? Should not be possible.
|
||||
// nil if no config
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
|
||||
// TODO: should ensure there is no other conflicting funnel
|
||||
// TODO: add error handling for if toggling for existing sc
|
||||
if allowFunnel {
|
||||
mak.Set(&sc.AllowFunnel, hp, true)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(tylersmalley) Refactor into setServe so handleWebServeFunnelRemove and handleTCPServeRemove.
|
||||
// apply serve config changes and we print a status message.
|
||||
func (e *serveEnv) unsetServe(ctx context.Context, srvType string, srvPort uint16, mount string) error {
|
||||
switch srvType {
|
||||
case "https", "http":
|
||||
mount, err := cleanMountPoint(mount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clean the mount point: %w", err)
|
||||
}
|
||||
err = e.handleWebServeFunnelRemove(ctx, srvPort, mount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
case "tcp", "tls-terminated-tcp":
|
||||
// TODO(tylersmalley) should remove funnel
|
||||
return e.removeTCPServe(ctx, srvPort)
|
||||
default:
|
||||
return fmt.Errorf("invalid type %q", srvType)
|
||||
}
|
||||
}
|
||||
|
||||
func srvTypeAndPortFromFlags(e *serveEnv) (srvType string, srvPort uint16, err error) {
|
||||
sourceMap := map[string]string{
|
||||
"http": e.http,
|
||||
"https": e.https,
|
||||
"tcp": e.tcp,
|
||||
"tls-terminated-tcp": e.tlsTerminatedTcp,
|
||||
}
|
||||
|
||||
var srcTypeCount int
|
||||
var srcValue string
|
||||
|
||||
for k, v := range sourceMap {
|
||||
if v != "" {
|
||||
srcTypeCount++
|
||||
srvType = k
|
||||
srcValue = v
|
||||
}
|
||||
}
|
||||
|
||||
if srcTypeCount > 1 {
|
||||
return "", 0, fmt.Errorf("cannot serve multiple types for a single mount point")
|
||||
} else if srcTypeCount == 0 {
|
||||
srvType = "https"
|
||||
srcValue = "443"
|
||||
}
|
||||
|
||||
srvPort, err = parseServePort(srcValue)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("invalid port %q: %w", srcValue, err)
|
||||
}
|
||||
|
||||
return srvType, srvPort, nil
|
||||
}
|
||||
|
||||
func checkLegacyServeInvocation(subcmd serveMode, args []string) error {
|
||||
if subcmd == serve && len(args) == 2 {
|
||||
prefixes := []string{"http:", "https:", "tls:", "tls-terminated-tcp:"}
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(args[0], prefix) {
|
||||
return errors.New("invalid invocation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleWebServeFunnelRemove removes a web handler from the serve config
|
||||
// and removes funnel if no remaining mounts exist for the serve port.
|
||||
// The srvPort argument is the serving port and the mount argument is
|
||||
// the mount point or registered path to remove.
|
||||
// TODO(tylersmalley): fork of handleWebServeRemove, return name once dev work is merged
|
||||
func (e *serveEnv) handleWebServeFunnelRemove(ctx context.Context, srvPort uint16, mount string) error {
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sc == nil {
|
||||
return errors.New("error: serve config does not exist")
|
||||
}
|
||||
dnsName, err := e.getSelfDNSName(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
return errors.New("cannot remove web handler; currently serving TCP")
|
||||
}
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
if !sc.WebHandlerExists(hp, mount) {
|
||||
return errors.New("error: handler does not exist")
|
||||
}
|
||||
// delete existing handler, then cascade delete if empty
|
||||
delete(sc.Web[hp].Handlers, mount)
|
||||
if len(sc.Web[hp].Handlers) == 0 {
|
||||
delete(sc.Web, hp)
|
||||
delete(sc.TCP, srvPort)
|
||||
}
|
||||
// clear empty maps mostly for testing
|
||||
if len(sc.Web) == 0 {
|
||||
sc.Web = nil
|
||||
}
|
||||
if len(sc.TCP) == 0 {
|
||||
sc.TCP = nil
|
||||
}
|
||||
|
||||
// disable funnel if no remaining mounts exist for the serve port
|
||||
if sc.Web == nil && sc.TCP == nil {
|
||||
delete(sc.AllowFunnel, hp)
|
||||
}
|
||||
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeTCPServe removes the TCP forwarding configuration for the
|
||||
// given srvPort, or serving port.
|
||||
func (e *serveEnv) removeTCPServe(ctx context.Context, src uint16) error {
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc := cursc.Clone() // nil if no config
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
if sc.IsServingWeb(src) {
|
||||
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
|
||||
}
|
||||
if ph := sc.GetTCPPortHandler(src); ph != nil {
|
||||
delete(sc.TCP, src)
|
||||
// clear map mostly for testing
|
||||
if len(sc.TCP) == 0 {
|
||||
sc.TCP = nil
|
||||
}
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
return errors.New("error: serve config does not exist")
|
||||
}
|
||||
|
848
cmd/tailscale/cli/serve_dev_test.go
Normal file
848
cmd/tailscale/cli/serve_dev_test.go
Normal file
@ -0,0 +1,848 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func TestServeDevConfigMutations(t *testing.T) {
|
||||
// Stateful mutations, starting from an empty config.
|
||||
type step struct {
|
||||
command []string // serve args; nil means no command to run (only reset)
|
||||
reset bool // if true, reset all ServeConfig state
|
||||
want *ipn.ServeConfig // non-nil means we want a save of this value
|
||||
wantErr func(error) (badErrMsg string) // nil means no error is wanted
|
||||
line int // line number of addStep call, for error messages
|
||||
|
||||
debugBreak func()
|
||||
}
|
||||
var steps []step
|
||||
add := func(s step) {
|
||||
_, _, s.line, _ = runtime.Caller(1)
|
||||
steps = append(steps, s)
|
||||
}
|
||||
|
||||
// using port number
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("funnel --bg 3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||
},
|
||||
})
|
||||
|
||||
// funnel background
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("funnel --bg localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||
},
|
||||
})
|
||||
|
||||
// serve background
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("serve --bg localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// --set-path runs in background
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("serve --set-path=/ localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// using http listener
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("serve --bg --http=80 localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// using https listener with a valid port
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("serve --bg --https=8443 localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// https
|
||||
add(step{reset: true})
|
||||
add(step{ // allow omitting port (default to 80)
|
||||
command: cmd("serve --http=80 --bg http://localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // support non Funnel port
|
||||
command: cmd("serve --http=9999 --set-path=/abc http://localhost:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 9999: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --http=9999 --set-path=/abc off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --http=8080 --set-path=/abc http://127.0.0.1:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 8080: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8080": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// // https
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --bg http://localhost:0"), // invalid port, too low
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --bg http://localhost:65536"), // invalid port, too high
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --bg http://somehost:3000"), // invalid host
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --bg httpz://127.0.0.1"), // invalid scheme
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{ // allow omitting port (default to 443)
|
||||
command: cmd("serve --https=443 --bg http://localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // support non Funnel port
|
||||
command: cmd("serve --https=9999 --set-path=/abc http://localhost:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 9999: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=9999 --set-path=/abc off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=8443 --set-path=/abc http://127.0.0.1:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=10000 --bg text:hi"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {HTTPS: true}, 8443: {HTTPS: true}, 10000: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
"foo.test.ts.net:10000": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Text: "hi"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --set-path=/foo off"),
|
||||
want: nil, // nothing to save
|
||||
wantErr: anyErr(),
|
||||
}) // handler doesn't exist, so we get an error
|
||||
add(step{
|
||||
command: cmd("serve --https=10000 off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=8443 --set-path=/abc off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
add(step{ // clean mount: "bar" becomes "/bar"
|
||||
command: cmd("serve --https=443 --set-path=bar https://127.0.0.1:8443"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/bar": {Proxy: "https://127.0.0.1:8443"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
// add(step{
|
||||
// command: cmd("serve --https=443 --set-path=bar https://127.0.0.1:8443"),
|
||||
// want: nil, // nothing to save
|
||||
// })
|
||||
add(step{ // try resetting using reset command
|
||||
command: cmd("serve reset"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --bg https+insecure://127.0.0.1:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "https+insecure://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --set-path=/foo localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // test a second handler on the same port
|
||||
command: cmd("serve --https=8443 --set-path=/foo localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{ // support path in proxy
|
||||
command: cmd("serve --https=443 --bg http://127.0.0.1:3000/foo/bar"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000/foo/bar"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// // tcp
|
||||
add(step{reset: true})
|
||||
add(step{ // must include scheme for tcp
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg localhost:5432"),
|
||||
wantErr: exactErr(errHelp, "errHelp"),
|
||||
})
|
||||
add(step{ // !somehost, must be localhost or 127.0.0.1
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:5432"),
|
||||
wantErr: exactErr(errHelp, "errHelp"),
|
||||
})
|
||||
add(step{ // bad target port, too low
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:0"),
|
||||
wantErr: exactErr(errHelp, "errHelp"),
|
||||
})
|
||||
add(step{ // bad target port, too high
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:65536"),
|
||||
wantErr: exactErr(errHelp, "errHelp"),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://127.0.0.1:8443"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:8443",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// add(step{
|
||||
// command: cmd("serve --tls-terminated-tcp=443 --bg tcp://127.0.0.1:8443"),
|
||||
// want: nil, // nothing to save
|
||||
// })
|
||||
add(step{
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:8444"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:8444",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://127.0.0.1:8445"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:8445",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:123"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:123",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // handler doesn't exist, so we get an error
|
||||
command: cmd("serve --tls-terminated-tcp=8443 off"),
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --tls-terminated-tcp=443 off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
|
||||
// // text
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --bg text:hello"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Text: "hello"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// path
|
||||
td := t.TempDir()
|
||||
writeFile := func(suffix, contents string) {
|
||||
if err := os.WriteFile(filepath.Join(td, suffix), []byte(contents), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
add(step{reset: true})
|
||||
writeFile("foo", "this is foo")
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --bg " + filepath.Join(td, "foo")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Path: filepath.Join(td, "foo")},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
os.MkdirAll(filepath.Join(td, "subdir"), 0700)
|
||||
writeFile("subdir/file-a", "this is A")
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --set-path=/some/where " + filepath.Join(td, "subdir/file-a")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Path: filepath.Join(td, "foo")},
|
||||
"/some/where": {Path: filepath.Join(td, "subdir/file-a")},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // bad path
|
||||
command: cmd("serve --https=443 --bg bad/path"),
|
||||
wantErr: exactErr(errHelp, "errHelp"),
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 --bg " + filepath.Join(td, "subdir")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Path: filepath.Join(td, "subdir/")},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("serve --https=443 off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
|
||||
// // combos
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("serve --bg localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // enable funnel for primary port
|
||||
command: cmd("funnel --bg localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // serving on secondary port doesn't change funnel on primary port
|
||||
command: cmd("serve --https=8443 --set-path=/bar localhost:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // turn funnel on for secondary port
|
||||
command: cmd("funnel --https=8443 --set-path=/bar localhost:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true, "foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
// TODO(tylersmalley) resolve these failures
|
||||
// add(step{ // turn funnel off for primary port 443
|
||||
// command: cmd("serve --https=443 --set-path=/bar localhost:3001"),
|
||||
// want: &ipn.ServeConfig{
|
||||
// AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
// TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
// Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
// "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
// "/": {Proxy: "http://127.0.0.1:3000"},
|
||||
// }},
|
||||
// "foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
// "/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||
// }},
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// add(step{ // remove secondary port
|
||||
// command: cmd("https:8443 /bar off"),
|
||||
// want: &ipn.ServeConfig{
|
||||
// AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
// TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
// Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
// "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
// "/": {Proxy: "http://127.0.0.1:3000"},
|
||||
// }},
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// add(step{ // start a tcp forwarder on 8443
|
||||
// command: cmd("tcp:8443 tcp://localhost:5432"),
|
||||
// want: &ipn.ServeConfig{
|
||||
// AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
// TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
|
||||
// Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
// "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
// "/": {Proxy: "http://127.0.0.1:3000"},
|
||||
// }},
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// add(step{ // remove primary port http handler
|
||||
// command: cmd("https:443 / off"),
|
||||
// want: &ipn.ServeConfig{
|
||||
// AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
// TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
|
||||
// },
|
||||
// })
|
||||
// add(step{ // remove tcp forwarder
|
||||
// command: cmd("tls-terminated-tcp:8443 off"),
|
||||
// want: &ipn.ServeConfig{
|
||||
// AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
// },
|
||||
// })
|
||||
// add(step{ // turn off funnel
|
||||
// command: cmd("funnel 8443 off"),
|
||||
// want: &ipn.ServeConfig{},
|
||||
// })
|
||||
|
||||
// // tricky steps
|
||||
add(step{reset: true})
|
||||
add(step{ // a directory with a trailing slash mount point
|
||||
command: cmd("serve --https=443 --set-path=/dir " + filepath.Join(td, "subdir")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/dir/": {Path: filepath.Join(td, "subdir/")},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // this should overwrite the previous one
|
||||
command: cmd("serve --https=443 --set-path=/dir " + filepath.Join(td, "foo")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/dir": {Path: filepath.Join(td, "foo")},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{reset: true}) // reset and do the opposite
|
||||
add(step{ // a file without a trailing slash mount point
|
||||
command: cmd("serve --https=443 --set-path=/dir " + filepath.Join(td, "foo")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/dir": {Path: filepath.Join(td, "foo")},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // this should overwrite the previous one
|
||||
command: cmd("serve --https=443 --set-path=/dir " + filepath.Join(td, "subdir")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/dir/": {Path: filepath.Join(td, "subdir/")},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// // error states
|
||||
add(step{reset: true})
|
||||
add(step{ // tcp forward 5432 on serve port 443
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // try to start a web handler on the same port
|
||||
command: cmd("serve --https=443 --bg localhost:3000"),
|
||||
wantErr: exactErr(errHelp, "errHelp"),
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{ // start a web handler on port 443
|
||||
command: cmd("serve --https=443 --bg localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // try to start a tcp forwarder on the same serve port
|
||||
command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:5432"),
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
|
||||
lc := &fakeLocalServeClient{}
|
||||
// And now run the steps above.
|
||||
for i, st := range steps {
|
||||
if st.debugBreak != nil {
|
||||
st.debugBreak()
|
||||
}
|
||||
if st.reset {
|
||||
t.Logf("Executing step #%d, line %v: [reset]", i, st.line)
|
||||
lc.config = nil
|
||||
}
|
||||
if st.command == nil {
|
||||
continue
|
||||
}
|
||||
t.Logf("Executing step #%d, line %v: %q ... ", i, st.line, st.command)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var flagOut bytes.Buffer
|
||||
e := &serveEnv{
|
||||
lc: lc,
|
||||
testFlagOut: &flagOut,
|
||||
testStdout: &stdout,
|
||||
}
|
||||
lastCount := lc.setCount
|
||||
var cmd *ffcli.Command
|
||||
var args []string
|
||||
|
||||
mode := serve
|
||||
if st.command[0] == "funnel" {
|
||||
mode = funnel
|
||||
}
|
||||
cmd = newServeDevCommand(e, mode)
|
||||
args = st.command[1:]
|
||||
|
||||
err := cmd.ParseAndRun(context.Background(), args)
|
||||
if flagOut.Len() > 0 {
|
||||
t.Logf("flag package output: %q", flagOut.Bytes())
|
||||
}
|
||||
if err != nil {
|
||||
if st.wantErr == nil {
|
||||
t.Fatalf("step #%d, line %v: unexpected error: %v", i, st.line, err)
|
||||
}
|
||||
if bad := st.wantErr(err); bad != "" {
|
||||
t.Fatalf("step #%d, line %v: unexpected error: %v", i, st.line, bad)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if st.wantErr != nil {
|
||||
t.Fatalf("step #%d, line %v: got success (saved=%v), but wanted an error", i, st.line, lc.config != nil)
|
||||
}
|
||||
var got *ipn.ServeConfig = nil
|
||||
if lc.setCount > lastCount {
|
||||
got = lc.config
|
||||
}
|
||||
if !reflect.DeepEqual(got, st.want) {
|
||||
t.Fatalf("[%d] %v: bad state. got:\n%v\n\nwant:\n%v\n",
|
||||
i, st.command, logger.AsJSON(got), logger.AsJSON(st.want))
|
||||
// NOTE: asJSON will omit empty fields, which might make
|
||||
// result in bad state got/want diffs being the same, even
|
||||
// though the actual state is different. Use below to debug:
|
||||
// t.Fatalf("[%d] %v: bad state. got:\n%+v\n\nwant:\n%+v\n",
|
||||
// i, st.command, got, st.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSrcTypeFromFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env *serveEnv
|
||||
expectedType string
|
||||
expectedPort uint16
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "only http set",
|
||||
env: &serveEnv{http: "80"},
|
||||
expectedType: "http",
|
||||
expectedPort: 80,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "only https set",
|
||||
env: &serveEnv{https: "10000"},
|
||||
expectedType: "https",
|
||||
expectedPort: 10000,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "only tcp set",
|
||||
env: &serveEnv{tcp: "8000"},
|
||||
expectedType: "tcp",
|
||||
expectedPort: 8000,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "only tls-terminated-tcp set",
|
||||
env: &serveEnv{tlsTerminatedTcp: "8080"},
|
||||
expectedType: "tls-terminated-tcp",
|
||||
expectedPort: 8080,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "defaults to https, port 443",
|
||||
env: &serveEnv{},
|
||||
expectedType: "https",
|
||||
expectedPort: 443,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple types set",
|
||||
env: &serveEnv{http: "80", https: "443"},
|
||||
expectedType: "",
|
||||
expectedPort: 0,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
srcType, srcPort, err := srvTypeAndPortFromFlags(tt.env)
|
||||
if (err != nil) != tt.expectedErr {
|
||||
t.Errorf("Expected error: %v, got: %v", tt.expectedErr, err)
|
||||
}
|
||||
if srcType != tt.expectedType {
|
||||
t.Errorf("Expected srcType: %s, got: %s", tt.expectedType, srcType)
|
||||
}
|
||||
if srcPort != tt.expectedPort {
|
||||
t.Errorf("Expected srcPort: %d, got: %d", tt.expectedPort, srcPort)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user