2020-10-08 10:12:26 +00:00
package origin
import (
"bufio"
2020-10-20 15:26:55 +00:00
"context"
2020-10-08 10:12:26 +00:00
"crypto/tls"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/cloudflare/cloudflared/buffer"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/logger"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/websocket"
"github.com/pkg/errors"
)
const (
TagHeaderNamePrefix = "Cf-Warp-Tag-"
)
type client struct {
config * ProxyConfig
logger logger . Service
bufferPool * buffer . Pool
}
func NewClient ( config * ProxyConfig , logger logger . Service ) connection . OriginClient {
return & client {
config : config ,
logger : logger ,
bufferPool : buffer . NewPool ( 512 * 1024 ) ,
}
}
type ProxyConfig struct {
Client http . RoundTripper
URL * url . URL
TLSConfig * tls . Config
HostHeader string
NoChunkedEncoding bool
Tags [ ] tunnelpogs . Tag
}
func ( c * client ) Proxy ( w connection . ResponseWriter , req * http . Request , isWebsocket bool ) error {
incrementRequests ( )
defer decrementConcurrentRequests ( )
cfRay := findCfRayHeader ( req )
lbProbe := isLBProbeRequest ( req )
c . appendTagHeaders ( req )
c . logRequest ( req , cfRay , lbProbe )
var (
resp * http . Response
err error
)
if isWebsocket {
resp , err = c . proxyWebsocket ( w , req )
} else {
resp , err = c . proxyHTTP ( w , req )
}
if err != nil {
c . logger . Errorf ( "HTTP request error: %s" , err )
responseByCode . WithLabelValues ( "502" ) . Inc ( )
w . WriteErrorResponse ( err )
return err
}
c . logResponseOk ( resp , cfRay , lbProbe )
return nil
}
func ( c * client ) proxyHTTP ( w connection . ResponseWriter , req * http . Request ) ( * http . Response , error ) {
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if c . config . NoChunkedEncoding {
req . TransferEncoding = [ ] string { "gzip" , "deflate" }
cLength , err := strconv . Atoi ( req . Header . Get ( "Content-Length" ) )
if err == nil {
req . ContentLength = int64 ( cLength )
}
}
// Request origin to keep connection alive to improve performance
req . Header . Set ( "Connection" , "keep-alive" )
c . setHostHeader ( req )
resp , err := c . config . Client . RoundTrip ( req )
if err != nil {
return nil , errors . Wrap ( err , "Error proxying request to origin" )
}
defer resp . Body . Close ( )
err = w . WriteRespHeaders ( resp )
if err != nil {
return nil , errors . Wrap ( err , "Error writing response header" )
}
if isEventStream ( resp ) {
//h.observer.Debug("Detected Server-Side Events from Origin")
c . writeEventStream ( w , resp . Body )
} else {
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
// compression generates dictionary on first write
buf := c . bufferPool . Get ( )
defer c . bufferPool . Put ( buf )
io . CopyBuffer ( w , resp . Body , buf )
}
return resp , nil
}
func ( c * client ) proxyWebsocket ( w connection . ResponseWriter , req * http . Request ) ( * http . Response , error ) {
c . setHostHeader ( req )
conn , resp , err := websocket . ClientConnect ( req , c . config . TLSConfig )
if err != nil {
return nil , err
}
2020-10-20 15:26:55 +00:00
serveCtx , cancel := context . WithCancel ( req . Context ( ) )
defer cancel ( )
go func ( ) {
<- serveCtx . Done ( )
conn . Close ( )
} ( )
2020-10-08 10:12:26 +00:00
err = w . WriteRespHeaders ( resp )
if err != nil {
return nil , errors . Wrap ( err , "Error writing response header" )
}
// Copy to/from stream to the undelying connection. Use the underlying
// connection because cloudflared doesn't operate on the message themselves
websocket . Stream ( conn . UnderlyingConn ( ) , w )
return resp , nil
}
func ( c * client ) writeEventStream ( w connection . ResponseWriter , respBody io . ReadCloser ) {
reader := bufio . NewReader ( respBody )
for {
line , err := reader . ReadBytes ( '\n' )
if err != nil {
break
}
w . Write ( line )
}
}
func ( c * client ) setHostHeader ( req * http . Request ) {
if c . config . HostHeader != "" {
req . Header . Set ( "Host" , c . config . HostHeader )
req . Host = c . config . HostHeader
}
}
func ( c * client ) appendTagHeaders ( r * http . Request ) {
for _ , tag := range c . config . Tags {
r . Header . Add ( TagHeaderNamePrefix + tag . Name , tag . Value )
}
}
func ( c * client ) logRequest ( r * http . Request , cfRay string , lbProbe bool ) {
if cfRay != "" {
c . logger . Debugf ( "CF-RAY: %s %s %s %s" , cfRay , r . Method , r . URL , r . Proto )
} else if lbProbe {
c . logger . Debugf ( "CF-RAY: %s Load Balancer health check %s %s %s" , cfRay , r . Method , r . URL , r . Proto )
} else {
c . logger . Debugf ( "CF-RAY: %s All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s " , cfRay , r . Method , r . URL , r . Proto )
}
c . logger . Debugf ( "CF-RAY: %s Request Headers %+v" , cfRay , r . Header )
if contentLen := r . ContentLength ; contentLen == - 1 {
c . logger . Debugf ( "CF-RAY: %s Request Content length unknown" , cfRay )
} else {
c . logger . Debugf ( "CF-RAY: %s Request content length %d" , cfRay , contentLen )
}
}
func ( c * client ) logResponseOk ( r * http . Response , cfRay string , lbProbe bool ) {
responseByCode . WithLabelValues ( "200" ) . Inc ( )
if cfRay != "" {
c . logger . Debugf ( "CF-RAY: %s %s" , cfRay , r . Status )
} else if lbProbe {
c . logger . Debugf ( "Response to Load Balancer health check %s" , r . Status )
} else {
c . logger . Infof ( "%s" , r . Status )
}
c . logger . Debugf ( "CF-RAY: %s Response Headers %+v" , cfRay , r . Header )
if contentLen := r . ContentLength ; contentLen == - 1 {
c . logger . Debugf ( "CF-RAY: %s Response content length unknown" , cfRay )
} else {
c . logger . Debugf ( "CF-RAY: %s Response content length %d" , cfRay , contentLen )
}
}
func findCfRayHeader ( req * http . Request ) string {
return req . Header . Get ( "Cf-Ray" )
}
func isLBProbeRequest ( req * http . Request ) bool {
return strings . HasPrefix ( req . UserAgent ( ) , lbProbeUserAgentPrefix )
}
func uint8ToString ( input uint8 ) string {
return strconv . FormatUint ( uint64 ( input ) , 10 )
}
func isEventStream ( response * http . Response ) bool {
if response . Header . Get ( "content-type" ) == "text/event-stream" {
return true
}
return false
}