mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +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.
|
// implementation of the tailscale funnel command.
|
||||||
// See https://github.com/tailscale/tailscale/issues/7844
|
// See https://github.com/tailscale/tailscale/issues/7844
|
||||||
if envknob.UseWIPCode() {
|
if envknob.UseWIPCode() {
|
||||||
return newServeDevCommand(se, "funnel")
|
return newServeDevCommand(se, funnel)
|
||||||
}
|
}
|
||||||
return newFunnelCommand(se)
|
return newFunnelCommand(se)
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
// implementation of the tailscale funnel command.
|
// implementation of the tailscale funnel command.
|
||||||
// See https://github.com/tailscale/tailscale/issues/7844
|
// See https://github.com/tailscale/tailscale/issues/7844
|
||||||
if envknob.UseWIPCode() {
|
if envknob.UseWIPCode() {
|
||||||
return newServeDevCommand(se, "serve")
|
return newServeDevCommand(se, serve)
|
||||||
}
|
}
|
||||||
return newServeCommand(se)
|
return newServeCommand(se)
|
||||||
}
|
}
|
||||||
@ -158,9 +158,18 @@ type localServeClient interface {
|
|||||||
//
|
//
|
||||||
// It also contains the flags, as registered with newServeCommand.
|
// It also contains the flags, as registered with newServeCommand.
|
||||||
type serveEnv struct {
|
type serveEnv struct {
|
||||||
// flags
|
// v1 flags
|
||||||
json bool // output JSON (status only for now)
|
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
|
lc localServeClient // localClient interface, specific to serve
|
||||||
|
|
||||||
// optional stuff for tests:
|
// optional stuff for tests:
|
||||||
|
@ -9,61 +9,110 @@
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/ipn/ipnstate"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
|
"tailscale.com/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
type execFunc func(ctx context.Context, args []string) error
|
type execFunc func(ctx context.Context, args []string) error
|
||||||
|
|
||||||
type commandInfo struct {
|
type commandInfo struct {
|
||||||
|
Name string
|
||||||
ShortHelp string
|
ShortHelp string
|
||||||
LongHelp string
|
LongHelp string
|
||||||
}
|
}
|
||||||
|
|
||||||
var infoMap = map[string]commandInfo{
|
var serveHelpCommon = strings.TrimSpace(`
|
||||||
"serve": {
|
<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",
|
ShortHelp: "Serve content and local servers on your tailnet",
|
||||||
LongHelp: strings.Join([]string{
|
LongHelp: strings.Join([]string{
|
||||||
"Serve lets you share a local server securely within your tailnet.",
|
"Serve enables you to share a local server securely within your tailnet.\n",
|
||||||
`To share a local server on the internet, use "tailscale funnel"`,
|
"To share a local server on the internet, use `tailscale funnel`\n\n",
|
||||||
}, "\n"),
|
}, "\n"),
|
||||||
},
|
},
|
||||||
"funnel": {
|
funnel: {
|
||||||
|
Name: "funnel",
|
||||||
ShortHelp: "Serve content and local servers on the internet",
|
ShortHelp: "Serve content and local servers on the internet",
|
||||||
LongHelp: strings.Join([]string{
|
LongHelp: strings.Join([]string{
|
||||||
"Funnel lets you share a local server on the internet using Tailscale.",
|
"Funnel enables you to share a local server on the internet using Tailscale.\n",
|
||||||
`To share only within your tailnet, use "tailscale serve"`,
|
"To share only within your tailnet, use `tailscale serve`\n\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.
|
// newServeDevCommand returns a new "serve" subcommand using e as its environment.
|
||||||
func newServeDevCommand(e *serveEnv, subcmd string) *ffcli.Command {
|
func newServeDevCommand(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||||
if subcmd != "serve" && subcmd != "funnel" {
|
if subcmd != serve && subcmd != funnel {
|
||||||
log.Fatalf("newServeDevCommand called with unknown subcmd %q", subcmd)
|
log.Fatalf("newServeDevCommand called with unknown subcmd %q", subcmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
info := infoMap[subcmd]
|
info := infoMap[subcmd]
|
||||||
|
|
||||||
return &ffcli.Command{
|
return &ffcli.Command{
|
||||||
Name: subcmd,
|
Name: info.Name,
|
||||||
ShortHelp: info.ShortHelp,
|
ShortHelp: info.ShortHelp,
|
||||||
ShortUsage: strings.Join([]string{
|
ShortUsage: strings.Join([]string{
|
||||||
fmt.Sprintf("%s <target>", subcmd),
|
fmt.Sprintf("%s <target>", info.Name),
|
||||||
fmt.Sprintf("%s status [--json]", subcmd),
|
fmt.Sprintf("%s status [--json]", info.Name),
|
||||||
fmt.Sprintf("%s reset", subcmd),
|
fmt.Sprintf("%s reset", info.Name),
|
||||||
}, "\n "),
|
}, "\n "),
|
||||||
LongHelp: info.LongHelp,
|
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), subcmd, subcmd),
|
||||||
Exec: e.runServeDev(subcmd == "funnel"),
|
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,
|
UsageFunc: usageFunc,
|
||||||
Subcommands: []*ffcli.Command{
|
Subcommands: []*ffcli.Command{
|
||||||
// TODO(tyler+marwan-at-work) Implement set, unset, and logs subcommands
|
|
||||||
{
|
{
|
||||||
Name: "status",
|
Name: "status",
|
||||||
Exec: e.runServeStatus,
|
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.
|
// runServeCombined is the entry point for the "tailscale {serve,funnel}" commands.
|
||||||
func (e *serveEnv) runServeDev(funnel bool) execFunc {
|
func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||||
|
e.subcmd = subcmd
|
||||||
|
|
||||||
return func(ctx context.Context, args []string) error {
|
return func(ctx context.Context, args []string) error {
|
||||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
if len(args) == 0 {
|
||||||
defer cancel()
|
|
||||||
if len(args) != 1 {
|
|
||||||
return flag.ErrHelp
|
return flag.ErrHelp
|
||||||
}
|
}
|
||||||
var source string
|
|
||||||
port64, err := strconv.ParseUint(args[0], 10, 16)
|
funnel := subcmd == funnel
|
||||||
if err == nil {
|
|
||||||
source = fmt.Sprintf("http://127.0.0.1:%d", port64)
|
err := checkLegacyServeInvocation(subcmd, args)
|
||||||
} else {
|
|
||||||
source, err = expandProxyTarget(args[0])
|
|
||||||
}
|
|
||||||
if err != nil {
|
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)
|
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getting client status: %w", err)
|
return fmt.Errorf("getting client status: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if funnel {
|
if funnel {
|
||||||
|
// verify node has funnel capabilities
|
||||||
if err := e.verifyFunnelEnabled(ctx, st, 443); err != nil {
|
if err := e.verifyFunnelEnabled(ctx, st, 443); err != nil {
|
||||||
return err
|
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, ".")
|
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||||
hp := ipn.HostPort(dnsName + ":443") // TODO(marwan-at-work): support the 2 other ports
|
hp := ipn.HostPort(dnsName + ":443") // TODO(marwan-at-work): support the 2 other ports
|
||||||
|
|
||||||
// In the streaming case, the process stays running in the
|
// TODO(marwan-at-work): combine this with the above setServe code.
|
||||||
// foreground and prints out connections to the HostPort.
|
// Foreground and background should be the same, we just pass
|
||||||
//
|
// a foreground config instead of the top level background one.
|
||||||
// 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
|
|
||||||
return e.streamServe(ctx, ipn.ServeStreamRequest{
|
return e.streamServe(ctx, ipn.ServeStreamRequest{
|
||||||
Funnel: funnel,
|
Funnel: funnel,
|
||||||
HostPort: hp,
|
HostPort: hp,
|
||||||
Source: source,
|
Source: target,
|
||||||
MountPoint: "/", // TODO(marwan-at-work): support multiple mount points
|
MountPoint: mount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,3 +282,431 @@ func setHandler(sc *ipn.ServeConfig, req ipn.ServeStreamRequest, sessionID strin
|
|||||||
})
|
})
|
||||||
mak.Set(&fconf.AllowFunnel, req.HostPort, true)
|
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