2020-10-09 00:12:29 +00:00
package ingress
2020-10-06 17:12:52 +00:00
import (
"fmt"
"net/url"
"regexp"
2020-10-07 21:34:53 +00:00
"strings"
2020-10-06 17:12:52 +00:00
"github.com/pkg/errors"
2020-10-20 17:00:34 +00:00
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
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\"" )
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
)
// Each rule route traffic from a hostname/path on the public
// internet to the service running on the given URL.
2020-10-09 00:12:29 +00:00
type Rule struct {
2020-10-06 17:12:52 +00:00
// Requests for this hostname will be proxied to this rule's service.
Hostname string
// Path is an optional regex that can specify path-driven ingress rules.
Path * regexp . Regexp
// A (probably local) address. Requests for a hostname which matches this
// rule's hostname pattern will be proxied to the service running on this
// address.
Service * url . URL
}
2020-10-15 17:41:50 +00:00
func ( r Rule ) MultiLineString ( ) string {
2020-10-07 21:34:53 +00:00
var out strings . Builder
if r . Hostname != "" {
out . WriteString ( "\thostname: " )
out . WriteString ( r . Hostname )
out . WriteRune ( '\n' )
}
if r . Path != nil {
out . WriteString ( "\tpath: " )
out . WriteString ( r . Path . String ( ) )
out . WriteRune ( '\n' )
}
out . WriteString ( "\tservice: " )
out . WriteString ( r . Service . String ( ) )
return out . String ( )
}
2020-10-12 17:54:15 +00:00
func ( r * Rule ) Matches ( hostname , path string ) bool {
hostMatch := r . Hostname == "" || r . Hostname == "*" || matchHost ( r . Hostname , hostname )
pathMatch := r . Path == nil || r . Path . MatchString ( path )
2020-10-07 21:34:53 +00:00
return hostMatch && pathMatch
}
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 17:41:50 +00:00
func ( ing Ingress ) FindMatchingRule ( hostname , path string ) int {
for i , rule := range ing . Rules {
2020-10-12 17:54:15 +00:00
if rule . Matches ( hostname , path ) {
return i
}
}
2020-10-15 17:41:50 +00:00
return len ( ing . Rules ) - 1
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 {
Rules [ ] Rule
}
// IsEmpty checks if there are any ingress rules.
func ( ing Ingress ) IsEmpty ( ) bool {
return len ( ing . Rules ) == 0
}
2020-10-20 17:00:34 +00:00
func validate ( ingress [ ] config . UnvalidatedIngressRule ) ( Ingress , error ) {
rules := make ( [ ] Rule , len ( ingress ) )
for i , r := range ingress {
2020-10-06 17:12:52 +00:00
service , err := url . Parse ( r . Service )
if err != nil {
2020-10-15 17:41:50 +00:00
return Ingress { } , err
2020-10-06 17:12:52 +00:00
}
2020-10-08 18:00:32 +00:00
if service . Scheme == "" || service . Hostname ( ) == "" {
2020-10-15 17:41:50 +00:00
return Ingress { } , fmt . Errorf ( "The service %s must have a scheme and a hostname" , r . Service )
2020-10-08 18:00:32 +00:00
}
2020-10-06 17:12:52 +00:00
2020-10-12 17:54:15 +00:00
if service . Path != "" {
2020-10-15 17:41:50 +00:00
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-12 17:54:15 +00:00
}
2020-10-07 21:34:53 +00:00
// Ensure that there are no wildcards anywhere except the first character
// of the hostname.
if strings . LastIndex ( r . Hostname , "*" ) > 0 {
2020-10-15 17:41:50 +00:00
return Ingress { } , errBadWildcard
2020-10-07 21:34:53 +00:00
}
2020-10-06 17:12:52 +00:00
// The last rule should catch all hostnames.
isCatchAllRule := ( r . Hostname == "" || r . Hostname == "*" ) && r . Path == ""
2020-10-20 17:00:34 +00:00
isLastRule := i == len ( ingress ) - 1
2020-10-06 17:12:52 +00:00
if isLastRule && ! isCatchAllRule {
2020-10-15 17:41:50 +00:00
return Ingress { } , errLastRuleNotCatchAll
2020-10-06 17:12:52 +00:00
}
// ONLY the last rule should catch all hostnames.
if ! isLastRule && isCatchAllRule {
2020-10-15 17:41:50 +00:00
return Ingress { } , errRuleShouldNotBeCatchAll { i : i , hostname : r . Hostname }
2020-10-06 17:12:52 +00:00
}
var pathRegex * regexp . Regexp
if r . Path != "" {
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-15 17:41:50 +00:00
return Ingress { Rules : rules } , nil
2020-10-06 17:12:52 +00:00
}
type errRuleShouldNotBeCatchAll struct {
i int
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 " +
"will never be triggered." , e . i + 1 , e . hostname )
}
2020-10-20 17:00:34 +00:00
func ParseIngress ( conf * config . Configuration ) ( Ingress , error ) {
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-20 17:00:34 +00:00
return validate ( conf . Ingress )
2020-10-06 17:12:52 +00:00
}