@ -87,6 +87,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
h . CBRaw = nil // allow GC to deallocate
h . CBRaw = nil // allow GC to deallocate
}
}
// set up transport
if h . Transport == nil {
if h . Transport == nil {
t := & HTTPTransport {
t := & HTTPTransport {
KeepAlive : & KeepAlive {
KeepAlive : & KeepAlive {
@ -102,6 +103,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
h . Transport = t
h . Transport = t
}
}
// set up load balancing
if h . LoadBalancing == nil {
if h . LoadBalancing == nil {
h . LoadBalancing = new ( LoadBalancing )
h . LoadBalancing = new ( LoadBalancing )
}
}
@ -152,85 +154,40 @@ func (h *Handler) Provision(ctx caddy.Context) error {
go h . activeHealthChecker ( )
go h . activeHealthChecker ( )
}
}
var allUpstreams [ ] * Upstream
// set up upstreams
for _ , upstream := range h . Upstreams {
for _ , upstream := range h . Upstreams {
// if a port was not specified (and the network type uses
// create or get the host representation for this upstream
// ports), then maybe we can figure out the default port
var host Host = new ( upstreamHost )
netw , host , port , err := caddy . SplitNetworkAddress ( upstream . Dial )
existingHost , loaded := hosts . LoadOrStore ( upstream . Dial , host )
if err != nil && port == "" && ! strings . Contains ( netw , "unix" ) {
if loaded {
if host == "" {
host = existingHost . ( Host )
// assume all that was given was the host, no port
host = upstream . Dial
}
// a port was not specified, but we may be able to
// infer it if we know the standard ports on which
// the transport protocol operates
if ht , ok := h . Transport . ( * HTTPTransport ) ; ok {
defaultPort := "80"
if ht . TLS != nil {
defaultPort = "443"
}
upstream . Dial = caddy . JoinNetworkAddress ( netw , host , defaultPort )
}
}
}
upstream . Host = host
// upstreams are allowed to map to only a single host,
// give it the circuit breaker, if any
// but an upstream's address may semantically represent
upstream . cb = h . CB
// multiple addresses, so make sure to handle each
// one in turn based on this one upstream config
network , addresses , err := caddy . ParseNetworkAddress ( upstream . Dial )
if err != nil {
return fmt . Errorf ( "parsing dial address: %v" , err )
}
for _ , addr := range addresses {
// make a new upstream based on the original
// that has a singular dial address
upstreamCopy := * upstream
upstreamCopy . dialInfo = NewDialInfo ( network , addr )
upstreamCopy . Dial = upstreamCopy . dialInfo . String ( )
upstreamCopy . cb = h . CB
// if host already exists from a current config,
// use that instead; otherwise, add it
// TODO: make hosts modular, so that their state can be distributed in enterprise for example
// TODO: If distributed, the pool should be stored in storage...
var host Host = new ( upstreamHost )
activeHost , loaded := hosts . LoadOrStore ( upstreamCopy . dialInfo . String ( ) , host )
if loaded {
host = activeHost . ( Host )
}
upstreamCopy . Host = host
// if the passive health checker has a non-zero "unhealthy
// request count" but the upstream has no MaxRequests set
// (they are the same thing, but one is a default value for
// for upstreams with a zero MaxRequests), copy the default
// value into this upstream, since the value in the upstream
// (MaxRequests) is what is used during availability checks
if h . HealthChecks != nil &&
h . HealthChecks . Passive != nil &&
h . HealthChecks . Passive . UnhealthyRequestCount > 0 &&
upstreamCopy . MaxRequests == 0 {
upstreamCopy . MaxRequests = h . HealthChecks . Passive . UnhealthyRequestCount
}
// upstreams need independent access to the passive
// if the passive health checker has a non-zero UnhealthyRequestCount
// health check policy because they run outside of the
// but the upstream has no MaxRequests set (they are the same thing,
// scope of a request handler
// but the passive health checker is a default value for for upstreams
if h . HealthChecks != nil {
// without MaxRequests), copy the value into this upstream, since the
upstreamCopy . healthCheckPolicy = h . HealthChecks . Passive
// value in the upstream (MaxRequests) is what is used during
}
// availability checks
if h . HealthChecks != nil &&
h . HealthChecks . Passive != nil &&
h . HealthChecks . Passive . UnhealthyRequestCount > 0 &&
upstream . MaxRequests == 0 {
upstream . MaxRequests = h . HealthChecks . Passive . UnhealthyRequestCount
}
allUpstreams = append ( allUpstreams , & upstreamCopy )
// upstreams need independent access to the passive
// health check policy because passive health checks
// run without access to h.
if h . HealthChecks != nil {
upstream . healthCheckPolicy = h . HealthChecks . Passive
}
}
}
}
// replace the unmarshaled upstreams (possible 1:many
// address mapping) with our list, which is mapped 1:1,
// thus may have expanded the original list
h . Upstreams = allUpstreams
return nil
return nil
}
}
@ -247,7 +204,7 @@ func (h *Handler) Cleanup() error {
// remove hosts from our config from the pool
// remove hosts from our config from the pool
for _ , upstream := range h . Upstreams {
for _ , upstream := range h . Upstreams {
hosts . Delete ( upstream . dialInfo . String ( ) )
hosts . Delete ( upstream . Dial )
}
}
return nil
return nil
@ -284,17 +241,25 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
continue
continue
}
}
// the dial address may vary per-request if placeholders are
// used, so perform those replacements here; the resulting
// DialInfo struct should have valid network address syntax
dialInfo , err := fillDialInfo ( upstream , repl )
if err != nil {
return fmt . Errorf ( "making dial info: %v" , err )
}
// attach to the request information about how to dial the upstream;
// attach to the request information about how to dial the upstream;
// this is necessary because the information cannot be sufficiently
// this is necessary because the information cannot be sufficiently
// or satisfactorily represented in a URL
// or satisfactorily represented in a URL
ctx := context . WithValue ( r . Context ( ) , DialInfoCtxKey , upstream . dialInfo )
ctx := context . WithValue ( r . Context ( ) , DialInfoCtxKey , dialInfo )
r = r . WithContext ( ctx )
r = r . WithContext ( ctx )
// set placeholders with information about this upstream
// set placeholders with information about this upstream
repl . Set ( "http.handlers.reverse_proxy.upstream.address" , upstream . dialInfo . String ( ) )
repl . Set ( "http.handlers.reverse_proxy.upstream.address" , dialInfo . String ( ) )
repl . Set ( "http.handlers.reverse_proxy.upstream.hostport" , upstream . dialInfo . Address )
repl . Set ( "http.handlers.reverse_proxy.upstream.hostport" , dialInfo . Address )
repl . Set ( "http.handlers.reverse_proxy.upstream.host" , upstream . dialInfo . Host )
repl . Set ( "http.handlers.reverse_proxy.upstream.host" , dialInfo . Host )
repl . Set ( "http.handlers.reverse_proxy.upstream.port" , upstream . dialInfo . Port )
repl . Set ( "http.handlers.reverse_proxy.upstream.port" , dialInfo . Port )
repl . Set ( "http.handlers.reverse_proxy.upstream.requests" , strconv . Itoa ( upstream . Host . NumRequests ( ) ) )
repl . Set ( "http.handlers.reverse_proxy.upstream.requests" , strconv . Itoa ( upstream . Host . NumRequests ( ) ) )
repl . Set ( "http.handlers.reverse_proxy.upstream.max_requests" , strconv . Itoa ( upstream . MaxRequests ) )
repl . Set ( "http.handlers.reverse_proxy.upstream.max_requests" , strconv . Itoa ( upstream . MaxRequests ) )
repl . Set ( "http.handlers.reverse_proxy.upstream.fails" , strconv . Itoa ( upstream . Host . Fails ( ) ) )
repl . Set ( "http.handlers.reverse_proxy.upstream.fails" , strconv . Itoa ( upstream . Host . Fails ( ) ) )
@ -311,7 +276,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
}
}
// proxy the request to that upstream
// proxy the request to that upstream
proxyErr = h . reverseProxy ( w , r , upstream )
proxyErr = h . reverseProxy ( w , r , dialInfo )
if proxyErr == nil || proxyErr == context . Canceled {
if proxyErr == nil || proxyErr == context . Canceled {
// context.Canceled happens when the downstream client
// context.Canceled happens when the downstream client
// cancels the request, which is not our failure
// cancels the request, which is not our failure
@ -405,12 +370,12 @@ func (h Handler) prepareRequest(req *http.Request) error {
// reverseProxy performs a round-trip to the given backend and processes the response with the client.
// reverseProxy performs a round-trip to the given backend and processes the response with the client.
// (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the
// (This method is mostly the beginning of what was borrowed from the net/http/httputil package in the
// Go standard library which was used as the foundation.)
// Go standard library which was used as the foundation.)
func ( h * Handler ) reverseProxy ( rw http . ResponseWriter , req * http . Request , upstream * Upstream ) error {
func ( h * Handler ) reverseProxy ( rw http . ResponseWriter , req * http . Request , di DialInfo ) error {
u pstream. Host . CountRequest ( 1 )
di . U pstream. Host . CountRequest ( 1 )
defer u pstream. Host . CountRequest ( - 1 )
defer di . U pstream. Host . CountRequest ( - 1 )
// point the request to this upstream
// point the request to this upstream
h . directRequest ( req , upstream )
h . directRequest ( req , di )
// do the round-trip
// do the round-trip
start := time . Now ( )
start := time . Now ( )
@ -421,8 +386,8 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstre
}
}
// update circuit breaker on current conditions
// update circuit breaker on current conditions
if u pstream. cb != nil {
if di . U pstream. cb != nil {
u pstream. cb . RecordMetric ( res . StatusCode , latency )
di . U pstream. cb . RecordMetric ( res . StatusCode , latency )
}
}
// perform passive health checks (if enabled)
// perform passive health checks (if enabled)
@ -430,14 +395,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstre
// strike if the status code matches one that is "bad"
// strike if the status code matches one that is "bad"
for _ , badStatus := range h . HealthChecks . Passive . UnhealthyStatus {
for _ , badStatus := range h . HealthChecks . Passive . UnhealthyStatus {
if caddyhttp . StatusCodeMatches ( res . StatusCode , badStatus ) {
if caddyhttp . StatusCodeMatches ( res . StatusCode , badStatus ) {
h . countFailure ( u pstream)
h . countFailure ( di . U pstream)
}
}
}
}
// strike if the roundtrip took too long
// strike if the roundtrip took too long
if h . HealthChecks . Passive . UnhealthyLatency > 0 &&
if h . HealthChecks . Passive . UnhealthyLatency > 0 &&
latency >= time . Duration ( h . HealthChecks . Passive . UnhealthyLatency ) {
latency >= time . Duration ( h . HealthChecks . Passive . UnhealthyLatency ) {
h . countFailure ( u pstream)
h . countFailure ( di . U pstream)
}
}
}
}
@ -554,23 +519,21 @@ func (lb LoadBalancing) tryAgain(start time.Time, proxyErr error, req *http.Requ
return true
return true
}
}
// directRequest modifies only req.URL so that it points to the
// directRequest modifies only req.URL so that it points to the upstream
// given upstream host . It must modify ONLY the request URL.
// in the given DialInfo . It must modify ONLY the request URL.
func ( h Handler ) directRequest ( req * http . Request , upstream * Upstream ) {
func ( h Handler ) directRequest ( req * http . Request , di DialInfo ) {
if req . URL . Host == "" {
if req . URL . Host == "" {
// we need a host, so set the upstream's host address
// we need a host, so set the upstream's host address
full Host := upstream . dialInfo . Address
req Host := di . Address
// but if the port matches the scheme, strip the port because
// if the port equates to the scheme, strip the port because
// it's weird to make a request like http://example.com:80/.
// it's weird to make a request like http://example.com:80/.
host , port , err := net . SplitHostPort ( fullHost )
if ( req . URL . Scheme == "http" && di . Port == "80" ) ||
if err == nil &&
( req . URL . Scheme == "https" && di . Port == "443" ) {
( req . URL . Scheme == "http" && port == "80" ) ||
reqHost = di . Host
( req . URL . Scheme == "https" && port == "443" ) {
fullHost = host
}
}
req . URL . Host = full Host
req . URL . Host = req Host
}
}
}
}