diff --git a/cmd/tailscale/cli/serve_legacy.go b/cmd/tailscale/cli/serve_legacy.go index 6058f2ee0..de990c3b2 100644 --- a/cmd/tailscale/cli/serve_legacy.go +++ b/cmd/tailscale/cli/serve_legacy.go @@ -171,6 +171,7 @@ type serveEnv struct { // optional stuff for tests: testFlagOut io.Writer testStdout io.Writer + testStderr io.Writer } // getSelfDNSName returns the DNS name of the current node. @@ -681,13 +682,6 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error { return nil } -func (e *serveEnv) stdout() io.Writer { - if e.testStdout != nil { - return e.testStdout - } - return os.Stdout -} - func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.Status) error { dnsName := strings.TrimSuffix(st.Self.DNSName, ".") for p, h := range sc.TCP { diff --git a/cmd/tailscale/cli/serve_v2.go b/cmd/tailscale/cli/serve_v2.go index f4011280f..7c79defd8 100644 --- a/cmd/tailscale/cli/serve_v2.go +++ b/cmd/tailscale/cli/serve_v2.go @@ -156,26 +156,26 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command { } } -func validateArgs(subcmd serveMode, args []string) error { +func (e *serveEnv) validateArgs(subcmd serveMode, args []string) error { if translation, ok := isLegacyInvocation(subcmd, args); ok { - fmt.Fprint(os.Stderr, "Error: the CLI for serve and funnel has changed.") + fmt.Fprint(e.stderr(), "Error: the CLI for serve and funnel has changed.") if translation != "" { - fmt.Fprint(os.Stderr, " You can run the following command instead:\n") - fmt.Fprintf(os.Stderr, "\t- %s\n", translation) + fmt.Fprint(e.stderr(), " You can run the following command instead:\n") + fmt.Fprintf(e.stderr(), "\t- %s\n", translation) } - fmt.Fprint(os.Stderr, "\nPlease see https://tailscale.com/kb/1242/tailscale-serve for more information.\n") + fmt.Fprint(e.stderr(), "\nPlease see https://tailscale.com/kb/1242/tailscale-serve for more information.\n") return errHelpFunc(subcmd) } if len(args) == 0 { return flag.ErrHelp } if len(args) > 2 { - fmt.Fprintf(os.Stderr, "Error: invalid number of arguments (%d)\n", len(args)) + fmt.Fprintf(e.stderr(), "Error: invalid number of arguments (%d)\n", len(args)) return errHelpFunc(subcmd) } turnOff := args[len(args)-1] == "off" if len(args) == 2 && !turnOff { - fmt.Fprintln(os.Stderr, "Error: invalid argument format") + fmt.Fprintln(e.stderr(), "Error: invalid argument format") return errHelpFunc(subcmd) } @@ -203,7 +203,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc { return e.lc.SetServeConfig(ctx, sc) } - if err := validateArgs(subcmd, args); err != nil { + if err := e.validateArgs(subcmd, args); err != nil { return err } @@ -230,7 +230,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc { srvType, srvPort, err := srvTypeAndPortFromFlags(e) if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n\n", err) + fmt.Fprintf(e.stderr(), "error: %v\n\n", err) return errHelpFunc(subcmd) } @@ -300,19 +300,19 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc { msg = e.messageForPort(sc, st, dnsName, srvType, srvPort) } if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n\n", err) + fmt.Fprintf(e.stderr(), "error: %v\n\n", err) return errHelpFunc(subcmd) } if err := e.lc.SetServeConfig(ctx, parentSC); err != nil { if tailscale.IsPreconditionsFailedError(err) { - fmt.Fprintln(os.Stderr, "Another client is changing the serve config; please try again.") + fmt.Fprintln(e.stderr(), "Another client is changing the serve config; please try again.") } return err } if msg != "" { - fmt.Fprintln(os.Stderr, msg) + fmt.Fprintln(e.stdout(), msg) } if watcher != nil { @@ -621,7 +621,7 @@ func (e *serveEnv) applyFunnel(sc *ipn.ServeConfig, dnsName string, srvPort uint if allowFunnel { mak.Set(&sc.AllowFunnel, hp, true) } else if _, exists := sc.AllowFunnel[hp]; exists { - fmt.Printf("Removing Funnel for %s\n", hp) + fmt.Fprintf(e.stderr(), "Removing Funnel for %s\n", hp) delete(sc.AllowFunnel, hp) } } @@ -953,3 +953,17 @@ func (s serveType) String() string { return "unknownServeType" } } + +func (e *serveEnv) stdout() io.Writer { + if e.testStdout != nil { + return e.testStdout + } + return os.Stdout +} + +func (e *serveEnv) stderr() io.Writer { + if e.testStderr != nil { + return e.testStderr + } + return os.Stderr +} diff --git a/cmd/tailscale/cli/serve_v2_test.go b/cmd/tailscale/cli/serve_v2_test.go index 7b9ae4d32..a663548d0 100644 --- a/cmd/tailscale/cli/serve_v2_test.go +++ b/cmd/tailscale/cli/serve_v2_test.go @@ -6,808 +6,830 @@ package cli import ( "bytes" "context" + "encoding/json" "fmt" "os" "path/filepath" "reflect" - "runtime" "strconv" "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/peterbourgon/ff/v3/ffcli" "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" - "tailscale.com/types/logger" ) func TestServeDevConfigMutations(t *testing.T) { - // Stateful mutations, starting from an empty config. + // step is a stateful mutation within a group 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) + before func(t *testing.T) } - // 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 --bg --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"}, - }}, - }, - }, - }) + // group is a group of steps that share the same + // config mutation, but always starts from an empty config + type group struct { + name string + steps []step + } - // 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"}, - }}, - }, - }, - }) + // creaet a temporary directory for path-based destinations + td := t.TempDir() + writeFile := func(suffix, contents string) { + if err := os.WriteFile(filepath.Join(td, suffix), []byte(contents), 0600); err != nil { + t.Fatal(err) + } + } + writeFile("foo", "this is foo") + err := os.MkdirAll(filepath.Join(td, "subdir"), 0700) + if err != nil { + t.Fatal(err) + } + writeFile("subdir/file-a", "this is subdir") - // 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 --bg --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 --bg --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"}, - }}, - }, + groups := [...]group{ + { + name: "using_port_number", + steps: []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}, + }, + }}, }, - }) - add(step{ - command: cmd("serve --http=8080 --bg --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"}, - }}, - }, + { + name: "funnel_background", + steps: []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}, + }, + }}, }, - }) - - // // 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"}, - }}, - }, + { + name: "serve_background", + steps: []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{ // support non Funnel port - command: cmd("serve --https=9999 --bg --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"}, - }}, - }, + { + name: "set_path_bg", + steps: []step{{ + command: cmd("serve --set-path=/ --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{ - command: cmd("serve --https=9999 --bg --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"}, - }}, - }, + { + name: "http_listener", + steps: []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"}, + }}, + }, + }, + }}, }, - }) - add(step{ - command: cmd("serve --https=8443 --bg --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"}, - }}, - }, + { + name: "https_listener_valid_port", + steps: []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"}, + }}, + }, + }, + }}, }, - }) - 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"}, - }}, + { + name: "multiple_http_with_off", + steps: []step{ + { + 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"}, + }}, + }, + }, + }, + { // support non Funnel port + command: cmd("serve --bg --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"}, + }}, + }, + }, + }, + { // turn off one handler + command: cmd("serve --bg --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 another handler + command: cmd("serve --bg --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"}, + }}, + }, + }, + }, }, }, - }) - add(step{ - command: cmd("serve --https=443 --bg --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"}, - }}, - }, + { + name: "invalid_port_too_low", + steps: []step{{ + command: cmd("serve --https=443 --bg http://localhost:0"), // invalid port, too low + wantErr: anyErr(), + }}, }, - }) - 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"}, - }}, - }, + { + name: "invalid_port_too_high", + steps: []step{{ + command: cmd("serve --https=443 --bg http://localhost:65536"), // invalid port, too high + wantErr: anyErr(), + }}, }, - }) - add(step{ - command: cmd("serve --https=8443 --bg --set-path=/abc off"), - want: &ipn.ServeConfig{}, - }) - add(step{ // clean mount: "bar" becomes "/bar" - command: cmd("serve --https=443 --bg --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"}, - }}, - }, + { + name: "invalid_host", + steps: []step{{ + command: cmd("serve --https=443 --bg http://somehost:3000"), // invalid host + wantErr: anyErr(), + }}, }, - }) - // 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"}, - }}, - }, + { + name: "invalid_scheme", + steps: []step{{ + command: cmd("serve --https=443 --bg httpz://127.0.0.1"), // invalid scheme + wantErr: anyErr(), + }}, }, - }) - add(step{reset: true}) - add(step{ - command: cmd("serve --https=443 --bg --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"}, - }}, + { + name: "turn_off_https", + steps: []step{ + { + command: cmd("serve --bg --https=443 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"}, + }}, + }, + }, + }, + { + command: cmd("serve --bg --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"}, + }}, + }, + }, + }, + { + command: cmd("serve --bg --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"}, + }}, + }, + }, + }, + { + command: cmd("serve --bg --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{ // test a second handler on the same port - command: cmd("serve --https=8443 --bg --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"}, - }}, - }, + { + name: "https_text_bg", + steps: []step{{ + command: cmd("serve --bg --https=10000 text:hi"), + want: &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{10000: {HTTPS: true}}, + Web: map[ipn.HostPort]*ipn.WebServerConfig{ + "foo.test.ts.net:10000": {Handlers: map[string]*ipn.HTTPHandler{ + "/": {Text: "hi"}, + }}, + }, + }, + }}, }, - }) - 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"}, - }}, - }, + { + name: "handler_not_found", + steps: []step{{ + command: cmd("serve --https=443 --set-path=/foo off"), + want: nil, // nothing to save + wantErr: anyErr(), + }}, }, - }) - - // // tcp - add(step{reset: true}) - add(step{ // !somehost, must be localhost or 127.0.0.1 - command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:5432"), - wantErr: exactErrMsg(errHelp), - }) - add(step{ // bad target port, too low - command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:0"), - wantErr: exactErrMsg(errHelp), - }) - add(step{ // bad target port, too high - command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:65536"), - wantErr: exactErrMsg(errHelp), - }) - add(step{ // support shorthand - command: cmd("serve --tls-terminated-tcp=443 --bg 5432"), - want: &ipn.ServeConfig{ - TCP: map[uint16]*ipn.TCPPortHandler{ - 443: { - TCPForward: "127.0.0.1:5432", - TerminateTLS: "foo.test.ts.net", + { + name: "clean_mount", // "bar" becomes "/bar" + steps: []step{{ + command: cmd("serve --bg --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{reset: true}) - 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", + { + name: "serve_reset", + steps: []step{ + { + command: cmd("serve --bg --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 --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", + { + command: cmd("serve reset"), + want: &ipn.ServeConfig{}, }, }, }, - }) - 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", + { + name: "https_insecure", + steps: []step{{ + command: cmd("serve --bg --https=443 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{ - 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", + { + name: "two_ports_same_dest", + steps: []step{ + { + command: cmd("serve --bg --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{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", + { + command: cmd("serve --bg --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{ // 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 --bg --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: exactErrMsg(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"}, - }}, - }, + { + name: "path_in_dest", + steps: []step{{ + command: cmd("serve --bg --https=443 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"}, + }}, + }, + }, + }}, }, - }) - 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"}, - }}, - }, + { + name: "unknown_host_tcp", + steps: []step{{ + command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:5432"), + wantErr: exactErrMsg(errHelp), + }}, }, - }) - add(step{ // serving on secondary port doesn't change funnel on primary port - command: cmd("serve --https=8443 --bg --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"}, - }}, - }, + { + name: "tcp_port_too_low", + steps: []step{{ + command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:0"), + wantErr: exactErrMsg(errHelp), + }}, }, - }) - add(step{ // turn funnel on for secondary port - command: cmd("funnel --https=8443 --bg --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"}, - }}, - }, + { + name: "tcp_port_too_high", + steps: []step{{ + command: cmd("serve --tls-terminated-tcp=443 --bg tcp://somehost:65536"), + wantErr: exactErrMsg(errHelp), + }}, }, - }) - add(step{ // turn funnel off for primary port 443 - command: cmd("serve --bg localhost:3000"), - 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"}, - }}, - }, + { + name: "tcp_shorthand", + steps: []step{{ + command: cmd("serve --tls-terminated-tcp=443 --bg 5432"), + want: &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{ + 443: { + TCPForward: "127.0.0.1:5432", + TerminateTLS: "foo.test.ts.net", + }, + }, + }, + }}, }, - }) - add(step{ // remove secondary port - command: cmd("serve --https=8443 --set-path=/bar 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"}, - }}, + { + name: "tls_terminated_tcp", + steps: []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", + }, + }, + }, + }, + { + 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{ // start a tcp forwarder on 8443 - command: cmd("serve --bg --tcp=8443 tcp://localhost:5432"), - want: &ipn.ServeConfig{ - 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"}, - }}, + { + name: "tcp_off", + steps: []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", + }, + }, + }, + }, + { // handler doesn't exist + command: cmd("serve --tls-terminated-tcp=8443 off"), + wantErr: anyErr(), + }, + { + command: cmd("serve --tls-terminated-tcp=443 off"), + want: &ipn.ServeConfig{}, + }, }, }, - }) - add(step{ // remove primary port http handler - command: cmd("serve off"), - want: &ipn.ServeConfig{ - TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}}, + { + name: "text", + steps: []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"}, + }}, + }, + }, + }}, }, - }) - add(step{ // remove tcp forwarder - command: cmd("serve --tls-terminated-tcp=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 --bg --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/")}, - }}, + { + name: "path", + steps: []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")}, + }}, + }, + }, + }, + { + command: cmd("serve --bg --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{ // this should overwrite the previous one - command: cmd("serve --https=443 --bg --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")}, - }}, - }, + { + name: "bad_path", + steps: []step{{ + command: cmd("serve --bg --https=443 bad/path"), + wantErr: exactErrMsg(errHelp), + }}, }, - }) - add(step{reset: true}) // reset and do the opposite - add(step{ // a file without a trailing slash mount point - command: cmd("serve --https=443 --bg --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")}, - }}, + { + name: "path_off", + steps: []step{ + { + command: cmd("serve --bg --https=443 " + 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/")}, + }}, + }, + }, + }, + { + command: cmd("serve --bg --https=443 off"), + want: &ipn.ServeConfig{}, + }, }, }, - }) - add(step{ // this should overwrite the previous one - command: cmd("serve --https=443 --bg --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/")}, - }}, + { + name: "combos", + steps: []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"}, + }}, + }, + }, + }, + { // 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"}, + }}, + }, + }, + }, + { // serving on secondary port doesn't change funnel on primary port + command: cmd("serve --bg --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"}, + }}, + }, + }, + }, + { // turn funnel on for secondary port + command: cmd("funnel --bg --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"}, + }}, + }, + }, + }, + { // turn funnel off for primary port 443 + command: cmd("serve --bg localhost:3000"), + 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"}, + }}, + }, + }, + }, + { // remove secondary port + command: cmd("serve --bg --https=8443 --set-path=/bar 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"}, + }}, + }, + }, + }, + { // start a tcp forwarder on 8443 + command: cmd("serve --bg --tcp=8443 tcp://localhost:5432"), + want: &ipn.ServeConfig{ + 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"}, + }}, + }, + }, + }, + { // remove primary port http handler + command: cmd("serve off"), + want: &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}}, + }, + }, + { // remove tcp forwarder + command: cmd("serve --tls-terminated-tcp=8443 off"), + want: &ipn.ServeConfig{}, + }, }, }, - }) - - // // 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", + { + name: "tricky_steps", + steps: []step{ + { // a directory with a trailing slash mount point + command: cmd("serve --bg --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/")}, + }}, + }, + }, + }, + { // this should overwrite the previous one + command: cmd("serve --bg --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")}, + }}, + }, + }, + }, + { // reset and do opposite + command: cmd("serve reset"), + want: &ipn.ServeConfig{}, + }, + { // a file without a trailing slash mount point + command: cmd("serve --bg --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")}, + }}, + }, + }, + }, + { // this should overwrite the previous one + command: cmd("serve --bg --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{ // try to start a web handler on the same port - command: cmd("serve --https=443 --bg localhost:3000"), - wantErr: anyErr(), - }) - 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"}, - }}, + { + name: "cannot_override_tcp_with_http", + steps: []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", + }, + }, + }, + }, + { + command: cmd("serve --https=443 --bg localhost:3000"), + wantErr: anyErr(), + }, }, }, - }) - 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(), - }) - - add(step{ - command: cmd("serve reset"), - want: &ipn.ServeConfig{}, - }) - - // start two handlers and turn them off in one command - add(step{ - command: cmd("serve --https=4545 --set-path=/foo --bg localhost:3000"), - want: &ipn.ServeConfig{ - TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}}, - Web: map[ipn.HostPort]*ipn.WebServerConfig{ - "foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{ - "/foo": {Proxy: "http://127.0.0.1:3000"}, - }}, + { + name: "cannot_override_http_with_tcp", + steps: []step{ + { + 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"}, + }}, + }, + }, + }, + { // try to start a tcp forwarder on the same serve port + command: cmd("serve --tls-terminated-tcp=443 --bg tcp://localhost:5432"), + wantErr: anyErr(), + }, }, }, - }) - add(step{ - command: cmd("serve --https=4545 --set-path=/bar --bg localhost:3000"), - want: &ipn.ServeConfig{ - TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}}, - Web: map[ipn.HostPort]*ipn.WebServerConfig{ - "foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{ - "/foo": {Proxy: "http://127.0.0.1:3000"}, - "/bar": {Proxy: "http://127.0.0.1:3000"}, - }}, + { + name: "turn_off_multiple_handlers", + steps: []step{ + { + command: cmd("serve --https=4545 --set-path=/foo --bg localhost:3000"), + want: &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}}, + Web: map[ipn.HostPort]*ipn.WebServerConfig{ + "foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{ + "/foo": {Proxy: "http://127.0.0.1:3000"}, + }}, + }, + }, + }, + { + command: cmd("serve --https=4545 --set-path=/bar --bg localhost:3000"), + want: &ipn.ServeConfig{ + TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}}, + Web: map[ipn.HostPort]*ipn.WebServerConfig{ + "foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{ + "/foo": {Proxy: "http://127.0.0.1:3000"}, + "/bar": {Proxy: "http://127.0.0.1:3000"}, + }}, + }, + }, + }, + { + command: cmd("serve --https=4545 --bg --yes localhost:3000 off"), + want: &ipn.ServeConfig{}, + }, }, }, - }) - add(step{ - command: cmd("serve --https=4545 --bg --yes localhost:3000 off"), - want: &ipn.ServeConfig{}, - }) + } - 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) + for _, group := range groups { + t.Run(group.name, func(t *testing.T) { + lc := &fakeLocalServeClient{} + for i, st := range group.steps { + var stderr bytes.Buffer + var stdout bytes.Buffer + var flagOut bytes.Buffer + e := &serveEnv{ + lc: lc, + testFlagOut: &flagOut, + testStdout: &stdout, + testStderr: &stderr, + } + lastCount := lc.setCount + var cmd *ffcli.Command + var args []string - 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 = newServeV2Command(e, mode) + args = st.command[1:] - mode := serve - if st.command[0] == "funnel" { - mode = funnel - } - cmd = newServeV2Command(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: unexpected error: %v", i, err) + } + if bad := st.wantErr(err); bad != "" { + t.Fatalf("step #%d: unexpected error: %v", i, bad) + } + continue + } + if st.wantErr != nil { + t.Fatalf("step #%d: got success (saved=%v), but wanted an error", i, lc.config != nil) + } + var got *ipn.ServeConfig = nil + if lc.setCount > lastCount { + got = lc.config + } + if !reflect.DeepEqual(got, st.want) { + gotbts, _ := json.MarshalIndent(got, "", "\t") + wantbts, _ := json.MarshalIndent(st.want, "", "\t") + t.Fatalf("step: %d, cmd: %v, diff:\n%s", i, st.command, cmp.Diff(string(gotbts), string(wantbts))) - 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) - } + }) + } }