From 0eebc7cef983cdee99f6800b89a2c59e4a203ebf Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Thu, 8 Oct 2020 19:12:29 -0500 Subject: [PATCH] TUN-3438: move ingress into own package, read into TunnelConfig --- cmd/cloudflared/config/configuration.go | 16 +++ cmd/cloudflared/tunnel/cmd.go | 62 +++++++++++ cmd/cloudflared/tunnel/configuration.go | 52 +++++---- .../cloudflared/tunnel => ingress}/ingress.go | 102 ++++-------------- .../tunnel => ingress}/ingress_test.go | 18 ++-- origin/tunnel.go | 2 + 6 files changed, 141 insertions(+), 111 deletions(-) rename {cmd/cloudflared/tunnel => ingress}/ingress.go (53%) rename {cmd/cloudflared/tunnel => ingress}/ingress_test.go (94%) diff --git a/cmd/cloudflared/config/configuration.go b/cmd/cloudflared/config/configuration.go index a442e2e4..620ade51 100644 --- a/cmd/cloudflared/config/configuration.go +++ b/cmd/cloudflared/config/configuration.go @@ -3,6 +3,7 @@ package config import ( "errors" "fmt" + "io/ioutil" "os" "path/filepath" "runtime" @@ -11,6 +12,7 @@ import ( "github.com/urfave/cli/v2" "gopkg.in/yaml.v2" + "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/validation" ) @@ -193,3 +195,17 @@ func ValidateUrl(c *cli.Context, allowFromArgs bool) (string, error) { validUrl, err := validation.ValidateUrl(url) return validUrl, err } + +func ReadRules(c *cli.Context) ([]ingress.Rule, error) { + configFilePath := c.String("config") + if configFilePath == "" { + return nil, ErrNoConfigFile + } + fmt.Printf("Reading from config file %s\n", configFilePath) + configBytes, err := ioutil.ReadFile(configFilePath) + if err != nil { + return nil, err + } + rules, err := ingress.ParseIngress(configBytes) + return rules, err +} diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 5c6a6e28..46f11fd0 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -26,6 +26,7 @@ import ( "github.com/cloudflare/cloudflared/dbconnect" "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/hello" + "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/origin" @@ -1248,3 +1249,64 @@ reconnect [delay] } } } + +func buildValidateCommand() *cli.Command { + return &cli.Command{ + Name: "validate", + Action: cliutil.ErrorHandler(ValidateCommand), + Usage: "Validate the ingress configuration ", + UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate", + Description: "Validates the configuration file, ensuring your ingress rules are OK.", + } +} + +func buildRuleCommand() *cli.Command { + return &cli.Command{ + Name: "rule", + Action: cliutil.ErrorHandler(RuleCommand), + Usage: "Check which ingress rule matches a given request URL", + UsageText: "cloudflared tunnel [--config FILEPATH] ingress rule URL", + ArgsUsage: "URL", + Description: "Check which ingress rule matches a given request URL. " + + "Ingress rules match a request's hostname and path. Hostname is " + + "optional and is either a full hostname like `www.example.com` or a " + + "hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " + + "is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`", + } +} + +// Validates the ingress rules in the cloudflared config file +func ValidateCommand(c *cli.Context) error { + _, err := config.ReadRules(c) + if err != nil { + return errors.Wrap(err, "Validation failed") + } + if c.IsSet("url") { + return ingress.ErrURLIncompatibleWithIngress + } + fmt.Println("OK") + return nil +} + +// Checks which ingress rule matches the given URL. +func RuleCommand(c *cli.Context) error { + rules, err := config.ReadRules(c) + if err != nil { + return err + } + requestArg := c.Args().First() + if requestArg == "" { + return errors.New("cloudflared tunnel rule expects a single argument, the URL to test") + } + requestURL, err := url.Parse(requestArg) + if err != nil { + return fmt.Errorf("%s is not a valid URL", requestArg) + } + if requestURL.Hostname() == "" && requestURL.Scheme == "" { + return fmt.Errorf("%s doesn't have a hostname, consider adding a scheme", requestArg) + } + if requestURL.Hostname() == "" { + return fmt.Errorf("%s doesn't have a hostname", requestArg) + } + return ingress.RuleCommand(rules, requestURL) +} diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index 13d61361..0490269d 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -14,6 +14,7 @@ import ( "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" "github.com/cloudflare/cloudflared/cmd/cloudflared/config" + "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/tlsconfig" @@ -184,12 +185,6 @@ func prepareTunnelConfig( tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID}) - originURL, err := config.ValidateUrl(c, compatibilityMode) - if err != nil { - logger.Errorf("Error validating origin URL: %s", err) - return nil, errors.Wrap(err, "Error validating origin URL") - } - var originCert []byte if !isFreeTunnel { originCert, err = getOriginCert(c, logger) @@ -224,6 +219,36 @@ func prepareTunnelConfig( } dialContext := dialer.DialContext + var ingressRules []ingress.Rule + if namedTunnel != nil { + clientUUID, err := uuid.NewRandom() + if err != nil { + return nil, errors.Wrap(err, "can't generate clientUUID") + } + namedTunnel.Client = tunnelpogs.ClientInfo{ + ClientID: clientUUID[:], + Features: []string{origin.FeatureSerializedHeaders}, + Version: version, + Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch), + } + ingressRules, err = config.ReadRules(c) + if err != nil { + return nil, err + } + if c.IsSet("url") { + return nil, ingress.ErrURLIncompatibleWithIngress + } + } + + var originURL string + if len(ingressRules) == 0 { + originURL, err = config.ValidateUrl(c, compatibilityMode) + if err != nil { + logger.Errorf("Error validating origin URL: %s", err) + return nil, errors.Wrap(err, "Error validating origin URL") + } + } + if c.IsSet("unix-socket") { unixSocket, err := config.ValidateUnixSocket(c) if err != nil { @@ -256,20 +281,6 @@ func prepareTunnelConfig( logger.Errorf("unable to create TLS config to connect with edge: %s", err) return nil, errors.Wrap(err, "unable to create TLS config to connect with edge") } - - if namedTunnel != nil { - clientUUID, err := uuid.NewRandom() - if err != nil { - return nil, errors.Wrap(err, "can't generate clientUUID") - } - namedTunnel.Client = tunnelpogs.ClientInfo{ - ClientID: clientUUID[:], - Features: []string{origin.FeatureSerializedHeaders}, - Version: version, - Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch), - } - } - return &origin.TunnelConfig{ BuildInfo: buildInfo, ClientID: clientID, @@ -301,6 +312,7 @@ func prepareTunnelConfig( TlsConfig: toEdgeTLSConfig, NamedTunnel: namedTunnel, ReplaceExisting: c.Bool("force"), + IngressRules: ingressRules, // turn off use of reconnect token and auth refresh when using named tunnels UseReconnectToken: compatibilityMode && c.Bool("use-reconnect-token"), }, nil diff --git a/cmd/cloudflared/tunnel/ingress.go b/ingress/ingress.go similarity index 53% rename from cmd/cloudflared/tunnel/ingress.go rename to ingress/ingress.go index 1c202433..fcfab77d 100644 --- a/cmd/cloudflared/tunnel/ingress.go +++ b/ingress/ingress.go @@ -1,30 +1,26 @@ -package tunnel +package ingress import ( "fmt" - "io/ioutil" "net/url" "regexp" "strings" - "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" - "github.com/cloudflare/cloudflared/cmd/cloudflared/config" - "github.com/pkg/errors" - "github.com/urfave/cli/v2" "gopkg.in/yaml.v2" ) var ( - errNoIngressRules = errors.New("No ingress rules were specified in the config file") - errLastRuleNotCatchAll = errors.New("The last ingress rule must match all hostnames (i.e. it must be missing, or must be \"*\")") - errBadWildcard = errors.New("Hostname patterns can have at most one wildcard character (\"*\") and it can only be used for subdomains, e.g. \"*.example.com\"") - errNoIngressRulesMatch = errors.New("The URL didn't match any ingress rules") + errNoIngressRules = errors.New("No ingress rules were specified in the config file") + errLastRuleNotCatchAll = errors.New("The last ingress rule must match all hostnames (i.e. it must be missing, or must be \"*\")") + errBadWildcard = errors.New("Hostname patterns can have at most one wildcard character (\"*\") and it can only be used for subdomains, e.g. \"*.example.com\"") + errNoIngressRulesMatch = errors.New("The URL didn't match any ingress rules") + ErrURLIncompatibleWithIngress = errors.New("You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules") ) // Each rule route traffic from a hostname/path on the public // internet to the service running on the given URL. -type rule struct { +type Rule struct { // Requests for this hostname will be proxied to this rule's service. Hostname string @@ -37,7 +33,7 @@ type rule struct { Service *url.URL } -func (r rule) String() string { +func (r Rule) String() string { var out strings.Builder if r.Hostname != "" { out.WriteString("\thostname: ") @@ -54,7 +50,7 @@ func (r rule) String() string { return out.String() } -func (r rule) matches(requestURL *url.URL) bool { +func (r Rule) matches(requestURL *url.URL) bool { hostMatch := r.Hostname == "" || r.Hostname == "*" || matchHost(r.Hostname, requestURL.Hostname()) pathMatch := r.Path == nil || r.Path.MatchString(requestURL.Path) return hostMatch && pathMatch @@ -81,10 +77,14 @@ type unvalidatedRule struct { type ingress struct { Ingress []unvalidatedRule + Url string } -func (ing ingress) validate() ([]rule, error) { - rules := make([]rule, len(ing.Ingress)) +func (ing ingress) validate() ([]Rule, error) { + if ing.Url != "" { + return nil, ErrURLIncompatibleWithIngress + } + rules := make([]Rule, len(ing.Ingress)) for i, r := range ing.Ingress { service, err := url.Parse(r.Service) if err != nil { @@ -119,7 +119,7 @@ func (ing ingress) validate() ([]rule, error) { } } - rules[i] = rule{ + rules[i] = Rule{ Hostname: r.Hostname, Service: service, Path: pathRegex, @@ -139,7 +139,7 @@ func (e errRuleShouldNotBeCatchAll) Error() string { "will never be triggered.", e.i+1, e.hostname) } -func parseIngress(rawYAML []byte) ([]rule, error) { +func ParseIngress(rawYAML []byte) ([]Rule, error) { var ing ingress if err := yaml.Unmarshal(rawYAML, &ing); err != nil { return nil, err @@ -150,57 +150,10 @@ func parseIngress(rawYAML []byte) ([]rule, error) { return ing.validate() } -func ingressContext(c *cli.Context) ([]rule, error) { - configFilePath := c.String("config") - if configFilePath == "" { - return nil, config.ErrNoConfigFile - } - fmt.Printf("Reading from config file %s\n", configFilePath) - configBytes, err := ioutil.ReadFile(configFilePath) - if err != nil { - return nil, err - } - rules, err := parseIngress(configBytes) - return rules, err -} - -// Validates the ingress rules in the cloudflared config file -func validateCommand(c *cli.Context) error { - _, err := ingressContext(c) - if err != nil { - fmt.Println(err.Error()) - return errors.New("Validation failed") - } - fmt.Println("OK") - return nil -} - -func buildValidateCommand() *cli.Command { - return &cli.Command{ - Name: "validate", - Action: cliutil.ErrorHandler(validateCommand), - Usage: "Validate the ingress configuration ", - UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate", - Description: "Validates the configuration file, ensuring your ingress rules are OK.", - } -} - -// Checks which ingress rule matches the given URL. -func ruleCommand(c *cli.Context) error { - rules, err := ingressContext(c) - if err != nil { - return err - } - requestArg := c.Args().First() - if requestArg == "" { - return errors.New("cloudflared tunnel rule expects a single argument, the URL to test") - } - requestURL, err := url.Parse(requestArg) - if err != nil { - return fmt.Errorf("%s is not a valid URL", requestArg) - } +// RuleCommand checks which ingress rule matches the given request URL. +func RuleCommand(rules []Rule, requestURL *url.URL) error { if requestURL.Hostname() == "" { - return fmt.Errorf("%s is malformed and doesn't have a hostname", requestArg) + return fmt.Errorf("%s is malformed and doesn't have a hostname", requestURL) } for i, r := range rules { if r.matches(requestURL) { @@ -211,18 +164,3 @@ func ruleCommand(c *cli.Context) error { } return errNoIngressRulesMatch } - -func buildRuleCommand() *cli.Command { - return &cli.Command{ - Name: "rule", - Action: cliutil.ErrorHandler(ruleCommand), - Usage: "Check which ingress rule matches a given request URL", - UsageText: "cloudflared [--config CONFIGFILE] tunnel ingress rule URL", - ArgsUsage: "URL", - Description: "Check which ingress rule matches a given request URL. " + - "Ingress rules match a request's hostname and path. Hostname is " + - "optional and is either a full hostname like `www.example.com` or a " + - "hostname with a `*` for its subdomains, e.g. `*.example.com`. Path " + - "is optional and matches a regular expression, like `/[a-zA-Z0-9_]+.html`", - } -} diff --git a/cmd/cloudflared/tunnel/ingress_test.go b/ingress/ingress_test.go similarity index 94% rename from cmd/cloudflared/tunnel/ingress_test.go rename to ingress/ingress_test.go index b8cd96ca..1d5cd379 100644 --- a/cmd/cloudflared/tunnel/ingress_test.go +++ b/ingress/ingress_test.go @@ -1,4 +1,4 @@ -package tunnel +package ingress import ( "net/url" @@ -20,7 +20,7 @@ func Test_parseIngress(t *testing.T) { tests := []struct { name string args args - want []rule + want []Rule wantErr bool }{ { @@ -37,7 +37,7 @@ ingress: - hostname: "*" service: https://localhost:8001 `}, - want: []rule{ + want: []Rule{ { Hostname: "tunnel1.example.com", Service: localhost8000, @@ -56,7 +56,7 @@ ingress: service: https://localhost:8000 extraKey: extraValue `}, - want: []rule{ + want: []Rule{ { Hostname: "*", Service: localhost8000, @@ -69,7 +69,7 @@ extraKey: extraValue ingress: - service: https://localhost:8000 `}, - want: []rule{ + want: []Rule{ { Service: localhost8000, }, @@ -141,13 +141,13 @@ ingress: } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := parseIngress([]byte(tt.args.rawYAML)) + got, err := ParseIngress([]byte(tt.args.rawYAML)) if (err != nil) != tt.wantErr { - t.Errorf("parseIngress() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("ParseIngress() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("parseIngress() = %v, want %v", got, tt.want) + t.Errorf("ParseIngress() = %v, want %v", got, tt.want) } }) } @@ -258,7 +258,7 @@ func Test_rule_matches(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := rule{ + r := Rule{ Hostname: tt.fields.Hostname, Path: tt.fields.Path, Service: tt.fields.Service, diff --git a/origin/tunnel.go b/origin/tunnel.go index f9319ea4..b9ea8c1e 100644 --- a/origin/tunnel.go +++ b/origin/tunnel.go @@ -24,6 +24,7 @@ import ( "github.com/cloudflare/cloudflared/cmd/cloudflared/ui" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/h2mux" + "github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/tunnelrpc" @@ -92,6 +93,7 @@ type TunnelConfig struct { NamedTunnel *NamedTunnelConfig ReplaceExisting bool TunnelEventChan chan<- ui.TunnelEvent + IngressRules []ingress.Rule } type dupConnRegisterTunnelError struct{}