Compare commits

...

17 Commits

Author SHA1 Message Date
Adam Chalmers f116ecfc29 Release 2021.5.0 2021-05-11 11:11:05 -05:00
Michael Borkenstein ea54ed62ba AUTH-3537: AUDs in JWTs are now always arrays 2021-05-10 20:37:24 +00:00
Adam Chalmers 4f362c97f6 TUN-4384: Silence log from automaxprocs 2021-05-10 13:33:47 -05:00
Adam Chalmers f5f9efa459 TUN-4359: Warn about unused keys in 'tunnel ingress validate' 2021-05-06 17:29:18 -05:00
Adam Chalmers 3dc095b4ce TUN-4357: Bump Go to 1.16 2021-05-06 20:53:32 +00:00
Adam Chalmers 76a1997b82 TUN-4356: Set AUTOMAXPROCS to the CPU limit when running in a Linux container 2021-05-06 15:08:50 -05:00
Adam Chalmers c010e10f7c TUN-4343: Fix broken build by setting debug field correctly 2021-05-03 16:46:43 -05:00
Daniel Hwang c605780c95 debug: log host / path
to help understand why the ingress rule logged is being selected.

in addition, combine "Request Headers..." and "Serving with ingress..." logs
into this updated log.

Co-authored-by: Silver <sssilver@users.noreply.github.com>
2021-05-03 15:47:42 +01:00
abelinkinbio 336dd7cd10 TUN-4000: Release notes for cloudflared replica model 2021-04-30 16:21:17 -05:00
Michael Borkenstein aca0c93461 AUTH-3513: Checks header for app info in case response is a 403/401 from the edge 2021-04-20 12:06:03 -05:00
Adam Chalmers eed7d7bbc9 TUN-4206: Better error message when user is only using one ingress rule 2021-04-12 12:04:54 -05:00
Areg Harutyunyan 1073f8db40 TUN-2853: rename STDIN-CONTROL env var to STDIN_CONTROL 2021-04-09 16:43:01 +01:00
Igor Postelnik e45695d4a3 Release 2021.4.0 2021-04-07 13:19:08 -05:00
Igor Postelnik 7ce0aefea4 Publish change log for 2021.4.0 2021-04-07 11:59:24 -05:00
Igor Postelnik 3ad99b241c TUN-4168: Transparently proxy websocket connections using stdlib HTTP client instead of gorilla/websocket; move websocket client code into carrier package since it's only used by access subcommands now (#345). 2021-04-07 16:25:46 +00:00
Nuno Diegues b25d38dd72 TUN-4177: Running with proxy-dns should not prevent running Named Tunnels 2021-04-05 11:28:05 +01:00
Nuno Diegues 1720ac0fc6 TUN-4178: Fix component test for running as a service in MacOS to not assume a named tunnel 2021-04-05 10:29:23 +01:00
44 changed files with 1433 additions and 386 deletions
+22 -3
View File
@@ -1,5 +1,25 @@
**Experimental**: This is a new format for release notes. The format and availability is subject to change.
## 2020.5.0
### New Features
- It is now possible to run the same tunnel using more than one `cloudflared` instance. This is a server-side change and
is compatible with any client version that uses Named Tunnels.
To get started, visit our [developer documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/deploy-cloudflared-replicas).
- `cloudflared tunnel ingress validate` will now warn about unused keys in your config file. This is helpful for
detecting typos in your config.
- If `cloudflared` detects it is running inside a Linux container, it will limit itself to use only the number of CPUs
the pod has been granted, instead of trying to use every CPU available.
## 2021.4.0
### Bug Fixes
- Fixed proxying of websocket requests to avoid possibility of losing initial frames that were sent in the same TCP
packet as response headers [#345](https://github.com/cloudflare/cloudflared/issues/345).
- `proxy-dns` option now works in conjunction with running a named tunnel [#346](https://github.com/cloudflare/cloudflared/issues/346).
## 2021.3.6
### Bug Fixes
@@ -23,7 +43,7 @@
### Improvements
- Tunnel create command, as well as, running ad-hoc tunnels using `cloudflared tunnel -name NAME`, will not overwrite
existing files when writing tunnel credentials.
existing files when writing tunnel credentials.
### Bug Fixes
@@ -69,7 +89,7 @@ ingress:
ports: [80, 443]
allow: true
```
### Improvements
@@ -121,4 +141,3 @@ ingress:
- 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 -1
View File
@@ -1,7 +1,7 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.15.7 as builder
FROM golang:1.16.4 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
TARGET_GOOS=${TARGET_GOOS} \
+20
View File
@@ -1,3 +1,23 @@
2021.5.0
- 2021-05-10 TUN-4384: Silence log from automaxprocs
- 2021-05-10 AUTH-3537: AUDs in JWTs are now always arrays
- 2021-05-10 Update changelog for 2021.5.0
- 2021-05-03 TUN-4343: Fix broken build by setting debug field correctly
- 2021-05-06 TUN-4356: Set AUTOMAXPROCS to the CPU limit when running in a Linux container
- 2021-05-06 TUN-4357: Bump Go to 1.16
- 2021-05-06 TUN-4359: Warn about unused keys in 'tunnel ingress validate'
- 2021-04-30 debug: log host / path
- 2021-04-20 AUTH-3513: Checks header for app info in case response is a 403/401 from the edge
- 2021-04-29 TUN-4000: Release notes for cloudflared replica model
- 2021-04-09 TUN-2853: rename STDIN-CONTROL env var to STDIN_CONTROL
- 2021-04-09 TUN-4206: Better error message when user is only using one ingress rule
2021.4.0
- 2021-04-05 TUN-4178: Fix component test for running as a service in MacOS to not assume a named tunnel
- 2021-04-05 TUN-4177: Running with proxy-dns should not prevent running Named Tunnels
- 2021-04-02 TUN-4168: Transparently proxy websocket connections using stdlib HTTP client instead of gorilla/websocket; move websocket client code into carrier package since it's only used by access subcommands now (#345).
- 2021-04-07 Publish change log for 2021.4.0
2021.3.6
- 2021-03-30 TUN-4150: Only show the connector table in 'tunnel info' if there are connectors. Don't show rows with zero connections.
- 2021-03-31 TUN-4153: Revert best-effort HTTP2 usage when talking to origins
+60 -2
View File
@@ -4,6 +4,7 @@ import (
"io"
"net/http"
"net/http/httputil"
"net/url"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
@@ -60,7 +61,7 @@ func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebso
TLSClientConfig: options.TLSClientConfig,
Proxy: http.ProxyFromEnvironment,
}
wsConn, resp, err := cfwebsocket.ClientConnect(req, dialer)
wsConn, resp, err := clientConnect(req, dialer)
defer closeRespBody(resp)
if err != nil && IsAccessResponse(resp) {
@@ -87,6 +88,63 @@ func createWebsocketStream(options *StartOptions, log *zerolog.Logger) (*cfwebso
return &cfwebsocket.GorillaConn{Conn: wsConn}, nil
}
var stripWebsocketHeaders = []string{
"Upgrade",
"Connection",
"Sec-Websocket-Key",
"Sec-Websocket-Version",
"Sec-Websocket-Extensions",
}
// the gorilla websocket library sets its own Upgrade, Connection, Sec-WebSocket-Key,
// Sec-WebSocket-Version and Sec-Websocket-Extensions headers.
// https://github.com/gorilla/websocket/blob/master/client.go#L189-L194.
func websocketHeaders(req *http.Request) http.Header {
wsHeaders := make(http.Header)
for key, val := range req.Header {
wsHeaders[key] = val
}
// Assume the header keys are in canonical format.
for _, header := range stripWebsocketHeaders {
wsHeaders.Del(header)
}
wsHeaders.Set("Host", req.Host) // See TUN-1097
return wsHeaders
}
// 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 *websocket.Dialer) (*websocket.Conn, *http.Response, error) {
req.URL.Scheme = changeRequestScheme(req.URL)
wsHeaders := websocketHeaders(req)
if dialler == nil {
dialler = &websocket.Dialer{
Proxy: http.ProxyFromEnvironment,
}
}
conn, response, err := dialler.Dial(req.URL.String(), wsHeaders)
if err != nil {
return nil, response, err
}
return conn, response, nil
}
// changeRequestScheme is needed as the gorilla websocket library requires the ws scheme.
// (even though it changes it back to http/https, but ¯\_(ツ)_/¯.)
func changeRequestScheme(reqURL *url.URL) string {
switch reqURL.Scheme {
case "https":
return "wss"
case "http":
return "ws"
case "":
return "ws"
default:
return reqURL.Scheme
}
}
// createAccessAuthenticatedStream will try load a token from storage and make
// a connection with the token set on the request. If it still get redirect,
// this probably means the token in storage is invalid (expired/revoked). If that
@@ -126,7 +184,7 @@ func createAccessWebSocketStream(options *StartOptions, log *zerolog.Logger) (*w
dump, err := httputil.DumpRequest(req, false)
log.Debug().Msgf("Access Websocket request: %s", string(dump))
conn, resp, err := cfwebsocket.ClientConnect(req, nil)
conn, resp, err := clientConnect(req, nil)
if resp != nil {
r, err := httputil.DumpResponse(resp, true)
+123
View File
@@ -0,0 +1,123 @@
package carrier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"math/rand"
"testing"
"time"
gws "github.com/gorilla/websocket"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/websocket"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/tlsconfig"
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
)
func websocketClientTLSConfig(t *testing.T) *tls.Config {
certPool := x509.NewCertPool()
helloCert, err := tlsconfig.GetHelloCertificateX509()
assert.NoError(t, err)
certPool.AddCert(helloCert)
assert.NotNil(t, certPool)
return &tls.Config{RootCAs: certPool}
}
func TestWebsocketHeaders(t *testing.T) {
req := testRequest(t, "http://example.com", nil)
wsHeaders := websocketHeaders(req)
for _, header := range stripWebsocketHeaders {
assert.Empty(t, wsHeaders[header])
}
assert.Equal(t, "curl/7.59.0", wsHeaders.Get("User-Agent"))
}
func TestServe(t *testing.T) {
log := zerolog.Nop()
shutdownC := make(chan struct{})
errC := make(chan error)
listener, err := hello.CreateTLSListener("localhost:1111")
assert.NoError(t, err)
defer listener.Close()
go func() {
errC <- hello.StartHelloWorldServer(&log, listener, shutdownC)
}()
req := testRequest(t, "https://localhost:1111/ws", nil)
tlsConfig := websocketClientTLSConfig(t)
assert.NotNil(t, tlsConfig)
d := gws.Dialer{TLSClientConfig: tlsConfig}
conn, resp, err := clientConnect(req, &d)
assert.NoError(t, err)
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
for i := 0; i < 1000; i++ {
messageSize := rand.Int()%2048 + 1
clientMessage := make([]byte, messageSize)
// rand.Read always returns len(clientMessage) and a nil error
rand.Read(clientMessage)
err = conn.WriteMessage(websocket.BinaryFrame, clientMessage)
assert.NoError(t, err)
messageType, message, err := conn.ReadMessage()
assert.NoError(t, err)
assert.Equal(t, websocket.BinaryFrame, messageType)
assert.Equal(t, clientMessage, message)
}
_ = conn.Close()
close(shutdownC)
<-errC
}
func TestWebsocketWrapper(t *testing.T) {
listener, err := hello.CreateTLSListener("localhost:0")
require.NoError(t, err)
serverErrorChan := make(chan error)
helloSvrCtx, cancelHelloSvr := context.WithCancel(context.Background())
defer func() { <-serverErrorChan }()
defer cancelHelloSvr()
go func() {
log := zerolog.Nop()
serverErrorChan <- hello.StartHelloWorldServer(&log, listener, helloSvrCtx.Done())
}()
tlsConfig := websocketClientTLSConfig(t)
d := gws.Dialer{TLSClientConfig: tlsConfig, HandshakeTimeout: time.Minute}
testAddr := fmt.Sprintf("https://%s/ws", listener.Addr().String())
req := testRequest(t, testAddr, nil)
conn, resp, err := clientConnect(req, &d)
require.NoError(t, err)
assert.Equal(t, "websocket", resp.Header.Get("Upgrade"))
// Websocket now connected to test server so lets check our wrapper
wrapper := cfwebsocket.GorillaConn{Conn: conn}
buf := make([]byte, 100)
wrapper.Write([]byte("abc"))
n, err := wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 3)
require.Equal(t, "abc", string(buf[:n]))
// Test partial read, read 1 of 3 bytes in one read and the other 2 in another read
wrapper.Write([]byte("abc"))
buf = buf[:1]
n, err = wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 1)
require.Equal(t, "a", string(buf[:n]))
buf = buf[:cap(buf)]
n, err = wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 2)
require.Equal(t, "bc", string(buf[:n]))
}
+5 -5
View File
@@ -1,4 +1,4 @@
pinned_go: &pinned_go go=1.15.7-1
pinned_go: &pinned_go go=1.16.4-3
pinned_go_fips: &pinned_go_fips go-fips=1.15.5-3
build_dir: &build_dir /cfsetup_build
@@ -279,8 +279,8 @@ centos-7:
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
pre-cache:
- yum install -y fakeroot
- wget https://golang.org/dl/go1.15.7.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.7.linux-amd64.tar.gz
- wget https://golang.org/dl/go1.16.4.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.16.4.linux-amd64.tar.gz
post-cache:
- export PATH=$PATH:/usr/local/go/bin
- export GOOS=linux
@@ -291,8 +291,8 @@ centos-7:
builddeps: *el7_builddeps
pre-cache:
- yum install -y fakeroot
- wget https://golang.org/dl/go1.15.7.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.15.7.linux-amd64.tar.gz
- wget https://golang.org/dl/go1.16.4.linux-amd64.tar.gz -P /tmp/
- tar -C /usr/local -xzf /tmp/go1.16.4.linux-amd64.tar.gz
post-cache:
- export PATH=$PATH:/usr/local/go/bin
- export GOOS=linux
+19 -8
View File
@@ -13,27 +13,38 @@ func Action(actionFunc cli.ActionFunc) cli.ActionFunc {
}
func ConfiguredAction(actionFunc cli.ActionFunc) cli.ActionFunc {
// Adapt actionFunc to the type signature required by ConfiguredActionWithWarnings
f := func(context *cli.Context, _ string) error {
return actionFunc(context)
}
return ConfiguredActionWithWarnings(f)
}
// Just like ConfiguredAction, but accepts a second parameter with configuration warnings.
func ConfiguredActionWithWarnings(actionFunc func(*cli.Context, string) error) cli.ActionFunc {
return WithErrorHandler(func(c *cli.Context) error {
if err := setFlagsFromConfigFile(c); err != nil {
warnings, err := setFlagsFromConfigFile(c)
if err != nil {
return err
}
return actionFunc(c)
return actionFunc(c, warnings)
})
}
func setFlagsFromConfigFile(c *cli.Context) error {
func setFlagsFromConfigFile(c *cli.Context) (configWarnings string, err error) {
const errorExitCode = 1
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
inputSource, err := config.ReadConfigFile(c, log)
inputSource, warnings, err := config.ReadConfigFile(c, log)
if err != nil {
if err == config.ErrNoConfigFile {
return nil
return "", nil
}
return cli.Exit(err, errorExitCode)
return "", cli.Exit(err, errorExitCode)
}
if err := altsrc.ApplyInputSource(c, inputSource); err != nil {
return cli.Exit(err, errorExitCode)
return "", cli.Exit(err, errorExitCode)
}
return nil
return warnings, nil
}
+1 -1
View File
@@ -238,7 +238,7 @@ func installLinuxService(c *cli.Context) error {
"--origincert", serviceConfigDir + "/" + serviceCredentialFile,
}
} else {
src, err := config.ReadConfigFile(c, log)
src, _, err := config.ReadConfigFile(c, log)
if err != nil {
return err
}
+2
View File
@@ -10,6 +10,7 @@ import (
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"go.uber.org/automaxprocs/maxprocs"
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
@@ -47,6 +48,7 @@ func main() {
rand.Seed(time.Now().UnixNano())
metrics.RegisterBuildInfo(BuildTime, Version)
raven.SetRelease(Version)
maxprocs.Set()
// Graceful shutdown channel used by the app. When closed, app must terminate gracefully.
// Windows service manager closes this channel when it receives stop command.
+3 -3
View File
@@ -296,7 +296,7 @@ func StartServer(
}()
// Serve DNS proxy stand-alone if no hostname or tag or app is going to run
if dnsProxyStandAlone(c) {
if dnsProxyStandAlone(c, namedTunnel) {
connectedSignal.Notify()
// no grace period, handle SIGINT/SIGTERM immediately
return waitToShutdown(&wg, cancel, errC, graceShutdownC, 0, log)
@@ -595,7 +595,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "stdin-control",
Usage: "Control the process using commands sent through stdin",
EnvVars: []string{"STDIN-CONTROL"},
EnvVars: []string{"STDIN_CONTROL"},
Hidden: true,
Value: false,
}),
@@ -699,7 +699,7 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: ingress.ProxyTCPKeepAlive,
Name: ingress.ProxyTCPKeepAliveFlag,
Usage: "HTTP proxy TCP keepalive duration",
Value: time.Second * 30,
Hidden: shouldHide,
+2 -2
View File
@@ -85,8 +85,8 @@ func logClientOptions(c *cli.Context, log *zerolog.Logger) {
}
}
func dnsProxyStandAlone(c *cli.Context) bool {
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelConfig) bool {
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world") && namedTunnel == nil)
}
func findOriginCert(originCertPath string, log *zerolog.Logger) (string, error) {
@@ -45,7 +45,7 @@ func buildIngressSubcommand() *cli.Command {
func buildValidateIngressCommand() *cli.Command {
return &cli.Command{
Name: "validate",
Action: cliutil.ConfiguredAction(validateIngressCommand),
Action: cliutil.ConfiguredActionWithWarnings(validateIngressCommand),
Usage: "Validate the ingress configuration ",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
Description: "Validates the configuration file, ensuring your ingress rules are OK.",
@@ -68,7 +68,7 @@ func buildTestURLCommand() *cli.Command {
}
// validateIngressCommand check the syntax of the ingress rules in the cloudflared config file
func validateIngressCommand(c *cli.Context) error {
func validateIngressCommand(c *cli.Context, warnings string) error {
conf := config.GetConfiguration()
if conf.Source() == "" {
fmt.Println("No configuration file was found. Please create one, or use the --config flag to specify its filepath. You can use the help command to learn more about configuration files")
@@ -81,6 +81,11 @@ func validateIngressCommand(c *cli.Context) error {
if c.IsSet("url") {
return ingress.ErrURLIncompatibleWithIngress
}
if warnings != "" {
fmt.Println("Warning: unused keys detected in your config file. Here is a list of unused keys:")
fmt.Println(warnings)
return nil
}
fmt.Println("OK")
return nil
}
+12 -2
View File
@@ -20,11 +20,21 @@ def component_tests_config():
LOGGER.info(f"component tests base config {config}")
def _component_tests_config(additional_config={}, named_tunnel=True):
# Regression test for TUN-4177, running with proxy-dns should not prevent tunnels from running
additional_config["proxy-dns"] = True
additional_config["proxy-dns-port"] = 9053
if named_tunnel:
return NamedTunnelConfig(additional_config=additional_config,
cloudflared_binary=config['cloudflared_binary'], tunnel=config['tunnel'], credentials_file=config['credentials_file'], ingress=config['ingress'])
cloudflared_binary=config['cloudflared_binary'],
tunnel=config['tunnel'],
credentials_file=config['credentials_file'],
ingress=config['ingress'])
return ClassicTunnelConfig(
additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'], hostname=config['classic_hostname'], origincert=config['origincert'])
additional_config=additional_config, cloudflared_binary=config['cloudflared_binary'],
hostname=config['classic_hostname'], origincert=config['origincert'])
return _component_tests_config
+3 -7
View File
@@ -7,21 +7,17 @@ from util import start_cloudflared, wait_tunnel_ready, send_requests
# Rolling logger rotate log files after 1 MB
rotate_after_size = 1000 * 1000
default_log_file = "cloudflared.log"
expect_message = "Starting tunnel"
expect_message = "Starting Hello World server"
def assert_log_to_terminal(cloudflared):
stderr = cloudflared.stderr.read(200)
# cloudflared logs the following when it first starts
# 2021-03-10T12:30:39Z INF Starting tunnel tunnelID=<tunnel ID>
stderr = cloudflared.stderr.read(1500)
assert expect_message.encode() in stderr, f"{stderr} doesn't contain {expect_message}"
def assert_log_in_file(file):
with open(file, "r") as f:
log = f.read(200)
# cloudflared logs the following when it first starts
# {"level":"info","tunnelID":"<tunnel ID>","time":"2021-03-10T12:21:13Z","message":"Starting tunnel"}
log = f.read(1850)
assert expect_message in log, f"{log} doesn't contain {expect_message}"
+18 -7
View File
@@ -358,13 +358,13 @@ func GetConfiguration() *Configuration {
// ReadConfigFile returns InputSourceContext initialized from the configuration file.
// On repeat calls returns with the same file, returns without reading the file again; however,
// if value of "config" flag changes, will read the new config file
func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (*configFileSettings, error) {
func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (settings *configFileSettings, warnings string, err error) {
configFile := c.String("config")
if configuration.Source() == configFile || configFile == "" {
if configuration.Source() == "" {
return nil, ErrNoConfigFile
return nil, "", ErrNoConfigFile
}
return &configuration, nil
return &configuration, "", nil
}
log.Debug().Msgf("Loading configuration from %s", configFile)
@@ -373,16 +373,27 @@ func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (*configFileSettings, e
if os.IsNotExist(err) {
err = ErrNoConfigFile
}
return nil, err
return nil, "", err
}
defer file.Close()
if err := yaml.NewDecoder(file).Decode(&configuration); err != nil {
if err == io.EOF {
log.Error().Msgf("Configuration file %s was empty", configFile)
return &configuration, nil
return &configuration, "", nil
}
return nil, errors.Wrap(err, "error parsing YAML in config file at "+configFile)
return nil, "", errors.Wrap(err, "error parsing YAML in config file at "+configFile)
}
configuration.sourceFile = configFile
return &configuration, nil
// Parse it again, with strict mode, to find warnings.
if file, err := os.Open(configFile); err == nil {
decoder := yaml.NewDecoder(file)
decoder.SetStrict(true)
var unusedConfig configFileSettings
if err := decoder.Decode(&unusedConfig); err != nil {
warnings = err.Error()
}
}
return &configuration, warnings, nil
}
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.15.7 as builder
FROM golang:1.16.4 as builder
ENV GO111MODULE=on \
CGO_ENABLED=0
WORKDIR /go/src/github.com/cloudflare/cloudflared/
+1
View File
@@ -43,6 +43,7 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/testify v1.6.0
github.com/urfave/cli/v2 v2.2.0
go.uber.org/automaxprocs v1.4.0
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/net v0.0.0-20200904194848-62affa334b73
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
+2
View File
@@ -609,6 +609,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/automaxprocs v1.4.0 h1:CpDZl6aOlLhReez+8S3eEotD7Jx0Os++lemPlMULQP0=
go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+5
View File
@@ -138,6 +138,11 @@ func (ing Ingress) IsEmpty() bool {
return len(ing.Rules) == 0
}
// IsSingleRule checks if the user only specified a single ingress rule.
func (ing Ingress) IsSingleRule() bool {
return len(ing.Rules) == 1
}
// StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World.
func (ing Ingress) StartOrigins(
wg *sync.WaitGroup,
+2 -2
View File
@@ -402,7 +402,7 @@ func TestSingleOriginSetsConfig(t *testing.T) {
flagSet.Bool("hello-world", true, "")
flagSet.Duration(ProxyConnectTimeoutFlag, time.Second, "")
flagSet.Duration(ProxyTLSTimeoutFlag, time.Second, "")
flagSet.Duration(ProxyTCPKeepAlive, time.Second, "")
flagSet.Duration(ProxyTCPKeepAliveFlag, time.Second, "")
flagSet.Bool(ProxyNoHappyEyeballsFlag, true, "")
flagSet.Int(ProxyKeepAliveConnectionsFlag, 10, "")
flagSet.Duration(ProxyKeepAliveTimeoutFlag, time.Second, "")
@@ -423,7 +423,7 @@ func TestSingleOriginSetsConfig(t *testing.T) {
require.NoError(t, err)
err = cliCtx.Set(ProxyTLSTimeoutFlag, "1s")
require.NoError(t, err)
err = cliCtx.Set(ProxyTCPKeepAlive, "1s")
err = cliCtx.Set(ProxyTCPKeepAliveFlag, "1s")
require.NoError(t, err)
err = cliCtx.Set(ProxyNoHappyEyeballsFlag, "true")
require.NoError(t, err)
+13 -32
View File
@@ -2,12 +2,9 @@ package ingress
import (
"context"
"crypto/tls"
"io"
"net"
"net/http"
gws "github.com/gorilla/websocket"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/ipaccess"
@@ -58,35 +55,6 @@ 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) {
websocket.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
}
// socksProxyOverWSConnection is an OriginConnection that streams SOCKS connections over WS.
// The connection to the origin happens inside the SOCKS code as the client specifies the origin
// details in the packet.
@@ -100,3 +68,16 @@ func (sp *socksProxyOverWSConnection) Stream(ctx context.Context, tunnelConn io.
func (sp *socksProxyOverWSConnection) Close() {
}
// wsProxyConnection represents a bidirectional stream for a websocket connection to the origin
type wsProxyConnection struct {
rwc io.ReadWriteCloser
}
func (conn *wsProxyConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
websocket.Stream(tunnelConn, conn.rwc, log)
}
func (conn *wsProxyConnection) Close() {
conn.rwc.Close()
}
+57 -14
View File
@@ -3,13 +3,13 @@ package ingress
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"
"time"
@@ -193,18 +193,26 @@ func TestSocksStreamWSOverTCPConnection(t *testing.T) {
func TestStreamWSConnection(t *testing.T) {
eyeballConn, edgeConn := net.Pipe()
origin := echoWSOrigin(t)
origin := echoWSOrigin(t, true)
defer origin.Close()
var svc httpService
err := svc.start(&sync.WaitGroup{}, testLogger, nil, nil, OriginRequestConfig{
NoTLSVerify: true,
})
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, origin.URL, nil)
require.NoError(t, err)
req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
conn, resp, err := svc.newWebsocketProxyConnection(req)
clientTLSConfig := &tls.Config{
InsecureSkipVerify: true,
}
wsConn, resp, err := newWSConnection(clientTLSConfig, req)
require.NoError(t, err)
defer conn.Close()
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"))
@@ -213,13 +221,37 @@ func TestStreamWSConnection(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testStreamTimeout)
defer cancel()
connClosed := make(chan struct{})
errGroup, ctx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
select {
case <-connClosed:
case <-ctx.Done():
}
if ctx.Err() == context.DeadlineExceeded {
eyeballConn.Close()
edgeConn.Close()
conn.Close()
}
return ctx.Err()
})
errGroup.Go(func() error {
echoWSEyeball(t, eyeballConn)
fmt.Println("closing pipe")
edgeConn.Close()
return eyeballConn.Close()
})
errGroup.Go(func() error {
defer conn.Close()
conn.Stream(ctx, edgeConn, testLogger)
close(connClosed)
return nil
})
wsConn.Stream(ctx, edgeConn, testLogger)
require.NoError(t, errGroup.Wait())
}
@@ -241,17 +273,23 @@ func (wse *wsEyeball) Write(p []byte) (int, error) {
}
func echoWSEyeball(t *testing.T, conn net.Conn) {
require.NoError(t, wsutil.WriteClientBinary(conn, testMessage))
defer func() {
assert.NoError(t, conn.Close())
}()
if !assert.NoError(t, wsutil.WriteClientBinary(conn, testMessage)) {
return
}
readMsg, err := wsutil.ReadServerBinary(conn)
require.NoError(t, err)
if !assert.NoError(t, err) {
return
}
require.Equal(t, testResponse, readMsg)
require.NoError(t, conn.Close())
assert.Equal(t, testResponse, readMsg)
}
func echoWSOrigin(t *testing.T) *httptest.Server {
func echoWSOrigin(t *testing.T, expectMessages bool) *httptest.Server {
var upgrader = gorillaWS.Upgrader{
ReadBufferSize: 10,
WriteBufferSize: 10,
@@ -268,12 +306,17 @@ func echoWSOrigin(t *testing.T) *httptest.Server {
require.NoError(t, err)
defer conn.Close()
sawMessage := false
for {
messageType, p, err := conn.ReadMessage()
if err != nil {
if expectMessages && !sawMessage {
t.Errorf("unexpected error: %v", err)
}
return
}
require.Equal(t, testMessage, p)
assert.Equal(t, testMessage, p)
sawMessage = true
if err := conn.WriteMessage(messageType, testResponse); err != nil {
return
}
+54 -13
View File
@@ -2,8 +2,10 @@ package ingress
import (
"fmt"
"io"
"net"
"net/http"
"strings"
"github.com/pkg/errors"
@@ -12,7 +14,8 @@ import (
)
var (
switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
errUnsupportedConnectionType = errors.New("internal error: unsupported connection type")
)
// HTTPOriginProxy can be implemented by origin services that want to proxy http requests.
@@ -42,26 +45,64 @@ func (o *httpService) RoundTrip(req *http.Request) (*http.Response, error) {
}
func (o *httpService) EstablishConnection(req *http.Request) (OriginConnection, *http.Response, error) {
req = req.Clone(req.Context())
req.URL.Host = o.url.Host
req.URL.Scheme = websocket.ChangeRequestScheme(o.url)
req.URL.Scheme = o.url.Scheme
// allow ws(s) scheme for websocket-only origins, normal http(s) requests will fail
switch req.URL.Scheme {
case "ws":
req.URL.Scheme = "http"
case "wss":
req.URL.Scheme = "https"
}
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)
return o.newWebsocketProxyConnection(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 *httpService) newWebsocketProxyConnection(req *http.Request) (OriginConnection, *http.Response, error) {
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Sec-WebSocket-Version", "13")
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)
req.ContentLength = 0
req.Body = nil
resp, err := o.transport.RoundTrip(req)
if err != nil {
return nil, nil, err
}
toClose := resp.Body
defer func() {
if toClose != nil {
_ = toClose.Close()
}
}()
if resp.StatusCode != http.StatusSwitchingProtocols {
return nil, nil, fmt.Errorf("unexpected origin response: %s", resp.Status)
}
if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" {
return nil, nil, fmt.Errorf("unexpected upgrade: %q", resp.Header.Get("Upgrade"))
}
rwc, ok := resp.Body.(io.ReadWriteCloser)
if !ok {
return nil, nil, errUnsupportedConnectionType
}
conn := wsProxyConnection{
rwc: rwc,
}
// clear to prevent defer from closing
toClose = nil
return &conn, resp, nil
}
func (o *statusCode) RoundTrip(_ *http.Request) (*http.Response, error) {
+4 -4
View File
@@ -33,7 +33,7 @@ func assertEstablishConnectionResponse(t *testing.T,
}
func TestHTTPServiceEstablishConnection(t *testing.T) {
origin := echoWSOrigin(t)
origin := echoWSOrigin(t, false)
defer origin.Close()
originURL, err := url.Parse(origin.URL)
require.NoError(t, err)
@@ -71,11 +71,11 @@ func TestHelloWorldEstablishConnection(t *testing.T) {
// 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)
req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
expectHeader := http.Header{
"Connection": {"Upgrade"},
// Accept key when Sec-Websocket-Key is not specified
"Sec-Websocket-Accept": {"Kfh9QIsMVZcl6xEPYxPHzW8SZ8w="},
"Connection": {"Upgrade"},
"Sec-Websocket-Accept": {"s3pPLMBiTxaQ9kYGzzhZRbK+xOo="},
"Upgrade": {"websocket"},
}
assertEstablishConnectionResponse(t, helloWorldSerivce, req, expectHeader)
+2 -2
View File
@@ -22,7 +22,7 @@ const (
Socks5Flag = "socks5"
ProxyConnectTimeoutFlag = "proxy-connect-timeout"
ProxyTLSTimeoutFlag = "proxy-tls-timeout"
ProxyTCPKeepAlive = "proxy-tcp-keepalive"
ProxyTCPKeepAliveFlag = "proxy-tcp-keepalive"
ProxyNoHappyEyeballsFlag = "proxy-no-happy-eyeballs"
ProxyKeepAliveConnectionsFlag = "proxy-keepalive-connections"
ProxyKeepAliveTimeoutFlag = "proxy-keepalive-timeout"
@@ -60,7 +60,7 @@ func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
if flag := ProxyTLSTimeoutFlag; c.IsSet(flag) {
tlsTimeout = c.Duration(flag)
}
if flag := ProxyTCPKeepAlive; c.IsSet(flag) {
if flag := ProxyTCPKeepAliveFlag; c.IsSet(flag) {
tcpKeepAlive = c.Duration(flag)
}
if flag := ProxyNoHappyEyeballsFlag; c.IsSet(flag) {
+10 -17
View File
@@ -11,7 +11,6 @@ import (
"sync"
"time"
gws "github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/rs/zerolog"
@@ -19,7 +18,6 @@ import (
"github.com/cloudflare/cloudflared/ipaccess"
"github.com/cloudflare/cloudflared/socks"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/websocket"
)
// originService is something a tunnel can proxy traffic to.
@@ -50,16 +48,6 @@ func (o *unixSocketPath) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdown
return nil
}
func (o *unixSocketPath) Dial(reqURL *url.URL, headers http.Header) (*gws.Conn, *http.Response, error) {
d := &gws.Dialer{
NetDial: o.transport.Dial,
NetDialContext: o.transport.DialContext,
TLSClientConfig: o.transport.TLSClientConfig,
}
reqURL.Scheme = websocket.ChangeRequestScheme(reqURL)
return d.Dial(reqURL.String(), headers)
}
type httpService struct {
url *url.URL
hostHeader string
@@ -171,8 +159,8 @@ func (o *socksProxyOverWSService) String() string {
// HelloWorld is an OriginService for the built-in Hello World server.
// Users only use this for testing and experimenting with cloudflared.
type helloWorld struct {
server net.Listener
transport *http.Transport
httpService
server net.Listener
}
func (o *helloWorld) String() string {
@@ -187,11 +175,10 @@ func (o *helloWorld) start(
errC chan error,
cfg OriginRequestConfig,
) error {
transport, err := newHTTPTransport(o, cfg, log)
if err != nil {
if err := o.httpService.start(wg, log, shutdownC, errC, cfg); err != nil {
return err
}
o.transport = transport
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil {
return errors.Wrap(err, "Cannot start Hello World Server")
@@ -202,6 +189,12 @@ func (o *helloWorld) start(
_ = hello.StartHelloWorldServer(log, helloListener, shutdownC)
}()
o.server = helloListener
o.httpService.url = &url.URL{
Scheme: "https",
Host: o.server.Addr().String(),
}
return nil
}
+27 -11
View File
@@ -19,6 +19,8 @@ import (
const (
TagHeaderNamePrefix = "Cf-Warp-Tag-"
LogFieldCFRay = "cfRay"
LogFieldRule = "ingressRule"
)
type proxy struct {
@@ -67,7 +69,7 @@ func (p *proxy) Proxy(w connection.ResponseWriter, req *http.Request, sourceConn
lbProbe: lbProbe,
rule: ingress.ServiceWarpRouting,
}
if err := p.proxyStreamRequest(serveCtx, w, req, sourceConnectionType, p.warpRouting.Proxy, logFields); err != nil {
if err := p.proxyStreamRequest(serveCtx, w, req, p.warpRouting.Proxy, logFields); err != nil {
p.logRequestError(err, cfRay, ingress.ServiceWarpRouting)
return err
}
@@ -84,7 +86,7 @@ func (p *proxy) Proxy(w connection.ResponseWriter, req *http.Request, sourceConn
if sourceConnectionType == connection.TypeHTTP {
if err := p.proxyHTTPRequest(w, req, rule, logFields); err != nil {
p.logRequestError(err, cfRay, ruleNum)
p.logRequestError(err, cfRay, ruleField(p.ingressRules, ruleNum))
return err
}
return nil
@@ -96,13 +98,20 @@ func (p *proxy) Proxy(w connection.ResponseWriter, req *http.Request, sourceConn
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)
if err := p.proxyStreamRequest(serveCtx, w, req, connectionProxy, logFields); err != nil {
p.logRequestError(err, cfRay, ruleField(p.ingressRules, ruleNum))
return err
}
return nil
}
func ruleField(ing ingress.Ingress, ruleNum int) string {
if ing.IsSingleRule() {
return ""
}
return fmt.Sprintf("%d", ruleNum)
}
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 {
@@ -152,7 +161,6 @@ func (p *proxy) proxyStreamRequest(
serveCtx context.Context,
w connection.ResponseWriter,
req *http.Request,
sourceConnectionType connection.Type,
connectionProxy ingress.StreamBasedOriginProxy,
fields logFields,
) error {
@@ -230,8 +238,13 @@ func (p *proxy) logRequest(r *http.Request, fields logFields) {
} else {
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)
}
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)
p.log.Debug().
Str("CF-RAY", fields.cfRay).
Str("Header", fmt.Sprintf("%+v", r.Header)).
Str("host", r.Host).
Str("path", r.URL.Path).
Interface("rule", fields.rule).
Msg("Inbound request")
if contentLen := r.ContentLength; contentLen == -1 {
p.log.Debug().Msgf("CF-RAY: %s Request Content length unknown", fields.cfRay)
@@ -258,13 +271,16 @@ func (p *proxy) logOriginResponse(resp *http.Response, fields logFields) {
}
}
func (p *proxy) logRequestError(err error, cfRay string, rule interface{}) {
func (p *proxy) logRequestError(err error, cfRay string, rule string) {
requestErrors.Inc()
log := p.log.Error().Err(err)
if cfRay != "" {
p.log.Error().Msgf("CF-RAY: %s Proxying to ingress %v error: %v", cfRay, rule, err)
} else {
p.log.Error().Msgf("Proxying to ingress %v error: %v", rule, err)
log = log.Str(LogFieldCFRay, cfRay)
}
if rule != "" {
log = log.Str(LogFieldRule, rule)
}
log.Msg("")
}
func findCfRayHeader(req *http.Request) string {
+45 -15
View File
@@ -19,6 +19,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/connection"
@@ -79,6 +80,11 @@ func (w *mockWSRespWriter) respBody() io.ReadWriter {
return bytes.NewBuffer(data)
}
func (w *mockWSRespWriter) Close() error {
close(w.writeNotification)
return nil
}
func (w *mockWSRespWriter) Read(data []byte) (int, error) {
return w.reader.Read(data)
}
@@ -125,14 +131,14 @@ func TestProxySingleOrigin(t *testing.T) {
require.NoError(t, ingressRule.StartOrigins(&wg, &log, ctx.Done(), errC))
proxy := NewOriginProxy(ingressRule, unusedWarpRoutingService, testTags, &log)
t.Run("testProxyHTTP", testProxyHTTP(t, proxy))
t.Run("testProxyWebsocket", testProxyWebsocket(t, proxy))
t.Run("testProxySSE", testProxySSE(t, proxy))
t.Run("testProxyHTTP", testProxyHTTP(proxy))
t.Run("testProxyWebsocket", testProxyWebsocket(proxy))
t.Run("testProxySSE", testProxySSE(proxy))
cancel()
wg.Wait()
}
func testProxyHTTP(t *testing.T, proxy connection.OriginProxy) func(t *testing.T) {
func testProxyHTTP(proxy connection.OriginProxy) func(t *testing.T) {
return func(t *testing.T) {
responseWriter := newMockHTTPRespWriter()
req, err := http.NewRequest(http.MethodGet, "http://localhost:8080", nil)
@@ -145,23 +151,43 @@ func testProxyHTTP(t *testing.T, proxy connection.OriginProxy) func(t *testing.T
}
}
func testProxyWebsocket(t *testing.T, proxy connection.OriginProxy) func(t *testing.T) {
func testProxyWebsocket(proxy connection.OriginProxy) func(t *testing.T) {
return func(t *testing.T) {
// WSRoute is a websocket echo handler
ctx, cancel := context.WithCancel(context.Background())
const testTimeout = 5 * time.Second * 1000
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
defer cancel()
readPipe, writePipe := io.Pipe()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("http://localhost:8080%s", hello.WSRoute), readPipe)
responseWriter := newMockWSRespWriter(readPipe)
req.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
responseWriter := newMockWSRespWriter(nil)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
finished := make(chan struct{})
errGroup, ctx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
err = proxy.Proxy(responseWriter, req, connection.TypeWebsocket)
require.NoError(t, err)
require.Equal(t, http.StatusSwitchingProtocols, responseWriter.Code)
}()
return nil
})
errGroup.Go(func() error {
select {
case <-finished:
case <-ctx.Done():
}
if ctx.Err() == context.DeadlineExceeded {
t.Errorf("Test timed out")
readPipe.Close()
writePipe.Close()
responseWriter.Close()
}
return nil
})
msg := []byte("test websocket")
err = wsutil.WriteClientText(writePipe, msg)
@@ -179,12 +205,16 @@ func testProxyWebsocket(t *testing.T, proxy connection.OriginProxy) func(t *test
require.NoError(t, err)
require.Equal(t, msg, returnedMsg)
cancel()
wg.Wait()
_ = readPipe.Close()
_ = writePipe.Close()
_ = responseWriter.Close()
close(finished)
errGroup.Wait()
}
}
func testProxySSE(t *testing.T, proxy connection.OriginProxy) func(t *testing.T) {
func testProxySSE(proxy connection.OriginProxy) func(t *testing.T) {
return func(t *testing.T) {
var (
pushCount = 50
+17 -17
View File
@@ -25,6 +25,7 @@ const (
keyName = "token"
tokenCookie = "CF_Authorization"
appDomainHeader = "CF-Access-Domain"
appAUDHeader = "CF-Access-Aud"
AccessLoginWorkerPath = "/cdn-cgi/access/login"
)
@@ -45,7 +46,7 @@ type signalHandler struct {
signals []os.Signal
}
type appJWTPayload struct {
type jwtPayload struct {
Aud []string `json:"aud"`
Email string `json:"email"`
Exp int `json:"exp"`
@@ -56,17 +57,12 @@ type appJWTPayload struct {
Subt string `json:"sub"`
}
type orgJWTPayload struct {
appJWTPayload
Aud string `json:"aud"`
}
type transferServiceResponse struct {
AppToken string `json:"app_token"`
OrgToken string `json:"org_token"`
}
func (p appJWTPayload) isExpired() bool {
func (p jwtPayload) isExpired() bool {
return int(time.Now().Unix()) > p.Exp
}
@@ -270,14 +266,19 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
return nil, errors.Wrap(err, "failed to get app info")
}
resp.Body.Close()
location := resp.Request.URL
if !strings.Contains(location.Path, AccessLoginWorkerPath) {
return nil, fmt.Errorf("failed to get Access app info for %s", reqURL.String())
}
aud := resp.Request.URL.Query().Get("kid")
if aud == "" {
return nil, errors.New("Empty app aud")
var aud string
location := resp.Request.URL
if strings.Contains(location.Path, AccessLoginWorkerPath) {
aud = resp.Request.URL.Query().Get("kid")
if aud == "" {
return nil, errors.New("Empty app aud")
}
} else if audHeader := resp.Header.Get(appAUDHeader); audHeader != "" {
// 403/401 from the edge will have aud in a header
aud = audHeader
} else {
return nil, fmt.Errorf("failed to get Access app info for %s", reqURL.String())
}
domain := resp.Header.Get(appDomainHeader)
@@ -286,7 +287,6 @@ func GetAppInfo(reqURL *url.URL) (*AppInfo, error) {
}
return &AppInfo{location.Hostname(), aud, domain}, nil
}
// exchangeOrgToken attaches an org token to a request to the appURL and returns an app token. This uses the Access SSO
@@ -341,7 +341,7 @@ func GetOrgTokenIfExists(authDomain string) (string, error) {
if err != nil {
return "", err
}
var payload orgJWTPayload
var payload jwtPayload
err = json.Unmarshal(token.Payload, &payload)
if err != nil {
return "", err
@@ -363,7 +363,7 @@ func GetAppTokenIfExists(appInfo *AppInfo) (string, error) {
if err != nil {
return "", err
}
var payload appJWTPayload
var payload jwtPayload
err = json.Unmarshal(token.Payload, &payload)
if err != nil {
return "", err
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2017 Uber Technologies, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+78
View File
@@ -0,0 +1,78 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// +build linux
package cgroups
import (
"bufio"
"io"
"os"
"path/filepath"
"strconv"
)
// CGroup represents the data structure for a Linux control group.
type CGroup struct {
path string
}
// NewCGroup returns a new *CGroup from a given path.
func NewCGroup(path string) *CGroup {
return &CGroup{path: path}
}
// Path returns the path of the CGroup*.
func (cg *CGroup) Path() string {
return cg.path
}
// ParamPath returns the path of the given cgroup param under itself.
func (cg *CGroup) ParamPath(param string) string {
return filepath.Join(cg.path, param)
}
// readFirstLine reads the first line from a cgroup param file.
func (cg *CGroup) readFirstLine(param string) (string, error) {
paramFile, err := os.Open(cg.ParamPath(param))
if err != nil {
return "", err
}
defer paramFile.Close()
scanner := bufio.NewScanner(paramFile)
if scanner.Scan() {
return scanner.Text(), nil
}
if err := scanner.Err(); err != nil {
return "", err
}
return "", io.ErrUnexpectedEOF
}
// readInt parses the first line from a cgroup param file as int.
func (cg *CGroup) readInt(param string) (int, error) {
text, err := cg.readFirstLine(param)
if err != nil {
return 0, err
}
return strconv.Atoi(text)
}
+117
View File
@@ -0,0 +1,117 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// +build linux
package cgroups
const (
// _cgroupFSType is the Linux CGroup file system type used in
// `/proc/$PID/mountinfo`.
_cgroupFSType = "cgroup"
// _cgroupSubsysCPU is the CPU CGroup subsystem.
_cgroupSubsysCPU = "cpu"
// _cgroupSubsysCPUAcct is the CPU accounting CGroup subsystem.
_cgroupSubsysCPUAcct = "cpuacct"
// _cgroupSubsysCPUSet is the CPUSet CGroup subsystem.
_cgroupSubsysCPUSet = "cpuset"
// _cgroupSubsysMemory is the Memory CGroup subsystem.
_cgroupSubsysMemory = "memory"
// _cgroupCPUCFSQuotaUsParam is the file name for the CGroup CFS quota
// parameter.
_cgroupCPUCFSQuotaUsParam = "cpu.cfs_quota_us"
// _cgroupCPUCFSPeriodUsParam is the file name for the CGroup CFS period
// parameter.
_cgroupCPUCFSPeriodUsParam = "cpu.cfs_period_us"
)
const (
_procPathCGroup = "/proc/self/cgroup"
_procPathMountInfo = "/proc/self/mountinfo"
)
// CGroups is a map that associates each CGroup with its subsystem name.
type CGroups map[string]*CGroup
// NewCGroups returns a new *CGroups from given `mountinfo` and `cgroup` files
// under for some process under `/proc` file system (see also proc(5) for more
// information).
func NewCGroups(procPathMountInfo, procPathCGroup string) (CGroups, error) {
cgroupSubsystems, err := parseCGroupSubsystems(procPathCGroup)
if err != nil {
return nil, err
}
cgroups := make(CGroups)
newMountPoint := func(mp *MountPoint) error {
if mp.FSType != _cgroupFSType {
return nil
}
for _, opt := range mp.SuperOptions {
subsys, exists := cgroupSubsystems[opt]
if !exists {
continue
}
cgroupPath, err := mp.Translate(subsys.Name)
if err != nil {
return err
}
cgroups[opt] = NewCGroup(cgroupPath)
}
return nil
}
if err := parseMountInfo(procPathMountInfo, newMountPoint); err != nil {
return nil, err
}
return cgroups, nil
}
// NewCGroupsForCurrentProcess returns a new *CGroups instance for the current
// process.
func NewCGroupsForCurrentProcess() (CGroups, error) {
return NewCGroups(_procPathMountInfo, _procPathCGroup)
}
// CPUQuota returns the CPU quota applied with the CPU cgroup controller.
// It is a result of `cpu.cfs_quota_us / cpu.cfs_period_us`. If the value of
// `cpu.cfs_quota_us` was not set (-1), the method returns `(-1, nil)`.
func (cg CGroups) CPUQuota() (float64, bool, error) {
cpuCGroup, exists := cg[_cgroupSubsysCPU]
if !exists {
return -1, false, nil
}
cfsQuotaUs, err := cpuCGroup.readInt(_cgroupCPUCFSQuotaUsParam)
if defined := cfsQuotaUs > 0; err != nil || !defined {
return -1, defined, err
}
cfsPeriodUs, err := cpuCGroup.readInt(_cgroupCPUCFSPeriodUsParam)
if err != nil {
return -1, false, err
}
return float64(cfsQuotaUs) / float64(cfsPeriodUs), true, nil
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// Package cgroups provides utilities to access Linux control group (CGroups)
// parameters (CPU quota, for example) for a given process.
package cgroups
+51
View File
@@ -0,0 +1,51 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// +build linux
package cgroups
import "fmt"
type cgroupSubsysFormatInvalidError struct {
line string
}
type mountPointFormatInvalidError struct {
line string
}
type pathNotExposedFromMountPointError struct {
mountPoint string
root string
path string
}
func (err cgroupSubsysFormatInvalidError) Error() string {
return fmt.Sprintf("invalid format for CGroupSubsys: %q", err.line)
}
func (err mountPointFormatInvalidError) Error() string {
return fmt.Sprintf("invalid format for MountPoint: %q", err.line)
}
func (err pathNotExposedFromMountPointError) Error() string {
return fmt.Sprintf("path %q is not a descendant of mount point root %q and cannot be exposed from %q", err.path, err.root, err.mountPoint)
}
+166
View File
@@ -0,0 +1,166 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// +build linux
package cgroups
import (
"bufio"
"os"
"path/filepath"
"strconv"
"strings"
)
const (
_mountInfoSep = " "
_mountInfoOptsSep = ","
_mountInfoOptionalFieldsSep = "-"
)
const (
_miFieldIDMountID = iota
_miFieldIDParentID
_miFieldIDDeviceID
_miFieldIDRoot
_miFieldIDMountPoint
_miFieldIDOptions
_miFieldIDOptionalFields
_miFieldCountFirstHalf
)
const (
_miFieldOffsetFSType = iota
_miFieldOffsetMountSource
_miFieldOffsetSuperOptions
_miFieldCountSecondHalf
)
const _miFieldCountMin = _miFieldCountFirstHalf + _miFieldCountSecondHalf
// MountPoint is the data structure for the mount points in
// `/proc/$PID/mountinfo`. See also proc(5) for more information.
type MountPoint struct {
MountID int
ParentID int
DeviceID string
Root string
MountPoint string
Options []string
OptionalFields []string
FSType string
MountSource string
SuperOptions []string
}
// NewMountPointFromLine parses a line read from `/proc/$PID/mountinfo` and
// returns a new *MountPoint.
func NewMountPointFromLine(line string) (*MountPoint, error) {
fields := strings.Split(line, _mountInfoSep)
if len(fields) < _miFieldCountMin {
return nil, mountPointFormatInvalidError{line}
}
mountID, err := strconv.Atoi(fields[_miFieldIDMountID])
if err != nil {
return nil, err
}
parentID, err := strconv.Atoi(fields[_miFieldIDParentID])
if err != nil {
return nil, err
}
for i, field := range fields[_miFieldIDOptionalFields:] {
if field == _mountInfoOptionalFieldsSep {
fsTypeStart := _miFieldIDOptionalFields + i + 1
if len(fields) != fsTypeStart+_miFieldCountSecondHalf {
return nil, mountPointFormatInvalidError{line}
}
miFieldIDFSType := _miFieldOffsetFSType + fsTypeStart
miFieldIDMountSource := _miFieldOffsetMountSource + fsTypeStart
miFieldIDSuperOptions := _miFieldOffsetSuperOptions + fsTypeStart
return &MountPoint{
MountID: mountID,
ParentID: parentID,
DeviceID: fields[_miFieldIDDeviceID],
Root: fields[_miFieldIDRoot],
MountPoint: fields[_miFieldIDMountPoint],
Options: strings.Split(fields[_miFieldIDOptions], _mountInfoOptsSep),
OptionalFields: fields[_miFieldIDOptionalFields:(fsTypeStart - 1)],
FSType: fields[miFieldIDFSType],
MountSource: fields[miFieldIDMountSource],
SuperOptions: strings.Split(fields[miFieldIDSuperOptions], _mountInfoOptsSep),
}, nil
}
}
return nil, mountPointFormatInvalidError{line}
}
// Translate converts an absolute path inside the *MountPoint's file system to
// the host file system path in the mount namespace the *MountPoint belongs to.
func (mp *MountPoint) Translate(absPath string) (string, error) {
relPath, err := filepath.Rel(mp.Root, absPath)
if err != nil {
return "", err
}
if relPath == ".." || strings.HasPrefix(relPath, "../") {
return "", pathNotExposedFromMountPointError{
mountPoint: mp.MountPoint,
root: mp.Root,
path: absPath,
}
}
return filepath.Join(mp.MountPoint, relPath), nil
}
// parseMountInfo parses procPathMountInfo (usually at `/proc/$PID/mountinfo`)
// and yields parsed *MountPoint into newMountPoint.
func parseMountInfo(procPathMountInfo string, newMountPoint func(*MountPoint) error) error {
mountInfoFile, err := os.Open(procPathMountInfo)
if err != nil {
return err
}
defer mountInfoFile.Close()
scanner := bufio.NewScanner(mountInfoFile)
for scanner.Scan() {
mountPoint, err := NewMountPointFromLine(scanner.Text())
if err != nil {
return err
}
if err := newMountPoint(mountPoint); err != nil {
return err
}
}
return scanner.Err()
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// +build linux
package cgroups
import (
"bufio"
"os"
"strconv"
"strings"
)
const (
_cgroupSep = ":"
_cgroupSubsysSep = ","
)
const (
_csFieldIDID = iota
_csFieldIDSubsystems
_csFieldIDName
_csFieldCount
)
// CGroupSubsys represents the data structure for entities in
// `/proc/$PID/cgroup`. See also proc(5) for more information.
type CGroupSubsys struct {
ID int
Subsystems []string
Name string
}
// NewCGroupSubsysFromLine returns a new *CGroupSubsys by parsing a string in
// the format of `/proc/$PID/cgroup`
func NewCGroupSubsysFromLine(line string) (*CGroupSubsys, error) {
fields := strings.SplitN(line, _cgroupSep, _csFieldCount)
if len(fields) != _csFieldCount {
return nil, cgroupSubsysFormatInvalidError{line}
}
id, err := strconv.Atoi(fields[_csFieldIDID])
if err != nil {
return nil, err
}
cgroup := &CGroupSubsys{
ID: id,
Subsystems: strings.Split(fields[_csFieldIDSubsystems], _cgroupSubsysSep),
Name: fields[_csFieldIDName],
}
return cgroup, nil
}
// parseCGroupSubsystems parses procPathCGroup (usually at `/proc/$PID/cgroup`)
// and returns a new map[string]*CGroupSubsys.
func parseCGroupSubsystems(procPathCGroup string) (map[string]*CGroupSubsys, error) {
cgroupFile, err := os.Open(procPathCGroup)
if err != nil {
return nil, err
}
defer cgroupFile.Close()
scanner := bufio.NewScanner(cgroupFile)
subsystems := make(map[string]*CGroupSubsys)
for scanner.Scan() {
cgroup, err := NewCGroupSubsysFromLine(scanner.Text())
if err != nil {
return nil, err
}
for _, subsys := range cgroup.Subsystems {
subsystems[subsys] = cgroup
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return subsystems, nil
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// +build linux
package runtime
import (
"math"
cg "go.uber.org/automaxprocs/internal/cgroups"
)
// CPUQuotaToGOMAXPROCS converts the CPU quota applied to the calling process
// to a valid GOMAXPROCS value.
func CPUQuotaToGOMAXPROCS(minValue int) (int, CPUQuotaStatus, error) {
cgroups, err := cg.NewCGroupsForCurrentProcess()
if err != nil {
return -1, CPUQuotaUndefined, err
}
quota, defined, err := cgroups.CPUQuota()
if !defined || err != nil {
return -1, CPUQuotaUndefined, err
}
maxProcs := int(math.Floor(quota))
if minValue > 0 && maxProcs < minValue {
return minValue, CPUQuotaMinUsed, nil
}
return maxProcs, CPUQuotaUsed, nil
}
@@ -0,0 +1,30 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// +build !linux
package runtime
// CPUQuotaToGOMAXPROCS converts the CPU quota applied to the calling process
// to a valid GOMAXPROCS value. This is Linux-specific and not supported in the
// current OS.
func CPUQuotaToGOMAXPROCS(_ int) (int, CPUQuotaStatus, error) {
return -1, CPUQuotaUndefined, nil
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package runtime
// CPUQuotaStatus presents the status of how CPU quota is used
type CPUQuotaStatus int
const (
// CPUQuotaUndefined is returned when CPU quota is undefined
CPUQuotaUndefined CPUQuotaStatus = iota
// CPUQuotaUsed is returned when a valid CPU quota can be used
CPUQuotaUsed
// CPUQuotaMinUsed is return when CPU quota is smaller than the min value
CPUQuotaMinUsed
)
+130
View File
@@ -0,0 +1,130 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// Package maxprocs lets Go programs easily configure runtime.GOMAXPROCS to
// match the configured Linux CPU quota. Unlike the top-level automaxprocs
// package, it lets the caller configure logging and handle errors.
package maxprocs // import "go.uber.org/automaxprocs/maxprocs"
import (
"os"
"runtime"
iruntime "go.uber.org/automaxprocs/internal/runtime"
)
const _maxProcsKey = "GOMAXPROCS"
func currentMaxProcs() int {
return runtime.GOMAXPROCS(0)
}
type config struct {
printf func(string, ...interface{})
procs func(int) (int, iruntime.CPUQuotaStatus, error)
minGOMAXPROCS int
}
func (c *config) log(fmt string, args ...interface{}) {
if c.printf != nil {
c.printf(fmt, args...)
}
}
// An Option alters the behavior of Set.
type Option interface {
apply(*config)
}
// Logger uses the supplied printf implementation for log output. By default,
// Set doesn't log anything.
func Logger(printf func(string, ...interface{})) Option {
return optionFunc(func(cfg *config) {
cfg.printf = printf
})
}
// Min sets the minimum GOMAXPROCS value that will be used.
// Any value below 1 is ignored.
func Min(n int) Option {
return optionFunc(func(cfg *config) {
if n >= 1 {
cfg.minGOMAXPROCS = n
}
})
}
type optionFunc func(*config)
func (of optionFunc) apply(cfg *config) { of(cfg) }
// Set GOMAXPROCS to match the Linux container CPU quota (if any), returning
// any error encountered and an undo function.
//
// Set is a no-op on non-Linux systems and in Linux environments without a
// configured CPU quota.
func Set(opts ...Option) (func(), error) {
cfg := &config{
procs: iruntime.CPUQuotaToGOMAXPROCS,
minGOMAXPROCS: 1,
}
for _, o := range opts {
o.apply(cfg)
}
undoNoop := func() {
cfg.log("maxprocs: No GOMAXPROCS change to reset")
}
// Honor the GOMAXPROCS environment variable if present. Otherwise, amend
// `runtime.GOMAXPROCS()` with the current process' CPU quota if the OS is
// Linux, and guarantee a minimum value of 1. The minimum guaranteed value
// can be overriden using `maxprocs.Min()`.
if max, exists := os.LookupEnv(_maxProcsKey); exists {
cfg.log("maxprocs: Honoring GOMAXPROCS=%q as set in environment", max)
return undoNoop, nil
}
maxProcs, status, err := cfg.procs(cfg.minGOMAXPROCS)
if err != nil {
return undoNoop, err
}
if status == iruntime.CPUQuotaUndefined {
cfg.log("maxprocs: Leaving GOMAXPROCS=%v: CPU quota undefined", currentMaxProcs())
return undoNoop, nil
}
prev := currentMaxProcs()
undo := func() {
cfg.log("maxprocs: Resetting GOMAXPROCS to %v", prev)
runtime.GOMAXPROCS(prev)
}
switch status {
case iruntime.CPUQuotaMinUsed:
cfg.log("maxprocs: Updating GOMAXPROCS=%v: using minimum allowed GOMAXPROCS", maxProcs)
case iruntime.CPUQuotaUsed:
cfg.log("maxprocs: Updating GOMAXPROCS=%v: determined from CPU quota", maxProcs)
}
runtime.GOMAXPROCS(maxProcs)
return undo, nil
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package maxprocs
// Version is the current package version.
const Version = "1.4.0"
+5
View File
@@ -250,6 +250,11 @@ github.com/stretchr/testify/require
## explicit
github.com/urfave/cli/v2
github.com/urfave/cli/v2/altsrc
# go.uber.org/automaxprocs v1.4.0
## explicit
go.uber.org/automaxprocs/internal/cgroups
go.uber.org/automaxprocs/internal/runtime
go.uber.org/automaxprocs/maxprocs
# golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
## explicit
golang.org/x/crypto/blake2b
+67 -79
View File
@@ -3,116 +3,47 @@ package websocket
import (
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
)
var stripWebsocketHeaders = []string{
"Upgrade",
"Connection",
"Sec-Websocket-Key",
"Sec-Websocket-Version",
"Sec-Websocket-Extensions",
}
// IsWebSocketUpgrade checks to see if the request is a WebSocket connection.
func IsWebSocketUpgrade(req *http.Request) bool {
return websocket.IsWebSocketUpgrade(req)
}
// 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 *websocket.Dialer) (*websocket.Conn, *http.Response, error) {
req.URL.Scheme = ChangeRequestScheme(req.URL)
wsHeaders := websocketHeaders(req)
if dialler == nil {
dialler = &websocket.Dialer{
Proxy: http.ProxyFromEnvironment,
}
}
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, nil
}
// 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("Sec-Websocket-Accept", generateAcceptKey(req.Header.Get("Sec-WebSocket-Key")))
header.Add("Upgrade", "websocket")
return header
}
// the gorilla websocket library sets its own Upgrade, Connection, Sec-WebSocket-Key,
// Sec-WebSocket-Version and Sec-Websocket-Extensions headers.
// https://github.com/gorilla/websocket/blob/master/client.go#L189-L194.
func websocketHeaders(req *http.Request) http.Header {
wsHeaders := make(http.Header)
for key, val := range req.Header {
wsHeaders[key] = val
}
// Assume the header keys are in canonical format.
for _, header := range stripWebsocketHeaders {
wsHeaders.Del(header)
}
wsHeaders.Set("Host", req.Host) // See TUN-1097
return wsHeaders
}
// sha1Base64 sha1 and then base64 encodes str.
func sha1Base64(str string) string {
hasher := sha1.New()
_, _ = io.WriteString(hasher, str)
hash := hasher.Sum(nil)
return base64.StdEncoding.EncodeToString(hash)
}
// generateAcceptKey returns the string needed for the Sec-WebSocket-Accept header.
// https://tools.ietf.org/html/rfc6455#section-1.3 describes this process in more detail.
func generateAcceptKey(req *http.Request) string {
return sha1Base64(req.Header.Get("Sec-WebSocket-Key") + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
}
// ChangeRequestScheme is needed as the gorilla websocket library requires the ws scheme.
// (even though it changes it back to http/https, but ¯\_(ツ)_/¯.)
func ChangeRequestScheme(reqURL *url.URL) string {
switch reqURL.Scheme {
case "https":
return "wss"
case "http":
return "ws"
case "":
return "ws"
default:
return reqURL.Scheme
}
}
// Stream copies copy data to & from provided io.ReadWriters.
func Stream(conn, backendConn io.ReadWriter, log *zerolog.Logger) {
func Stream(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) {
proxyDone := make(chan struct{}, 2)
go func() {
_, err := io.Copy(conn, backendConn)
_, err := copyData(tunnelConn, originConn, "origin->tunnel")
if err != nil {
log.Debug().Msgf("conn to backendConn copy: %v", err)
log.Debug().Msgf("origin to tunnel copy: %v", err)
}
proxyDone <- struct{}{}
}()
go func() {
_, err := io.Copy(backendConn, conn)
_, err := copyData(originConn, tunnelConn, "tunnel->origin")
if err != nil {
log.Debug().Msgf("backendConn to conn copy: %v", err)
log.Debug().Msgf("tunnel to origin copy: %v", err)
}
proxyDone <- struct{}{}
}()
@@ -120,3 +51,60 @@ func Stream(conn, backendConn io.ReadWriter, log *zerolog.Logger) {
// If one side is done, we are done.
<-proxyDone
}
// when set to true, enables logging of content copied to/from origin and tunnel
const debugCopy = false
func copyData(dst io.Writer, src io.Reader, dir string) (written int64, err error) {
if debugCopy {
// copyBuffer is based on stdio Copy implementation but shows copied data
copyBuffer := func(dst io.Writer, src io.Reader, dir string) (written int64, err error) {
var buf []byte
size := 32 * 1024
buf = make([]byte, size)
for {
t := time.Now()
nr, er := src.Read(buf)
if nr > 0 {
fmt.Println(dir, t.UnixNano(), "\n"+hex.Dump(buf[0:nr]))
nw, ew := dst.Write(buf[0:nr])
if nw < 0 || nr < nw {
nw = 0
if ew == nil {
ew = errors.New("invalid write")
}
}
written += int64(nw)
if ew != nil {
err = ew
break
}
if nr != nw {
err = io.ErrShortWrite
break
}
}
if er != nil {
if er != io.EOF {
err = er
}
break
}
}
return written, err
}
return copyBuffer(dst, src, dir)
} else {
return io.Copy(dst, src)
}
}
// from RFC-6455
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
func generateAcceptKey(challengeKey string) string {
h := sha1.New()
h.Write([]byte(challengeKey))
h.Write(keyGUID)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
+1 -136
View File
@@ -1,24 +1,9 @@
package websocket
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"math/rand"
"net/http"
"testing"
"time"
gws "github.com/gorilla/websocket"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/websocket"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/tlsconfig"
)
const (
@@ -28,126 +13,6 @@ const (
testSecWebsocketAccept = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
)
func testRequest(t *testing.T, url string, stream io.ReadWriter) *http.Request {
req, err := http.NewRequest("GET", url, stream)
if err != nil {
t.Fatalf("testRequestHeader error")
}
req.Header.Add("Connection", "Upgrade")
req.Header.Add("Upgrade", "WebSocket")
req.Header.Add("Sec-Websocket-Key", testSecWebsocketKey)
req.Header.Add("Sec-Websocket-Protocol", "tunnel-protocol")
req.Header.Add("Sec-Websocket-Version", "13")
req.Header.Add("User-Agent", "curl/7.59.0")
return req
}
func websocketClientTLSConfig(t *testing.T) *tls.Config {
certPool := x509.NewCertPool()
helloCert, err := tlsconfig.GetHelloCertificateX509()
assert.NoError(t, err)
certPool.AddCert(helloCert)
assert.NotNil(t, certPool)
return &tls.Config{RootCAs: certPool}
}
func TestWebsocketHeaders(t *testing.T) {
req := testRequest(t, "http://example.com", nil)
wsHeaders := websocketHeaders(req)
for _, header := range stripWebsocketHeaders {
assert.Empty(t, wsHeaders[header])
}
assert.Equal(t, "curl/7.59.0", wsHeaders.Get("User-Agent"))
}
func TestGenerateAcceptKey(t *testing.T) {
req := testRequest(t, "http://example.com", nil)
assert.Equal(t, testSecWebsocketAccept, generateAcceptKey(req))
}
func TestServe(t *testing.T) {
log := zerolog.Nop()
shutdownC := make(chan struct{})
errC := make(chan error)
listener, err := hello.CreateTLSListener("localhost:1111")
assert.NoError(t, err)
defer listener.Close()
go func() {
errC <- hello.StartHelloWorldServer(&log, listener, shutdownC)
}()
req := testRequest(t, "https://localhost:1111/ws", nil)
tlsConfig := websocketClientTLSConfig(t)
assert.NotNil(t, 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"))
for i := 0; i < 1000; i++ {
messageSize := rand.Int()%2048 + 1
clientMessage := make([]byte, messageSize)
// rand.Read always returns len(clientMessage) and a nil error
rand.Read(clientMessage)
err = conn.WriteMessage(websocket.BinaryFrame, clientMessage)
assert.NoError(t, err)
messageType, message, err := conn.ReadMessage()
assert.NoError(t, err)
assert.Equal(t, websocket.BinaryFrame, messageType)
assert.Equal(t, clientMessage, message)
}
_ = conn.Close()
close(shutdownC)
<-errC
}
func TestWebsocketWrapper(t *testing.T) {
listener, err := hello.CreateTLSListener("localhost:0")
require.NoError(t, err)
serverErrorChan := make(chan error)
helloSvrCtx, cancelHelloSvr := context.WithCancel(context.Background())
defer func() { <-serverErrorChan }()
defer cancelHelloSvr()
go func() {
log := zerolog.Nop()
serverErrorChan <- hello.StartHelloWorldServer(&log, listener, helloSvrCtx.Done())
}()
tlsConfig := websocketClientTLSConfig(t)
d := gws.Dialer{TLSClientConfig: tlsConfig, HandshakeTimeout: time.Minute}
testAddr := fmt.Sprintf("https://%s/ws", listener.Addr().String())
req := testRequest(t, testAddr, nil)
conn, resp, err := ClientConnect(req, &d)
require.NoError(t, err)
require.Equal(t, testSecWebsocketAccept, resp.Header.Get("Sec-WebSocket-Accept"))
// Websocket now connected to test server so lets check our wrapper
wrapper := GorillaConn{Conn: conn}
buf := make([]byte, 100)
wrapper.Write([]byte("abc"))
n, err := wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 3)
require.Equal(t, "abc", string(buf[:n]))
// Test partial read, read 1 of 3 bytes in one read and the other 2 in another read
wrapper.Write([]byte("abc"))
buf = buf[:1]
n, err = wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 1)
require.Equal(t, "a", string(buf[:n]))
buf = buf[:cap(buf)]
n, err = wrapper.Read(buf)
require.NoError(t, err)
require.Equal(t, n, 2)
require.Equal(t, "bc", string(buf[:n]))
assert.Equal(t, testSecWebsocketAccept, generateAcceptKey(testSecWebsocketKey))
}