2020-10-09 00:12:29 +00:00
package ingress
2020-10-06 17:12:52 +00:00
import (
"fmt"
2020-11-04 15:44:15 +00:00
"net"
2020-10-06 17:12:52 +00:00
"net/url"
"regexp"
2020-11-04 18:22:21 +00:00
"strconv"
2020-10-07 21:34:53 +00:00
"strings"
2020-10-15 21:41:03 +00:00
"sync"
2020-10-06 17:12:52 +00:00
"github.com/pkg/errors"
2020-10-15 21:41:03 +00:00
"github.com/urfave/cli/v2"
2020-10-20 17:00:34 +00:00
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
2020-10-15 21:41:03 +00:00
"github.com/cloudflare/cloudflared/logger"
2020-10-06 17:12:52 +00:00
)
var (
2020-10-12 17:54:15 +00:00
ErrNoIngressRules = errors . New ( "No ingress rules were specified in the config file" )
2020-10-09 00:12:29 +00:00
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\"" )
2020-11-04 15:44:15 +00:00
errHostnameContainsPort = errors . New ( "Hostname cannot contain port" )
2020-10-09 00:12:29 +00:00
ErrURLIncompatibleWithIngress = errors . New ( "You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules" )
2020-10-06 17:12:52 +00:00
)
2020-10-12 17:54:15 +00:00
// FindMatchingRule returns the index of the Ingress Rule which matches the given
// hostname and path. This function assumes the last rule matches everything,
// which is the case if the rules were instantiated via the ingress#Validate method
2020-10-15 21:41:03 +00:00
func ( ing Ingress ) FindMatchingRule ( hostname , path string ) ( * Rule , int ) {
2020-11-04 15:44:15 +00:00
// The hostname might contain port. We only want to compare the host part with the rule
host , _ , err := net . SplitHostPort ( hostname )
if err == nil {
hostname = host
}
2020-10-15 17:41:50 +00:00
for i , rule := range ing . Rules {
2020-10-12 17:54:15 +00:00
if rule . Matches ( hostname , path ) {
2020-10-15 21:41:03 +00:00
return & rule , i
2020-10-12 17:54:15 +00:00
}
}
2020-10-15 21:41:03 +00:00
i := len ( ing . Rules ) - 1
return & ing . Rules [ i ] , i
2020-10-12 17:54:15 +00:00
}
2020-10-07 21:34:53 +00:00
func matchHost ( ruleHost , reqHost string ) bool {
if ruleHost == reqHost {
return true
}
// Validate hostnames that use wildcards at the start
if strings . HasPrefix ( ruleHost , "*." ) {
toMatch := strings . TrimPrefix ( ruleHost , "*." )
return strings . HasSuffix ( reqHost , toMatch )
}
return false
}
2020-10-15 17:41:50 +00:00
// Ingress maps eyeball requests to origins.
type Ingress struct {
2020-10-15 21:41:03 +00:00
Rules [ ] Rule
defaults OriginRequestConfig
}
// NewSingleOrigin constructs an Ingress set with only one rule, constructed from
// legacy CLI parameters like --url or --no-chunked-encoding.
func NewSingleOrigin ( c * cli . Context , compatibilityMode bool , logger logger . Service ) ( Ingress , error ) {
service , err := parseSingleOriginService ( c , compatibilityMode )
if err != nil {
return Ingress { } , err
}
// Construct an Ingress with the single rule.
2020-11-11 13:14:51 +00:00
defaults := originRequestFromSingeRule ( c )
2020-10-15 21:41:03 +00:00
ing := Ingress {
Rules : [ ] Rule {
{
Service : service ,
2020-11-11 13:14:51 +00:00
Config : setConfig ( defaults , config . OriginRequestConfig { } ) ,
2020-10-15 21:41:03 +00:00
} ,
} ,
2020-11-11 13:14:51 +00:00
defaults : defaults ,
2020-10-15 21:41:03 +00:00
}
return ing , err
}
// Get a single origin service from the CLI/config.
func parseSingleOriginService ( c * cli . Context , compatibilityMode bool ) ( OriginService , error ) {
if c . IsSet ( "hello-world" ) {
2020-10-30 21:37:40 +00:00
return new ( helloWorld ) , nil
2020-10-15 21:41:03 +00:00
}
if c . IsSet ( "url" ) {
originURLStr , err := config . ValidateUrl ( c , compatibilityMode )
if err != nil {
return nil , errors . Wrap ( err , "Error validating origin URL" )
}
originURL , err := url . Parse ( originURLStr )
if err != nil {
return nil , errors . Wrap ( err , "couldn't parse origin URL" )
}
2020-10-30 21:37:40 +00:00
return & localService { URL : originURL , RootURL : originURL } , nil
2020-10-15 21:41:03 +00:00
}
if c . IsSet ( "unix-socket" ) {
2020-10-30 21:37:40 +00:00
path , err := config . ValidateUnixSocket ( c )
2020-10-15 21:41:03 +00:00
if err != nil {
return nil , errors . Wrap ( err , "Error validating --unix-socket" )
}
2020-10-30 21:37:40 +00:00
return & unixSocketPath { path : path } , nil
2020-10-15 21:41:03 +00:00
}
return nil , errors . New ( "You must either set ingress rules in your config file, or use --url or use --unix-socket" )
2020-10-15 17:41:50 +00:00
}
// IsEmpty checks if there are any ingress rules.
func ( ing Ingress ) IsEmpty ( ) bool {
return len ( ing . Rules ) == 0
}
2020-10-15 21:41:03 +00:00
// StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World.
2020-11-10 01:22:03 +00:00
func ( ing Ingress ) StartOrigins ( wg * sync . WaitGroup , log logger . Service , shutdownC <- chan struct { } , errC chan error ) {
2020-10-15 21:41:03 +00:00
for _ , rule := range ing . Rules {
2020-10-30 21:37:40 +00:00
if err := rule . Service . start ( wg , log , shutdownC , errC , rule . Config ) ; err != nil {
2020-11-10 01:22:03 +00:00
log . Errorf ( "Error starting local service %s: %s" , rule . Service , err )
2020-10-15 21:41:03 +00:00
}
}
}
// CatchAll returns the catch-all rule (i.e. the last rule)
func ( ing Ingress ) CatchAll ( ) * Rule {
return & ing . Rules [ len ( ing . Rules ) - 1 ]
}
func validate ( ingress [ ] config . UnvalidatedIngressRule , defaults OriginRequestConfig ) ( Ingress , error ) {
2020-10-20 17:00:34 +00:00
rules := make ( [ ] Rule , len ( ingress ) )
for i , r := range ingress {
2020-10-15 21:41:03 +00:00
var service OriginService
2020-10-30 21:37:40 +00:00
if prefix := "unix:" ; strings . HasPrefix ( r . Service , prefix ) {
2020-10-15 21:41:03 +00:00
// No validation necessary for unix socket filepath services
2020-10-30 21:37:40 +00:00
path := strings . TrimPrefix ( r . Service , prefix )
service = & unixSocketPath { path : path }
2020-11-04 18:22:21 +00:00
} else if prefix := "http_status:" ; strings . HasPrefix ( r . Service , prefix ) {
status , err := strconv . Atoi ( strings . TrimPrefix ( r . Service , prefix ) )
if err != nil {
return Ingress { } , errors . Wrap ( err , "invalid HTTP status" )
}
srv := newStatusCode ( status )
service = & srv
2020-10-15 21:41:03 +00:00
} else if r . Service == "hello_world" || r . Service == "hello-world" || r . Service == "helloworld" {
2020-10-30 21:37:40 +00:00
service = new ( helloWorld )
2020-10-15 21:41:03 +00:00
} else {
// Validate URL services
u , err := url . Parse ( r . Service )
if err != nil {
return Ingress { } , err
}
if u . Scheme == "" || u . Hostname ( ) == "" {
return Ingress { } , fmt . Errorf ( "The service %s must have a scheme and a hostname" , r . Service )
}
2020-10-06 17:12:52 +00:00
2020-10-15 21:41:03 +00:00
if u . Path != "" {
return Ingress { } , fmt . Errorf ( "%s is an invalid address, ingress rules don't support proxying to a different path on the origin service. The path will be the same as the eyeball request's path" , r . Service )
}
2020-10-30 21:37:40 +00:00
serviceURL := localService { URL : u }
2020-10-15 21:41:03 +00:00
service = & serviceURL
2020-10-12 17:54:15 +00:00
}
2020-11-04 15:44:15 +00:00
if err := validateHostname ( r , i , len ( ingress ) ) ; err != nil {
return Ingress { } , err
2020-10-06 17:12:52 +00:00
}
var pathRegex * regexp . Regexp
if r . Path != "" {
2020-10-15 21:41:03 +00:00
var err error
2020-10-06 17:12:52 +00:00
pathRegex , err = regexp . Compile ( r . Path )
if err != nil {
2020-10-15 17:41:50 +00:00
return Ingress { } , errors . Wrapf ( err , "Rule #%d has an invalid regex" , i + 1 )
2020-10-06 17:12:52 +00:00
}
}
2020-10-09 00:12:29 +00:00
rules [ i ] = Rule {
2020-10-06 17:12:52 +00:00
Hostname : r . Hostname ,
Service : service ,
Path : pathRegex ,
2020-10-30 21:37:40 +00:00
Config : setConfig ( defaults , r . OriginRequest ) ,
2020-10-06 17:12:52 +00:00
}
}
2020-10-15 21:41:03 +00:00
return Ingress { Rules : rules , defaults : defaults } , nil
2020-10-06 17:12:52 +00:00
}
2020-11-04 15:44:15 +00:00
func validateHostname ( r config . UnvalidatedIngressRule , ruleIndex , totalRules int ) error {
// Ensure that the hostname doesn't contain port
_ , _ , err := net . SplitHostPort ( r . Hostname )
if err == nil {
return errHostnameContainsPort
}
// Ensure that there are no wildcards anywhere except the first character
// of the hostname.
if strings . LastIndex ( r . Hostname , "*" ) > 0 {
return errBadWildcard
}
// The last rule should catch all hostnames.
isCatchAllRule := ( r . Hostname == "" || r . Hostname == "*" ) && r . Path == ""
isLastRule := ruleIndex == totalRules - 1
if isLastRule && ! isCatchAllRule {
return errLastRuleNotCatchAll
}
// ONLY the last rule should catch all hostnames.
if ! isLastRule && isCatchAllRule {
return errRuleShouldNotBeCatchAll { index : ruleIndex , hostname : r . Hostname }
}
return nil
}
2020-10-06 17:12:52 +00:00
type errRuleShouldNotBeCatchAll struct {
2020-11-04 15:44:15 +00:00
index int
2020-10-06 17:12:52 +00:00
hostname string
}
func ( e errRuleShouldNotBeCatchAll ) Error ( ) string {
return fmt . Sprintf ( "Rule #%d is matching the hostname '%s', but " +
"this will match every hostname, meaning the rules which follow it " +
2020-11-04 15:44:15 +00:00
"will never be triggered." , e . index + 1 , e . hostname )
2020-10-06 17:12:52 +00:00
}
2020-10-30 21:37:40 +00:00
// ParseIngress parses ingress rules, but does not send HTTP requests to the origins.
func ParseIngress ( conf * config . Configuration ) ( Ingress , error ) {
2020-10-20 17:00:34 +00:00
if len ( conf . Ingress ) == 0 {
2020-10-15 17:41:50 +00:00
return Ingress { } , ErrNoIngressRules
2020-10-06 17:12:52 +00:00
}
2020-10-30 21:37:40 +00:00
return validate ( conf . Ingress , originRequestFromYAML ( conf . OriginRequest ) )
2020-10-06 17:12:52 +00:00
}