mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 792520d313 | |||
| 8b9cfcde78 | |||
| 5ba3b3b309 | |||
| 63a29f421a | |||
| e20c4f8752 | |||
| ab4dda5427 | |||
| 5943808746 | |||
| ed57ee64e8 | |||
| 9c298e4851 | |||
| 8b794390e5 | |||
| a6c2348127 | |||
| 6681d179dc | |||
| 2146f71b45 | |||
| 3b93914612 | |||
| b4700a52e3 | |||
| 368066a966 | |||
| e2262085e5 | |||
| 5e2b43adb5 | |||
| c7dca16300 | |||
| 9d5bd256be | |||
| e9c2afec56 |
+18
-5
@@ -1,16 +1,29 @@
|
||||
# Experimental: This is a new format for release notes. The format and availability is subject to change.
|
||||
**Experimental**: This is a new format for release notes. The format and availability is subject to change.
|
||||
|
||||
## 2021.2.5
|
||||
|
||||
### New Features
|
||||
- We introduce [Cloudflare One Routing](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel) in beta mode, where users can create private networks with RFC 1918 IP addresses on Cloudflare's network. Clients running Cloudflare WARP client in the same organization can then connect to services made available by Argo Tunnel. Please share your feedback in the GitHub issue opened.
|
||||
|
||||
|
||||
## 2021.2.4
|
||||
|
||||
### Bug Fixes
|
||||
- Reverts the Improvement released in 2021.2.3 for CLI arguments as it introduced a regression where cloudflared failed to read URLs in configuration files.
|
||||
- Cloudflared now logs the reason for failed connections if the error is recoverable.
|
||||
|
||||
|
||||
## 2021.2.3
|
||||
|
||||
### Backward Incompatible Changes
|
||||
[db-connect] Removes db-connect from `cloudflared`. The Cloudflare Workers product will continue to support db-connect implementations with versions of `cloudflared` that predate this release and include support for db-connect.
|
||||
- Removes db-connect. The Cloudflare Workers product will continue to support db-connect implementations with versions of cloudflared that predate this release and include support for db-connect.
|
||||
|
||||
### New Features
|
||||
[TCP connects] Introduces support for proxy configurations with websockets in arbitrary TCP connections (#318)
|
||||
- Introduces support for proxy configurations with websockets in arbitrary TCP connections (#318).
|
||||
|
||||
### Improvements
|
||||
[command line interface] Nested commands (such as cloudflared tunnel run) now consider CLI arguments even if they appear earlier in the command. E.g. `cloudflared --config config.yaml tunnel run` will now behave similarly to `cloudflared tunnel --config config.yaml run`
|
||||
- Nested command line arguments (such as `cloudflared tunnel run`) now consider CLI arguments even if they appear earlier in the command. E.g. `cloudflared --config config.yaml tunnel run` will now behave similarly to `cloudflared tunnel --config config.yaml run`
|
||||
|
||||
### Bug Fixes
|
||||
[dns-proxy] The maximum number of upstream connections is now limited by default which should fix reported issues of cloudflared exhausting CPU usage when faced with connectivity issues.
|
||||
- The maximum number of upstream connections is now limited by default which should fix reported issues of cloudflared exhausting CPU usage when faced with connectivity issues.
|
||||
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
2021.2.5
|
||||
- 2021-02-23 Publish change notes for 2021.2.5
|
||||
- 2021-02-11 TUN-3838: ResponseWriter no longer reads and origin error tests
|
||||
- 2021-02-10 TUN-3895: Tests for socks stream handler
|
||||
- 2021-02-19 TUN-3939: Add logging that shows that Warp-routing is enabled
|
||||
- 2021-02-02 TUN-3817: Adds tests for websocket based streaming regression
|
||||
- 2021-02-04 TUN-3799: extended the Stream interface to take a logger and added debug logs for io.Copy errors
|
||||
- 2021-02-03 TUN-3855: Add ability to override target of 'access ssh' command to a different host for testing
|
||||
- 2021-02-04 TUN-3853: Respond with ws headers from the origin service rather than generating our own
|
||||
- 2021-02-08 TUN-3889: Move host header override logic to httpService
|
||||
- 2021-02-05 TUN-3868: Refactor singleTCPService and bridgeService to tcpOverWSService and rawTCPService
|
||||
- 2021-01-21 TUN-3753: Select http2 protocol when warp routing is enabled
|
||||
- 2021-01-26 TUN-3809: Allow routes ip show to output as JSON or YAML
|
||||
- 2021-01-11 TUN-3615: added support to proxy tcp streams
|
||||
- 2021-01-17 TUN-3725: Warp-routing is independent of ingress
|
||||
- 2021-01-15 TUN-3764: Actively flush data for TCP streams
|
||||
- 2020-12-09 TUN-3617: Separate service from client, and implement different client for http vs. tcp origins
|
||||
|
||||
2021.2.4
|
||||
- 2021-02-22 TUN-3948: Log error when retrying connection
|
||||
- 2021-02-23 TUN-3964: Revert "TUN-3922: Repoint urfave/cli/v2 library at patched branch at github.com/ipostelnik/cli/v2@fixed which correctly handles reading flags declared at multiple levels of subcommands."
|
||||
- 2021-02-23 Publish release notes for 2021.2.4
|
||||
|
||||
2021.2.3
|
||||
- 2021-02-23 Publish release notes for 2021.2.3
|
||||
- 2021-02-10 TUN-3902: Add jitter to backoffhandler
|
||||
|
||||
+5
-2
@@ -4,6 +4,7 @@
|
||||
package carrier
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -20,8 +21,10 @@ import (
|
||||
const LogFieldOriginURL = "originURL"
|
||||
|
||||
type StartOptions struct {
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
OriginURL string
|
||||
Headers http.Header
|
||||
Host string
|
||||
TLSClientConfig *tls.Config
|
||||
}
|
||||
|
||||
// Connection wraps up all the needed functions to forward over the tunnel
|
||||
|
||||
@@ -44,7 +44,7 @@ func (s *testStreamer) Write(p []byte) (int, error) {
|
||||
func TestStartClient(t *testing.T) {
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
log := zerolog.Nop()
|
||||
wsConn := NewWSConnection(&log, false)
|
||||
wsConn := NewWSConnection(&log)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestStartServer(t *testing.T) {
|
||||
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
|
||||
log := zerolog.Nop()
|
||||
shutdownC := make(chan struct{})
|
||||
wsConn := NewWSConnection(&log, false)
|
||||
wsConn := NewWSConnection(&log)
|
||||
ts := newTestWebSocketServer()
|
||||
defer ts.Close()
|
||||
options := &StartOptions{
|
||||
|
||||
+15
-17
@@ -8,6 +8,7 @@ import (
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
|
||||
|
||||
@@ -23,7 +24,7 @@ type Websocket struct {
|
||||
}
|
||||
|
||||
type wsdialer struct {
|
||||
conn *cfwebsocket.Conn
|
||||
conn *cfwebsocket.GorillaConn
|
||||
}
|
||||
|
||||
func (d *wsdialer) Dial(address string) (io.ReadWriteCloser, *socks.AddrSpec, error) {
|
||||
@@ -37,10 +38,9 @@ func (d *wsdialer) Dial(address string) (io.ReadWriteCloser, *socks.AddrSpec, er
|
||||
}
|
||||
|
||||
// NewWSConnection returns a new connection object
|
||||
func NewWSConnection(log *zerolog.Logger, isSocks bool) Connection {
|
||||
func NewWSConnection(log *zerolog.Logger) Connection {
|
||||
return &Websocket{
|
||||
log: log,
|
||||
isSocks: isSocks,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,38 +54,36 @@ func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) erro
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
if ws.isSocks {
|
||||
dialer := &wsdialer{conn: wsConn}
|
||||
requestHandler := socks.NewRequestHandler(dialer)
|
||||
socksServer := socks.NewConnectionHandler(requestHandler)
|
||||
|
||||
_ = socksServer.Serve(conn)
|
||||
} else {
|
||||
cfwebsocket.Stream(wsConn, conn)
|
||||
}
|
||||
ingress.Stream(wsConn, conn, ws.log)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartServer creates a Websocket server to listen for connections.
|
||||
// This is used on the origin (tunnel) side to take data from the muxer and send it to the origin
|
||||
func (ws *Websocket) StartServer(listener net.Listener, remote string, shutdownC <-chan struct{}) error {
|
||||
return cfwebsocket.StartProxyServer(ws.log, listener, remote, shutdownC, cfwebsocket.DefaultStreamHandler)
|
||||
return cfwebsocket.StartProxyServer(ws.log, listener, remote, shutdownC, ingress.DefaultStreamHandler)
|
||||
}
|
||||
|
||||
// createWebsocketStream will create a WebSocket connection to stream data over
|
||||
// It also handles redirects from Access and will present that flow if
|
||||
// the token is not present on the request
|
||||
func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebsocket.Conn, error) {
|
||||
func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebsocket.GorillaConn, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = options.Headers
|
||||
if options.Host != "" {
|
||||
req.Host = options.Host
|
||||
}
|
||||
|
||||
dump, err := httputil.DumpRequest(req, false)
|
||||
log.Debug().Msgf("Websocket request: %s", string(dump))
|
||||
|
||||
wsConn, resp, err := cfwebsocket.ClientConnect(req, nil)
|
||||
dialer := &websocket.Dialer{
|
||||
TLSClientConfig: options.TLSClientConfig,
|
||||
}
|
||||
wsConn, resp, err := cfwebsocket.ClientConnect(req, dialer)
|
||||
defer closeRespBody(resp)
|
||||
|
||||
if err != nil && IsAccessResponse(resp) {
|
||||
@@ -97,7 +95,7 @@ func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebso
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfwebsocket.Conn{Conn: wsConn}, nil
|
||||
return &cfwebsocket.GorillaConn{Conn: wsConn}, nil
|
||||
}
|
||||
|
||||
// createAccessAuthenticatedStream will try load a token from storage and make
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -46,7 +48,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
wsConn := carrier.NewWSConnection(log, false)
|
||||
wsConn := carrier.NewWSConnection(log)
|
||||
|
||||
log.Info().Str(LogFieldHost, validURL.Host).Msg("Start Websocket listener")
|
||||
return carrier.StartForwarder(wsConn, validURL.Host, shutdown, options)
|
||||
@@ -84,10 +86,30 @@ func ssh(c *cli.Context) error {
|
||||
options := &carrier.StartOptions{
|
||||
OriginURL: originURL,
|
||||
Headers: headers,
|
||||
Host: hostname,
|
||||
}
|
||||
|
||||
if connectTo := c.String(sshConnectTo); connectTo != "" {
|
||||
parts := strings.Split(connectTo, ":")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
options.OriginURL = fmt.Sprintf("https://%s", parts[0])
|
||||
case 2:
|
||||
options.OriginURL = fmt.Sprintf("https://%s:%s", parts[0], parts[1])
|
||||
case 3:
|
||||
options.OriginURL = fmt.Sprintf("https://%s:%s", parts[2], parts[1])
|
||||
options.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: parts[0],
|
||||
}
|
||||
log.Warn().Msgf("Using insecure SSL connection because SNI overridden to %s", parts[0])
|
||||
default:
|
||||
return fmt.Errorf("invalid connection override: %s", connectTo)
|
||||
}
|
||||
}
|
||||
|
||||
// we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side
|
||||
wsConn := carrier.NewWSConnection(log, false)
|
||||
wsConn := carrier.NewWSConnection(log)
|
||||
|
||||
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
|
||||
forwarder, err := config.ValidateUrl(c, true)
|
||||
@@ -95,7 +117,6 @@ func ssh(c *cli.Context) error {
|
||||
log.Err(err).Msg("Error validating origin URL")
|
||||
return errors.Wrap(err, "error validating origin URL")
|
||||
}
|
||||
|
||||
log.Info().Str(LogFieldHost, forwarder.Host).Msg("Start Websocket listener")
|
||||
err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options)
|
||||
if err != nil {
|
||||
|
||||
@@ -33,6 +33,7 @@ const (
|
||||
sshTokenIDFlag = "service-token-id"
|
||||
sshTokenSecretFlag = "service-token-secret"
|
||||
sshGenCertFlag = "short-lived-cert"
|
||||
sshConnectTo = "connect-to"
|
||||
sshConfigTemplate = `
|
||||
Add to your {{.Home}}/.ssh/config:
|
||||
|
||||
@@ -54,7 +55,7 @@ Host cfpipe-{{.Hostname}}
|
||||
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
|
||||
|
||||
var (
|
||||
shutdownC chan struct{}
|
||||
shutdownC chan struct{}
|
||||
)
|
||||
|
||||
// Init will initialize and store vars from the main program
|
||||
@@ -164,6 +165,11 @@ func Commands() []*cli.Command {
|
||||
Aliases: []string{"loglevel"}, //added to match the tunnel side
|
||||
Usage: "Application logging level {fatal, error, info, debug}. ",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: sshConnectTo,
|
||||
Hidden: true,
|
||||
Usage: "Connect to alternate location for testing, value is host, host:port, or sni:port:host",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -226,10 +226,15 @@ type OriginRequestConfig struct {
|
||||
type Configuration struct {
|
||||
TunnelID string `yaml:"tunnel"`
|
||||
Ingress []UnvalidatedIngressRule
|
||||
WarpRouting WarpRoutingConfig `yaml:"warp-routing"`
|
||||
OriginRequest OriginRequestConfig `yaml:"originRequest"`
|
||||
sourceFile string
|
||||
}
|
||||
|
||||
type WarpRoutingConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
type configFileSettings struct {
|
||||
Configuration `yaml:",inline"`
|
||||
// older settings will be aggregated into the generic map, should be read via cli.Context
|
||||
|
||||
@@ -20,6 +20,9 @@ func TestConfigFileSettings(t *testing.T) {
|
||||
Path: "",
|
||||
Service: "https://localhost:8001",
|
||||
}
|
||||
warpRouting = WarpRoutingConfig{
|
||||
Enabled: true,
|
||||
}
|
||||
)
|
||||
rawYAML := `
|
||||
tunnel: config-file-test
|
||||
@@ -29,6 +32,8 @@ ingress:
|
||||
service: https://localhost:8000
|
||||
- hostname: "*"
|
||||
service: https://localhost:8001
|
||||
warp-routing:
|
||||
enabled: true
|
||||
retries: 5
|
||||
grace-period: 30s
|
||||
percentage: 3.14
|
||||
@@ -47,6 +52,7 @@ counters:
|
||||
assert.Equal(t, "config-file-test", config.TunnelID)
|
||||
assert.Equal(t, firstIngress, config.Ingress[0])
|
||||
assert.Equal(t, secondIngress, config.Ingress[1])
|
||||
assert.Equal(t, warpRouting, config.WarpRouting)
|
||||
|
||||
retries, err := config.Int("retries")
|
||||
assert.NoError(t, err)
|
||||
@@ -73,4 +79,5 @@ counters:
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 123, counters[0])
|
||||
assert.Equal(t, 456, counters[1])
|
||||
|
||||
}
|
||||
|
||||
@@ -195,6 +195,7 @@ func prepareTunnelConfig(
|
||||
ingressRules ingress.Ingress
|
||||
classicTunnel *connection.ClassicTunnelConfig
|
||||
)
|
||||
cfg := config.GetConfiguration()
|
||||
if isNamedTunnel {
|
||||
clientUUID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
@@ -206,7 +207,7 @@ func prepareTunnelConfig(
|
||||
Version: version,
|
||||
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
|
||||
}
|
||||
ingressRules, err = ingress.ParseIngress(config.GetConfiguration())
|
||||
ingressRules, err = ingress.ParseIngress(cfg)
|
||||
if err != nil && err != ingress.ErrNoIngressRules {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
@@ -230,7 +231,14 @@ func prepareTunnelConfig(
|
||||
}
|
||||
}
|
||||
|
||||
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), namedTunnel, edgediscovery.HTTP2Percentage, origin.ResolveTTL, log)
|
||||
var warpRoutingService *ingress.WarpRoutingService
|
||||
warpRoutingEnabled := isWarpRoutingEnabled(cfg.WarpRouting, isNamedTunnel)
|
||||
if warpRoutingEnabled {
|
||||
warpRoutingService = ingress.NewWarpRoutingService()
|
||||
log.Info().Msgf("Warp-routing is enabled")
|
||||
}
|
||||
|
||||
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), warpRoutingEnabled, namedTunnel, edgediscovery.HTTP2Percentage, origin.ResolveTTL, log)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
@@ -245,9 +253,9 @@ func prepareTunnelConfig(
|
||||
edgeTLSConfigs[p] = edgeTLSConfig
|
||||
}
|
||||
|
||||
originClient := origin.NewClient(ingressRules, tags, log)
|
||||
originProxy := origin.NewOriginProxy(ingressRules, warpRoutingService, tags, log)
|
||||
connectionConfig := &connection.Config{
|
||||
OriginClient: originClient,
|
||||
OriginProxy: originProxy,
|
||||
GracePeriod: c.Duration("grace-period"),
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
}
|
||||
@@ -286,6 +294,10 @@ func prepareTunnelConfig(
|
||||
}, ingressRules, nil
|
||||
}
|
||||
|
||||
func isWarpRoutingEnabled(warpConfig config.WarpRoutingConfig, isNamedTunnel bool) bool {
|
||||
return warpConfig.Enabled && isNamedTunnel
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func buildRouteIPSubcommand() *cli.Command {
|
||||
Usage: "Show the routing table",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip show [flags]",
|
||||
Description: `Shows your organization's private route table. You can use flags to filter the results.`,
|
||||
Flags: teamnet.FilterFlags,
|
||||
Flags: showRoutesFlags(),
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
@@ -62,6 +62,13 @@ func buildRouteIPSubcommand() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func showRoutesFlags() []cli.Flag {
|
||||
flags := make([]cli.Flag, 0)
|
||||
flags = append(flags, teamnet.FilterFlags...)
|
||||
flags = append(flags, outputFormatFlag)
|
||||
return flags
|
||||
}
|
||||
|
||||
func showRoutesCommand(c *cli.Context) error {
|
||||
sc, err := newSubcommandContext(c)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
const LogFieldConnIndex = "connIndex"
|
||||
|
||||
type Config struct {
|
||||
OriginClient OriginClient
|
||||
OriginProxy OriginProxy
|
||||
GracePeriod time.Duration
|
||||
ReplaceExisting bool
|
||||
}
|
||||
@@ -50,14 +51,49 @@ func (c *ClassicTunnelConfig) IsTrialZone() bool {
|
||||
return c.Hostname == ""
|
||||
}
|
||||
|
||||
type OriginClient interface {
|
||||
Proxy(w ResponseWriter, req *http.Request, isWebsocket bool) error
|
||||
// Type indicates the connection type of the connection.
|
||||
type Type int
|
||||
|
||||
const (
|
||||
TypeWebsocket Type = iota
|
||||
TypeTCP
|
||||
TypeControlStream
|
||||
TypeHTTP
|
||||
)
|
||||
|
||||
// ShouldFlush returns whether this kind of connection should actively flush data
|
||||
func (t Type) shouldFlush() bool {
|
||||
switch t {
|
||||
case TypeWebsocket, TypeTCP, TypeControlStream:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (t Type) String() string {
|
||||
switch t {
|
||||
case TypeWebsocket:
|
||||
return "websocket"
|
||||
case TypeTCP:
|
||||
return "tcp"
|
||||
case TypeControlStream:
|
||||
return "control stream"
|
||||
case TypeHTTP:
|
||||
return "http"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown Type %d", t)
|
||||
}
|
||||
}
|
||||
|
||||
type OriginProxy interface {
|
||||
// If Proxy returns an error, the caller is responsible for writing the error status to ResponseWriter
|
||||
Proxy(w ResponseWriter, req *http.Request, sourceConnectionType Type) error
|
||||
}
|
||||
|
||||
type ResponseWriter interface {
|
||||
WriteRespHeaders(*http.Response) error
|
||||
WriteErrorResponse()
|
||||
io.ReadWriter
|
||||
WriteRespHeaders(status int, header http.Header) error
|
||||
io.Writer
|
||||
}
|
||||
|
||||
type ConnectedFuse interface {
|
||||
|
||||
@@ -19,8 +19,8 @@ const (
|
||||
|
||||
var (
|
||||
testConfig = &Config{
|
||||
OriginClient: &mockOriginClient{},
|
||||
GracePeriod: time.Millisecond * 100,
|
||||
OriginProxy: &mockOriginProxy{},
|
||||
GracePeriod: time.Millisecond * 100,
|
||||
}
|
||||
log = zerolog.Nop()
|
||||
testOriginURL = &url.URL{
|
||||
@@ -38,11 +38,11 @@ type testRequest struct {
|
||||
isProxyError bool
|
||||
}
|
||||
|
||||
type mockOriginClient struct {
|
||||
type mockOriginProxy struct {
|
||||
}
|
||||
|
||||
func (moc *mockOriginClient) Proxy(w ResponseWriter, r *http.Request, isWebsocket bool) error {
|
||||
if isWebsocket {
|
||||
func (moc *mockOriginProxy) Proxy(w ResponseWriter, r *http.Request, sourceConnectionType Type) error {
|
||||
if sourceConnectionType == TypeWebsocket {
|
||||
return wsEndpoint(w, r)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
@@ -74,7 +74,7 @@ func wsEndpoint(w ResponseWriter, r *http.Request) error {
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
}
|
||||
_ = w.WriteRespHeaders(resp)
|
||||
_ = w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
||||
clientReader := nowriter{r.Body}
|
||||
go func() {
|
||||
for {
|
||||
@@ -95,7 +95,7 @@ func originRespEndpoint(w ResponseWriter, status int, data []byte) {
|
||||
resp := &http.Response{
|
||||
StatusCode: status,
|
||||
}
|
||||
_ = w.WriteRespHeaders(resp)
|
||||
_ = w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -216,7 +216,12 @@ func (h *h2muxConnection) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
return reqErr
|
||||
}
|
||||
|
||||
err := h.config.OriginClient.Proxy(respWriter, req, websocket.IsWebSocketUpgrade(req))
|
||||
var sourceConnectionType = TypeHTTP
|
||||
if websocket.IsWebSocketUpgrade(req) {
|
||||
sourceConnectionType = TypeWebsocket
|
||||
}
|
||||
|
||||
err := h.config.OriginProxy.Proxy(respWriter, req, sourceConnectionType)
|
||||
if err != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
return err
|
||||
@@ -240,8 +245,8 @@ type h2muxRespWriter struct {
|
||||
*h2mux.MuxedStream
|
||||
}
|
||||
|
||||
func (rp *h2muxRespWriter) WriteRespHeaders(resp *http.Response) error {
|
||||
headers := h2mux.H1ResponseToH2ResponseHeaders(resp)
|
||||
func (rp *h2muxRespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
headers := h2mux.H1ResponseToH2ResponseHeaders(status, header)
|
||||
headers = append(headers, h2mux.Header{Name: ResponseMetaHeaderField, Value: responseMetaHeaderOrigin})
|
||||
return rp.WriteHeaders(headers)
|
||||
}
|
||||
|
||||
+59
-29
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
const (
|
||||
internalUpgradeHeader = "Cf-Cloudflared-Proxy-Connection-Upgrade"
|
||||
tcpStreamHeader = "Cf-Cloudflared-Proxy-Src"
|
||||
websocketUpgrade = "websocket"
|
||||
controlStreamUpgrade = "control-stream"
|
||||
)
|
||||
@@ -96,31 +97,24 @@ func (c *http2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.activeRequestsWG.Add(1)
|
||||
defer c.activeRequestsWG.Done()
|
||||
|
||||
respWriter := &http2RespWriter{
|
||||
r: r.Body,
|
||||
w: w,
|
||||
}
|
||||
flusher, isFlusher := w.(http.Flusher)
|
||||
if !isFlusher {
|
||||
c.observer.log.Error().Msgf("%T doesn't implement http.Flusher", w)
|
||||
respWriter.WriteErrorResponse()
|
||||
connType := determineHTTP2Type(r)
|
||||
respWriter, err := newHTTP2RespWriter(r, w, connType)
|
||||
if err != nil {
|
||||
c.observer.log.Error().Msg(err.Error())
|
||||
return
|
||||
}
|
||||
respWriter.flusher = flusher
|
||||
var err error
|
||||
if isControlStreamUpgrade(r) {
|
||||
respWriter.shouldFlush = true
|
||||
err = c.serveControlStream(r.Context(), respWriter)
|
||||
c.controlStreamErr = err
|
||||
} else if isWebsocketUpgrade(r) {
|
||||
respWriter.shouldFlush = true
|
||||
stripWebsocketUpgradeHeader(r)
|
||||
err = c.config.OriginClient.Proxy(respWriter, r, true)
|
||||
} else {
|
||||
err = c.config.OriginClient.Proxy(respWriter, r, false)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
var proxyErr error
|
||||
switch connType {
|
||||
case TypeControlStream:
|
||||
proxyErr = c.serveControlStream(r.Context(), respWriter)
|
||||
case TypeWebsocket:
|
||||
stripWebsocketUpgradeHeader(r)
|
||||
proxyErr = c.config.OriginProxy.Proxy(respWriter, r, TypeWebsocket)
|
||||
default:
|
||||
proxyErr = c.config.OriginProxy.Proxy(respWriter, r, connType)
|
||||
}
|
||||
if proxyErr != nil {
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
}
|
||||
@@ -161,10 +155,29 @@ type http2RespWriter struct {
|
||||
shouldFlush bool
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) WriteRespHeaders(resp *http.Response) error {
|
||||
func newHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (*http2RespWriter, error) {
|
||||
flusher, isFlusher := w.(http.Flusher)
|
||||
if !isFlusher {
|
||||
respWriter := &http2RespWriter{
|
||||
r: r.Body,
|
||||
w: w,
|
||||
}
|
||||
respWriter.WriteErrorResponse()
|
||||
return nil, fmt.Errorf("%T doesn't implement http.Flusher", w)
|
||||
}
|
||||
|
||||
return &http2RespWriter{
|
||||
r: r.Body,
|
||||
w: w,
|
||||
flusher: flusher,
|
||||
shouldFlush: connType.shouldFlush(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rp *http2RespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
dest := rp.w.Header()
|
||||
userHeaders := make(http.Header, len(resp.Header))
|
||||
for header, values := range resp.Header {
|
||||
userHeaders := make(http.Header, len(header))
|
||||
for header, values := range header {
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
h2name := strings.ToLower(header)
|
||||
for _, v := range values {
|
||||
@@ -184,13 +197,12 @@ func (rp *http2RespWriter) WriteRespHeaders(resp *http.Response) error {
|
||||
// Perform user header serialization and set them in the single header
|
||||
dest.Set(canonicalResponseUserHeadersField, h2mux.SerializeHeaders(userHeaders))
|
||||
rp.setResponseMetaHeader(responseMetaHeaderOrigin)
|
||||
status := resp.StatusCode
|
||||
// HTTP2 removes support for 101 Switching Protocols https://tools.ietf.org/html/rfc7540#section-8.1.1
|
||||
if status == http.StatusSwitchingProtocols {
|
||||
status = http.StatusOK
|
||||
}
|
||||
rp.w.WriteHeader(status)
|
||||
if IsServerSentEvent(resp.Header) {
|
||||
if IsServerSentEvent(header) {
|
||||
rp.shouldFlush = true
|
||||
}
|
||||
if rp.shouldFlush {
|
||||
@@ -231,12 +243,30 @@ func (rp *http2RespWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func determineHTTP2Type(r *http.Request) Type {
|
||||
switch {
|
||||
case isWebsocketUpgrade(r):
|
||||
return TypeWebsocket
|
||||
case IsTCPStream(r):
|
||||
return TypeTCP
|
||||
case isControlStreamUpgrade(r):
|
||||
return TypeControlStream
|
||||
default:
|
||||
return TypeHTTP
|
||||
}
|
||||
}
|
||||
|
||||
func isControlStreamUpgrade(r *http.Request) bool {
|
||||
return strings.ToLower(r.Header.Get(internalUpgradeHeader)) == controlStreamUpgrade
|
||||
return r.Header.Get(internalUpgradeHeader) == controlStreamUpgrade
|
||||
}
|
||||
|
||||
func isWebsocketUpgrade(r *http.Request) bool {
|
||||
return strings.ToLower(r.Header.Get(internalUpgradeHeader)) == websocketUpgrade
|
||||
return r.Header.Get(internalUpgradeHeader) == websocketUpgrade
|
||||
}
|
||||
|
||||
// IsTCPStream discerns if the connection request needs a tcp stream proxy.
|
||||
func IsTCPStream(r *http.Request) bool {
|
||||
return r.Header.Get(tcpStreamHeader) != ""
|
||||
}
|
||||
|
||||
func stripWebsocketUpgradeHeader(r *http.Request) {
|
||||
|
||||
@@ -141,16 +141,29 @@ type PercentageFetcher func() (int32, error)
|
||||
|
||||
func NewProtocolSelector(
|
||||
protocolFlag string,
|
||||
warpRoutingEnabled bool,
|
||||
namedTunnel *NamedTunnelConfig,
|
||||
fetchFunc PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
log *zerolog.Logger,
|
||||
) (ProtocolSelector, error) {
|
||||
// Classic tunnel is only supported with h2mux
|
||||
if namedTunnel == nil {
|
||||
return &staticProtocolSelector{
|
||||
current: H2mux,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// warp routing can only be served over http2 connections
|
||||
if warpRoutingEnabled {
|
||||
if protocolFlag == H2mux.String() {
|
||||
log.Warn().Msg("Warp routing is only supported by http2 protocol. Upgrading protocol to http2")
|
||||
}
|
||||
return &staticProtocolSelector{
|
||||
current: HTTP2,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if protocolFlag == H2mux.String() {
|
||||
return &staticProtocolSelector{
|
||||
current: H2mux,
|
||||
|
||||
+45
-13
@@ -9,7 +9,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
testNoTTL = 0
|
||||
testNoTTL = 0
|
||||
noWarpRoutingEnabled = false
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -48,14 +49,15 @@ func (dmf *dynamicMockFetcher) fetch() PercentageFetcher {
|
||||
|
||||
func TestNewProtocolSelector(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol string
|
||||
expectedProtocol Protocol
|
||||
hasFallback bool
|
||||
expectedFallback Protocol
|
||||
namedTunnelConfig *NamedTunnelConfig
|
||||
fetchFunc PercentageFetcher
|
||||
wantErr bool
|
||||
name string
|
||||
protocol string
|
||||
expectedProtocol Protocol
|
||||
hasFallback bool
|
||||
expectedFallback Protocol
|
||||
warpRoutingEnabled bool
|
||||
namedTunnelConfig *NamedTunnelConfig
|
||||
fetchFunc PercentageFetcher
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "classic tunnel",
|
||||
@@ -108,6 +110,36 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
fetchFunc: mockFetcher(100),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "warp routing requesting h2mux",
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: HTTP2,
|
||||
hasFallback: false,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(100),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "warp routing http2",
|
||||
protocol: "http2",
|
||||
expectedProtocol: HTTP2,
|
||||
hasFallback: false,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(100),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
name: "warp routing auto",
|
||||
protocol: "auto",
|
||||
expectedProtocol: HTTP2,
|
||||
hasFallback: false,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(100),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
},
|
||||
{
|
||||
// None named tunnel can only use h2mux, so specifying an unknown protocol is not an error
|
||||
name: "classic tunnel unknown protocol",
|
||||
@@ -131,7 +163,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
selector, err := NewProtocolSelector(test.protocol, test.namedTunnelConfig, test.fetchFunc, testNoTTL, &log)
|
||||
selector, err := NewProtocolSelector(test.protocol, test.warpRoutingEnabled, test.namedTunnelConfig, test.fetchFunc, testNoTTL, &log)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err, fmt.Sprintf("test %s failed", test.name))
|
||||
} else {
|
||||
@@ -148,7 +180,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
|
||||
func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
selector, err := NewProtocolSelector("auto", testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
|
||||
selector, err := NewProtocolSelector("auto", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
|
||||
@@ -177,7 +209,7 @@ func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
|
||||
func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
selector, err := NewProtocolSelector("http2", testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
|
||||
selector, err := NewProtocolSelector("http2", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
@@ -206,7 +238,7 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
|
||||
func TestProtocolSelectorRefreshTTL(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{percentage: 100}
|
||||
selector, err := NewProtocolSelector("auto", testNamedTunnelConfig, fetcher.fetch(), time.Hour, &log)
|
||||
selector, err := NewProtocolSelector("auto", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), time.Hour, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ module github.com/cloudflare/cloudflared
|
||||
|
||||
go 1.15
|
||||
|
||||
replace github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210212191405-2b6ed1f5ef69
|
||||
|
||||
require (
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
||||
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93
|
||||
|
||||
@@ -350,8 +350,6 @@ github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/infobloxopen/go-trees v0.0.0-20190313150506-2af4e13f9062/go.mod h1:PcNJqIlcX/dj3DTG/+QQnRvSgTMG6CLpRMjWcv4+J6w=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210212191405-2b6ed1f5ef69 h1:TuZfNu8oRuQr6Y8UIvQlpXq+G+SzytMUGx8NrBt+2jg=
|
||||
github.com/ipostelnik/cli/v2 v2.3.1-0.20210212191405-2b6ed1f5ef69/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
|
||||
@@ -980,7 +978,6 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
+4
-4
@@ -125,12 +125,12 @@ func IsWebsocketClientHeader(headerName string) bool {
|
||||
headerName == "upgrade"
|
||||
}
|
||||
|
||||
func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
|
||||
func H1ResponseToH2ResponseHeaders(status int, h1 http.Header) (h2 []Header) {
|
||||
h2 = []Header{
|
||||
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
|
||||
{Name: ":status", Value: strconv.Itoa(status)},
|
||||
}
|
||||
userHeaders := make(http.Header, len(h1.Header))
|
||||
for header, values := range h1.Header {
|
||||
userHeaders := make(http.Header, len(h1))
|
||||
for header, values := range h1 {
|
||||
h2name := strings.ToLower(header)
|
||||
if h2name == "content-length" {
|
||||
// This header has meaning in HTTP/2 and will be used by the edge,
|
||||
|
||||
@@ -579,7 +579,7 @@ func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
|
||||
Header: mockHeaders,
|
||||
}
|
||||
|
||||
headers := H1ResponseToH2ResponseHeaders(&mockResponse)
|
||||
headers := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
|
||||
|
||||
serializedHeadersIndex := -1
|
||||
for i, header := range headers {
|
||||
@@ -622,7 +622,7 @@ func TestHeaderSize(t *testing.T) {
|
||||
Header: largeHeaders,
|
||||
}
|
||||
|
||||
serializedHeaders := H1ResponseToH2ResponseHeaders(&mockResponse)
|
||||
serializedHeaders := H1ResponseToH2ResponseHeaders(mockResponse.StatusCode, mockResponse.Header)
|
||||
request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
|
||||
assert.NoError(t, err)
|
||||
for _, header := range serializedHeaders {
|
||||
@@ -669,6 +669,6 @@ func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = H1ResponseToH2ResponseHeaders(h1resp)
|
||||
_ = H1ResponseToH2ResponseHeaders(h1resp.StatusCode, h1resp.Header)
|
||||
}
|
||||
}
|
||||
|
||||
+40
-9
@@ -24,6 +24,11 @@ var (
|
||||
ErrURLIncompatibleWithIngress = errors.New("You can't set the --url flag (or $TUNNEL_URL) when using multiple-origin ingress rules")
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceBastion = "bastion"
|
||||
ServiceWarpRouting = "warp-routing"
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -38,6 +43,7 @@ func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) {
|
||||
return &rule, i
|
||||
}
|
||||
}
|
||||
|
||||
i := len(ing.Rules) - 1
|
||||
return &ing.Rules[i], i
|
||||
}
|
||||
@@ -84,17 +90,35 @@ func NewSingleOrigin(c *cli.Context, allowURLFromArgs bool) (Ingress, error) {
|
||||
return ing, err
|
||||
}
|
||||
|
||||
// WarpRoutingService starts a tcp stream between the origin and requests from
|
||||
// warp clients.
|
||||
type WarpRoutingService struct {
|
||||
Proxy StreamBasedOriginProxy
|
||||
}
|
||||
|
||||
func NewWarpRoutingService() *WarpRoutingService {
|
||||
return &WarpRoutingService{Proxy: &rawTCPService{name: ServiceWarpRouting}}
|
||||
}
|
||||
|
||||
// Get a single origin service from the CLI/config.
|
||||
func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginService, error) {
|
||||
func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (originService, error) {
|
||||
if c.IsSet("hello-world") {
|
||||
return new(helloWorld), nil
|
||||
}
|
||||
if c.IsSet("url") || c.IsSet(config.BastionFlag) {
|
||||
if c.IsSet(config.BastionFlag) {
|
||||
return newBastionService(), nil
|
||||
}
|
||||
if c.IsSet("url") {
|
||||
originURL, err := config.ValidateUrl(c, allowURLFromArgs)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error validating origin URL")
|
||||
}
|
||||
return &localService{URL: originURL, RootURL: originURL}, nil
|
||||
if isHTTPService(originURL) {
|
||||
return &httpService{
|
||||
url: originURL,
|
||||
}, nil
|
||||
}
|
||||
return newTCPOverWSService(originURL), nil
|
||||
}
|
||||
if c.IsSet("unix-socket") {
|
||||
path, err := config.ValidateUnixSocket(c)
|
||||
@@ -104,7 +128,7 @@ func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginServ
|
||||
return &unixSocketPath{path: path}, nil
|
||||
}
|
||||
u, err := url.Parse("http://localhost:8080")
|
||||
return &localService{URL: u, RootURL: u}, err
|
||||
return &httpService{url: u}, err
|
||||
}
|
||||
|
||||
// IsEmpty checks if there are any ingress rules.
|
||||
@@ -136,7 +160,7 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
|
||||
rules := make([]Rule, len(ingress))
|
||||
for i, r := range ingress {
|
||||
cfg := setConfig(defaults, r.OriginRequest)
|
||||
var service OriginService
|
||||
var service originService
|
||||
|
||||
if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) {
|
||||
// No validation necessary for unix socket filepath services
|
||||
@@ -151,12 +175,12 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
|
||||
service = &srv
|
||||
} else if r.Service == "hello_world" || r.Service == "hello-world" || r.Service == "helloworld" {
|
||||
service = new(helloWorld)
|
||||
} else if r.Service == "bastion" || cfg.BastionMode {
|
||||
} else if r.Service == ServiceBastion || cfg.BastionMode {
|
||||
// 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
|
||||
service = new(localService)
|
||||
service = newBastionService()
|
||||
} else {
|
||||
// Validate URL services
|
||||
u, err := url.Parse(r.Service)
|
||||
@@ -171,8 +195,11 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
|
||||
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)
|
||||
}
|
||||
serviceURL := localService{URL: u}
|
||||
service = &serviceURL
|
||||
if isHTTPService(u) {
|
||||
service = &httpService{url: u}
|
||||
} else {
|
||||
service = newTCPOverWSService(u)
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateHostname(r, i, len(ingress)); err != nil {
|
||||
@@ -241,3 +268,7 @@ func ParseIngress(conf *config.Configuration) (Ingress, error) {
|
||||
}
|
||||
return validate(conf.Ingress, originRequestFromYAML(conf.OriginRequest))
|
||||
}
|
||||
|
||||
func isHTTPService(url *url.URL) bool {
|
||||
return url.Scheme == "http" || url.Scheme == "https" || url.Scheme == "ws" || url.Scheme == "wss"
|
||||
}
|
||||
|
||||
+136
-8
@@ -3,6 +3,7 @@ package ingress
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"testing"
|
||||
@@ -61,12 +62,12 @@ ingress:
|
||||
want: []Rule{
|
||||
{
|
||||
Hostname: "tunnel1.example.com",
|
||||
Service: &localService{URL: localhost8000},
|
||||
Service: &httpService{url: localhost8000},
|
||||
Config: defaultConfig,
|
||||
},
|
||||
{
|
||||
Hostname: "*",
|
||||
Service: &localService{URL: localhost8001},
|
||||
Service: &httpService{url: localhost8001},
|
||||
Config: defaultConfig,
|
||||
},
|
||||
},
|
||||
@@ -82,7 +83,22 @@ extraKey: extraValue
|
||||
want: []Rule{
|
||||
{
|
||||
Hostname: "*",
|
||||
Service: &localService{URL: localhost8000},
|
||||
Service: &httpService{url: localhost8000},
|
||||
Config: defaultConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ws service",
|
||||
args: args{rawYAML: `
|
||||
ingress:
|
||||
- hostname: "*"
|
||||
service: wss://localhost:8000
|
||||
`},
|
||||
want: []Rule{
|
||||
{
|
||||
Hostname: "*",
|
||||
Service: &httpService{url: MustParseURL(t, "wss://localhost:8000")},
|
||||
Config: defaultConfig,
|
||||
},
|
||||
},
|
||||
@@ -95,7 +111,7 @@ ingress:
|
||||
`},
|
||||
want: []Rule{
|
||||
{
|
||||
Service: &localService{URL: localhost8000},
|
||||
Service: &httpService{url: localhost8000},
|
||||
Config: defaultConfig,
|
||||
},
|
||||
},
|
||||
@@ -209,6 +225,85 @@ ingress:
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TCP services",
|
||||
args: args{rawYAML: `
|
||||
ingress:
|
||||
- hostname: tcp.foo.com
|
||||
service: tcp://127.0.0.1
|
||||
- hostname: tcp2.foo.com
|
||||
service: tcp://localhost:8000
|
||||
- service: http_status:404
|
||||
`},
|
||||
want: []Rule{
|
||||
{
|
||||
Hostname: "tcp.foo.com",
|
||||
Service: newTCPOverWSService(MustParseURL(t, "tcp://127.0.0.1:7864")),
|
||||
Config: defaultConfig,
|
||||
},
|
||||
{
|
||||
Hostname: "tcp2.foo.com",
|
||||
Service: newTCPOverWSService(MustParseURL(t, "tcp://localhost:8000")),
|
||||
Config: defaultConfig,
|
||||
},
|
||||
{
|
||||
Service: &fourOhFour,
|
||||
Config: defaultConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSH services",
|
||||
args: args{rawYAML: `
|
||||
ingress:
|
||||
- service: ssh://127.0.0.1
|
||||
`},
|
||||
want: []Rule{
|
||||
{
|
||||
Service: newTCPOverWSService(MustParseURL(t, "ssh://127.0.0.1:22")),
|
||||
Config: defaultConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "RDP services",
|
||||
args: args{rawYAML: `
|
||||
ingress:
|
||||
- service: rdp://127.0.0.1
|
||||
`},
|
||||
want: []Rule{
|
||||
{
|
||||
Service: newTCPOverWSService(MustParseURL(t, "rdp://127.0.0.1:3389")),
|
||||
Config: defaultConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SMB services",
|
||||
args: args{rawYAML: `
|
||||
ingress:
|
||||
- service: smb://127.0.0.1
|
||||
`},
|
||||
want: []Rule{
|
||||
{
|
||||
Service: newTCPOverWSService(MustParseURL(t, "smb://127.0.0.1:445")),
|
||||
Config: defaultConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Other TCP services",
|
||||
args: args{rawYAML: `
|
||||
ingress:
|
||||
- service: ftp://127.0.0.1
|
||||
`},
|
||||
want: []Rule{
|
||||
{
|
||||
Service: newTCPOverWSService(MustParseURL(t, "ftp://127.0.0.1")),
|
||||
Config: defaultConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "URL isn't necessary if using bastion",
|
||||
args: args{rawYAML: `
|
||||
@@ -221,7 +316,7 @@ ingress:
|
||||
want: []Rule{
|
||||
{
|
||||
Hostname: "bastion.foo.com",
|
||||
Service: &localService{},
|
||||
Service: newBastionService(),
|
||||
Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
|
||||
},
|
||||
{
|
||||
@@ -241,7 +336,7 @@ ingress:
|
||||
want: []Rule{
|
||||
{
|
||||
Hostname: "bastion.foo.com",
|
||||
Service: &localService{},
|
||||
Service: newBastionService(),
|
||||
Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
|
||||
},
|
||||
{
|
||||
@@ -369,6 +464,7 @@ func TestFindMatchingRule(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
path string
|
||||
req *http.Request
|
||||
wantRuleIndex int
|
||||
}{
|
||||
{
|
||||
@@ -403,9 +499,40 @@ func TestFindMatchingRule(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
for _, test := range tests {
|
||||
_, ruleIndex := ingress.FindMatchingRule(test.host, test.path)
|
||||
assert.Equal(t, test.wantRuleIndex, ruleIndex, fmt.Sprintf("Expect host=%s, path=%s to match rule %d, got %d", test.host, test.path, test.wantRuleIndex, i))
|
||||
assert.Equal(t, test.wantRuleIndex, ruleIndex, fmt.Sprintf("Expect host=%s, path=%s to match rule %d, got %d", test.host, test.path, test.wantRuleIndex, ruleIndex))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsHTTPService(t *testing.T) {
|
||||
tests := []struct {
|
||||
url *url.URL
|
||||
isHTTP bool
|
||||
}{
|
||||
{
|
||||
url: MustParseURL(t, "http://localhost"),
|
||||
isHTTP: true,
|
||||
},
|
||||
{
|
||||
url: MustParseURL(t, "https://127.0.0.1:8000"),
|
||||
isHTTP: true,
|
||||
},
|
||||
{
|
||||
url: MustParseURL(t, "ws://localhost"),
|
||||
isHTTP: true,
|
||||
},
|
||||
{
|
||||
url: MustParseURL(t, "wss://localhost:8000"),
|
||||
isHTTP: true,
|
||||
},
|
||||
{
|
||||
url: MustParseURL(t, "tcp://localhost:9000"),
|
||||
isHTTP: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.isHTTP, isHTTPService(test.url))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,6 +563,7 @@ ingress:
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
ing.FindMatchingRule("tunnel1.example.com", "")
|
||||
ing.FindMatchingRule("tunnel2.example.com", "")
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// OriginConnection is a way to stream to a service running on the user's origin.
|
||||
// Different concrete implementations will stream different protocols as long as they are io.ReadWriters.
|
||||
type OriginConnection interface {
|
||||
// Stream should generally be implemented as a bidirectional io.Copy.
|
||||
Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger)
|
||||
Close()
|
||||
}
|
||||
|
||||
type streamHandlerFunc func(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger)
|
||||
|
||||
// Stream copies copy data to & from provided io.ReadWriters.
|
||||
func Stream(conn, backendConn io.ReadWriter, log *zerolog.Logger) {
|
||||
proxyDone := make(chan struct{}, 2)
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(conn, backendConn)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("conn to backendConn copy: %v", err)
|
||||
}
|
||||
proxyDone <- struct{}{}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(backendConn, conn)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("backendConn to conn copy: %v", err)
|
||||
}
|
||||
proxyDone <- struct{}{}
|
||||
}()
|
||||
|
||||
// If one side is done, we are done.
|
||||
<-proxyDone
|
||||
}
|
||||
|
||||
// DefaultStreamHandler is an implementation of streamHandlerFunc that
|
||||
// performs a two way io.Copy between originConn and remoteConn.
|
||||
func DefaultStreamHandler(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger) {
|
||||
Stream(originConn, remoteConn, log)
|
||||
}
|
||||
|
||||
// tcpConnection is an OriginConnection that directly streams to raw TCP.
|
||||
type tcpConnection struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (tc *tcpConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
Stream(tunnelConn, tc.conn, log)
|
||||
}
|
||||
|
||||
func (tc *tcpConnection) Close() {
|
||||
tc.conn.Close()
|
||||
}
|
||||
|
||||
// tcpOverWSConnection is an OriginConnection that streams to TCP over WS.
|
||||
type tcpOverWSConnection struct {
|
||||
conn net.Conn
|
||||
streamHandler streamHandlerFunc
|
||||
}
|
||||
|
||||
func (wc *tcpOverWSConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
wc.streamHandler(websocket.NewConn(ctx, tunnelConn, log), wc.conn, log)
|
||||
}
|
||||
|
||||
func (wc *tcpOverWSConnection) Close() {
|
||||
wc.conn.Close()
|
||||
}
|
||||
|
||||
// wsConnection is an OriginConnection that streams WS between eyeball and origin.
|
||||
type wsConnection struct {
|
||||
wsConn *gws.Conn
|
||||
resp *http.Response
|
||||
}
|
||||
|
||||
func (wsc *wsConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
|
||||
Stream(tunnelConn, wsc.wsConn.UnderlyingConn(), log)
|
||||
}
|
||||
|
||||
func (wsc *wsConnection) Close() {
|
||||
wsc.resp.Body.Close()
|
||||
wsc.wsConn.Close()
|
||||
}
|
||||
|
||||
func newWSConnection(clientTLSConfig *tls.Config, r *http.Request) (OriginConnection, *http.Response, error) {
|
||||
d := &gws.Dialer{
|
||||
TLSClientConfig: clientTLSConfig,
|
||||
}
|
||||
wsConn, resp, err := websocket.ClientConnect(r, d)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &wsConnection{
|
||||
wsConn,
|
||||
resp,
|
||||
}, resp, nil
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/socks"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
gorillaWS "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/proxy"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
testStreamTimeout = time.Second * 3
|
||||
echoHeaderName = "Test-Cloudflared-Echo"
|
||||
)
|
||||
|
||||
var (
|
||||
testLogger = logger.Create(nil)
|
||||
testMessage = []byte("TestStreamOriginConnection")
|
||||
testResponse = []byte(fmt.Sprintf("echo-%s", testMessage))
|
||||
)
|
||||
|
||||
func TestStreamTCPConnection(t *testing.T) {
|
||||
cfdConn, originConn := net.Pipe()
|
||||
tcpConn := tcpConnection{
|
||||
conn: cfdConn,
|
||||
}
|
||||
|
||||
eyeballConn, edgeConn := net.Pipe()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
_, err := eyeballConn.Write(testMessage)
|
||||
|
||||
readBuffer := make([]byte, len(testResponse))
|
||||
_, err = eyeballConn.Read(readBuffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, testResponse, readBuffer)
|
||||
|
||||
return nil
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
echoTCPOrigin(t, originConn)
|
||||
originConn.Close()
|
||||
return nil
|
||||
})
|
||||
|
||||
tcpConn.Stream(ctx, edgeConn, testLogger)
|
||||
require.NoError(t, errGroup.Wait())
|
||||
}
|
||||
|
||||
func TestDefaultStreamWSOverTCPConnection(t *testing.T) {
|
||||
cfdConn, originConn := net.Pipe()
|
||||
tcpOverWSConn := tcpOverWSConnection{
|
||||
conn: cfdConn,
|
||||
streamHandler: DefaultStreamHandler,
|
||||
}
|
||||
|
||||
eyeballConn, edgeConn := net.Pipe()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
echoWSEyeball(t, eyeballConn)
|
||||
return nil
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
echoTCPOrigin(t, originConn)
|
||||
originConn.Close()
|
||||
return nil
|
||||
})
|
||||
|
||||
tcpOverWSConn.Stream(ctx, edgeConn, testLogger)
|
||||
require.NoError(t, errGroup.Wait())
|
||||
}
|
||||
|
||||
// TestSocksStreamWSOverTCPConnection simulates proxying in socks mode.
|
||||
// Eyeball side runs cloudflared accesss tcp with --url flag to start a websocket forwarder which
|
||||
// wraps SOCKS5 traffic in websocket
|
||||
// Origin side runs a tcpOverWSConnection with socks.StreamHandler
|
||||
func TestSocksStreamWSOverTCPConnection(t *testing.T) {
|
||||
var (
|
||||
sendMessage = t.Name()
|
||||
echoHeaderIncomingValue = fmt.Sprintf("header-%s", sendMessage)
|
||||
echoMessage = fmt.Sprintf("echo-%s", sendMessage)
|
||||
echoHeaderReturnValue = fmt.Sprintf("echo-%s", echoHeaderIncomingValue)
|
||||
)
|
||||
|
||||
statusCodes := []int{
|
||||
http.StatusOK,
|
||||
http.StatusTemporaryRedirect,
|
||||
http.StatusBadRequest,
|
||||
http.StatusInternalServerError,
|
||||
}
|
||||
for _, status := range statusCodes {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte(sendMessage), body)
|
||||
|
||||
require.Equal(t, echoHeaderIncomingValue, r.Header.Get(echoHeaderName))
|
||||
w.Header().Set(echoHeaderName, echoHeaderReturnValue)
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(echoMessage))
|
||||
}
|
||||
origin := httptest.NewServer(http.HandlerFunc(handler))
|
||||
defer origin.Close()
|
||||
|
||||
originURL, err := url.Parse(origin.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
originConn, err := net.Dial("tcp", originURL.Host)
|
||||
require.NoError(t, err)
|
||||
|
||||
tcpOverWSConn := tcpOverWSConnection{
|
||||
conn: originConn,
|
||||
streamHandler: socks.StreamHandler,
|
||||
}
|
||||
|
||||
wsForwarderOutConn, edgeConn := net.Pipe()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
tcpOverWSConn.Stream(ctx, edgeConn, testLogger)
|
||||
return nil
|
||||
})
|
||||
|
||||
wsForwarderListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
errGroup.Go(func() error {
|
||||
wsForwarderInConn, err := wsForwarderListener.Accept()
|
||||
require.NoError(t, err)
|
||||
defer wsForwarderInConn.Close()
|
||||
|
||||
Stream(wsForwarderInConn, &wsEyeball{wsForwarderOutConn}, testLogger)
|
||||
return nil
|
||||
})
|
||||
|
||||
eyeballDialer, err := proxy.SOCKS5("tcp", wsForwarderListener.Addr().String(), nil, proxy.Direct)
|
||||
require.NoError(t, err)
|
||||
|
||||
transport := &http.Transport{
|
||||
Dial: eyeballDialer.Dial,
|
||||
}
|
||||
|
||||
// Request URL doesn't matter because the transport is using eyeballDialer to connectq
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://test-socks-stream.com", bytes.NewBuffer([]byte(sendMessage)))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set(echoHeaderName, echoHeaderIncomingValue)
|
||||
|
||||
resp, err := transport.RoundTrip(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, status, resp.StatusCode)
|
||||
require.Equal(t, echoHeaderReturnValue, resp.Header.Get(echoHeaderName))
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte(echoMessage), body)
|
||||
|
||||
wsForwarderOutConn.Close()
|
||||
edgeConn.Close()
|
||||
tcpOverWSConn.Close()
|
||||
|
||||
require.NoError(t, errGroup.Wait())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamWSConnection(t *testing.T) {
|
||||
eyeballConn, edgeConn := net.Pipe()
|
||||
|
||||
origin := echoWSOrigin(t)
|
||||
defer origin.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, origin.URL, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
|
||||
clientTLSConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
wsConn, resp, err := newWSConnection(clientTLSConfig, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
|
||||
require.Equal(t, "Upgrade", resp.Header.Get("Connection"))
|
||||
require.Equal(t, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", resp.Header.Get("Sec-Websocket-Accept"))
|
||||
require.Equal(t, "websocket", resp.Header.Get("Upgrade"))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testStreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
echoWSEyeball(t, eyeballConn)
|
||||
return nil
|
||||
})
|
||||
|
||||
wsConn.Stream(ctx, edgeConn, testLogger)
|
||||
require.NoError(t, errGroup.Wait())
|
||||
}
|
||||
|
||||
type wsEyeball struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (wse *wsEyeball) Read(p []byte) (int, error) {
|
||||
data, err := wsutil.ReadServerBinary(wse.conn)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return copy(p, data), nil
|
||||
}
|
||||
|
||||
func (wse *wsEyeball) Write(p []byte) (int, error) {
|
||||
err := wsutil.WriteClientBinary(wse.conn, p)
|
||||
return len(p), err
|
||||
}
|
||||
|
||||
func echoWSEyeball(t *testing.T, conn net.Conn) {
|
||||
require.NoError(t, wsutil.WriteClientBinary(conn, testMessage))
|
||||
|
||||
readMsg, err := wsutil.ReadServerBinary(conn)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, testResponse, readMsg)
|
||||
|
||||
require.NoError(t, conn.Close())
|
||||
}
|
||||
|
||||
func echoWSOrigin(t *testing.T) *httptest.Server {
|
||||
var upgrader = gorillaWS.Upgrader{
|
||||
ReadBufferSize: 10,
|
||||
WriteBufferSize: 10,
|
||||
}
|
||||
|
||||
ws := func(w http.ResponseWriter, r *http.Request) {
|
||||
header := make(http.Header)
|
||||
for k, vs := range r.Header {
|
||||
if k == "Test-Cloudflared-Echo" {
|
||||
header[k] = vs
|
||||
}
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, header)
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
messageType, p, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
require.Equal(t, testMessage, p)
|
||||
if err := conn.WriteMessage(messageType, testResponse); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewTLSServer starts the server in another thread
|
||||
return httptest.NewTLSServer(http.HandlerFunc(ws))
|
||||
}
|
||||
|
||||
func echoTCPOrigin(t *testing.T, conn net.Conn) {
|
||||
readBuffer := make([]byte, len(testMessage))
|
||||
_, err := conn.Read(readBuffer)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, testMessage, readBuffer)
|
||||
|
||||
_, err = conn.Write(testResponse)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
|
||||
)
|
||||
|
||||
// HTTPOriginProxy can be implemented by origin services that want to proxy http requests.
|
||||
type HTTPOriginProxy interface {
|
||||
// RoundTrip is how cloudflared proxies eyeball requests to the actual origin services
|
||||
http.RoundTripper
|
||||
}
|
||||
|
||||
// StreamBasedOriginProxy can be implemented by origin services that want to proxy ws/TCP.
|
||||
type StreamBasedOriginProxy interface {
|
||||
EstablishConnection(r *http.Request) (OriginConnection, *http.Response, error)
|
||||
}
|
||||
|
||||
func (o *unixSocketPath) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return o.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (o *httpService) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Rewrite the request URL so that it goes to the origin service.
|
||||
req.URL.Host = o.url.Host
|
||||
req.URL.Scheme = o.url.Scheme
|
||||
if o.hostHeader != "" {
|
||||
// For incoming requests, the Host header is promoted to the Request.Host field and removed from the Header map.
|
||||
req.Host = o.hostHeader
|
||||
}
|
||||
return o.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (o *httpService) EstablishConnection(req *http.Request) (OriginConnection, *http.Response, error) {
|
||||
req.URL.Host = o.url.Host
|
||||
req.URL.Scheme = websocket.ChangeRequestScheme(o.url)
|
||||
if o.hostHeader != "" {
|
||||
// For incoming requests, the Host header is promoted to the Request.Host field and removed from the Header map.
|
||||
req.Host = o.hostHeader
|
||||
}
|
||||
return newWSConnection(o.transport.TLSClientConfig, req)
|
||||
}
|
||||
|
||||
func (o *helloWorld) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Rewrite the request URL so that it goes to the Hello World server.
|
||||
req.URL.Host = o.server.Addr().String()
|
||||
req.URL.Scheme = "https"
|
||||
return o.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (o *helloWorld) EstablishConnection(req *http.Request) (OriginConnection, *http.Response, error) {
|
||||
req.URL.Host = o.server.Addr().String()
|
||||
req.URL.Scheme = "wss"
|
||||
return newWSConnection(o.transport.TLSClientConfig, req)
|
||||
}
|
||||
|
||||
func (o *statusCode) RoundTrip(_ *http.Request) (*http.Response, error) {
|
||||
return o.resp, nil
|
||||
}
|
||||
|
||||
func (o *rawTCPService) EstablishConnection(r *http.Request) (OriginConnection, *http.Response, error) {
|
||||
dest, err := getRequestHost(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conn, err := net.Dial("tcp", dest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
originConn := &tcpConnection{
|
||||
conn: conn,
|
||||
}
|
||||
resp := &http.Response{
|
||||
Status: switchingProtocolText,
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
ContentLength: -1,
|
||||
}
|
||||
return originConn, resp, nil
|
||||
}
|
||||
|
||||
// getRequestHost returns the host of the http.Request.
|
||||
func getRequestHost(r *http.Request) (string, error) {
|
||||
if r.Host != "" {
|
||||
return r.Host, nil
|
||||
}
|
||||
if r.URL != nil {
|
||||
return r.URL.Host, nil
|
||||
}
|
||||
return "", errors.New("host not found")
|
||||
}
|
||||
|
||||
func (o *tcpOverWSService) EstablishConnection(r *http.Request) (OriginConnection, *http.Response, error) {
|
||||
var err error
|
||||
dest := o.dest
|
||||
if o.isBastion {
|
||||
dest, err = o.bastionDest(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", dest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
originConn := &tcpOverWSConnection{
|
||||
conn: conn,
|
||||
streamHandler: o.streamHandler,
|
||||
}
|
||||
resp := &http.Response{
|
||||
Status: switchingProtocolText,
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
Header: websocket.NewResponseHeader(r),
|
||||
ContentLength: -1,
|
||||
}
|
||||
return originConn, resp, nil
|
||||
|
||||
}
|
||||
|
||||
func (o *tcpOverWSService) bastionDest(r *http.Request) (string, error) {
|
||||
jumpDestination := r.Header.Get(h2mux.CFJumpDestinationHeader)
|
||||
if jumpDestination == "" {
|
||||
return "", fmt.Errorf("Did not receive final destination from client. The --destination flag is likely not set on the client side")
|
||||
}
|
||||
// Strip scheme and path set by client. Without a scheme
|
||||
// Parsing a hostname and path without scheme might not return an error due to parsing ambiguities
|
||||
if jumpURL, err := url.Parse(jumpDestination); err == nil && jumpURL.Host != "" {
|
||||
return removePath(jumpURL.Host), nil
|
||||
}
|
||||
return removePath(jumpDestination), nil
|
||||
}
|
||||
|
||||
func removePath(dest string) string {
|
||||
return strings.SplitN(dest, "/", 2)[0]
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestEstablishConnectionResponse ensures each implementation of StreamBasedOriginProxy returns
|
||||
// the expected response
|
||||
func assertEstablishConnectionResponse(t *testing.T,
|
||||
originProxy StreamBasedOriginProxy,
|
||||
req *http.Request,
|
||||
expectHeader http.Header,
|
||||
) {
|
||||
_, resp, err := originProxy.EstablishConnection(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, switchingProtocolText, resp.Status)
|
||||
assert.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
|
||||
assert.Equal(t, expectHeader, resp.Header)
|
||||
}
|
||||
|
||||
func TestHTTPServiceEstablishConnection(t *testing.T) {
|
||||
origin := echoWSOrigin(t)
|
||||
defer origin.Close()
|
||||
originURL, err := url.Parse(origin.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
httpService := &httpService{
|
||||
url: originURL,
|
||||
hostHeader: origin.URL,
|
||||
transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, origin.URL, nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
req.Header.Set("Test-Cloudflared-Echo", t.Name())
|
||||
|
||||
expectHeader := http.Header{
|
||||
"Connection": {"Upgrade"},
|
||||
"Sec-Websocket-Accept": {"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="},
|
||||
"Upgrade": {"websocket"},
|
||||
"Test-Cloudflared-Echo": {t.Name()},
|
||||
}
|
||||
assertEstablishConnectionResponse(t, httpService, req, expectHeader)
|
||||
}
|
||||
|
||||
func TestHelloWorldEstablishConnection(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
shutdownC := make(chan struct{})
|
||||
errC := make(chan error)
|
||||
helloWorldSerivce := &helloWorld{}
|
||||
helloWorldSerivce.start(&wg, testLogger, shutdownC, errC, OriginRequestConfig{})
|
||||
|
||||
// Scheme and Host of URL will be override by the Scheme and Host of the helloWorld service
|
||||
req, err := http.NewRequest(http.MethodGet, "https://place-holder/ws", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectHeader := http.Header{
|
||||
"Connection": {"Upgrade"},
|
||||
// Accept key when Sec-Websocket-Key is not specified
|
||||
"Sec-Websocket-Accept": {"Kfh9QIsMVZcl6xEPYxPHzW8SZ8w="},
|
||||
"Upgrade": {"websocket"},
|
||||
}
|
||||
assertEstablishConnectionResponse(t, helloWorldSerivce, req, expectHeader)
|
||||
|
||||
close(shutdownC)
|
||||
}
|
||||
|
||||
func TestRawTCPServiceEstablishConnection(t *testing.T) {
|
||||
originListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
listenerClosed := make(chan struct{})
|
||||
tcpListenRoutine(originListener, listenerClosed)
|
||||
|
||||
rawTCPService := &rawTCPService{name: ServiceWarpRouting}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", originListener.Addr()), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assertEstablishConnectionResponse(t, rawTCPService, req, nil)
|
||||
|
||||
originListener.Close()
|
||||
<-listenerClosed
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", originListener.Addr()), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Origin not listening for new connection, should return an error
|
||||
_, resp, err := rawTCPService.EstablishConnection(req)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, resp)
|
||||
}
|
||||
|
||||
func TestTCPOverWSServiceEstablishConnection(t *testing.T) {
|
||||
originListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
listenerClosed := make(chan struct{})
|
||||
tcpListenRoutine(originListener, listenerClosed)
|
||||
|
||||
originURL := &url.URL{
|
||||
Scheme: "tcp",
|
||||
Host: originListener.Addr().String(),
|
||||
}
|
||||
|
||||
baseReq, err := http.NewRequest(http.MethodGet, "https://place-holder", nil)
|
||||
require.NoError(t, err)
|
||||
baseReq.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
|
||||
|
||||
bastionReq := baseReq.Clone(context.Background())
|
||||
bastionReq.Header.Set(h2mux.CFJumpDestinationHeader, originListener.Addr().String())
|
||||
|
||||
expectHeader := http.Header{
|
||||
"Connection": {"Upgrade"},
|
||||
"Sec-Websocket-Accept": {"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="},
|
||||
"Upgrade": {"websocket"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
service *tcpOverWSService
|
||||
req *http.Request
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
service: newTCPOverWSService(originURL),
|
||||
req: baseReq,
|
||||
},
|
||||
{
|
||||
service: newBastionService(),
|
||||
req: bastionReq,
|
||||
},
|
||||
{
|
||||
service: newBastionService(),
|
||||
req: baseReq,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if test.expectErr {
|
||||
_, resp, err := test.service.EstablishConnection(test.req)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, resp)
|
||||
} else {
|
||||
assertEstablishConnectionResponse(t, test.service, test.req, expectHeader)
|
||||
}
|
||||
}
|
||||
|
||||
originListener.Close()
|
||||
<-listenerClosed
|
||||
|
||||
for _, service := range []*tcpOverWSService{newTCPOverWSService(originURL), newBastionService()} {
|
||||
// Origin not listening for new connection, should return an error
|
||||
_, resp, err := service.EstablishConnection(bastionReq)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBastionDestination(t *testing.T) {
|
||||
canonicalJumpDestHeader := http.CanonicalHeaderKey(h2mux.CFJumpDestinationHeader)
|
||||
tests := []struct {
|
||||
name string
|
||||
header http.Header
|
||||
expectedDest string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "hostname destination",
|
||||
header: http.Header{
|
||||
canonicalJumpDestHeader: []string{"localhost"},
|
||||
},
|
||||
expectedDest: "localhost",
|
||||
},
|
||||
{
|
||||
name: "hostname destination with port",
|
||||
header: http.Header{
|
||||
canonicalJumpDestHeader: []string{"localhost:9000"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "hostname destination with scheme and port",
|
||||
header: http.Header{
|
||||
canonicalJumpDestHeader: []string{"ssh://localhost:9000"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "full hostname url",
|
||||
header: http.Header{
|
||||
canonicalJumpDestHeader: []string{"ssh://localhost:9000/metrics"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "hostname destination with port and path",
|
||||
header: http.Header{
|
||||
canonicalJumpDestHeader: []string{"localhost:9000/metrics"},
|
||||
},
|
||||
expectedDest: "localhost:9000",
|
||||
},
|
||||
{
|
||||
name: "ip destination",
|
||||
header: http.Header{
|
||||
canonicalJumpDestHeader: []string{"127.0.0.1"},
|
||||
},
|
||||
expectedDest: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
name: "ip destination with port",
|
||||
header: http.Header{
|
||||
canonicalJumpDestHeader: []string{"127.0.0.1:9000"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "ip destination with port and path",
|
||||
header: http.Header{
|
||||
canonicalJumpDestHeader: []string{"127.0.0.1:9000/metrics"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "ip destination with schem and port",
|
||||
header: http.Header{
|
||||
canonicalJumpDestHeader: []string{"tcp://127.0.0.1:9000"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "full ip url",
|
||||
header: http.Header{
|
||||
canonicalJumpDestHeader: []string{"ssh://127.0.0.1:9000/metrics"},
|
||||
},
|
||||
expectedDest: "127.0.0.1:9000",
|
||||
},
|
||||
{
|
||||
name: "no destination",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
s := newBastionService()
|
||||
for _, test := range tests {
|
||||
r := &http.Request{
|
||||
Header: test.header,
|
||||
}
|
||||
dest, err := s.bastionDest(r)
|
||||
if test.wantErr {
|
||||
assert.Error(t, err, "Test %s expects error", test.name)
|
||||
} else {
|
||||
assert.NoError(t, err, "Test %s expects no error, got error %v", test.name, err)
|
||||
assert.Equal(t, test.expectedDest, dest, "Test %s expect dest %s, got %s", test.name, test.expectedDest, dest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPServiceHostHeaderOverride(t *testing.T) {
|
||||
cfg := OriginRequestConfig{
|
||||
HTTPHostHeader: t.Name(),
|
||||
}
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, r.Host, t.Name())
|
||||
if websocket.IsWebSocketUpgrade(r) {
|
||||
respHeaders := websocket.NewResponseHeader(r)
|
||||
for k, v := range respHeaders {
|
||||
w.Header().Set(k, v[0])
|
||||
}
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
origin := httptest.NewServer(http.HandlerFunc(handler))
|
||||
defer origin.Close()
|
||||
|
||||
originURL, err := url.Parse(origin.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
httpService := &httpService{
|
||||
url: originURL,
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
shutdownC := make(chan struct{})
|
||||
errC := make(chan error)
|
||||
require.NoError(t, httpService.start(&wg, testLogger, shutdownC, errC, cfg))
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, originURL.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := httpService.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
req = req.Clone(context.Background())
|
||||
_, resp, err = httpService.EstablishConnection(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusSwitchingProtocols, resp.StatusCode)
|
||||
}
|
||||
|
||||
func tcpListenRoutine(listener net.Listener, closeChan chan struct{}) {
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
close(closeChan)
|
||||
return
|
||||
}
|
||||
// Close immediately, this test is not about testing read/write on connection
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
+68
-139
@@ -8,7 +8,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -21,10 +20,8 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// OriginService is something a tunnel can proxy traffic to.
|
||||
type OriginService interface {
|
||||
// RoundTrip is how cloudflared proxies eyeball requests to the actual origin services
|
||||
http.RoundTripper
|
||||
// originService is something a tunnel can proxy traffic to.
|
||||
type originService interface {
|
||||
String() string
|
||||
// Start the origin service if it's managed by cloudflared, e.g. proxy servers or Hello World.
|
||||
// If it's not managed by cloudflared, this is a no-op because the user is responsible for
|
||||
@@ -51,10 +48,6 @@ func (o *unixSocketPath) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *unixSocketPath) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return o.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (o *unixSocketPath) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *http.Response, error) {
|
||||
d := &gws.Dialer{
|
||||
NetDial: o.transport.Dial,
|
||||
@@ -65,130 +58,90 @@ func (o *unixSocketPath) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn,
|
||||
return d.Dial(reqURL.String(), headers)
|
||||
}
|
||||
|
||||
// localService is an OriginService listening on a TCP/IP address the user's origin can route to.
|
||||
type localService struct {
|
||||
// The URL for the user's origin service
|
||||
RootURL *url.URL
|
||||
// The URL that cloudflared should send requests to.
|
||||
// If this origin requires starting a proxy, this is the proxy's address,
|
||||
// and that proxy points to RootURL. Otherwise, this is equal to RootURL.
|
||||
URL *url.URL
|
||||
transport *http.Transport
|
||||
type httpService struct {
|
||||
url *url.URL
|
||||
hostHeader string
|
||||
transport *http.Transport
|
||||
}
|
||||
|
||||
func (o *localService) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *http.Response, error) {
|
||||
d := &gws.Dialer{TLSClientConfig: o.transport.TLSClientConfig}
|
||||
// Rewrite the request URL so that it goes to the origin service.
|
||||
reqURL.Host = o.URL.Host
|
||||
reqURL.Scheme = websocket.ChangeRequestScheme(o.URL)
|
||||
return d.Dial(reqURL.String(), headers)
|
||||
}
|
||||
|
||||
func (o *localService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
|
||||
func (o *httpService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
|
||||
transport, err := newHTTPTransport(o, cfg, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.hostHeader = cfg.HTTPHostHeader
|
||||
o.transport = transport
|
||||
|
||||
// Start a proxy if one is needed
|
||||
if staticHost := o.staticHost(); originRequiresProxy(staticHost, cfg) {
|
||||
if err := o.startProxy(staticHost, wg, log, shutdownC, errC, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *localService) startProxy(staticHost string, wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
|
||||
func (o *httpService) String() string {
|
||||
return o.url.String()
|
||||
}
|
||||
|
||||
// Start a listener for the proxy
|
||||
proxyAddress := net.JoinHostPort(cfg.ProxyAddress, strconv.Itoa(int(cfg.ProxyPort)))
|
||||
listener, err := net.Listen("tcp", proxyAddress)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Cannot start Websocket Proxy Server: %s", err)
|
||||
return errors.Wrap(err, "Cannot start Websocket Proxy Server")
|
||||
}
|
||||
// rawTCPService dials TCP to the destination specified by the client
|
||||
// It's used by warp routing
|
||||
type rawTCPService struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// Start the proxy itself
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
streamHandler := websocket.DefaultStreamHandler
|
||||
// This origin's config specifies what type of proxy to start.
|
||||
switch cfg.ProxyType {
|
||||
case socksProxy:
|
||||
log.Info().Msg("SOCKS5 server started")
|
||||
streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, _ http.Header) {
|
||||
dialer := socks.NewConnDialer(remoteConn)
|
||||
requestHandler := socks.NewRequestHandler(dialer)
|
||||
socksServer := socks.NewConnectionHandler(requestHandler)
|
||||
func (o *rawTCPService) String() string {
|
||||
return o.name
|
||||
}
|
||||
|
||||
_ = socksServer.Serve(wsConn)
|
||||
}
|
||||
case "":
|
||||
log.Debug().Msg("Not starting any websocket proxy")
|
||||
default:
|
||||
log.Error().Msgf("%s isn't a valid proxy (valid options are {%s})", cfg.ProxyType, socksProxy)
|
||||
}
|
||||
|
||||
errC <- websocket.StartProxyServer(log, listener, staticHost, shutdownC, streamHandler)
|
||||
}()
|
||||
|
||||
// Modify this origin, so that it no longer points at the origin service directly.
|
||||
// Instead, it points at the proxy to the origin service.
|
||||
newURL, err := url.Parse("http://" + listener.Addr().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.URL = newURL
|
||||
func (o *rawTCPService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *localService) String() string {
|
||||
if o.isBastion() {
|
||||
return "Bastion"
|
||||
}
|
||||
return o.URL.String()
|
||||
// tcpOverWSService models TCP origins serving eyeballs connecting over websocket, such as
|
||||
// cloudflared access commands.
|
||||
type tcpOverWSService struct {
|
||||
dest string
|
||||
isBastion bool
|
||||
streamHandler streamHandlerFunc
|
||||
}
|
||||
|
||||
func (o *localService) isBastion() bool {
|
||||
return o.URL == nil
|
||||
}
|
||||
|
||||
func (o *localService) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Rewrite the request URL so that it goes to the origin service.
|
||||
req.URL.Host = o.URL.Host
|
||||
req.URL.Scheme = o.URL.Scheme
|
||||
return o.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (o *localService) staticHost() string {
|
||||
|
||||
if o.URL == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
addPortIfMissing := func(uri *url.URL, port int) string {
|
||||
if uri.Port() != "" {
|
||||
return uri.Host
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", uri.Hostname(), port)
|
||||
}
|
||||
|
||||
switch o.URL.Scheme {
|
||||
func newTCPOverWSService(url *url.URL) *tcpOverWSService {
|
||||
switch url.Scheme {
|
||||
case "ssh":
|
||||
return addPortIfMissing(o.URL, 22)
|
||||
addPortIfMissing(url, 22)
|
||||
case "rdp":
|
||||
return addPortIfMissing(o.URL, 3389)
|
||||
addPortIfMissing(url, 3389)
|
||||
case "smb":
|
||||
return addPortIfMissing(o.URL, 445)
|
||||
addPortIfMissing(url, 445)
|
||||
case "tcp":
|
||||
return addPortIfMissing(o.URL, 7864) // just a random port since there isn't a default in this case
|
||||
addPortIfMissing(url, 7864) // just a random port since there isn't a default in this case
|
||||
}
|
||||
return ""
|
||||
return &tcpOverWSService{
|
||||
dest: url.Host,
|
||||
}
|
||||
}
|
||||
|
||||
func newBastionService() *tcpOverWSService {
|
||||
return &tcpOverWSService{
|
||||
isBastion: true,
|
||||
}
|
||||
}
|
||||
|
||||
func addPortIfMissing(uri *url.URL, port int) {
|
||||
if uri.Port() == "" {
|
||||
uri.Host = fmt.Sprintf("%s:%d", uri.Hostname(), port)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *tcpOverWSService) String() string {
|
||||
if o.isBastion {
|
||||
return ServiceBastion
|
||||
}
|
||||
return o.dest
|
||||
}
|
||||
|
||||
func (o *tcpOverWSService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
|
||||
if cfg.ProxyType == socksProxy {
|
||||
o.streamHandler = socks.StreamHandler
|
||||
} else {
|
||||
o.streamHandler = DefaultStreamHandler
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HelloWorld is an OriginService for the built-in Hello World server.
|
||||
@@ -228,26 +181,6 @@ func (o *helloWorld) start(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *helloWorld) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Rewrite the request URL so that it goes to the Hello World server.
|
||||
req.URL.Host = o.server.Addr().String()
|
||||
req.URL.Scheme = "https"
|
||||
return o.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (o *helloWorld) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *http.Response, error) {
|
||||
d := &gws.Dialer{
|
||||
TLSClientConfig: o.transport.TLSClientConfig,
|
||||
}
|
||||
reqURL.Host = o.server.Addr().String()
|
||||
reqURL.Scheme = "wss"
|
||||
return d.Dial(reqURL.String(), headers)
|
||||
}
|
||||
|
||||
func originRequiresProxy(staticHost string, cfg OriginRequestConfig) bool {
|
||||
return staticHost != "" || cfg.BastionMode
|
||||
}
|
||||
|
||||
// statusCode is an OriginService that just responds with a given HTTP status.
|
||||
// Typical use-case is "user wants the catch-all rule to just respond 404".
|
||||
type statusCode struct {
|
||||
@@ -277,10 +210,6 @@ func (o *statusCode) start(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *statusCode) RoundTrip(_ *http.Request) (*http.Response, error) {
|
||||
return o.resp, nil
|
||||
}
|
||||
|
||||
type NopReadCloser struct{}
|
||||
|
||||
// Read always returns EOF to signal end of input
|
||||
@@ -292,7 +221,7 @@ func (nrc *NopReadCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log *zerolog.Logger) (*http.Transport, error) {
|
||||
func newHTTPTransport(service originService, cfg OriginRequestConfig, log *zerolog.Logger) (*http.Transport, error) {
|
||||
originCertPool, err := tlsconfig.LoadOriginCA(cfg.CAPool, log)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error loading cert pool")
|
||||
@@ -337,19 +266,19 @@ func newHTTPTransport(service OriginService, cfg OriginRequestConfig, log *zerol
|
||||
return &httpTransport, nil
|
||||
}
|
||||
|
||||
// MockOriginService should only be used by other packages to mock OriginService. Set Transport to configure desired RoundTripper behavior.
|
||||
type MockOriginService struct {
|
||||
// MockOriginHTTPService should only be used by other packages to mock OriginService. Set Transport to configure desired RoundTripper behavior.
|
||||
type MockOriginHTTPService struct {
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
func (mos MockOriginService) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
func (mos MockOriginHTTPService) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return mos.Transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (mos MockOriginService) String() string {
|
||||
func (mos MockOriginHTTPService) String() string {
|
||||
return "MockOriginService"
|
||||
}
|
||||
|
||||
func (mos MockOriginService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
|
||||
func (mos MockOriginHTTPService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ type Rule struct {
|
||||
// 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 OriginService
|
||||
Service originService
|
||||
|
||||
// Configure the request cloudflared sends to this specific origin.
|
||||
Config OriginRequestConfig
|
||||
|
||||
@@ -14,7 +14,7 @@ func Test_rule_matches(t *testing.T) {
|
||||
type fields struct {
|
||||
Hostname string
|
||||
Path *regexp.Regexp
|
||||
Service OriginService
|
||||
Service originService
|
||||
}
|
||||
type args struct {
|
||||
requestURL *url.URL
|
||||
|
||||
+156
-105
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -14,8 +13,6 @@ import (
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -24,52 +21,89 @@ const (
|
||||
TagHeaderNamePrefix = "Cf-Warp-Tag-"
|
||||
)
|
||||
|
||||
type client struct {
|
||||
type proxy struct {
|
||||
ingressRules ingress.Ingress
|
||||
warpRouting *ingress.WarpRoutingService
|
||||
tags []tunnelpogs.Tag
|
||||
log *zerolog.Logger
|
||||
bufferPool *buffer.Pool
|
||||
}
|
||||
|
||||
func NewClient(ingressRules ingress.Ingress, tags []tunnelpogs.Tag, log *zerolog.Logger) connection.OriginClient {
|
||||
return &client{
|
||||
func NewOriginProxy(
|
||||
ingressRules ingress.Ingress,
|
||||
warpRouting *ingress.WarpRoutingService,
|
||||
tags []tunnelpogs.Tag,
|
||||
log *zerolog.Logger) connection.OriginProxy {
|
||||
|
||||
return &proxy{
|
||||
ingressRules: ingressRules,
|
||||
warpRouting: warpRouting,
|
||||
tags: tags,
|
||||
log: log,
|
||||
bufferPool: buffer.NewPool(512 * 1024),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) Proxy(w connection.ResponseWriter, req *http.Request, isWebsocket bool) error {
|
||||
// Caller is responsible for writing any error to ResponseWriter
|
||||
func (p *proxy) Proxy(w connection.ResponseWriter, req *http.Request, sourceConnectionType connection.Type) error {
|
||||
incrementRequests()
|
||||
defer decrementConcurrentRequests()
|
||||
|
||||
cfRay := findCfRayHeader(req)
|
||||
lbProbe := isLBProbeRequest(req)
|
||||
|
||||
c.appendTagHeaders(req)
|
||||
rule, ruleNum := c.ingressRules.FindMatchingRule(req.Host, req.URL.Path)
|
||||
c.logRequest(req, cfRay, lbProbe, ruleNum)
|
||||
serveCtx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
||||
var (
|
||||
resp *http.Response
|
||||
err error
|
||||
)
|
||||
if isWebsocket {
|
||||
resp, err = c.proxyWebsocket(w, req, rule)
|
||||
} else {
|
||||
resp, err = c.proxyHTTP(w, req, rule)
|
||||
p.appendTagHeaders(req)
|
||||
if sourceConnectionType == connection.TypeTCP {
|
||||
if p.warpRouting == nil {
|
||||
err := errors.New(`cloudflared received a request from Warp client, but your configuration has disabled ingress from Warp clients. To enable this, set "warp-routing:\n\t enabled: true" in your config.yaml`)
|
||||
p.log.Error().Msg(err.Error())
|
||||
return err
|
||||
}
|
||||
logFields := logFields{
|
||||
cfRay: cfRay,
|
||||
lbProbe: lbProbe,
|
||||
rule: ingress.ServiceWarpRouting,
|
||||
}
|
||||
if err := p.proxyStreamRequest(serveCtx, w, req, sourceConnectionType, p.warpRouting.Proxy, logFields); err != nil {
|
||||
p.logRequestError(err, cfRay, ingress.ServiceWarpRouting)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
c.logRequestError(err, cfRay, ruleNum)
|
||||
w.WriteErrorResponse()
|
||||
|
||||
rule, ruleNum := p.ingressRules.FindMatchingRule(req.Host, req.URL.Path)
|
||||
logFields := logFields{
|
||||
cfRay: cfRay,
|
||||
lbProbe: lbProbe,
|
||||
rule: ruleNum,
|
||||
}
|
||||
p.logRequest(req, logFields)
|
||||
|
||||
if sourceConnectionType == connection.TypeHTTP {
|
||||
if err := p.proxyHTTPRequest(w, req, rule, logFields); err != nil {
|
||||
p.logRequestError(err, cfRay, ruleNum)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
connectionProxy, ok := rule.Service.(ingress.StreamBasedOriginProxy)
|
||||
if !ok {
|
||||
p.log.Error().Msgf("%s is not a connection-oriented service", rule.Service)
|
||||
return fmt.Errorf("Not a connection-oriented service")
|
||||
}
|
||||
|
||||
if err := p.proxyStreamRequest(serveCtx, w, req, sourceConnectionType, connectionProxy, logFields); err != nil {
|
||||
p.logRequestError(err, cfRay, ruleNum)
|
||||
return err
|
||||
}
|
||||
c.logOriginResponse(resp, cfRay, lbProbe, ruleNum)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) proxyHTTP(w connection.ResponseWriter, req *http.Request, rule *ingress.Rule) (*http.Response, error) {
|
||||
func (p *proxy) proxyHTTPRequest(w connection.ResponseWriter, req *http.Request, rule *ingress.Rule, fields logFields) error {
|
||||
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
|
||||
if rule.Config.DisableChunkedEncoding {
|
||||
req.TransferEncoding = []string{"gzip", "deflate"}
|
||||
@@ -82,78 +116,90 @@ func (c *client) proxyHTTP(w connection.ResponseWriter, req *http.Request, rule
|
||||
// Request origin to keep connection alive to improve performance
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
if hostHeader := rule.Config.HTTPHostHeader; hostHeader != "" {
|
||||
req.Header.Set("Host", hostHeader)
|
||||
req.Host = hostHeader
|
||||
httpService, ok := rule.Service.(ingress.HTTPOriginProxy)
|
||||
if !ok {
|
||||
p.log.Error().Msgf("%s is not a http service", rule.Service)
|
||||
return fmt.Errorf("Not a http service")
|
||||
}
|
||||
|
||||
resp, err := rule.Service.RoundTrip(req)
|
||||
resp, err := httpService.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error proxying request to origin")
|
||||
return errors.Wrap(err, "Error proxying request to origin")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = w.WriteRespHeaders(resp)
|
||||
err = w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error writing response header")
|
||||
return errors.Wrap(err, "Error writing response header")
|
||||
}
|
||||
if connection.IsServerSentEvent(resp.Header) {
|
||||
c.log.Debug().Msg("Detected Server-Side Events from Origin")
|
||||
c.writeEventStream(w, resp.Body)
|
||||
p.log.Debug().Msg("Detected Server-Side Events from Origin")
|
||||
p.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)
|
||||
buf := p.bufferPool.Get()
|
||||
defer p.bufferPool.Put(buf)
|
||||
_, _ = io.CopyBuffer(w, resp.Body, buf)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *client) proxyWebsocket(w connection.ResponseWriter, req *http.Request, rule *ingress.Rule) (*http.Response, error) {
|
||||
if hostHeader := rule.Config.HTTPHostHeader; hostHeader != "" {
|
||||
req.Header.Set("Host", hostHeader)
|
||||
req.Host = hostHeader
|
||||
}
|
||||
|
||||
dialler, ok := rule.Service.(websocket.Dialler)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Websockets aren't supported by the origin service '%s'", rule.Service)
|
||||
}
|
||||
conn, resp, err := websocket.ClientConnect(req, dialler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serveCtx, cancel := context.WithCancel(req.Context())
|
||||
connClosedChan := make(chan struct{})
|
||||
go func() {
|
||||
// serveCtx is done if req is cancelled, or streamWebsocket returns
|
||||
<-serveCtx.Done()
|
||||
_ = conn.Close()
|
||||
close(connClosedChan)
|
||||
}()
|
||||
|
||||
// Copy to/from stream to the undelying connection. Use the underlying
|
||||
// connection because cloudflared doesn't operate on the message themselves
|
||||
err = c.streamWebsocket(w, conn.UnderlyingConn(), resp)
|
||||
cancel()
|
||||
|
||||
// We need to make sure conn is closed before returning, otherwise we might write to conn after Proxy returns
|
||||
<-connClosedChan
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (c *client) streamWebsocket(w connection.ResponseWriter, conn net.Conn, resp *http.Response) error {
|
||||
err := w.WriteRespHeaders(resp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error writing websocket response header")
|
||||
}
|
||||
websocket.Stream(conn, w)
|
||||
p.logOriginResponse(resp, fields)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *client) writeEventStream(w connection.ResponseWriter, respBody io.ReadCloser) {
|
||||
// proxyStreamRequest first establish a connection with origin, then it writes the status code and headers, and finally it streams data between
|
||||
// eyeball and origin.
|
||||
func (p *proxy) proxyStreamRequest(
|
||||
serveCtx context.Context,
|
||||
w connection.ResponseWriter,
|
||||
req *http.Request,
|
||||
sourceConnectionType connection.Type,
|
||||
connectionProxy ingress.StreamBasedOriginProxy,
|
||||
fields logFields,
|
||||
) error {
|
||||
originConn, resp, err := connectionProxy.EstablishConnection(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err = w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
streamCtx, cancel := context.WithCancel(serveCtx)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
// streamCtx is done if req is cancelled or if Stream returns
|
||||
<-streamCtx.Done()
|
||||
originConn.Close()
|
||||
}()
|
||||
|
||||
eyeballStream := &bidirectionalStream{
|
||||
writer: w,
|
||||
reader: req.Body,
|
||||
}
|
||||
originConn.Stream(serveCtx, eyeballStream, p.log)
|
||||
p.logOriginResponse(resp, fields)
|
||||
return nil
|
||||
}
|
||||
|
||||
type bidirectionalStream struct {
|
||||
reader io.Reader
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func (wr *bidirectionalStream) Read(p []byte) (n int, err error) {
|
||||
return wr.reader.Read(p)
|
||||
}
|
||||
|
||||
func (wr *bidirectionalStream) Write(p []byte) (n int, err error) {
|
||||
return wr.writer.Write(p)
|
||||
}
|
||||
|
||||
func (p *proxy) writeEventStream(w connection.ResponseWriter, respBody io.ReadCloser) {
|
||||
reader := bufio.NewReader(respBody)
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
@@ -164,56 +210,61 @@ func (c *client) writeEventStream(w connection.ResponseWriter, respBody io.ReadC
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) appendTagHeaders(r *http.Request) {
|
||||
for _, tag := range c.tags {
|
||||
func (p *proxy) appendTagHeaders(r *http.Request) {
|
||||
for _, tag := range p.tags {
|
||||
r.Header.Add(TagHeaderNamePrefix+tag.Name, tag.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) logRequest(r *http.Request, cfRay string, lbProbe bool, ruleNum int) {
|
||||
if cfRay != "" {
|
||||
c.log.Debug().Msgf("CF-RAY: %s %s %s %s", cfRay, r.Method, r.URL, r.Proto)
|
||||
} else if lbProbe {
|
||||
c.log.Debug().Msgf("CF-RAY: %s Load Balancer health check %s %s %s", cfRay, r.Method, r.URL, r.Proto)
|
||||
type logFields struct {
|
||||
cfRay string
|
||||
lbProbe bool
|
||||
rule interface{}
|
||||
}
|
||||
|
||||
func (p *proxy) logRequest(r *http.Request, fields logFields) {
|
||||
if fields.cfRay != "" {
|
||||
p.log.Debug().Msgf("CF-RAY: %s %s %s %s", fields.cfRay, r.Method, r.URL, r.Proto)
|
||||
} else if fields.lbProbe {
|
||||
p.log.Debug().Msgf("CF-RAY: %s Load Balancer health check %s %s %s", fields.cfRay, r.Method, r.URL, r.Proto)
|
||||
} else {
|
||||
c.log.Debug().Msgf("All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", r.Method, r.URL, r.Proto)
|
||||
p.log.Debug().Msgf("All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", r.Method, r.URL, r.Proto)
|
||||
}
|
||||
c.log.Debug().Msgf("CF-RAY: %s Request Headers %+v", cfRay, r.Header)
|
||||
c.log.Debug().Msgf("CF-RAY: %s Serving with ingress rule %d", cfRay, ruleNum)
|
||||
p.log.Debug().Msgf("CF-RAY: %s Request Headers %+v", fields.cfRay, r.Header)
|
||||
p.log.Debug().Msgf("CF-RAY: %s Serving with ingress rule %v", fields.cfRay, fields.rule)
|
||||
|
||||
if contentLen := r.ContentLength; contentLen == -1 {
|
||||
c.log.Debug().Msgf("CF-RAY: %s Request Content length unknown", cfRay)
|
||||
p.log.Debug().Msgf("CF-RAY: %s Request Content length unknown", fields.cfRay)
|
||||
} else {
|
||||
c.log.Debug().Msgf("CF-RAY: %s Request content length %d", cfRay, contentLen)
|
||||
p.log.Debug().Msgf("CF-RAY: %s Request content length %d", fields.cfRay, contentLen)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) logOriginResponse(r *http.Response, cfRay string, lbProbe bool, ruleNum int) {
|
||||
responseByCode.WithLabelValues(strconv.Itoa(r.StatusCode)).Inc()
|
||||
if cfRay != "" {
|
||||
c.log.Debug().Msgf("CF-RAY: %s Status: %s served by ingress %d", cfRay, r.Status, ruleNum)
|
||||
} else if lbProbe {
|
||||
c.log.Debug().Msgf("Response to Load Balancer health check %s", r.Status)
|
||||
func (p *proxy) logOriginResponse(resp *http.Response, fields logFields) {
|
||||
responseByCode.WithLabelValues(strconv.Itoa(resp.StatusCode)).Inc()
|
||||
if fields.cfRay != "" {
|
||||
p.log.Debug().Msgf("CF-RAY: %s Status: %s served by ingress %d", fields.cfRay, resp.Status, fields.rule)
|
||||
} else if fields.lbProbe {
|
||||
p.log.Debug().Msgf("Response to Load Balancer health check %s", resp.Status)
|
||||
} else {
|
||||
c.log.Debug().Msgf("Status: %s served by ingress %d", r.Status, ruleNum)
|
||||
p.log.Debug().Msgf("Status: %s served by ingress %v", resp.Status, fields.rule)
|
||||
}
|
||||
c.log.Debug().Msgf("CF-RAY: %s Response Headers %+v", cfRay, r.Header)
|
||||
p.log.Debug().Msgf("CF-RAY: %s Response Headers %+v", fields.cfRay, resp.Header)
|
||||
|
||||
if contentLen := r.ContentLength; contentLen == -1 {
|
||||
c.log.Debug().Msgf("CF-RAY: %s Response content length unknown", cfRay)
|
||||
if contentLen := resp.ContentLength; contentLen == -1 {
|
||||
p.log.Debug().Msgf("CF-RAY: %s Response content length unknown", fields.cfRay)
|
||||
} else {
|
||||
c.log.Debug().Msgf("CF-RAY: %s Response content length %d", cfRay, contentLen)
|
||||
p.log.Debug().Msgf("CF-RAY: %s Response content length %d", fields.cfRay, contentLen)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) logRequestError(err error, cfRay string, ruleNum int) {
|
||||
func (p *proxy) logRequestError(err error, cfRay string, rule interface{}) {
|
||||
requestErrors.Inc()
|
||||
if cfRay != "" {
|
||||
c.log.Error().Msgf("CF-RAY: %s Proxying to ingress %d error: %v", cfRay, ruleNum, err)
|
||||
p.log.Error().Msgf("CF-RAY: %s Proxying to ingress %v error: %v", cfRay, rule, err)
|
||||
} else {
|
||||
c.log.Error().Msgf("Proxying to ingress %d error: %v", ruleNum, err)
|
||||
p.log.Error().Msgf("Proxying to ingress %v error: %v", rule, err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func findCfRayHeader(req *http.Request) string {
|
||||
|
||||
+539
-45
@@ -6,17 +6,21 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
gorillaWS "github.com/gorilla/websocket"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
@@ -26,7 +30,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
testTags = []tunnelpogs.Tag(nil)
|
||||
testTags = []tunnelpogs.Tag(nil)
|
||||
unusedWarpRoutingService = (*ingress.WarpRoutingService)(nil)
|
||||
)
|
||||
|
||||
type mockHTTPRespWriter struct {
|
||||
@@ -39,19 +44,14 @@ func newMockHTTPRespWriter() *mockHTTPRespWriter {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *mockHTTPRespWriter) WriteRespHeaders(resp *http.Response) error {
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
for header, val := range resp.Header {
|
||||
func (w *mockHTTPRespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
w.WriteHeader(status)
|
||||
for header, val := range header {
|
||||
w.Header()[header] = val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *mockHTTPRespWriter) WriteErrorResponse() {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_, _ = w.Write([]byte("http response error"))
|
||||
}
|
||||
|
||||
func (w *mockHTTPRespWriter) Read(data []byte) (int, error) {
|
||||
return 0, fmt.Errorf("mockHTTPRespWriter doesn't implement io.Reader")
|
||||
}
|
||||
@@ -125,59 +125,58 @@ func TestProxySingleOrigin(t *testing.T) {
|
||||
errC := make(chan error)
|
||||
require.NoError(t, ingressRule.StartOrigins(&wg, &log, ctx.Done(), errC))
|
||||
|
||||
client := NewClient(ingressRule, testTags, &log)
|
||||
t.Run("testProxyHTTP", testProxyHTTP(t, client))
|
||||
t.Run("testProxyWebsocket", testProxyWebsocket(t, client))
|
||||
t.Run("testProxySSE", testProxySSE(t, client))
|
||||
proxy := NewOriginProxy(ingressRule, unusedWarpRoutingService, testTags, &log)
|
||||
t.Run("testProxyHTTP", testProxyHTTP(t, proxy))
|
||||
t.Run("testProxyWebsocket", testProxyWebsocket(t, proxy))
|
||||
t.Run("testProxySSE", testProxySSE(t, proxy))
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func testProxyHTTP(t *testing.T, client connection.OriginClient) func(t *testing.T) {
|
||||
func testProxyHTTP(t *testing.T, proxy connection.OriginProxy) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
respWriter := newMockHTTPRespWriter()
|
||||
responseWriter := newMockHTTPRespWriter()
|
||||
req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Proxy(respWriter, req, false)
|
||||
err = proxy.Proxy(responseWriter, req, connection.TypeHTTP)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, http.StatusOK, respWriter.Code)
|
||||
assert.Equal(t, http.StatusOK, responseWriter.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func testProxyWebsocket(t *testing.T, client connection.OriginClient) func(t *testing.T) {
|
||||
func testProxyWebsocket(t *testing.T, proxy connection.OriginProxy) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
// WSRoute is a websocket echo handler
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://localhost:8080%s", hello.WSRoute), nil)
|
||||
|
||||
readPipe, writePipe := io.Pipe()
|
||||
respWriter := newMockWSRespWriter(readPipe)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://localhost:8080%s", hello.WSRoute), readPipe)
|
||||
responseWriter := newMockWSRespWriter(readPipe)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err = client.Proxy(respWriter, req, true)
|
||||
err = proxy.Proxy(responseWriter, req, connection.TypeWebsocket)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, http.StatusSwitchingProtocols, respWriter.Code)
|
||||
require.Equal(t, http.StatusSwitchingProtocols, responseWriter.Code)
|
||||
}()
|
||||
|
||||
msg := []byte("test websocket")
|
||||
err = wsutil.WriteClientText(writePipe, msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// ReadServerText reads next data message from rw, considering that caller represents client side.
|
||||
returnedMsg, err := wsutil.ReadServerText(respWriter.respBody())
|
||||
// ReadServerText reads next data message from rw, considering that caller represents proxy side.
|
||||
returnedMsg, err := wsutil.ReadServerText(responseWriter.respBody())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, msg, returnedMsg)
|
||||
|
||||
err = wsutil.WriteClientBinary(writePipe, msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
returnedMsg, err = wsutil.ReadServerBinary(respWriter.respBody())
|
||||
returnedMsg, err = wsutil.ReadServerBinary(responseWriter.respBody())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, msg, returnedMsg)
|
||||
|
||||
@@ -186,13 +185,13 @@ func testProxyWebsocket(t *testing.T, client connection.OriginClient) func(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func testProxySSE(t *testing.T, client connection.OriginClient) func(t *testing.T) {
|
||||
func testProxySSE(t *testing.T, proxy connection.OriginProxy) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
var (
|
||||
pushCount = 50
|
||||
pushFreq = time.Millisecond * 10
|
||||
)
|
||||
respWriter := newMockSSERespWriter()
|
||||
responseWriter := newMockSSERespWriter()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://localhost:8080%s?freq=%s", hello.SSERoute, pushFreq), nil)
|
||||
require.NoError(t, err)
|
||||
@@ -201,18 +200,18 @@ func testProxySSE(t *testing.T, client connection.OriginClient) func(t *testing.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err = client.Proxy(respWriter, req, false)
|
||||
err = proxy.Proxy(responseWriter, req, connection.TypeHTTP)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, http.StatusOK, respWriter.Code)
|
||||
require.Equal(t, http.StatusOK, responseWriter.Code)
|
||||
}()
|
||||
|
||||
for i := 0; i < pushCount; i++ {
|
||||
line := respWriter.ReadBytes()
|
||||
line := responseWriter.ReadBytes()
|
||||
expect := fmt.Sprintf("%d\n", i)
|
||||
require.Equal(t, []byte(expect), line, fmt.Sprintf("Expect to read %v, got %v", expect, line))
|
||||
|
||||
line = respWriter.ReadBytes()
|
||||
line = responseWriter.ReadBytes()
|
||||
require.Equal(t, []byte("\n"), line, fmt.Sprintf("Expect to read '\n', got %v", line))
|
||||
}
|
||||
|
||||
@@ -258,7 +257,7 @@ func TestProxyMultipleOrigins(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
require.NoError(t, ingress.StartOrigins(&wg, &log, ctx.Done(), errC))
|
||||
|
||||
client := NewClient(ingress, testTags, &log)
|
||||
proxy := NewOriginProxy(ingress, unusedWarpRoutingService, testTags, &log)
|
||||
|
||||
tests := []struct {
|
||||
url string
|
||||
@@ -290,18 +289,18 @@ func TestProxyMultipleOrigins(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
respWriter := newMockHTTPRespWriter()
|
||||
responseWriter := newMockHTTPRespWriter()
|
||||
req, err := http.NewRequest(http.MethodGet, test.url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Proxy(respWriter, req, false)
|
||||
err = proxy.Proxy(responseWriter, req, connection.TypeHTTP)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expectedStatus, respWriter.Code)
|
||||
assert.Equal(t, test.expectedStatus, responseWriter.Code)
|
||||
if test.expectedBody != nil {
|
||||
assert.Equal(t, test.expectedBody, respWriter.Body.Bytes())
|
||||
assert.Equal(t, test.expectedBody, responseWriter.Body.Bytes())
|
||||
} else {
|
||||
assert.Equal(t, 0, respWriter.Body.Len())
|
||||
assert.Equal(t, 0, responseWriter.Body.Len())
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
@@ -327,7 +326,7 @@ func TestProxyError(t *testing.T) {
|
||||
{
|
||||
Hostname: "*",
|
||||
Path: nil,
|
||||
Service: ingress.MockOriginService{
|
||||
Service: ingress.MockOriginHTTPService{
|
||||
Transport: errorOriginTransport{},
|
||||
},
|
||||
},
|
||||
@@ -336,14 +335,509 @@ func TestProxyError(t *testing.T) {
|
||||
|
||||
log := zerolog.Nop()
|
||||
|
||||
client := NewClient(ingress, testTags, &log)
|
||||
proxy := NewOriginProxy(ingress, unusedWarpRoutingService, testTags, &log)
|
||||
|
||||
respWriter := newMockHTTPRespWriter()
|
||||
responseWriter := newMockHTTPRespWriter()
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = client.Proxy(respWriter, req, false)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, http.StatusBadGateway, respWriter.Code)
|
||||
assert.Equal(t, "http response error", respWriter.Body.String())
|
||||
assert.Error(t, proxy.Proxy(responseWriter, req, connection.TypeHTTP))
|
||||
}
|
||||
|
||||
type replayer struct {
|
||||
sync.RWMutex
|
||||
writeDone chan struct{}
|
||||
rw *bytes.Buffer
|
||||
}
|
||||
|
||||
func newReplayer(buffer *bytes.Buffer) {
|
||||
|
||||
}
|
||||
|
||||
func (r *replayer) Read(p []byte) (int, error) {
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
return r.rw.Read(p)
|
||||
}
|
||||
|
||||
func (r *replayer) Write(p []byte) (int, error) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
n, err := r.rw.Write(p)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *replayer) String() string {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return r.rw.String()
|
||||
}
|
||||
|
||||
func (r *replayer) Bytes() []byte {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return r.rw.Bytes()
|
||||
}
|
||||
|
||||
// TestConnections tests every possible permutation of connection protocols
|
||||
// proxied by cloudflared.
|
||||
//
|
||||
// WS - WS : When a websocket based ingress is configured on the origin and
|
||||
// the eyeball is also a websocket client streaming data.
|
||||
// TCP - TCP : When teamnet is enabled and an http or tcp service is running
|
||||
// on the origin.
|
||||
// TCP - WS: When teamnet is enabled and a websocket based service is running
|
||||
// on the origin.
|
||||
// WS - TCP: When a tcp based ingress is configured on the origin and the
|
||||
// eyeball sends tcp packets wrapped in websockets. (E.g: cloudflared access).
|
||||
func TestConnections(t *testing.T) {
|
||||
logger := logger.Create(nil)
|
||||
replayer := &replayer{rw: &bytes.Buffer{}}
|
||||
type args struct {
|
||||
ingressServiceScheme string
|
||||
originService func(*testing.T, net.Listener)
|
||||
eyeballResponseWriter connection.ResponseWriter
|
||||
eyeballRequestBody io.ReadCloser
|
||||
|
||||
// Can be set to nil to show warp routing is not enabled.
|
||||
warpRoutingService *ingress.WarpRoutingService
|
||||
|
||||
// eyeball connection type.
|
||||
connectionType connection.Type
|
||||
|
||||
//requestheaders to be sent in the call to proxy.Proxy
|
||||
requestHeaders http.Header
|
||||
}
|
||||
|
||||
type want struct {
|
||||
message []byte
|
||||
headers http.Header
|
||||
err bool
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "ws-ws proxy",
|
||||
args: args{
|
||||
ingressServiceScheme: "ws://",
|
||||
originService: runEchoWSService,
|
||||
eyeballResponseWriter: newWSRespWriter(replayer),
|
||||
eyeballRequestBody: newWSRequestBody([]byte("test1")),
|
||||
connectionType: connection.TypeWebsocket,
|
||||
requestHeaders: map[string][]string{
|
||||
// Example key from https://tools.ietf.org/html/rfc6455#section-1.2
|
||||
"Sec-Websocket-Key": {"dGhlIHNhbXBsZSBub25jZQ=="},
|
||||
"Test-Cloudflared-Echo": {"Echo"},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
message: []byte("echo-test1"),
|
||||
headers: map[string][]string{
|
||||
"Connection": {"Upgrade"},
|
||||
"Sec-Websocket-Accept": {"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="},
|
||||
"Upgrade": {"websocket"},
|
||||
"Test-Cloudflared-Echo": {"Echo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp-tcp proxy",
|
||||
args: args{
|
||||
ingressServiceScheme: "tcp://",
|
||||
originService: runEchoTCPService,
|
||||
eyeballResponseWriter: newTCPRespWriter(replayer),
|
||||
eyeballRequestBody: newTCPRequestBody([]byte("test2")),
|
||||
warpRoutingService: ingress.NewWarpRoutingService(),
|
||||
connectionType: connection.TypeTCP,
|
||||
requestHeaders: map[string][]string{
|
||||
"Cf-Cloudflared-Proxy-Src": {"non-blank-value"},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
message: []byte("echo-test2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp-ws proxy",
|
||||
args: args{
|
||||
ingressServiceScheme: "ws://",
|
||||
originService: runEchoWSService,
|
||||
//eyeballResponseWriter gets set after roundtrip dial.
|
||||
eyeballRequestBody: newPipedWSRequestBody([]byte("test3")),
|
||||
warpRoutingService: ingress.NewWarpRoutingService(),
|
||||
requestHeaders: map[string][]string{
|
||||
"Cf-Cloudflared-Proxy-Src": {"non-blank-value"},
|
||||
},
|
||||
connectionType: connection.TypeTCP,
|
||||
},
|
||||
want: want{
|
||||
message: []byte("echo-test3"),
|
||||
// We expect no headers here because they are sent back via
|
||||
// the stream.
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ws-tcp proxy",
|
||||
args: args{
|
||||
ingressServiceScheme: "tcp://",
|
||||
originService: runEchoTCPService,
|
||||
eyeballResponseWriter: newWSRespWriter(replayer),
|
||||
eyeballRequestBody: newWSRequestBody([]byte("test4")),
|
||||
connectionType: connection.TypeWebsocket,
|
||||
requestHeaders: map[string][]string{
|
||||
// Example key from https://tools.ietf.org/html/rfc6455#section-1.2
|
||||
"Sec-Websocket-Key": {"dGhlIHNhbXBsZSBub25jZQ=="},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
message: []byte("echo-test4"),
|
||||
headers: map[string][]string{
|
||||
"Connection": {"Upgrade"},
|
||||
"Sec-Websocket-Accept": {"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="},
|
||||
"Upgrade": {"websocket"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp-tcp proxy without warpRoutingService enabled",
|
||||
args: args{
|
||||
ingressServiceScheme: "tcp://",
|
||||
originService: runEchoTCPService,
|
||||
eyeballResponseWriter: newTCPRespWriter(replayer),
|
||||
eyeballRequestBody: newTCPRequestBody([]byte("test2")),
|
||||
connectionType: connection.TypeTCP,
|
||||
requestHeaders: map[string][]string{
|
||||
"Cf-Cloudflared-Proxy-Src": {"non-blank-value"},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
message: []byte{},
|
||||
err: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ws-ws proxy when origin is different",
|
||||
args: args{
|
||||
ingressServiceScheme: "ws://",
|
||||
originService: runEchoWSService,
|
||||
eyeballResponseWriter: newWSRespWriter(replayer),
|
||||
eyeballRequestBody: newWSRequestBody([]byte("test1")),
|
||||
connectionType: connection.TypeWebsocket,
|
||||
requestHeaders: map[string][]string{
|
||||
// Example key from https://tools.ietf.org/html/rfc6455#section-1.2
|
||||
"Sec-Websocket-Key": {"dGhlIHNhbXBsZSBub25jZQ=="},
|
||||
"Origin": {"Different origin"},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
message: []byte{},
|
||||
err: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp-* proxy when origin service has already closed the connection/ is no longer running",
|
||||
args: args{
|
||||
ingressServiceScheme: "tcp://",
|
||||
originService: func(t *testing.T, ln net.Listener) {
|
||||
// closing the listener created by the test.
|
||||
ln.Close()
|
||||
},
|
||||
eyeballResponseWriter: newTCPRespWriter(replayer),
|
||||
eyeballRequestBody: newTCPRequestBody([]byte("test2")),
|
||||
connectionType: connection.TypeTCP,
|
||||
requestHeaders: map[string][]string{
|
||||
"Cf-Cloudflared-Proxy-Src": {"non-blank-value"},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
message: []byte{},
|
||||
err: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
// Starts origin service
|
||||
test.args.originService(t, ln)
|
||||
|
||||
ingressRule := createSingleIngressConfig(t, test.args.ingressServiceScheme+ln.Addr().String())
|
||||
var wg sync.WaitGroup
|
||||
errC := make(chan error)
|
||||
ingressRule.StartOrigins(&wg, logger, ctx.Done(), errC)
|
||||
proxy := NewOriginProxy(ingressRule, test.args.warpRoutingService, testTags, logger)
|
||||
|
||||
req, err := http.NewRequest(
|
||||
http.MethodGet,
|
||||
test.args.ingressServiceScheme+ln.Addr().String(),
|
||||
test.args.eyeballRequestBody,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header = test.args.requestHeaders
|
||||
respWriter := test.args.eyeballResponseWriter
|
||||
|
||||
if pipedReqBody, ok := test.args.eyeballRequestBody.(*pipedRequestBody); ok {
|
||||
respWriter = newTCPRespWriter(pipedReqBody.pipedConn)
|
||||
go func() {
|
||||
resp := pipedReqBody.roundtrip(test.args.ingressServiceScheme + ln.Addr().String())
|
||||
replayer.Write(resp)
|
||||
}()
|
||||
}
|
||||
|
||||
err = proxy.Proxy(respWriter, req, test.args.connectionType)
|
||||
|
||||
cancel()
|
||||
assert.Equal(t, test.want.err, err != nil)
|
||||
assert.Equal(t, test.want.message, replayer.Bytes())
|
||||
respPrinter := respWriter.(responsePrinter)
|
||||
assert.Equal(t, test.want.headers, respPrinter.headers())
|
||||
replayer.rw.Reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type requestBody struct {
|
||||
pw *io.PipeWriter
|
||||
pr *io.PipeReader
|
||||
}
|
||||
|
||||
func newWSRequestBody(data []byte) *requestBody {
|
||||
pr, pw := io.Pipe()
|
||||
go wsutil.WriteClientBinary(pw, data)
|
||||
return &requestBody{
|
||||
pr: pr,
|
||||
pw: pw,
|
||||
}
|
||||
}
|
||||
func newTCPRequestBody(data []byte) *requestBody {
|
||||
pr, pw := io.Pipe()
|
||||
go pw.Write(data)
|
||||
return &requestBody{
|
||||
pr: pr,
|
||||
pw: pw,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *requestBody) Read(p []byte) (n int, err error) {
|
||||
return r.pr.Read(p)
|
||||
}
|
||||
|
||||
func (r *requestBody) Close() error {
|
||||
r.pw.Close()
|
||||
r.pr.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
type pipedRequestBody struct {
|
||||
dialer gorillaWS.Dialer
|
||||
pipedConn net.Conn
|
||||
wsConn net.Conn
|
||||
messageToWrite []byte
|
||||
}
|
||||
|
||||
func newPipedWSRequestBody(data []byte) *pipedRequestBody {
|
||||
conn1, conn2 := net.Pipe()
|
||||
dialer := gorillaWS.Dialer{
|
||||
NetDial: func(network, addr string) (net.Conn, error) {
|
||||
return conn2, nil
|
||||
},
|
||||
}
|
||||
return &pipedRequestBody{
|
||||
dialer: dialer,
|
||||
pipedConn: conn1,
|
||||
wsConn: conn2,
|
||||
messageToWrite: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pipedRequestBody) roundtrip(addr string) []byte {
|
||||
header := http.Header{}
|
||||
conn, resp, err := p.dialer.Dial(addr, header)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
panic(fmt.Errorf("resp returned status code: %d", resp.StatusCode))
|
||||
}
|
||||
|
||||
err = conn.WriteMessage(gorillaWS.TextMessage, p.messageToWrite)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (p *pipedRequestBody) Read(data []byte) (n int, err error) {
|
||||
return p.pipedConn.Read(data)
|
||||
}
|
||||
|
||||
func (p *pipedRequestBody) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type responsePrinter interface {
|
||||
headers() http.Header
|
||||
}
|
||||
|
||||
type wsRespWriter struct {
|
||||
w io.Writer
|
||||
responseHeaders http.Header
|
||||
code int
|
||||
}
|
||||
|
||||
// newWSRespWriter uses wsutil.WriteClientText to generate websocket frames.
|
||||
// and wsutil.ReadClientText to translate frames from server to byte data.
|
||||
// In essence, this acts as a wsClient.
|
||||
func newWSRespWriter(w io.Writer) *wsRespWriter {
|
||||
return &wsRespWriter{
|
||||
w: w,
|
||||
}
|
||||
}
|
||||
|
||||
// Write is written to by ingress.Stream and serves as the output to the client.
|
||||
func (w *wsRespWriter) Write(p []byte) (int, error) {
|
||||
returnedMsg, err := wsutil.ReadServerBinary(bytes.NewBuffer(p))
|
||||
if err != nil {
|
||||
// The data was not returned by a websocket connecton.
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
return w.w.Write(p)
|
||||
}
|
||||
}
|
||||
return w.w.Write(returnedMsg)
|
||||
}
|
||||
|
||||
func (w *wsRespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
w.responseHeaders = header
|
||||
w.code = status
|
||||
return nil
|
||||
}
|
||||
|
||||
// respHeaders is a test function to read respHeaders
|
||||
func (w *wsRespWriter) headers() http.Header {
|
||||
return w.responseHeaders
|
||||
}
|
||||
|
||||
type mockTCPRespWriter struct {
|
||||
w io.Writer
|
||||
responseHeaders http.Header
|
||||
code int
|
||||
}
|
||||
|
||||
func newTCPRespWriter(w io.Writer) *mockTCPRespWriter {
|
||||
return &mockTCPRespWriter{
|
||||
w: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockTCPRespWriter) Write(p []byte) (n int, err error) {
|
||||
return m.w.Write(p)
|
||||
}
|
||||
|
||||
func (m *mockTCPRespWriter) WriteRespHeaders(status int, header http.Header) error {
|
||||
m.responseHeaders = header
|
||||
m.code = status
|
||||
return nil
|
||||
}
|
||||
|
||||
// respHeaders is a test function to read respHeaders
|
||||
func (m *mockTCPRespWriter) headers() http.Header {
|
||||
return m.responseHeaders
|
||||
}
|
||||
|
||||
func createSingleIngressConfig(t *testing.T, service string) ingress.Ingress {
|
||||
ingressConfig := &config.Configuration{
|
||||
Ingress: []config.UnvalidatedIngressRule{
|
||||
{
|
||||
Hostname: "*",
|
||||
Service: service,
|
||||
},
|
||||
},
|
||||
}
|
||||
ingressRule, err := ingress.ParseIngress(ingressConfig)
|
||||
require.NoError(t, err)
|
||||
return ingressRule
|
||||
}
|
||||
|
||||
func runEchoTCPService(t *testing.T, l net.Listener) {
|
||||
go func() {
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
buf := make([]byte, 1024)
|
||||
size, err := conn.Read(buf)
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
data := []byte("echo-")
|
||||
data = append(data, buf[:size]...)
|
||||
_, err = conn.Write(data)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func runEchoWSService(t *testing.T, l net.Listener) {
|
||||
var upgrader = gorillaWS.Upgrader{
|
||||
ReadBufferSize: 10,
|
||||
WriteBufferSize: 10,
|
||||
}
|
||||
|
||||
var ws = func(w http.ResponseWriter, r *http.Request) {
|
||||
header := make(http.Header)
|
||||
for k, vs := range r.Header {
|
||||
if k == "Test-Cloudflared-Echo" {
|
||||
header[k] = vs
|
||||
}
|
||||
}
|
||||
conn, err := upgrader.Upgrade(w, r, header)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
messageType, p, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data := []byte("echo-")
|
||||
data = append(data, p...)
|
||||
if err := conn.WriteMessage(messageType, data); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server := http.Server{
|
||||
Handler: http.HandlerFunc(ws),
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := server.Serve(l)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
}
|
||||
|
||||
+10
-8
@@ -244,7 +244,7 @@ func selectNextProtocol(
|
||||
// on error returns a flag indicating if error can be retried
|
||||
func ServeTunnel(
|
||||
ctx context.Context,
|
||||
connLong *zerolog.Logger,
|
||||
connLog *zerolog.Logger,
|
||||
credentialManager *reconnectCredentialManager,
|
||||
config *TunnelConfig,
|
||||
addr *net.TCPAddr,
|
||||
@@ -273,6 +273,7 @@ func ServeTunnel(
|
||||
|
||||
edgeConn, err := edgediscovery.DialEdge(ctx, dialTimeout, config.EdgeTLSConfigs[protocol], addr)
|
||||
if err != nil {
|
||||
connLog.Err(err).Msg("Unable to establish connection with Cloudflare edge")
|
||||
return err, true
|
||||
}
|
||||
connectedFuse := &connectedFuse{
|
||||
@@ -284,7 +285,7 @@ func ServeTunnel(
|
||||
connOptions := config.ConnectionOptions(edgeConn.LocalAddr().String(), uint8(backoff.retries))
|
||||
err = ServeHTTP2(
|
||||
ctx,
|
||||
connLong,
|
||||
connLog,
|
||||
config,
|
||||
edgeConn,
|
||||
connOptions,
|
||||
@@ -296,7 +297,7 @@ func ServeTunnel(
|
||||
} else {
|
||||
err = ServeH2mux(
|
||||
ctx,
|
||||
connLong,
|
||||
connLog,
|
||||
credentialManager,
|
||||
config,
|
||||
edgeConn,
|
||||
@@ -311,28 +312,29 @@ func ServeTunnel(
|
||||
if err != nil {
|
||||
switch err := err.(type) {
|
||||
case connection.DupConnRegisterTunnelError:
|
||||
connLog.Err(err).Msg("Unable to establish connection.")
|
||||
// don't retry this connection anymore, let supervisor pick a new address
|
||||
return err, false
|
||||
case connection.ServerRegisterTunnelError:
|
||||
connLong.Err(err).Msg("Register tunnel error from server side")
|
||||
connLog.Err(err).Msg("Register tunnel error from server side")
|
||||
// Don't send registration error return from server to Sentry. They are
|
||||
// logged on server side
|
||||
if incidents := config.IncidentLookup.ActiveIncidents(); len(incidents) > 0 {
|
||||
connLong.Error().Msg(activeIncidentsMsg(incidents))
|
||||
connLog.Error().Msg(activeIncidentsMsg(incidents))
|
||||
}
|
||||
return err.Cause, !err.Permanent
|
||||
case ReconnectSignal:
|
||||
connLong.Info().
|
||||
connLog.Info().
|
||||
Uint8(connection.LogFieldConnIndex, connIndex).
|
||||
Msgf("Restarting connection due to reconnect signal in %s", err.Delay)
|
||||
err.DelayBeforeReconnect()
|
||||
return err, true
|
||||
default:
|
||||
if err == context.Canceled {
|
||||
connLong.Debug().Err(err).Msgf("Serve tunnel error")
|
||||
connLog.Debug().Err(err).Msgf("Serve tunnel error")
|
||||
return err, false
|
||||
}
|
||||
connLong.Err(err).Msgf("Serve tunnel error")
|
||||
connLog.Err(err).Msgf("Serve tunnel error")
|
||||
_, permanent := err.(unrecoverableError)
|
||||
return err, !permanent
|
||||
}
|
||||
|
||||
@@ -40,8 +40,10 @@ func TestWaitForBackoffFallback(t *testing.T) {
|
||||
mockFetcher := dynamicMockFetcher{
|
||||
percentage: 0,
|
||||
}
|
||||
warpRoutingEnabled := false
|
||||
protocolSelector, err := connection.NewProtocolSelector(
|
||||
connection.HTTP2.String(),
|
||||
warpRoutingEnabled,
|
||||
namedTunnel,
|
||||
mockFetcher.fetch(),
|
||||
resolveTTL,
|
||||
|
||||
@@ -3,7 +3,10 @@ package socks
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// RequestHandler is the functions needed to handle a SOCKS5 command
|
||||
@@ -104,3 +107,13 @@ func (h *StandardRequestHandler) handleAssociate(conn io.ReadWriter, req *Reques
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func StreamHandler(tunnelConn io.ReadWriter, originConn net.Conn, log *zerolog.Logger) {
|
||||
dialer := NewConnDialer(originConn)
|
||||
requestHandler := NewRequestHandler(dialer)
|
||||
socksServer := NewConnectionHandler(requestHandler)
|
||||
|
||||
if err := socksServer.Serve(tunnelConn); err != nil {
|
||||
log.Debug().Err(err).Msg("Socks stream handler error")
|
||||
}
|
||||
}
|
||||
|
||||
+15
-6
@@ -16,9 +16,9 @@ import (
|
||||
// network, and says that eyeballs can reach that route using the corresponding
|
||||
// tunnel.
|
||||
type Route struct {
|
||||
Network CIDR
|
||||
Network CIDR `json:"network"`
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
Comment string
|
||||
Comment string `json:"comment"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
}
|
||||
@@ -26,11 +26,20 @@ type Route struct {
|
||||
// CIDR is just a newtype wrapper around net.IPNet. It adds JSON unmarshalling.
|
||||
type CIDR net.IPNet
|
||||
|
||||
func (c *CIDR) String() string {
|
||||
n := net.IPNet(*c)
|
||||
func (c CIDR) String() string {
|
||||
n := net.IPNet(c)
|
||||
return n.String()
|
||||
}
|
||||
|
||||
func (c CIDR) MarshalJSON() ([]byte, error) {
|
||||
str := c.String()
|
||||
json, err := json.Marshal(str)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error serializing CIDR into JSON")
|
||||
}
|
||||
return json, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses a JSON string into net.IPNet
|
||||
func (c *CIDR) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
@@ -68,9 +77,9 @@ func (r NewRoute) MarshalJSON() ([]byte, error) {
|
||||
|
||||
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
|
||||
type DetailedRoute struct {
|
||||
Network CIDR
|
||||
Network CIDR `json:"network"`
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
Comment string
|
||||
Comment string `json:"comment"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
TunnelName string `json:"tunnel_name"`
|
||||
|
||||
+10
-3
@@ -33,15 +33,15 @@ func TestUnmarshalRoute(t *testing.T) {
|
||||
require.Equal(t, "test", r.Comment)
|
||||
}
|
||||
|
||||
func TestUnmarshalDetailedRoute(t *testing.T) {
|
||||
func TestDetailedRouteJsonRoundtrip(t *testing.T) {
|
||||
// Response from the teamnet route backend
|
||||
data := `{
|
||||
"network":"10.1.2.40/29",
|
||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||
"tunnel_name":"Mr. Tun",
|
||||
"comment":"test",
|
||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||
"deleted_at":null
|
||||
"deleted_at":"2021-01-14T05:01:42.183002Z",
|
||||
"tunnel_name":"Mr. Tun"
|
||||
}`
|
||||
var r DetailedRoute
|
||||
err := json.Unmarshal([]byte(data), &r)
|
||||
@@ -55,6 +55,13 @@ func TestUnmarshalDetailedRoute(t *testing.T) {
|
||||
require.Equal(t, CIDR(*cidr), r.Network)
|
||||
require.Equal(t, "test", r.Comment)
|
||||
require.Equal(t, "Mr. Tun", r.TunnelName)
|
||||
|
||||
bytes, err := json.Marshal(r)
|
||||
require.NoError(t, err)
|
||||
obtainedJson := string(bytes)
|
||||
data = strings.Replace(data, "\t", "", -1)
|
||||
data = strings.Replace(data, "\n", "", -1)
|
||||
require.Equal(t, data, obtainedJson)
|
||||
}
|
||||
|
||||
func TestMarshalNewRoute(t *testing.T) {
|
||||
|
||||
+2
@@ -1,5 +1,7 @@
|
||||
module github.com/rs/zerolog
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e
|
||||
github.com/pkg/errors v0.8.1
|
||||
|
||||
+4
-8
@@ -1,7 +1,7 @@
|
||||
cli
|
||||
===
|
||||
|
||||
[](https://pkg.go.dev/github.com/urfave/cli/v2)
|
||||
[](https://godoc.org/github.com/urfave/cli)
|
||||
[](https://codebeat.co/projects/github-com-urfave-cli)
|
||||
[](https://goreportcard.com/report/urfave/cli)
|
||||
[](https://codecov.io/gh/urfave/cli)
|
||||
@@ -17,15 +17,11 @@ Usage documentation exists for each major version. Don't know what version you'r
|
||||
- `v2` - [./docs/v2/manual.md](./docs/v2/manual.md)
|
||||
- `v1` - [./docs/v1/manual.md](./docs/v1/manual.md)
|
||||
|
||||
Guides for migrating to newer versions:
|
||||
|
||||
- `v1-to-v2` - [./docs/migrate-v1-to-v2.md](./docs/migrate-v1-to-v2.md)
|
||||
|
||||
## Installation
|
||||
|
||||
Using this package requires a working Go environment. [See the install instructions for Go](http://golang.org/doc/install.html).
|
||||
Make sure you have a working Go environment. Go version 1.11+ is supported. [See the install instructions for Go](http://golang.org/doc/install.html).
|
||||
|
||||
Go Modules are required when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules).
|
||||
Go Modules are strongly recommended when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules).
|
||||
|
||||
### Using `v2` releases
|
||||
|
||||
@@ -67,4 +63,4 @@ export PATH=$PATH:$GOPATH/bin
|
||||
|
||||
cli is tested against multiple versions of Go on Linux, and against the latest
|
||||
released version of Go on OS X and Windows. This project uses Github Actions for
|
||||
builds. To see our currently supported go versions and platforms, look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/master/.github/workflows/cli.yml).
|
||||
builds. For more build info, please look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/master/.github/workflows/cli.yml).
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
package altsrc
|
||||
|
||||
// defaultInputSource creates a default InputSourceContext.
|
||||
func defaultInputSource() (InputSourceContext, error) {
|
||||
return &MapInputSource{file: "", valueMap: map[interface{}]interface{}{}}, nil
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
# Only types that provide implementation of FlagInputSourceExtension can be listed here
|
||||
# please keep list sorted alphabetically
|
||||
flag_types = [
|
||||
"Bool",
|
||||
"Duration",
|
||||
"Float64",
|
||||
"Generic",
|
||||
"Int",
|
||||
"IntSlice",
|
||||
"Path",
|
||||
"String",
|
||||
"StringSlice",
|
||||
]
|
||||
|
||||
print('''// Code generated by fg.py; DO NOT EDIT.
|
||||
|
||||
package altsrc
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)''')
|
||||
|
||||
for t in flag_types:
|
||||
print(f'''
|
||||
// {t}Flag is the flag type that wraps cli.{t}Flag to allow
|
||||
// for other values to be specified
|
||||
type {t}Flag struct {{
|
||||
*cli.{t}Flag
|
||||
set *flag.FlagSet
|
||||
}}
|
||||
var _ FlagInputSourceExtension = (*{t}Flag)(nil)
|
||||
|
||||
// New{t}Flag creates a new {t}Flag
|
||||
func New{t}Flag(fl *cli.{t}Flag) *{t}Flag {{
|
||||
return &{t}Flag{{{t}Flag: fl, set: nil}}
|
||||
}}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls
|
||||
// the wrapped {t}Flag.Apply
|
||||
func (f *{t}Flag) Apply(set *flag.FlagSet) error {{
|
||||
f.set = set
|
||||
return f.{t}Flag.Apply(set)
|
||||
}}''')
|
||||
+105
-19
@@ -1,4 +1,4 @@
|
||||
// Code generated by fg.py; DO NOT EDIT.
|
||||
// Code generated by fg; DO NOT EDIT.
|
||||
|
||||
package altsrc
|
||||
|
||||
@@ -14,7 +14,6 @@ type BoolFlag struct {
|
||||
*cli.BoolFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
var _ FlagInputSourceExtension = (*BoolFlag)(nil)
|
||||
|
||||
// NewBoolFlag creates a new BoolFlag
|
||||
func NewBoolFlag(fl *cli.BoolFlag) *BoolFlag {
|
||||
@@ -34,7 +33,6 @@ type DurationFlag struct {
|
||||
*cli.DurationFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
var _ FlagInputSourceExtension = (*DurationFlag)(nil)
|
||||
|
||||
// NewDurationFlag creates a new DurationFlag
|
||||
func NewDurationFlag(fl *cli.DurationFlag) *DurationFlag {
|
||||
@@ -54,7 +52,6 @@ type Float64Flag struct {
|
||||
*cli.Float64Flag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
var _ FlagInputSourceExtension = (*Float64Flag)(nil)
|
||||
|
||||
// NewFloat64Flag creates a new Float64Flag
|
||||
func NewFloat64Flag(fl *cli.Float64Flag) *Float64Flag {
|
||||
@@ -74,7 +71,6 @@ type GenericFlag struct {
|
||||
*cli.GenericFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
var _ FlagInputSourceExtension = (*GenericFlag)(nil)
|
||||
|
||||
// NewGenericFlag creates a new GenericFlag
|
||||
func NewGenericFlag(fl *cli.GenericFlag) *GenericFlag {
|
||||
@@ -88,13 +84,31 @@ func (f *GenericFlag) Apply(set *flag.FlagSet) error {
|
||||
return f.GenericFlag.Apply(set)
|
||||
}
|
||||
|
||||
// Int64Flag is the flag type that wraps cli.Int64Flag to allow
|
||||
// for other values to be specified
|
||||
type Int64Flag struct {
|
||||
*cli.Int64Flag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewInt64Flag creates a new Int64Flag
|
||||
func NewInt64Flag(fl *cli.Int64Flag) *Int64Flag {
|
||||
return &Int64Flag{Int64Flag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls
|
||||
// the wrapped Int64Flag.Apply
|
||||
func (f *Int64Flag) Apply(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.Int64Flag.Apply(set)
|
||||
}
|
||||
|
||||
// IntFlag is the flag type that wraps cli.IntFlag to allow
|
||||
// for other values to be specified
|
||||
type IntFlag struct {
|
||||
*cli.IntFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
var _ FlagInputSourceExtension = (*IntFlag)(nil)
|
||||
|
||||
// NewIntFlag creates a new IntFlag
|
||||
func NewIntFlag(fl *cli.IntFlag) *IntFlag {
|
||||
@@ -114,7 +128,6 @@ type IntSliceFlag struct {
|
||||
*cli.IntSliceFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
var _ FlagInputSourceExtension = (*IntSliceFlag)(nil)
|
||||
|
||||
// NewIntSliceFlag creates a new IntSliceFlag
|
||||
func NewIntSliceFlag(fl *cli.IntSliceFlag) *IntSliceFlag {
|
||||
@@ -128,24 +141,42 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
|
||||
return f.IntSliceFlag.Apply(set)
|
||||
}
|
||||
|
||||
// PathFlag is the flag type that wraps cli.PathFlag to allow
|
||||
// Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow
|
||||
// for other values to be specified
|
||||
type PathFlag struct {
|
||||
*cli.PathFlag
|
||||
type Int64SliceFlag struct {
|
||||
*cli.Int64SliceFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
var _ FlagInputSourceExtension = (*PathFlag)(nil)
|
||||
|
||||
// NewPathFlag creates a new PathFlag
|
||||
func NewPathFlag(fl *cli.PathFlag) *PathFlag {
|
||||
return &PathFlag{PathFlag: fl, set: nil}
|
||||
// NewInt64SliceFlag creates a new Int64SliceFlag
|
||||
func NewInt64SliceFlag(fl *cli.Int64SliceFlag) *Int64SliceFlag {
|
||||
return &Int64SliceFlag{Int64SliceFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls
|
||||
// the wrapped PathFlag.Apply
|
||||
func (f *PathFlag) Apply(set *flag.FlagSet) error {
|
||||
// the wrapped Int64SliceFlag.Apply
|
||||
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.PathFlag.Apply(set)
|
||||
return f.Int64SliceFlag.Apply(set)
|
||||
}
|
||||
|
||||
// Float64SliceFlag is the flag type that wraps cli.Float64SliceFlag to allow
|
||||
// for other values to be specified
|
||||
type Float64SliceFlag struct {
|
||||
*cli.Float64SliceFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewFloat64SliceFlag creates a new Float64SliceFlag
|
||||
func NewFloat64SliceFlag(fl *cli.Float64SliceFlag) *Float64SliceFlag {
|
||||
return &Float64SliceFlag{Float64SliceFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped Float64SliceFlag.Apply
|
||||
func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.Float64SliceFlag.Apply(set)
|
||||
}
|
||||
|
||||
// StringFlag is the flag type that wraps cli.StringFlag to allow
|
||||
@@ -154,7 +185,6 @@ type StringFlag struct {
|
||||
*cli.StringFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
var _ FlagInputSourceExtension = (*StringFlag)(nil)
|
||||
|
||||
// NewStringFlag creates a new StringFlag
|
||||
func NewStringFlag(fl *cli.StringFlag) *StringFlag {
|
||||
@@ -168,13 +198,31 @@ func (f *StringFlag) Apply(set *flag.FlagSet) error {
|
||||
return f.StringFlag.Apply(set)
|
||||
}
|
||||
|
||||
// PathFlag is the flag type that wraps cli.PathFlag to allow
|
||||
// for other values to be specified
|
||||
type PathFlag struct {
|
||||
*cli.PathFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewPathFlag creates a new PathFlag
|
||||
func NewPathFlag(fl *cli.PathFlag) *PathFlag {
|
||||
return &PathFlag{PathFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls the
|
||||
// wrapped PathFlag.Apply
|
||||
func (f *PathFlag) Apply(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.PathFlag.Apply(set)
|
||||
}
|
||||
|
||||
// StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow
|
||||
// for other values to be specified
|
||||
type StringSliceFlag struct {
|
||||
*cli.StringSliceFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
var _ FlagInputSourceExtension = (*StringSliceFlag)(nil)
|
||||
|
||||
// NewStringSliceFlag creates a new StringSliceFlag
|
||||
func NewStringSliceFlag(fl *cli.StringSliceFlag) *StringSliceFlag {
|
||||
@@ -187,3 +235,41 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.StringSliceFlag.Apply(set)
|
||||
}
|
||||
|
||||
// Uint64Flag is the flag type that wraps cli.Uint64Flag to allow
|
||||
// for other values to be specified
|
||||
type Uint64Flag struct {
|
||||
*cli.Uint64Flag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewUint64Flag creates a new Uint64Flag
|
||||
func NewUint64Flag(fl *cli.Uint64Flag) *Uint64Flag {
|
||||
return &Uint64Flag{Uint64Flag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls
|
||||
// the wrapped Uint64Flag.Apply
|
||||
func (f *Uint64Flag) Apply(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.Uint64Flag.Apply(set)
|
||||
}
|
||||
|
||||
// UintFlag is the flag type that wraps cli.UintFlag to allow
|
||||
// for other values to be specified
|
||||
type UintFlag struct {
|
||||
*cli.UintFlag
|
||||
set *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewUintFlag creates a new UintFlag
|
||||
func NewUintFlag(fl *cli.UintFlag) *UintFlag {
|
||||
return &UintFlag{UintFlag: fl, set: nil}
|
||||
}
|
||||
|
||||
// Apply saves the flagSet for later usage calls, then calls
|
||||
// the wrapped UintFlag.Apply
|
||||
func (f *UintFlag) Apply(set *flag.FlagSet) error {
|
||||
f.set = set
|
||||
return f.UintFlag.Apply(set)
|
||||
}
|
||||
|
||||
+1
-5
@@ -17,11 +17,7 @@ import (
|
||||
// by the given flag.
|
||||
func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceContext, error) {
|
||||
return func(context *cli.Context) (InputSourceContext, error) {
|
||||
if context.IsSet(flag) {
|
||||
return NewJSONSourceFromFile(context.String(flag))
|
||||
}
|
||||
|
||||
return defaultInputSource()
|
||||
return NewJSONSourceFromFile(context.String(flag))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-5
@@ -16,11 +16,6 @@ type MapInputSource struct {
|
||||
valueMap map[interface{}]interface{}
|
||||
}
|
||||
|
||||
// NewMapInputSource creates a new MapInputSource for implementing custom input sources.
|
||||
func NewMapInputSource(file string, valueMap map[interface{}]interface{}) *MapInputSource {
|
||||
return &MapInputSource{file: file, valueMap: valueMap}
|
||||
}
|
||||
|
||||
// nestedVal checks if the name has '.' delimiters.
|
||||
// If so, it tries to traverse the tree by the '.' delimited sections to find
|
||||
// a nested value for the key.
|
||||
|
||||
+2
-6
@@ -87,12 +87,8 @@ func NewTomlSourceFromFile(file string) (InputSourceContext, error) {
|
||||
// NewTomlSourceFromFlagFunc creates a new TOML InputSourceContext from a provided flag name and source context.
|
||||
func NewTomlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
|
||||
return func(context *cli.Context) (InputSourceContext, error) {
|
||||
if context.IsSet(flagFileName) {
|
||||
filePath := context.String(flagFileName)
|
||||
return NewTomlSourceFromFile(filePath)
|
||||
}
|
||||
|
||||
return defaultInputSource()
|
||||
filePath := context.String(flagFileName)
|
||||
return NewTomlSourceFromFile(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-6
@@ -33,12 +33,8 @@ func NewYamlSourceFromFile(file string) (InputSourceContext, error) {
|
||||
// NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context.
|
||||
func NewYamlSourceFromFlagFunc(flagFileName string) func(context *cli.Context) (InputSourceContext, error) {
|
||||
return func(context *cli.Context) (InputSourceContext, error) {
|
||||
if context.IsSet(flagFileName) {
|
||||
filePath := context.String(flagFileName)
|
||||
return NewYamlSourceFromFile(filePath)
|
||||
}
|
||||
|
||||
return defaultInputSource()
|
||||
filePath := context.String(flagFileName)
|
||||
return NewYamlSourceFromFile(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-19
@@ -64,7 +64,7 @@ type App struct {
|
||||
Action ActionFunc
|
||||
// Execute this function if the proper command cannot be found
|
||||
CommandNotFound CommandNotFoundFunc
|
||||
// Execute this function if a usage error occurs
|
||||
// Execute this function if an usage error occurs
|
||||
OnUsageError OnUsageErrorFunc
|
||||
// Compilation date
|
||||
Compiled time.Time
|
||||
@@ -72,15 +72,12 @@ type App struct {
|
||||
Authors []*Author
|
||||
// Copyright of the binary if any
|
||||
Copyright string
|
||||
// Reader reader to write input to (useful for tests)
|
||||
Reader io.Reader
|
||||
// Writer writer to write output to
|
||||
Writer io.Writer
|
||||
// ErrWriter writes error output
|
||||
ErrWriter io.Writer
|
||||
// ExitErrHandler processes any error encountered while running an App before
|
||||
// it is returned to the caller. If no function is provided, HandleExitCoder
|
||||
// is used as the default behavior.
|
||||
// Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to
|
||||
// function as a default, so this is optional.
|
||||
ExitErrHandler ExitErrHandlerFunc
|
||||
// Other custom info
|
||||
Metadata map[string]interface{}
|
||||
@@ -119,9 +116,7 @@ func NewApp() *App {
|
||||
BashComplete: DefaultAppComplete,
|
||||
Action: helpCommand.Action,
|
||||
Compiled: compileTime(),
|
||||
Reader: os.Stdin,
|
||||
Writer: os.Stdout,
|
||||
ErrWriter: os.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +135,7 @@ func (a *App) Setup() {
|
||||
}
|
||||
|
||||
if a.HelpName == "" {
|
||||
a.HelpName = a.Name
|
||||
a.HelpName = filepath.Base(os.Args[0])
|
||||
}
|
||||
|
||||
if a.Usage == "" {
|
||||
@@ -163,18 +158,10 @@ func (a *App) Setup() {
|
||||
a.Compiled = compileTime()
|
||||
}
|
||||
|
||||
if a.Reader == nil {
|
||||
a.Reader = os.Stdin
|
||||
}
|
||||
|
||||
if a.Writer == nil {
|
||||
a.Writer = os.Stdout
|
||||
}
|
||||
|
||||
if a.ErrWriter == nil {
|
||||
a.ErrWriter = os.Stderr
|
||||
}
|
||||
|
||||
var newCommands []*Command
|
||||
|
||||
for _, c := range a.Commands {
|
||||
@@ -208,6 +195,10 @@ func (a *App) Setup() {
|
||||
if a.Metadata == nil {
|
||||
a.Metadata = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if a.Writer == nil {
|
||||
a.Writer = os.Stdout
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) newFlagSet() (*flag.FlagSet, error) {
|
||||
@@ -299,6 +290,8 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
|
||||
if a.Before != nil {
|
||||
beforeErr := a.Before(context)
|
||||
if beforeErr != nil {
|
||||
_, _ = fmt.Fprintf(a.Writer, "%v\n\n", beforeErr)
|
||||
_ = ShowAppHelp(context)
|
||||
a.handleExitCoder(context, beforeErr)
|
||||
err = beforeErr
|
||||
return err
|
||||
@@ -328,11 +321,11 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
|
||||
// RunAndExitOnError calls .Run() and exits non-zero if an error was returned
|
||||
//
|
||||
// Deprecated: instead you should return an error that fulfills cli.ExitCoder
|
||||
// to cli.App.Run. This will cause the application to exit with the given error
|
||||
// to cli.App.Run. This will cause the application to exit with the given eror
|
||||
// code in the cli.ExitCoder
|
||||
func (a *App) RunAndExitOnError() {
|
||||
if err := a.Run(os.Args); err != nil {
|
||||
_, _ = fmt.Fprintln(a.ErrWriter, err)
|
||||
_, _ = fmt.Fprintln(a.errWriter(), err)
|
||||
OsExiter(1)
|
||||
}
|
||||
}
|
||||
@@ -486,6 +479,15 @@ func (a *App) VisibleFlags() []Flag {
|
||||
return visibleFlags(a.Flags)
|
||||
}
|
||||
|
||||
func (a *App) errWriter() io.Writer {
|
||||
// When the app ErrWriter is nil use the package level one.
|
||||
if a.ErrWriter == nil {
|
||||
return ErrWriter
|
||||
}
|
||||
|
||||
return a.ErrWriter
|
||||
}
|
||||
|
||||
func (a *App) appendFlag(fl Flag) {
|
||||
if !hasFlag(a.Flags, fl) {
|
||||
a.Flags = append(a.Flags, fl)
|
||||
|
||||
+2
-2
@@ -150,6 +150,7 @@ func (c *Command) Run(ctx *Context) (err error) {
|
||||
if c.Before != nil {
|
||||
err = c.Before(context)
|
||||
if err != nil {
|
||||
_ = ShowCommandHelp(context, c.Name)
|
||||
context.App.handleExitCoder(context, err)
|
||||
return err
|
||||
}
|
||||
@@ -241,9 +242,8 @@ func (c *Command) startApp(ctx *Context) error {
|
||||
app.HideHelpCommand = c.HideHelpCommand
|
||||
|
||||
app.Version = ctx.App.Version
|
||||
app.HideVersion = true
|
||||
app.HideVersion = ctx.App.HideVersion
|
||||
app.Compiled = ctx.App.Compiled
|
||||
app.Reader = ctx.App.Reader
|
||||
app.Writer = ctx.App.Writer
|
||||
app.ErrWriter = ctx.App.ErrWriter
|
||||
app.ExitErrHandler = ctx.App.ExitErrHandler
|
||||
|
||||
+18
-63
@@ -18,7 +18,6 @@ type Context struct {
|
||||
Command *Command
|
||||
shellComplete bool
|
||||
flagSet *flag.FlagSet
|
||||
fromFlagSet map[string]bool
|
||||
parentContext *Context
|
||||
}
|
||||
|
||||
@@ -33,14 +32,6 @@ func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context {
|
||||
}
|
||||
}
|
||||
|
||||
// pre-compute flag seen on the command line at this context
|
||||
c.fromFlagSet = make(map[string]bool)
|
||||
if set != nil {
|
||||
set.Visit(func(f *flag.Flag) {
|
||||
c.fromFlagSet[f.Name] = true
|
||||
})
|
||||
}
|
||||
|
||||
c.Command = &Command{}
|
||||
|
||||
if c.Context == nil {
|
||||
@@ -57,49 +48,35 @@ func (c *Context) NumFlags() int {
|
||||
|
||||
// Set sets a context flag to a value.
|
||||
func (c *Context) Set(name, value string) error {
|
||||
err := c.flagSet.Set(name, value)
|
||||
if err == nil {
|
||||
c.fromFlagSet[name] = true
|
||||
}
|
||||
|
||||
return err
|
||||
return c.flagSet.Set(name, value)
|
||||
}
|
||||
|
||||
// IsSet determines if the flag was actually set
|
||||
func (c *Context) IsSet(name string) bool {
|
||||
for ctx := c; ctx != nil; ctx = ctx.parentContext {
|
||||
// try flags parsed from command line first
|
||||
if ctx.flagSet.Lookup(name) == nil {
|
||||
// flag not defined in this context
|
||||
continue
|
||||
}
|
||||
if ctx.flagOnCommandLine(name) {
|
||||
return true
|
||||
}
|
||||
|
||||
// now see if value was set externally via environment
|
||||
definedFlags := ctx.Command.Flags
|
||||
if ctx.Command.Name == "" && ctx.App != nil {
|
||||
definedFlags = ctx.App.Flags
|
||||
}
|
||||
for _, ff := range definedFlags {
|
||||
for _, fn := range ff.Names() {
|
||||
if fn == name {
|
||||
if ff.IsSet() {
|
||||
return true
|
||||
}
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
isSet := false
|
||||
fs.Visit(func(f *flag.Flag) {
|
||||
if f.Name == name {
|
||||
isSet = true
|
||||
}
|
||||
})
|
||||
if isSet {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
f := lookupFlag(name, c)
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return f.IsSet()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Context) flagOnCommandLine(name string) bool {
|
||||
return c.fromFlagSet[name]
|
||||
}
|
||||
|
||||
// LocalFlagNames returns a slice of flag names used in this context.
|
||||
func (c *Context) LocalFlagNames() []string {
|
||||
var names []string
|
||||
@@ -131,10 +108,7 @@ func (c *Context) Lineage() []*Context {
|
||||
|
||||
// Value returns the value of the flag corresponding to `name`
|
||||
func (c *Context) Value(name string) interface{} {
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return fs.Lookup(name).Value.(flag.Getter).Get()
|
||||
}
|
||||
return nil
|
||||
return c.flagSet.Lookup(name).Value.(flag.Getter).Get()
|
||||
}
|
||||
|
||||
// Args returns the command line arguments associated with the context.
|
||||
@@ -176,25 +150,6 @@ func lookupFlag(name string, ctx *Context) Flag {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Context) resolveFlagDeep(name string) *flag.Flag {
|
||||
var src *flag.Flag
|
||||
for cur := c; cur != nil; cur = cur.parentContext {
|
||||
if f := cur.flagSet.Lookup(name); f != nil {
|
||||
if cur.flagOnCommandLine(name) {
|
||||
// we've found most specific instance on command line, use it
|
||||
src = f
|
||||
break
|
||||
}
|
||||
if src == nil {
|
||||
// flag was defined, but value is not present among flags of the current context
|
||||
// remember the most specific instance of the flag not from command line as fallback
|
||||
src = f
|
||||
}
|
||||
}
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
func lookupFlagSet(name string, ctx *Context) *flag.FlagSet {
|
||||
for _, c := range ctx.Lineage() {
|
||||
if f := c.flagSet.Lookup(name); f != nil {
|
||||
|
||||
+5
-14
@@ -15,39 +15,31 @@ import (
|
||||
// The function errors if either parsing or writing of the string fails.
|
||||
func (a *App) ToMarkdown() (string, error) {
|
||||
var w bytes.Buffer
|
||||
if err := a.writeDocTemplate(&w, 8); err != nil {
|
||||
if err := a.writeDocTemplate(&w); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return w.String(), nil
|
||||
}
|
||||
|
||||
// ToMan creates a man page string with section number for the `*App`
|
||||
// ToMan creates a man page string for the `*App`
|
||||
// The function errors if either parsing or writing of the string fails.
|
||||
func (a *App) ToManWithSection(sectionNumber int) (string, error) {
|
||||
func (a *App) ToMan() (string, error) {
|
||||
var w bytes.Buffer
|
||||
if err := a.writeDocTemplate(&w, sectionNumber); err != nil {
|
||||
if err := a.writeDocTemplate(&w); err != nil {
|
||||
return "", err
|
||||
}
|
||||
man := md2man.Render(w.Bytes())
|
||||
return string(man), nil
|
||||
}
|
||||
|
||||
// ToMan creates a man page string for the `*App`
|
||||
// The function errors if either parsing or writing of the string fails.
|
||||
func (a *App) ToMan() (string, error) {
|
||||
man, err := a.ToManWithSection(8)
|
||||
return man, err
|
||||
}
|
||||
|
||||
type cliTemplate struct {
|
||||
App *App
|
||||
SectionNum int
|
||||
Commands []string
|
||||
GlobalArgs []string
|
||||
SynopsisArgs []string
|
||||
}
|
||||
|
||||
func (a *App) writeDocTemplate(w io.Writer, sectionNum int) error {
|
||||
func (a *App) writeDocTemplate(w io.Writer) error {
|
||||
const name = "cli"
|
||||
t, err := template.New(name).Parse(MarkdownDocTemplate)
|
||||
if err != nil {
|
||||
@@ -55,7 +47,6 @@ func (a *App) writeDocTemplate(w io.Writer, sectionNum int) error {
|
||||
}
|
||||
return t.ExecuteTemplate(w, name, &cliTemplate{
|
||||
App: a,
|
||||
SectionNum: sectionNum,
|
||||
Commands: prepareCommands(a.Commands, 0),
|
||||
GlobalArgs: prepareArgsWithValues(a.VisibleFlags()),
|
||||
SynopsisArgs: prepareArgsSynopsis(a.VisibleFlags()),
|
||||
|
||||
+9
-19
@@ -17,10 +17,11 @@ var ErrWriter io.Writer = os.Stderr
|
||||
// MultiError is an error that wraps multiple errors.
|
||||
type MultiError interface {
|
||||
error
|
||||
// Errors returns a copy of the errors slice
|
||||
Errors() []error
|
||||
}
|
||||
|
||||
// newMultiError creates a new MultiError. Pass in one or more errors.
|
||||
// NewMultiError creates a new MultiError. Pass in one or more errors.
|
||||
func newMultiError(err ...error) MultiError {
|
||||
ret := multiError(err)
|
||||
return &ret
|
||||
@@ -64,20 +65,13 @@ type exitError struct {
|
||||
message interface{}
|
||||
}
|
||||
|
||||
// NewExitError calls Exit to create a new ExitCoder.
|
||||
//
|
||||
// Deprecated: This function is a duplicate of Exit and will eventually be removed.
|
||||
// NewExitError makes a new *exitError
|
||||
func NewExitError(message interface{}, exitCode int) ExitCoder {
|
||||
return Exit(message, exitCode)
|
||||
}
|
||||
|
||||
// Exit wraps a message and exit code into an error, which by default is
|
||||
// handled with a call to os.Exit during default error handling.
|
||||
//
|
||||
// This is the simplest way to trigger a non-zero exit code for an App without
|
||||
// having to call os.Exit manually. During testing, this behavior can be avoided
|
||||
// by overiding the ExitErrHandler function on an App or the package-global
|
||||
// OsExiter function.
|
||||
// Exit wraps a message and exit code into an ExitCoder suitable for handling by
|
||||
// HandleExitCoder
|
||||
func Exit(message interface{}, exitCode int) ExitCoder {
|
||||
return &exitError{
|
||||
message: message,
|
||||
@@ -93,14 +87,10 @@ func (ee *exitError) ExitCode() int {
|
||||
return ee.exitCode
|
||||
}
|
||||
|
||||
// HandleExitCoder handles errors implementing ExitCoder by printing their
|
||||
// message and calling OsExiter with the given exit code.
|
||||
//
|
||||
// If the given error instead implements MultiError, each error will be checked
|
||||
// for the ExitCoder interface, and OsExiter will be called with the last exit
|
||||
// code found, or exit code 1 if no ExitCoder is found.
|
||||
//
|
||||
// This function is the default error-handling behavior for an App.
|
||||
// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if
|
||||
// so prints the error to stderr (if it is non-empty) and calls OsExiter with the
|
||||
// given exit code. If the given error is a MultiError, then this func is
|
||||
// called on all members of the Errors slice and calls OsExiter with the last exit code.
|
||||
func HandleExitCoder(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
|
||||
-4
@@ -171,10 +171,6 @@ func fishAddFileFlag(flag Flag, completion *strings.Builder) {
|
||||
if f.TakesFile {
|
||||
return
|
||||
}
|
||||
case *PathFlag:
|
||||
if f.TakesFile {
|
||||
return
|
||||
}
|
||||
}
|
||||
completion.WriteString(" -f")
|
||||
}
|
||||
|
||||
+1
-5
@@ -359,11 +359,7 @@ func stringifySliceFlag(usage string, names, defaultVals []string) string {
|
||||
}
|
||||
|
||||
usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal))
|
||||
multiInputString := "(accepts multiple inputs)"
|
||||
if usageWithDefault != "" {
|
||||
multiInputString = "\t" + multiInputString
|
||||
}
|
||||
return fmt.Sprintf("%s\t%s%s", prefixedNames(names, placeholder), usageWithDefault, multiInputString)
|
||||
return fmt.Sprintf("%s\t%s", prefixedNames(names, placeholder), usageWithDefault)
|
||||
}
|
||||
|
||||
func hasFlag(flags []Flag, fl Flag) bool {
|
||||
|
||||
+6
-2
@@ -87,10 +87,14 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) error {
|
||||
// Bool looks up the value of a local BoolFlag, returns
|
||||
// false if not found
|
||||
func (c *Context) Bool(name string) bool {
|
||||
return lookupBool(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupBool(name, fs)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func lookupBool(f *flag.Flag) bool {
|
||||
func lookupBool(name string, set *flag.FlagSet) bool {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := strconv.ParseBool(f.Value.String())
|
||||
if err != nil {
|
||||
|
||||
+6
-2
@@ -86,10 +86,14 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) error {
|
||||
// Duration looks up the value of a local DurationFlag, returns
|
||||
// 0 if not found
|
||||
func (c *Context) Duration(name string) time.Duration {
|
||||
return lookupDuration(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupDuration(name, fs)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupDuration(f *flag.Flag) time.Duration {
|
||||
func lookupDuration(name string, set *flag.FlagSet) time.Duration {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := time.ParseDuration(f.Value.String())
|
||||
if err != nil {
|
||||
|
||||
+6
-2
@@ -87,10 +87,14 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) error {
|
||||
// Float64 looks up the value of a local Float64Flag, returns
|
||||
// 0 if not found
|
||||
func (c *Context) Float64(name string) float64 {
|
||||
return lookupFloat64(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupFloat64(name, fs)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupFloat64(f *flag.Flag) float64 {
|
||||
func lookupFloat64(name string, set *flag.FlagSet) float64 {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := strconv.ParseFloat(f.Value.String(), 64)
|
||||
if err != nil {
|
||||
|
||||
+6
-2
@@ -146,10 +146,14 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
|
||||
// Float64Slice looks up the value of a local Float64SliceFlag, returns
|
||||
// nil if not found
|
||||
func (c *Context) Float64Slice(name string) []float64 {
|
||||
return lookupFloat64Slice(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupFloat64Slice(name, fs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupFloat64Slice(f *flag.Flag) []float64 {
|
||||
func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
if slice, ok := f.Value.(*Float64Slice); ok {
|
||||
return slice.Value()
|
||||
|
||||
+6
-2
@@ -89,10 +89,14 @@ func (f GenericFlag) Apply(set *flag.FlagSet) error {
|
||||
// Generic looks up the value of a local GenericFlag, returns
|
||||
// nil if not found
|
||||
func (c *Context) Generic(name string) interface{} {
|
||||
return lookupGeneric(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupGeneric(name, fs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupGeneric(f *flag.Flag) interface{} {
|
||||
func lookupGeneric(name string, set *flag.FlagSet) interface{} {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := f.Value, error(nil)
|
||||
if err != nil {
|
||||
|
||||
+6
-2
@@ -87,10 +87,14 @@ func (f *IntFlag) Apply(set *flag.FlagSet) error {
|
||||
// Int looks up the value of a local IntFlag, returns
|
||||
// 0 if not found
|
||||
func (c *Context) Int(name string) int {
|
||||
return lookupInt(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupInt(name, fs)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupInt(f *flag.Flag) int {
|
||||
func lookupInt(name string, set *flag.FlagSet) int {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := strconv.ParseInt(f.Value.String(), 0, 64)
|
||||
if err != nil {
|
||||
|
||||
+6
-2
@@ -86,10 +86,14 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) error {
|
||||
// Int64 looks up the value of a local Int64Flag, returns
|
||||
// 0 if not found
|
||||
func (c *Context) Int64(name string) int64 {
|
||||
return lookupInt64(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupInt64(name, fs)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupInt64(f *flag.Flag) int64 {
|
||||
func lookupInt64(name string, set *flag.FlagSet) int64 {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := strconv.ParseInt(f.Value.String(), 0, 64)
|
||||
if err != nil {
|
||||
|
||||
+3
-2
@@ -145,10 +145,11 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
|
||||
// Int64Slice looks up the value of a local Int64SliceFlag, returns
|
||||
// nil if not found
|
||||
func (c *Context) Int64Slice(name string) []int64 {
|
||||
return lookupInt64Slice(c.resolveFlagDeep(name))
|
||||
return lookupInt64Slice(name, c.flagSet)
|
||||
}
|
||||
|
||||
func lookupInt64Slice(f *flag.Flag) []int64 {
|
||||
func lookupInt64Slice(name string, set *flag.FlagSet) []int64 {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
if slice, ok := f.Value.(*Int64Slice); ok {
|
||||
return slice.Value()
|
||||
|
||||
+6
-2
@@ -156,10 +156,14 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
|
||||
// IntSlice looks up the value of a local IntSliceFlag, returns
|
||||
// nil if not found
|
||||
func (c *Context) IntSlice(name string) []int {
|
||||
return lookupIntSlice(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupIntSlice(name, c.flagSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupIntSlice(f *flag.Flag) []int {
|
||||
func lookupIntSlice(name string, set *flag.FlagSet) []int {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
if slice, ok := f.Value.(*IntSlice); ok {
|
||||
return slice.Value()
|
||||
|
||||
+7
-2
@@ -75,10 +75,15 @@ func (f *PathFlag) Apply(set *flag.FlagSet) error {
|
||||
// Path looks up the value of a local PathFlag, returns
|
||||
// "" if not found
|
||||
func (c *Context) Path(name string) string {
|
||||
return lookupPath(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupPath(name, fs)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func lookupPath(f *flag.Flag) string {
|
||||
func lookupPath(name string, set *flag.FlagSet) string {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := f.Value.String(), error(nil)
|
||||
if err != nil {
|
||||
|
||||
+6
-2
@@ -76,10 +76,14 @@ func (f *StringFlag) Apply(set *flag.FlagSet) error {
|
||||
// String looks up the value of a local StringFlag, returns
|
||||
// "" if not found
|
||||
func (c *Context) String(name string) string {
|
||||
return lookupString(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupString(name, fs)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func lookupString(f *flag.Flag) string {
|
||||
func lookupString(name string, set *flag.FlagSet) string {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := f.Value.String(), error(nil)
|
||||
if err != nil {
|
||||
|
||||
+7
-12
@@ -116,17 +116,8 @@ func (f *StringSliceFlag) GetValue() string {
|
||||
|
||||
// Apply populates the flag given the flag set and environment
|
||||
func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
|
||||
|
||||
if f.Destination != nil && f.Value != nil {
|
||||
f.Destination.slice = make([]string, len(f.Value.slice))
|
||||
copy(f.Destination.slice, f.Value.slice)
|
||||
|
||||
}
|
||||
|
||||
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
|
||||
if f.Value == nil {
|
||||
f.Value = &StringSlice{}
|
||||
}
|
||||
f.Value = &StringSlice{}
|
||||
destination := f.Value
|
||||
if f.Destination != nil {
|
||||
destination = f.Destination
|
||||
@@ -163,10 +154,14 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
|
||||
// StringSlice looks up the value of a local StringSliceFlag, returns
|
||||
// nil if not found
|
||||
func (c *Context) StringSlice(name string) []string {
|
||||
return lookupStringSlice(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupStringSlice(name, fs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupStringSlice(f *flag.Flag) []string {
|
||||
func lookupStringSlice(name string, set *flag.FlagSet) []string {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
if slice, ok := f.Value.(*StringSlice); ok {
|
||||
return slice.Value()
|
||||
|
||||
+7
-5
@@ -118,9 +118,7 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
|
||||
if f.Layout == "" {
|
||||
return fmt.Errorf("timestamp Layout is required")
|
||||
}
|
||||
if f.Value == nil {
|
||||
f.Value = &Timestamp{}
|
||||
}
|
||||
f.Value = &Timestamp{}
|
||||
f.Value.SetLayout(f.Layout)
|
||||
|
||||
if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok {
|
||||
@@ -138,11 +136,15 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error {
|
||||
|
||||
// Timestamp gets the timestamp from a flag name
|
||||
func (c *Context) Timestamp(name string) *time.Time {
|
||||
return lookupTimestamp(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupTimestamp(name, fs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fetches the timestamp value from the local timestampWrap
|
||||
func lookupTimestamp(f *flag.Flag) *time.Time {
|
||||
func lookupTimestamp(name string, set *flag.FlagSet) *time.Time {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
return (f.Value.(*Timestamp)).Value()
|
||||
}
|
||||
|
||||
+6
-2
@@ -86,10 +86,14 @@ func (f *UintFlag) GetValue() string {
|
||||
// Uint looks up the value of a local UintFlag, returns
|
||||
// 0 if not found
|
||||
func (c *Context) Uint(name string) uint {
|
||||
return lookupUint(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupUint(name, fs)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupUint(f *flag.Flag) uint {
|
||||
func lookupUint(name string, set *flag.FlagSet) uint {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := strconv.ParseUint(f.Value.String(), 0, 64)
|
||||
if err != nil {
|
||||
|
||||
+6
-2
@@ -86,10 +86,14 @@ func (f *Uint64Flag) GetValue() string {
|
||||
// Uint64 looks up the value of a local Uint64Flag, returns
|
||||
// 0 if not found
|
||||
func (c *Context) Uint64(name string) uint64 {
|
||||
return lookupUint64(c.resolveFlagDeep(name))
|
||||
if fs := lookupFlagSet(name, c); fs != nil {
|
||||
return lookupUint64(name, fs)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func lookupUint64(f *flag.Flag) uint64 {
|
||||
func lookupUint64(name string, set *flag.FlagSet) uint64 {
|
||||
f := set.Lookup(name)
|
||||
if f != nil {
|
||||
parsed, err := strconv.ParseUint(f.Value.String(), 0, 64)
|
||||
if err != nil {
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@ go 1.11
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d
|
||||
gopkg.in/yaml.v2 v2.2.3
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
)
|
||||
|
||||
+2
-2
@@ -10,5 +10,5 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
+6
-24
@@ -72,13 +72,13 @@ func ShowAppHelpAndExit(c *Context, exitCode int) {
|
||||
|
||||
// ShowAppHelp is an action that displays the help.
|
||||
func ShowAppHelp(c *Context) error {
|
||||
tpl := c.App.CustomAppHelpTemplate
|
||||
if tpl == "" {
|
||||
tpl = AppHelpTemplate
|
||||
template := c.App.CustomAppHelpTemplate
|
||||
if template == "" {
|
||||
template = AppHelpTemplate
|
||||
}
|
||||
|
||||
if c.App.ExtraInfo == nil {
|
||||
HelpPrinter(c.App.Writer, tpl, c.App)
|
||||
HelpPrinter(c.App.Writer, template, c.App)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func ShowAppHelp(c *Context) error {
|
||||
"ExtraInfo": c.App.ExtraInfo,
|
||||
}
|
||||
}
|
||||
HelpPrinterCustom(c.App.Writer, tpl, c.App, customAppData())
|
||||
HelpPrinterCustom(c.App.Writer, template, c.App, customAppData())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -214,12 +214,6 @@ func ShowCommandHelp(ctx *Context, command string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits with exit code.
|
||||
func ShowSubcommandHelpAndExit(c *Context, exitCode int) {
|
||||
_ = ShowSubcommandHelp(c)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
// ShowSubcommandHelp prints help for the given subcommand
|
||||
func ShowSubcommandHelp(c *Context) error {
|
||||
if c == nil {
|
||||
@@ -269,10 +263,7 @@ func ShowCommandCompletions(ctx *Context, command string) {
|
||||
// allow using arbitrary functions in template rendering.
|
||||
func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) {
|
||||
funcMap := template.FuncMap{
|
||||
"join": strings.Join,
|
||||
"indent": indent,
|
||||
"nindent": nindent,
|
||||
"trim": strings.TrimSpace,
|
||||
"join": strings.Join,
|
||||
}
|
||||
for key, value := range customFuncs {
|
||||
funcMap[key] = value
|
||||
@@ -375,12 +366,3 @@ func checkCommandCompletions(c *Context, name string) bool {
|
||||
ShowCommandCompletions(c, name)
|
||||
return true
|
||||
}
|
||||
|
||||
func indent(spaces int, v string) string {
|
||||
pad := strings.Repeat(" ", spaces)
|
||||
return pad + strings.Replace(v, "\n", "\n"+pad, -1)
|
||||
}
|
||||
|
||||
func nindent(spaces int, v string) string {
|
||||
return "\n" + indent(spaces, v)
|
||||
}
|
||||
|
||||
+4
-4
@@ -13,7 +13,7 @@ VERSION:
|
||||
{{.Version}}{{end}}{{end}}{{if .Description}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description | nindent 3 | trim}}{{end}}{{if len .Authors}}
|
||||
{{.Description}}{{end}}{{if len .Authors}}
|
||||
|
||||
AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}:
|
||||
{{range $index, $author := .Authors}}{{if $index}}
|
||||
@@ -45,7 +45,7 @@ CATEGORY:
|
||||
{{.Category}}{{end}}{{if .Description}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlags}}
|
||||
{{.Description}}{{end}}{{if .VisibleFlags}}
|
||||
|
||||
OPTIONS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
@@ -62,7 +62,7 @@ USAGE:
|
||||
{{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}}
|
||||
|
||||
DESCRIPTION:
|
||||
{{.Description | nindent 3 | trim}}{{end}}
|
||||
{{.Description}}{{end}}
|
||||
|
||||
COMMANDS:{{range .VisibleCategories}}{{if .Name}}
|
||||
{{.Name}}:{{range .VisibleCommands}}
|
||||
@@ -74,7 +74,7 @@ OPTIONS:
|
||||
{{end}}{{end}}
|
||||
`
|
||||
|
||||
var MarkdownDocTemplate = `% {{ .App.Name }} {{ .SectionNum }}
|
||||
var MarkdownDocTemplate = `% {{ .App.Name }} 8
|
||||
|
||||
# NAME
|
||||
|
||||
|
||||
Vendored
+1
-2
@@ -246,7 +246,7 @@ github.com/shurcooL/sanitized_anchor_name
|
||||
## explicit
|
||||
github.com/stretchr/testify/assert
|
||||
github.com/stretchr/testify/require
|
||||
# github.com/urfave/cli/v2 v2.2.0 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210212191405-2b6ed1f5ef69
|
||||
# github.com/urfave/cli/v2 v2.2.0
|
||||
## explicit
|
||||
github.com/urfave/cli/v2
|
||||
github.com/urfave/cli/v2/altsrc
|
||||
@@ -431,4 +431,3 @@ zombiezen.com/go/capnproto2/rpc/internal/refcount
|
||||
zombiezen.com/go/capnproto2/schemas
|
||||
zombiezen.com/go/capnproto2/server
|
||||
zombiezen.com/go/capnproto2/std/capnp/rpc
|
||||
# github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210212191405-2b6ed1f5ef69
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
gobwas "github.com/gobwas/ws"
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the peer.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
)
|
||||
|
||||
// GorillaConn is a wrapper around the standard gorilla websocket but implements a ReadWriter
|
||||
// This is still used by access carrier
|
||||
type GorillaConn struct {
|
||||
*websocket.Conn
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
// Read will read messages from the websocket connection
|
||||
func (c *GorillaConn) Read(p []byte) (int, error) {
|
||||
_, message, err := c.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return copy(p, message), nil
|
||||
|
||||
}
|
||||
|
||||
// Write will write messages to the websocket connection
|
||||
func (c *GorillaConn) Write(p []byte) (int, error) {
|
||||
if err := c.Conn.WriteMessage(websocket.BinaryMessage, p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// pinger simulates the websocket connection to keep it alive
|
||||
func (c *GorillaConn) pinger(ctx context.Context) {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := c.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil {
|
||||
c.log.Debug().Msgf("failed to send ping message: %s", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
rw io.ReadWriter
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewConn(ctx context.Context, rw io.ReadWriter, log *zerolog.Logger) *Conn {
|
||||
c := &Conn{
|
||||
rw: rw,
|
||||
log: log,
|
||||
}
|
||||
go c.pinger(ctx)
|
||||
return c
|
||||
}
|
||||
|
||||
// Read will read messages from the websocket connection
|
||||
func (c *Conn) Read(reader []byte) (int, error) {
|
||||
data, err := wsutil.ReadClientBinary(c.rw)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return copy(reader, data), nil
|
||||
}
|
||||
|
||||
// Write will write messages to the websocket connection
|
||||
func (c *Conn) Write(p []byte) (int, error) {
|
||||
if err := wsutil.WriteServerBinary(c.rw, p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *Conn) pinger(ctx context.Context) {
|
||||
pongMessge := wsutil.Message{
|
||||
OpCode: gobwas.OpPong,
|
||||
Payload: []byte{},
|
||||
}
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := wsutil.WriteServerMessage(c.rw, gobwas.OpPing, []byte{}); err != nil {
|
||||
c.log.Err(err).Msgf("failed to write ping message")
|
||||
}
|
||||
if err := wsutil.HandleClientControlMessage(c.rw, pongMessge); err != nil {
|
||||
c.log.Err(err).Msgf("failed to write pong message")
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-109
@@ -2,7 +2,6 @@ package websocket
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net"
|
||||
@@ -16,17 +15,6 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
// Time allowed to write a message to the peer.
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Time allowed to read the next pong message from the peer.
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
)
|
||||
|
||||
var stripWebsocketHeaders = []string{
|
||||
"Upgrade",
|
||||
"Connection",
|
||||
@@ -35,94 +23,28 @@ var stripWebsocketHeaders = []string{
|
||||
"Sec-Websocket-Extensions",
|
||||
}
|
||||
|
||||
// Conn is a wrapper around the standard gorilla websocket
|
||||
// but implements a ReadWriter
|
||||
type Conn struct {
|
||||
*websocket.Conn
|
||||
}
|
||||
|
||||
// Read will read messages from the websocket connection
|
||||
func (c *Conn) Read(p []byte) (int, error) {
|
||||
_, message, err := c.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return copy(p, message), nil
|
||||
|
||||
}
|
||||
|
||||
// Write will write messages to the websocket connection
|
||||
func (c *Conn) Write(p []byte) (int, error) {
|
||||
if err := c.Conn.WriteMessage(websocket.BinaryMessage, p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// IsWebSocketUpgrade checks to see if the request is a WebSocket connection.
|
||||
func IsWebSocketUpgrade(req *http.Request) bool {
|
||||
return websocket.IsWebSocketUpgrade(req)
|
||||
}
|
||||
|
||||
// Dialler is something that can proxy websocket requests.
|
||||
type Dialler interface {
|
||||
Dial(url *url.URL, headers http.Header) (*websocket.Conn, *http.Response, error)
|
||||
}
|
||||
|
||||
type defaultDialler struct {
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
func (dd *defaultDialler) Dial(url *url.URL, header http.Header) (*websocket.Conn, *http.Response, error) {
|
||||
d := &websocket.Dialer{
|
||||
TLSClientConfig: dd.tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
return d.Dial(url.String(), header)
|
||||
}
|
||||
|
||||
// ClientConnect creates a WebSocket client connection for provided request. Caller is responsible for closing
|
||||
// the connection. The response body may not contain the entire response and does
|
||||
// not need to be closed by the application.
|
||||
func ClientConnect(req *http.Request, dialler Dialler) (*websocket.Conn, *http.Response, error) {
|
||||
func ClientConnect(req *http.Request, dialler *websocket.Dialer) (*websocket.Conn, *http.Response, error) {
|
||||
req.URL.Scheme = ChangeRequestScheme(req.URL)
|
||||
wsHeaders := websocketHeaders(req)
|
||||
|
||||
if dialler == nil {
|
||||
dialler = new(defaultDialler)
|
||||
dialler = &websocket.Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
}
|
||||
conn, response, err := dialler.Dial(req.URL, wsHeaders)
|
||||
conn, response, err := dialler.Dial(req.URL.String(), wsHeaders)
|
||||
if err != nil {
|
||||
return nil, response, err
|
||||
}
|
||||
response.Header.Set("Sec-WebSocket-Accept", generateAcceptKey(req))
|
||||
return conn, response, err
|
||||
}
|
||||
|
||||
// Stream copies copy data to & from provided io.ReadWriters.
|
||||
func Stream(conn, backendConn io.ReadWriter) {
|
||||
proxyDone := make(chan struct{}, 2)
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(conn, backendConn)
|
||||
proxyDone <- struct{}{}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, _ = io.Copy(backendConn, conn)
|
||||
proxyDone <- struct{}{}
|
||||
}()
|
||||
|
||||
// If one side is done, we are done.
|
||||
<-proxyDone
|
||||
}
|
||||
|
||||
// DefaultStreamHandler is provided to the the standard websocket to origin stream
|
||||
// This exist to allow SOCKS to deframe data before it gets to the origin
|
||||
func DefaultStreamHandler(wsConn *Conn, remoteConn net.Conn, _ http.Header) {
|
||||
Stream(wsConn, remoteConn)
|
||||
return conn, response, nil
|
||||
}
|
||||
|
||||
// StartProxyServer will start a websocket server that will decode
|
||||
@@ -132,7 +54,7 @@ func StartProxyServer(
|
||||
listener net.Listener,
|
||||
staticHost string,
|
||||
shutdownC <-chan struct{},
|
||||
streamHandler func(wsConn *Conn, remoteConn net.Conn, requestHeaders http.Header),
|
||||
streamHandler func(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger),
|
||||
) error {
|
||||
upgrader := websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
@@ -159,7 +81,7 @@ type handler struct {
|
||||
log *zerolog.Logger
|
||||
staticHost string
|
||||
upgrader websocket.Upgrader
|
||||
streamHandler func(wsConn *Conn, remoteConn net.Conn, requestHeaders http.Header)
|
||||
streamHandler func(originConn io.ReadWriter, remoteConn net.Conn, log *zerolog.Logger)
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -192,14 +114,20 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
conn.SetPongHandler(func(string) error { _ = conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
||||
done := make(chan struct{})
|
||||
go pinger(h.log, conn, done)
|
||||
defer func() {
|
||||
done <- struct{}{}
|
||||
_ = conn.Close()
|
||||
}()
|
||||
gorillaConn := &GorillaConn{conn, h.log}
|
||||
go gorillaConn.pinger(r.Context())
|
||||
defer conn.Close()
|
||||
|
||||
h.streamHandler(&Conn{conn}, stream, r.Header)
|
||||
h.streamHandler(gorillaConn, stream, h.log)
|
||||
}
|
||||
|
||||
// NewResponseHeader returns headers needed to return to origin for completing handshake
|
||||
func NewResponseHeader(req *http.Request) http.Header {
|
||||
header := http.Header{}
|
||||
header.Add("Connection", "Upgrade")
|
||||
header.Add("Sec-Websocket-Accept", generateAcceptKey(req))
|
||||
header.Add("Upgrade", "websocket")
|
||||
return header
|
||||
}
|
||||
|
||||
// the gorilla websocket library sets its own Upgrade, Connection, Sec-WebSocket-Key,
|
||||
@@ -246,19 +174,3 @@ func ChangeRequestScheme(reqURL *url.URL) string {
|
||||
return reqURL.Scheme
|
||||
}
|
||||
}
|
||||
|
||||
// pinger simulates the websocket connection to keep it alive
|
||||
func pinger(logger *zerolog.Logger, ws *websocket.Conn, done chan struct{}) {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil {
|
||||
logger.Debug().Msgf("failed to send ping message: %s", err)
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/cloudflare/cloudflared/hello"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
|
||||
gws "github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
@@ -78,7 +78,7 @@ func TestServe(t *testing.T) {
|
||||
|
||||
tlsConfig := websocketClientTLSConfig(t)
|
||||
assert.NotNil(t, tlsConfig)
|
||||
d := defaultDialler{tlsConfig: tlsConfig}
|
||||
d := gws.Dialer{TLSClientConfig: tlsConfig}
|
||||
conn, resp, err := ClientConnect(req, &d)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testSecWebsocketAccept, resp.Header.Get("Sec-WebSocket-Accept"))
|
||||
|
||||
Reference in New Issue
Block a user