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
2021-03-08 16:46:23 +00:00
"github.com/cloudflare/cloudflared/config"
2020-11-25 06:55:13 +00:00
2020-10-06 17:12:52 +00:00
"github.com/pkg/errors"
2020-11-25 06:55:13 +00:00
"github.com/rs/zerolog"
2020-10-15 21:41:03 +00:00
"github.com/urfave/cli/v2"
2020-10-06 17:12:52 +00:00
)
var (
2020-11-12 23:57:19 +00:00
ErrNoIngressRules = errors . New ( "The config file doesn't contain any ingress rules" )
errLastRuleNotCatchAll = errors . New ( "The last ingress rule must match all URLs (i.e. it should not have a hostname or path filter)" )
2020-10-09 00:12:29 +00:00
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-12 23:57:19 +00:00
errHostnameContainsPort = errors . New ( "Hostname cannot contain a 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
)
2021-01-11 19:59:45 +00:00
const (
2021-01-17 20:22:53 +00:00
ServiceBastion = "bastion"
ServiceWarpRouting = "warp-routing"
2021-01-11 19:59:45 +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
}
}
2021-01-17 20:22:53 +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.
2020-11-25 06:55:13 +00:00
func NewSingleOrigin ( c * cli . Context , allowURLFromArgs bool ) ( Ingress , error ) {
2020-10-15 21:41:03 +00:00
2020-11-02 11:21:34 +00:00
service , err := parseSingleOriginService ( c , allowURLFromArgs )
2020-10-15 21:41:03 +00:00
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
}
2021-01-17 20:22:53 +00:00
// WarpRoutingService starts a tcp stream between the origin and requests from
// warp clients.
type WarpRoutingService struct {
Proxy StreamBasedOriginProxy
}
func NewWarpRoutingService ( ) * WarpRoutingService {
2021-02-05 13:01:53 +00:00
return & WarpRoutingService { Proxy : & rawTCPService { name : ServiceWarpRouting } }
2021-01-17 20:22:53 +00:00
}
2020-10-15 21:41:03 +00:00
// Get a single origin service from the CLI/config.
2020-12-09 21:46:53 +00:00
func parseSingleOriginService ( c * cli . Context , allowURLFromArgs bool ) ( originService , error ) {
2020-10-15 21:41:03 +00:00
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
}
2020-12-09 21:46:53 +00:00
if c . IsSet ( config . BastionFlag ) {
2021-02-05 13:01:53 +00:00
return newBastionService ( ) , nil
2020-12-09 21:46:53 +00:00
}
if c . IsSet ( "url" ) {
2020-11-02 11:21:34 +00:00
originURL , err := config . ValidateUrl ( c , allowURLFromArgs )
2020-10-15 21:41:03 +00:00
if err != nil {
return nil , errors . Wrap ( err , "Error validating origin URL" )
}
2020-12-09 21:46:53 +00:00
if isHTTPService ( originURL ) {
return & httpService {
url : originURL ,
} , nil
}
2021-02-05 13:01:53 +00:00
return newTCPOverWSService ( 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
}
2020-11-17 20:47:40 +00:00
u , err := url . Parse ( "http://localhost:8080" )
2020-12-09 21:46:53 +00:00
return & httpService { url : u } , err
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-12-30 19:48:19 +00:00
func ( ing Ingress ) StartOrigins (
wg * sync . WaitGroup ,
log * zerolog . Logger ,
shutdownC <- chan struct { } ,
errC chan error ,
) 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-12-30 19:48:19 +00:00
return errors . Wrapf ( err , "Error starting local service %s" , rule . Service )
2020-10-15 21:41:03 +00:00
}
}
2020-12-30 19:48:19 +00:00
return nil
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-11-15 18:47:51 +00:00
cfg := setConfig ( defaults , r . OriginRequest )
2020-12-09 21:46:53 +00:00
var service originService
2020-10-15 21:41:03 +00:00
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 )
2021-01-11 19:59:45 +00:00
} else if r . Service == ServiceBastion || cfg . BastionMode {
2020-11-15 18:47:51 +00:00
// Bastion mode will always start a Websocket proxy server, which will
// overwrite the localService.URL field when `start` is called. So,
// leave the URL field empty for now.
cfg . BastionMode = true
2021-02-05 13:01:53 +00:00
service = newBastionService ( )
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 ( ) == "" {
2020-11-12 23:57:19 +00:00
return Ingress { } , fmt . Errorf ( "%s is an invalid address, please make sure it has a scheme and a hostname" , r . Service )
2020-10-15 21:41:03 +00:00
}
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-12-09 21:46:53 +00:00
if isHTTPService ( u ) {
service = & httpService { url : u }
} else {
2021-02-05 13:01:53 +00:00
service = newTCPOverWSService ( u )
2020-12-09 21:46:53 +00:00
}
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-11-15 18:47:51 +00:00
Config : cfg ,
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
}
2020-12-09 21:46:53 +00:00
func isHTTPService ( url * url . URL ) bool {
return url . Scheme == "http" || url . Scheme == "https" || url . Scheme == "ws" || url . Scheme == "wss"
}