mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-18 02:48:40 +00:00
cmd/tailscale: funnel wip cleanup and additional test coverage (#9316)
General cleanup and additional test coverage of WIP code. * use enum for serveType * combine instances of ServeConfig access within unset * cleanMountPoint rewritten into cleanURLPath as it only handles URL paths * refactor and test expandProxyTargetDev > **Note** > Behind the `TAILSCALE_USE_WIP_CODE` flag updates #8489 Signed-off-by: Tyler Smalley <tyler@tailscale.com>
This commit is contained in:
parent
3c276d7de2
commit
82c1dd8732
@ -167,7 +167,7 @@ type serveEnv struct {
|
||||
https string // HTTP port
|
||||
http string // HTTP port
|
||||
tcp string // TCP port
|
||||
tlsTerminatedTcp string // a TLS terminated TCP port
|
||||
tlsTerminatedTCP string // a TLS terminated TCP port
|
||||
subcmd serveMode // subcommand
|
||||
|
||||
lc localServeClient // localClient interface, specific to serve
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
@ -57,6 +58,15 @@ const (
|
||||
funnel
|
||||
)
|
||||
|
||||
type serveType int
|
||||
|
||||
const (
|
||||
serveTypeHTTPS serveType = iota
|
||||
serveTypeHTTP
|
||||
serveTypeTCP
|
||||
serveTypeTLSTerminatedTCP
|
||||
)
|
||||
|
||||
var infoMap = map[serveMode]commandInfo{
|
||||
serve: {
|
||||
Name: "serve",
|
||||
@ -100,7 +110,7 @@ func newServeDevCommand(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
fmt.Sprintf("%s status [--json]", info.Name),
|
||||
fmt.Sprintf("%s reset", info.Name),
|
||||
}, "\n "),
|
||||
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), subcmd, subcmd),
|
||||
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), info.Name, info.Name),
|
||||
Exec: e.runServeCombined(subcmd),
|
||||
|
||||
FlagSet: e.newFlags("serve-set", func(fs *flag.FlagSet) {
|
||||
@ -109,7 +119,7 @@ func newServeDevCommand(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
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")
|
||||
fs.StringVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", "", "TLS terminated TCP listener")
|
||||
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
@ -134,38 +144,30 @@ func newServeDevCommand(e *serveEnv, subcmd serveMode) *ffcli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func validateArgs(subcmd serveMode, args []string) error {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return flag.ErrHelp
|
||||
case 1, 2:
|
||||
if isLegacyInvocation(subcmd, args) {
|
||||
fmt.Fprintf(os.Stderr, "error: the CLI for serve and funnel has changed.")
|
||||
fmt.Fprintf(os.Stderr, "Please see https://tailscale.com/kb/1242/tailscale-serve for more information.")
|
||||
return errHelp
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments (%d)", len(args))
|
||||
return errHelp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runServeCombined is the entry point for the "tailscale {serve,funnel}" commands.
|
||||
func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
e.subcmd = subcmd
|
||||
|
||||
return func(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
funnel := subcmd == funnel
|
||||
|
||||
err := checkLegacyServeInvocation(subcmd, args)
|
||||
if err != nil {
|
||||
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)
|
||||
if err := validateArgs(subcmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
@ -176,6 +178,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
|
||||
funnel := subcmd == funnel
|
||||
if funnel {
|
||||
// verify node has funnel capabilities
|
||||
if err := e.verifyFunnelEnabled(ctx, st, 443); err != nil {
|
||||
@ -183,10 +186,9 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// default mount point to "/"
|
||||
mount := e.setPath
|
||||
if mount == "" {
|
||||
mount = "/"
|
||||
mount, err := cleanURLPath(e.setPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clean the mount point: %w", err)
|
||||
}
|
||||
|
||||
if e.setPath != "" {
|
||||
@ -220,7 +222,8 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
// foreground or background.
|
||||
parentSC := sc
|
||||
|
||||
if !turnOff && srvType == "https" {
|
||||
turnOff := "off" == args[len(args)-1]
|
||||
if !turnOff && srvType == serveTypeHTTPS {
|
||||
// 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.
|
||||
@ -263,7 +266,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
if turnOff {
|
||||
err = e.unsetServe(sc, dnsName, srvType, srvPort, mount)
|
||||
} else {
|
||||
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, target, funnel)
|
||||
err = e.setServe(sc, st, dnsName, srvType, srvPort, mount, args[0], funnel)
|
||||
msg = e.messageForPort(sc, st, dnsName, srvPort)
|
||||
}
|
||||
if err != nil {
|
||||
@ -275,7 +278,9 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, msg)
|
||||
if msg != "" {
|
||||
fmt.Fprintln(os.Stderr, msg)
|
||||
}
|
||||
|
||||
if watcher != nil {
|
||||
for {
|
||||
@ -293,20 +298,16 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName, srvType string, srvPort uint16, mount string, target string, allowFunnel bool) error {
|
||||
func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool) error {
|
||||
// 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)
|
||||
case serveTypeHTTPS, serveTypeHTTP:
|
||||
useTLS := srvType == serveTypeHTTPS
|
||||
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":
|
||||
case serveTypeTCP, serveTypeTLSTerminatedTCP:
|
||||
err := e.applyTCPServe(sc, dnsName, srvType, srvPort, target)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply TCP serve: %w", err)
|
||||
@ -321,6 +322,8 @@ func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName, s
|
||||
return nil
|
||||
}
|
||||
|
||||
// messageForPort returns a message for the given port based on the
|
||||
// serve config and status.
|
||||
func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvPort uint16) string {
|
||||
var output strings.Builder
|
||||
|
||||
@ -345,6 +348,11 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
|
||||
output.WriteString(fmt.Sprintf("%s://%s%s\n\n", scheme, dnsName, portPart))
|
||||
|
||||
if !e.bg {
|
||||
output.WriteString("Press Ctrl+C to exit.")
|
||||
return output.String()
|
||||
}
|
||||
|
||||
srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) {
|
||||
switch {
|
||||
case h.Path != "":
|
||||
@ -389,42 +397,28 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN
|
||||
output.WriteString(fmt.Sprintf("|--> tcp://%s\n", h.TCPForward))
|
||||
}
|
||||
|
||||
if e.bg {
|
||||
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))
|
||||
} else {
|
||||
// TODO(marwan-at-work): give the user more context on their foreground process.
|
||||
}
|
||||
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() + "\n"
|
||||
return output.String()
|
||||
}
|
||||
|
||||
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":
|
||||
case strings.HasPrefix(target, "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
|
||||
case filepath.IsAbs(target):
|
||||
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 {
|
||||
@ -438,6 +432,12 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
|
||||
mount += "/"
|
||||
}
|
||||
h.Path = target
|
||||
default:
|
||||
t, err := expandProxyTargetDev(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Proxy = t
|
||||
}
|
||||
|
||||
// TODO: validation needs to check nested foreground configs
|
||||
@ -472,12 +472,12 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType string, srcPort uint16, target string) error {
|
||||
func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType serveType, srcPort uint16, target string) error {
|
||||
var terminateTLS bool
|
||||
switch srcType {
|
||||
case "tcp":
|
||||
case serveTypeTCP:
|
||||
terminateTLS = false
|
||||
case "tls-terminated-tcp":
|
||||
case serveTypeTLSTerminatedTCP:
|
||||
terminateTLS = true
|
||||
default:
|
||||
return fmt.Errorf("invalid TCP target %q", target)
|
||||
@ -535,35 +535,34 @@ func (e *serveEnv) applyFunnel(sc *ipn.ServeConfig, dnsName string, srvPort uint
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(tylersmalley) Refactor into setServe so handleWebServeFunnelRemove and handleTCPServeRemove.
|
||||
// apply serve config changes and we print a status message.
|
||||
func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, dnsName string, srvType string, srvPort uint16, mount string) error {
|
||||
// unsetServe removes the serve config for the given serve port.
|
||||
func (e *serveEnv) unsetServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string) error {
|
||||
switch srvType {
|
||||
case "https", "http":
|
||||
mount, err := cleanMountPoint(mount)
|
||||
case serveTypeHTTPS, serveTypeHTTP:
|
||||
err := e.removeWebServe(sc, dnsName, srvPort, mount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clean the mount point: %w", err)
|
||||
return fmt.Errorf("failed to remove web serve: %w", err)
|
||||
}
|
||||
err = e.handleWebServeFunnelRemove(sc, dnsName, srvPort, mount)
|
||||
case serveTypeTCP, serveTypeTLSTerminatedTCP:
|
||||
err := e.removeTCPServe(sc, srvPort)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to remove TCP serve: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
case "tcp", "tls-terminated-tcp":
|
||||
// TODO(tylersmalley) should remove funnel
|
||||
return e.removeTCPServe(sc, srvPort)
|
||||
default:
|
||||
return fmt.Errorf("invalid type %q", srvType)
|
||||
}
|
||||
|
||||
// TODO(tylersmalley): remove funnel
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
func srvTypeAndPortFromFlags(e *serveEnv) (srvType serveType, srvPort uint16, err error) {
|
||||
sourceMap := map[serveType]string{
|
||||
serveTypeHTTP: e.http,
|
||||
serveTypeHTTPS: e.https,
|
||||
serveTypeTCP: e.tcp,
|
||||
serveTypeTLSTerminatedTCP: e.tlsTerminatedTCP,
|
||||
}
|
||||
|
||||
var srcTypeCount int
|
||||
@ -578,60 +577,60 @@ func srvTypeAndPortFromFlags(e *serveEnv) (srvType string, srvPort uint16, err e
|
||||
}
|
||||
|
||||
if srcTypeCount > 1 {
|
||||
return "", 0, fmt.Errorf("cannot serve multiple types for a single mount point")
|
||||
return 0, 0, fmt.Errorf("cannot serve multiple types for a single mount point")
|
||||
} else if srcTypeCount == 0 {
|
||||
srvType = "https"
|
||||
srvType = serveTypeHTTPS
|
||||
srcValue = "443"
|
||||
}
|
||||
|
||||
srvPort, err = parseServePort(srcValue)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("invalid port %q: %w", srcValue, err)
|
||||
return 0, 0, fmt.Errorf("invalid port %q: %w", srcValue, err)
|
||||
}
|
||||
|
||||
return srvType, srvPort, nil
|
||||
}
|
||||
|
||||
func checkLegacyServeInvocation(subcmd serveMode, args []string) error {
|
||||
func isLegacyInvocation(subcmd serveMode, args []string) bool {
|
||||
if subcmd == serve && len(args) == 2 {
|
||||
prefixes := []string{"http:", "https:", "tls:", "tls-terminated-tcp:"}
|
||||
prefixes := []string{"http", "https", "tcp", "tls-terminated-tcp"}
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(args[0], prefix) {
|
||||
return errors.New("invalid invocation")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return false
|
||||
}
|
||||
|
||||
// handleWebServeFunnelRemove removes a web handler from the serve config
|
||||
// removeWebServe 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(sc *ipn.ServeConfig, dnsName string, srvPort uint16, mount string) error {
|
||||
if sc == nil {
|
||||
return errors.New("error: serve config does not exist")
|
||||
}
|
||||
func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, mount string) error {
|
||||
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
|
||||
}
|
||||
@ -663,3 +662,94 @@ func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, src uint16) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// expandProxyTargetDev expands the supported target values to be proxied
|
||||
// allowing for input values to be a port number, a partial URL, or a full URL
|
||||
// including a path.
|
||||
//
|
||||
// examples:
|
||||
// - 3000
|
||||
// - localhost:3000
|
||||
// - http://localhost:3000
|
||||
// - https://localhost:3000
|
||||
// - https-insecure://localhost:3000
|
||||
// - https-insecure://localhost:3000/foo
|
||||
func expandProxyTargetDev(target string) (string, error) {
|
||||
var (
|
||||
scheme = "http"
|
||||
host = "127.0.0.1"
|
||||
)
|
||||
|
||||
// support target being a port number
|
||||
if port, err := strconv.ParseUint(target, 10, 16); err == nil {
|
||||
return fmt.Sprintf("%s://%s:%d", scheme, host, port), nil
|
||||
}
|
||||
|
||||
// prepend scheme if not present
|
||||
if !strings.Contains(target, "://") {
|
||||
target = scheme + "://" + target
|
||||
}
|
||||
|
||||
// make sure we can parse the target
|
||||
u, err := url.ParseRequestURI(target)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid URL %w", err)
|
||||
}
|
||||
|
||||
// ensure a supported scheme
|
||||
switch u.Scheme {
|
||||
case "http", "https", "https+insecure":
|
||||
default:
|
||||
return "", errors.New("must be a URL starting with http://, https://, or https+insecure://")
|
||||
}
|
||||
|
||||
// validate the port
|
||||
port, err := strconv.ParseUint(u.Port(), 10, 16)
|
||||
if err != nil || port == 0 {
|
||||
return "", fmt.Errorf("invalid port %q", u.Port())
|
||||
}
|
||||
|
||||
// validate the host.
|
||||
switch u.Hostname() {
|
||||
case "localhost", "127.0.0.1":
|
||||
u.Host = fmt.Sprintf("%s:%d", host, port)
|
||||
default:
|
||||
return "", errors.New("only localhost or 127.0.0.1 proxies are currently supported")
|
||||
}
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// cleanURLPath ensures the path is clean and has a leading "/".
|
||||
func cleanURLPath(urlPath string) (string, error) {
|
||||
if urlPath == "" {
|
||||
return "/", nil
|
||||
}
|
||||
|
||||
// TODO(tylersmalley) verify still needed with path being a flag
|
||||
urlPath = cleanMinGWPathConversionIfNeeded(urlPath)
|
||||
if !strings.HasPrefix(urlPath, "/") {
|
||||
urlPath = "/" + urlPath
|
||||
}
|
||||
|
||||
c := path.Clean(urlPath)
|
||||
if urlPath == c || urlPath == c+"/" {
|
||||
return urlPath, nil
|
||||
}
|
||||
return "", fmt.Errorf("invalid mount point %q", urlPath)
|
||||
}
|
||||
|
||||
func (s serveType) String() string {
|
||||
switch s {
|
||||
case serveTypeHTTP:
|
||||
return "httpListener"
|
||||
case serveTypeHTTPS:
|
||||
return "httpsListener"
|
||||
case serveTypeTCP:
|
||||
return "tcpListener"
|
||||
case serveTypeTLSTerminatedTCP:
|
||||
return "tlsTerminatedTCPListener"
|
||||
default:
|
||||
return "unknownServeType"
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,12 @@ package cli
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
@ -783,49 +785,48 @@ func TestSrcTypeFromFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env *serveEnv
|
||||
expectedType string
|
||||
expectedType serveType
|
||||
expectedPort uint16
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "only http set",
|
||||
env: &serveEnv{http: "80"},
|
||||
expectedType: "http",
|
||||
expectedType: serveTypeHTTP,
|
||||
expectedPort: 80,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "only https set",
|
||||
env: &serveEnv{https: "10000"},
|
||||
expectedType: "https",
|
||||
expectedType: serveTypeHTTPS,
|
||||
expectedPort: 10000,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "only tcp set",
|
||||
env: &serveEnv{tcp: "8000"},
|
||||
expectedType: "tcp",
|
||||
expectedType: serveTypeTCP,
|
||||
expectedPort: 8000,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "only tls-terminated-tcp set",
|
||||
env: &serveEnv{tlsTerminatedTcp: "8080"},
|
||||
expectedType: "tls-terminated-tcp",
|
||||
env: &serveEnv{tlsTerminatedTCP: "8080"},
|
||||
expectedType: serveTypeTLSTerminatedTCP,
|
||||
expectedPort: 8080,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "defaults to https, port 443",
|
||||
env: &serveEnv{},
|
||||
expectedType: "https",
|
||||
expectedType: serveTypeHTTPS,
|
||||
expectedPort: 443,
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple types set",
|
||||
env: &serveEnv{http: "80", https: "443"},
|
||||
expectedType: "",
|
||||
expectedPort: 0,
|
||||
expectedErr: true,
|
||||
},
|
||||
@ -838,7 +839,7 @@ func TestSrcTypeFromFlags(t *testing.T) {
|
||||
t.Errorf("Expected error: %v, got: %v", tt.expectedErr, err)
|
||||
}
|
||||
if srcType != tt.expectedType {
|
||||
t.Errorf("Expected srcType: %s, got: %s", tt.expectedType, srcType)
|
||||
t.Errorf("Expected srcType: %s, got: %s", tt.expectedType.String(), srcType)
|
||||
}
|
||||
if srcPort != tt.expectedPort {
|
||||
t.Errorf("Expected srcPort: %d, got: %d", tt.expectedPort, srcPort)
|
||||
@ -846,3 +847,109 @@ func TestSrcTypeFromFlags(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandProxyTargetDev(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
wantErr bool
|
||||
}{
|
||||
{input: "8080", expected: "http://127.0.0.1:8080"},
|
||||
{input: "localhost:8080", expected: "http://127.0.0.1:8080"},
|
||||
{input: "http://localhost:8080", expected: "http://127.0.0.1:8080"},
|
||||
{input: "http://127.0.0.1:8080", expected: "http://127.0.0.1:8080"},
|
||||
{input: "http://127.0.0.1:8080/foo", expected: "http://127.0.0.1:8080/foo"},
|
||||
{input: "https://localhost:8080", expected: "https://127.0.0.1:8080"},
|
||||
{input: "https+insecure://localhost:8080", expected: "https+insecure://127.0.0.1:8080"},
|
||||
|
||||
// errors
|
||||
{input: "localhost:9999999", wantErr: true},
|
||||
{input: "ftp://localhost:8080", expected: "", wantErr: true},
|
||||
{input: "https://tailscale.com:8080", expected: "", wantErr: true},
|
||||
{input: "", expected: "", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
actual, err := expandProxyTargetDev(tt.input)
|
||||
|
||||
if tt.wantErr == true && err == nil {
|
||||
t.Errorf("Expected an error but got none")
|
||||
return
|
||||
}
|
||||
|
||||
if tt.wantErr == false && err != nil {
|
||||
t.Errorf("Got an error, but didn't expect one: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if actual != tt.expected {
|
||||
t.Errorf("Got: %q; expected: %q", actual, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanURLPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
wantErr bool
|
||||
}{
|
||||
{input: "", expected: "/"},
|
||||
{input: "/", expected: "/"},
|
||||
{input: "/foo", expected: "/foo"},
|
||||
{input: "/foo/", expected: "/foo/"},
|
||||
{input: "/../bar", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
actual, err := cleanURLPath(tt.input)
|
||||
|
||||
if tt.wantErr == true && err == nil {
|
||||
t.Errorf("Expected an error but got none")
|
||||
return
|
||||
}
|
||||
|
||||
if tt.wantErr == false && err != nil {
|
||||
t.Errorf("Got an error, but didn't expect one: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if actual != tt.expected {
|
||||
t.Errorf("Got: %q; expected: %q", actual, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLegacyInvocation(t *testing.T) {
|
||||
tests := []struct {
|
||||
subcmd serveMode
|
||||
args []string
|
||||
expected bool
|
||||
}{
|
||||
{subcmd: serve, args: []string{"https", "localhost:3000"}, expected: true},
|
||||
{subcmd: serve, args: []string{"https:8443", "localhost:3000"}, expected: true},
|
||||
{subcmd: serve, args: []string{"http", "localhost:3000"}, expected: true},
|
||||
{subcmd: serve, args: []string{"http:80", "localhost:3000"}, expected: true},
|
||||
{subcmd: serve, args: []string{"tcp:2222", "tcp://localhost:22"}, expected: true},
|
||||
{subcmd: serve, args: []string{"tls-terminated-tcp:443", "tcp://localhost:80"}, expected: true},
|
||||
|
||||
// false
|
||||
{subcmd: serve, args: []string{"3000"}, expected: false},
|
||||
{subcmd: serve, args: []string{"localhost:3000"}, expected: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
args := strings.Join(tt.args, " ")
|
||||
t.Run(fmt.Sprintf("%v %s", infoMap[tt.subcmd].Name, args), func(t *testing.T) {
|
||||
actual := isLegacyInvocation(tt.subcmd, tt.args)
|
||||
|
||||
if actual != tt.expected {
|
||||
t.Errorf("Got: %v; expected: %v", actual, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@ -902,11 +901,6 @@ func (lc *fakeLocalServeClient) IncrementCounter(ctx context.Context, name strin
|
||||
return nil // unused in tests
|
||||
}
|
||||
|
||||
func (lc *fakeLocalServeClient) StreamServe(ctx context.Context, req ipn.ServeStreamRequest) (io.ReadCloser, error) {
|
||||
// TODO: testing :)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// exactError returns an error checker that wants exactly the provided want error.
|
||||
// If optName is non-empty, it's used in the error message.
|
||||
func exactErr(want error, optName ...string) func(error) string {
|
||||
|
21
ipn/serve.go
21
ipn/serve.go
@ -85,27 +85,6 @@ type FunnelConn struct {
|
||||
Src netip.AddrPort
|
||||
}
|
||||
|
||||
// ServeStreamRequest defines the JSON request body
|
||||
// for the serve stream endpoint
|
||||
type ServeStreamRequest struct {
|
||||
// HostPort is the DNS and port of the tailscale
|
||||
// URL.
|
||||
HostPort HostPort `json:",omitempty"`
|
||||
|
||||
// Source is the user's serve source
|
||||
// as defined in the `tailscale serve`
|
||||
// command such as http://127.0.0.1:3000
|
||||
Source string `json:",omitempty"`
|
||||
|
||||
// MountPoint is the path prefix for
|
||||
// the given HostPort.
|
||||
MountPoint string `json:",omitempty"`
|
||||
|
||||
// Funnel indicates whether the request
|
||||
// is a serve request or a funnel one.
|
||||
Funnel bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// WebServerConfig describes a web server's configuration.
|
||||
type WebServerConfig struct {
|
||||
Handlers map[string]*HTTPHandler // mountPoint => handler
|
||||
|
Loading…
x
Reference in New Issue
Block a user