Merge branch 'master' into add_arm64_support

This commit is contained in:
jnozsc 2020-06-25 16:27:10 -07:00 committed by GitHub
commit 85c12594f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
477 changed files with 25620 additions and 33014 deletions

8
.docker-images Normal file
View File

@ -0,0 +1,8 @@
images:
- name: cloudflared
dockerfile: Dockerfile
context: .
registries:
- name: docker.io/michael9127
user: env:DOCKER_USER
password: env:DOCKER_PASSWORD

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ guide/public
\#*\#
cscope.*
cloudflared
cloudflared.pkg
cloudflared.exe
!cmd/cloudflared/
.DS_Store

View File

@ -0,0 +1,7 @@
#!/bin/bash
# uninstall first in case this is an upgrade
/usr/local/bin/cloudflared service uninstall
# install the new service using launchctl
/usr/local/bin/cloudflared service install

View File

@ -0,0 +1,5 @@
#!/bin/bash
/usr/local/bin/cloudflared service uninstall
rm /usr/local/bin/cloudflared
pkgutil --forget com.cloudflare.cloudflared

21
.teamcity/build-macos.sh vendored Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
set -euo pipefail
if [[ "$(uname)" != "Darwin" ]] ; then
echo "This should be run on macOS"
exit 1
fi
go version
export GO111MODULE=on
# build 'cloudflared-darwin-amd64.tgz'
mkdir -p artifacts
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
export PATH="$PATH:/usr/local/bin"
mkdir -p ../src/github.com/cloudflare/
cp -r . ../src/github.com/cloudflare/cloudflared
cd ../src/github.com/cloudflare/cloudflared
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
tar czf "$FILENAME" cloudflared

70
.teamcity/update-homebrew.sh vendored Executable file
View File

@ -0,0 +1,70 @@
#!/bin/bash
set -euo pipefail
FILENAME="${PWD}/artifacts/cloudflared-darwin-amd64.tgz"
if ! VERSION="$(git describe --tags --exact-match 2>/dev/null)" ; then
echo "Skipping public release for an untagged commit."
echo "##teamcity[buildStatus status='SUCCESS' text='Skipped due to lack of tag']"
exit 0
fi
if [[ ! -f "$FILENAME" ]] ; then
echo "Missing $FILENAME"
exit 1
fi
if [[ "${GITHUB_PRIVATE_KEY:-}" == "" ]] ; then
echo "Missing GITHUB_PRIVATE_KEY"
exit 1
fi
# upload to s3 bucket for use by Homebrew formula
s3cmd \
--acl-public --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
put "$FILENAME" "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz"
s3cmd \
--acl-public --signature-v2 --access_key="$AWS_ACCESS_KEY_ID" --secret_key="$AWS_SECRET_ACCESS_KEY" --host-bucket="%(bucket)s.s3.cfdata.org" \
cp "s3://cftunnel-docs/dl/cloudflared-$VERSION-darwin-amd64.tgz" "s3://cftunnel-docs/dl/cloudflared-stable-darwin-amd64.tgz"
SHA256=$(sha256sum "$FILENAME" | cut -b1-64)
# set up git (note that UserKnownHostsFile is an absolute path so we can cd wherever)
mkdir -p tmp
ssh-keyscan -t rsa github.com > tmp/github.txt
echo "$GITHUB_PRIVATE_KEY" > tmp/private.key
chmod 0400 tmp/private.key
export GIT_SSH_COMMAND="ssh -o UserKnownHostsFile=$PWD/tmp/github.txt -i $PWD/tmp/private.key -o IdentitiesOnly=yes"
# clone Homebrew repo into tmp/homebrew-cloudflare
git clone git@github.com:cloudflare/homebrew-cloudflare.git tmp/homebrew-cloudflare
cd tmp/homebrew-cloudflare
git checkout -f master
git reset --hard origin/master
# modify cloudflared.rb
URL="https://developers.cloudflare.com/argo-tunnel/dl/cloudflared-$VERSION-darwin-amd64.tgz"
tee cloudflared.rb <<EOF
class Cloudflared < Formula
desc 'Argo Tunnel'
homepage 'https://developers.cloudflare.com/argo-tunnel/'
url '$URL'
sha256 '$SHA256'
version '$VERSION'
def install
bin.install 'cloudflared'
end
end
EOF
# push cloudflared.rb
git add cloudflared.rb
git diff
git config user.name "cloudflare-warp-bot"
git config user.email "warp-bot@cloudflare.com"
git commit -m "Release Argo Tunnel $VERSION"
#git push -v origin master
# don't push to master, push to some testing branch
git checkout -b deploy-test
git push -v origin deploy-test

View File

@ -1,8 +1,11 @@
# use a builder image for building cloudflare
ARG TARGET_GOOS
ARG TARGET_GOARCH
FROM golang:1.13.3 as builder
ENV GO111MODULE=on
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GO111MODULE=on \
CGO_ENABLED=0 \
TARGET_GOOS=${TARGET_GOOS} \
TARGET_GOARCH=${TARGET_GOARCH}
WORKDIR /go/src/github.com/cloudflare/cloudflared/
@ -12,8 +15,6 @@ COPY . .
# compile cloudflared
RUN make cloudflared
# ---
# use a distroless base image with glibc
FROM gcr.io/distroless/base-debian10:nonroot

View File

@ -7,17 +7,43 @@ PACKAGE_DIR := $(CURDIR)/packaging
INSTALL_BINDIR := usr/local/bin
EQUINOX_FLAGS = --version="$(VERSION)" \
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
--app="$(EQUINOX_APP_ID)" \
--token="$(EQUINOX_TOKEN)" \
--channel="$(EQUINOX_CHANNEL)"
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
--app="$(EQUINOX_APP_ID)" \
--token="$(EQUINOX_TOKEN)" \
--channel="$(EQUINOX_CHANNEL)"
ifeq ($(EQUINOX_IS_DRAFT), true)
EQUINOX_FLAGS := --draft $(EQUINOX_FLAGS)
endif
ifeq ($(GOARCH),)
GOARCH := amd64
LOCAL_ARCH ?= $(shell uname -m)
ifeq ($(LOCAL_ARCH),x86_64)
TARGET_ARCH ?= amd64
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
TARGET_ARCH ?= arm64
else ifeq ($(LOCAL_ARCH),aarch64)
TARGET_ARCH ?= arm64
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 4),armv)
TARGET_ARCH ?= arm
else
$(error This system's architecture $(LOCAL_ARCH) isn't supported)
endif
LOCAL_OS ?= $(shell go env GOOS)
ifeq ($(LOCAL_OS),linux)
TARGET_OS ?= linux
else ifeq ($(LOCAL_OS),darwin)
TARGET_OS ?= darwin
else ifeq ($(LOCAL_OS),windows)
TARGET_OS ?= windows
else
$(error This system's OS $(LOCAL_OS) isn't supported)
endif
ifeq ($(TARGET_OS), windows)
EXECUTABLE_PATH=./cloudflared.exe
else
EXECUTABLE_PATH=./cloudflared
endif
.PHONY: all
@ -29,11 +55,11 @@ clean:
.PHONY: cloudflared
cloudflared: tunnel-deps
go build -v -mod=vendor $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
GOOS=$(TARGET_OS) GOARCH=$(TARGET_ARCH) go build -v -mod=vendor $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
.PHONY: container
container:
docker build -t cloudflare/cloudflared:"$(VERSION)" .
docker build --build-arg=TARGET_ARCH=$(TARGET_ARCH) --build-arg=TARGET_OS=$(TARGET_OS) -t cloudflare/cloudflared-$(TARGET_OS)-$(TARGET_ARCH):"$(VERSION)" .
.PHONY: test
test: vet
@ -48,7 +74,7 @@ cloudflared-deb: cloudflared
mkdir -p $(PACKAGE_DIR)
cp cloudflared $(PACKAGE_DIR)/cloudflared
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t deb --deb-compression bzip2 \
-a $(GOARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
-a $(TARGET_ARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
.PHONY: cloudflared-darwin-amd64.tgz
cloudflared-darwin-amd64.tgz: cloudflared
@ -68,6 +94,10 @@ homebrew-release: homebrew-upload
release: bin/equinox
bin/equinox release $(EQUINOX_FLAGS) -- $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
.PHONY: github-release
github-release: cloudflared
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
bin/equinox:
mkdir -p bin
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/

View File

@ -1,3 +1,112 @@
2020.6.5
- 2020-06-16 DEVTOOLS-7321: Don't skip macOS builds based on tag
- 2020-06-16 fix for a flaky test
- 2020-06-16 AUTH-2815 flag check was wrong. stupid oversight
- 2020-06-16 TUN-3101: Tunnel list command should only show non-deleted, by default
- 2020-06-16 TUN-3066: Command line action for tunnel run
- 2020-06-16 TUN-3100 make updater report the right text
2020.6.4
- 2020-06-11 TUN-3085: Pass connection authentication information using TunnelAuth struct
- 2020-06-15 TUN-3084: Generate and store tunnel_secret value during tunnel creation
- 2020-06-16 AUTH-2815 add the log file to support the config.yaml file
- 2020-06-02 TUN-3015: Add a new cap'n'proto RPC interface for connection registration as well as matching client and server implementations. The old interface extends the new one for backward compatibility.
2020.6.3
- 2020-06-15 DEVTOOLS-7321: Add openssh-client pkg for missing ssh-keyscan
- 2020-06-15 AUTH-2813 adds back a single file support a cloudflared log file
2020.6.2
- 2020-06-11 AUTH-2648 updated usage text
- 2020-06-11 AUTH-2763 don't redirect from curl command
- 2020-06-12 TUN-3090: Upgrade crypto dep
- 2020-06-11 TUN-3038: Add connections to tunnel list table
- 2020-06-12 AUTH-2810 added warn for backwards compatibility sake
2020.6.1
- 2020-06-09 AUTH-2796 fixed windows build
2020.6.0
- 2020-06-05 AUTH-2645 protect against user mistaken flag input
- 2020-06-05 AUTH-2687 don't copy config unnecessarily
- 2020-06-05 AUTH-2169 make access login page more generic
- 2020-06-05 AUTH-2729 added log file and level to cmd flags to match config file settings
- 2020-06-08 AUTH-2785 service token flag fix and logger fix
- 2020-05-20 AUTH-2682: Create buster build
- 2020-05-21 TUN-2928, TUN-2929, TUN-2930: Add tunnel subcommands to interact with tunnel store service
- 2020-05-29 Adding support for multi-architecture images and binaries (#184)
- 2020-05-29 TUN-3019: Remove declarative tunnel entry code
- 2020-05-29 TUN-3020: Remove declarative tunnel related RPC code
- 2020-05-13 AUTH-2505 added aliases
- 2020-05-14 AUTH-2529 added deprecation text to db-connect command
- 2020-05-18 AUTH-2686: Added error handling to tunnel subcommand
- 2020-05-04 AUTH-2369: RDP Bastion prototype
- 2020-04-29 AUTH-2596 added new logger package and replaced logrus
- 2020-04-25 DEVTOOLS-7321: Use SSH key from env for pushing to GitHub
- 2020-04-25 DEVTOOLS-7321: Push to a test branch instead of to master
- 2020-03-30 DEVTOOLS-7321: Add scripts for macOS builds and homebrew uploads
2020.5.1
- 2020-05-07 TUN-2860: Enable quick reconnect feature by default
- 2020-05-07 AUTH-2564: error handling and minor fixes
- 2020-05-01 AUTH-2588 add DoH to service mode
2020.5.0
- 2020-05-01 TUN-2943: Copy certutil from edge into cloudflared
- 2020-05-05 TUN-2955: Fix connection and goroutine leaks when tunnel conection is terminated on error. Only unregister tunnels that had connected successfully. Close edge connection used to unregister the tunnel. Use buffered channels for error channels where receiver may quit early on context cancellation.
- 2020-04-30 TUN-2940: Added delay parameter to stdin reconnect command.
- 2020-04-27 TUN-2921: Rework address selection logic to avoid corner cases
- 2020-04-28 TUN-2872: Exit with non-0 status code when the binary is updated so launchd will restart the service
- 2020-04-13 AUTH-2587 add config watcher and reload logic for access client forwarder
2020.4.0
- 2020-04-10 TUN-2881: Parameterize response meta information header name in the generating function
- 2020-04-11 TUN-2894: ResponseMetaHeader should be public
- 2020-04-09 TUN-2880: Return metadata about source of the response from cloudflared
- 2020-04-04 ARES-899: Fixes DoH client as system resolver. Fixes #91
- 2020-03-31 AUTH-2394 added socks5 proxy
- 2020-02-24 AUTH-2235 GetTokenIfExists now parses JWT payload for json expiry field to detect if the cached access token is expired
2020.3.2
- 2020-03-31 TUN-2854: Quick Reconnects should be an optional supported feature
- 2020-03-30 TUN-2850: Tunnel stripping Cloudflare headers
2020.3.1
- 2020-03-27 TUN-2846: Trigger debug reconnects from stdin commands, not SIGUSR1
2020.3.0
- 2020-03-23 AUTH-2394 fixed header for websockets. Added TCP alias
- 2020-03-10 TUN-2797: Fix panic in SetConnDigest by making mutexes values.
- 2020-03-13 TUN-2807: cloudflared hello-world shouldn't assume it's my first tunnel
- 2020-03-13 TUN-2756: Set connection digest after reconnect.
- 2020-03-16 TUN-2812: Tunnel proxies and RPCs can share an edge address
- 2020-03-18 TUN-2816: cloudflared metrics server should be more discoverable
- 2020-03-19 TUN-2820: Serialized headers for Websockets
- 2020-03-19 TUN-2819: cloudflared should close its connections when a signal is sent
- 2020-03-19 TUN-2823: Bugfix. cloudflared would hang forever if error occurred.
- 2020-03-10 TUN-2796: Implement HTTP2 CONTINUATION headers correctly
- 2020-03-02 TUN-2779: update sample HTML pages
- 2020-03-04 TUN-2785: Use reconnect token by default
- 2020-03-05 TUN-2754: Add ConnDigest to cloudflared and its RPCs
- 2020-03-06 TUN-2755: ReconnectTunnel RPC now transmits ConnectionDigest
- 2020-03-06 TUN-2761: Use the new header management functions in cloudflared
- 2020-03-06 TUN-2788: cloudflared should store one ConnDigest per HA connection
- 2020-02-26 TUN-2767: Test for large headers
- 2020-02-28 do not terminate tunnel if origin is not reachable on start-up (#177)
- 2020-02-28 TUN-2776: Add header serialization feature in cloudflared
- 2020-02-21 TUN-2748: Insecure randomness vulnerability in github.com/miekg/dns
2020.2.1
- 2020-02-20 TUN-2745: Rename existing header management functions
- 2020-02-21 TUN-2746: Add the new header management functions
- 2020-02-25 perf(cloudflared): reuse memory from buffer pool to get better throughput (#161)
- 2020-02-25 Tweak HTTP host header. Fixes #107 (#168)
- 2020-02-25 TUN-2765: Add list of features to tunnelrpc
- 2020-02-19 TUN-2725: Specify in code that --edge is for internal testing only
- 2020-02-19 TUN-2703: Muxer.Serve terminates when its context is Done
- 2020-02-09 TUN-2717: Function to serialize/deserialize HTTP headers
- 2020-02-05 TUN-2714: New edge discovery. Connections try to reconnect to the same edge IP.
2020.2.0
- 2020-01-30 TUN-2651: Fix panic in h2mux reader when a stream error is encountered
- 2020-01-27 TUN-2645: Revert "TUN-2645: Turn on reconnect tokens"

View File

@ -5,12 +5,12 @@ import (
"path/filepath"
"time"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
)
// DirectoryUploadManager is used to manage file uploads on an interval from a directory
type DirectoryUploadManager struct {
logger *logrus.Logger
logger logger.Service
uploader Uploader
rootDirectory string
sweepInterval time.Duration
@ -23,7 +23,7 @@ type DirectoryUploadManager struct {
// uploader is an Uploader to use as an actual uploading engine
// directory is the directory to sweep for files to upload
// sweepInterval is how often to iterate the directory and upload the files within
func NewDirectoryUploadManager(logger *logrus.Logger, uploader Uploader, directory string, sweepInterval time.Duration, shutdownC chan struct{}) *DirectoryUploadManager {
func NewDirectoryUploadManager(logger logger.Service, uploader Uploader, directory string, sweepInterval time.Duration, shutdownC chan struct{}) *DirectoryUploadManager {
workerCount := 10
manager := &DirectoryUploadManager{
logger: logger,
@ -97,7 +97,7 @@ func (m *DirectoryUploadManager) worker() {
return
case filepath := <-m.workQueue:
if err := m.Upload(filepath); err != nil {
m.logger.WithError(err).Error("Cannot upload file to s3 bucket")
m.logger.Errorf("Cannot upload file to s3 bucket: %s", err)
} else {
os.Remove(filepath)
}

View File

@ -9,7 +9,7 @@ import (
"testing"
"time"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
)
type MockUploader struct {
@ -49,7 +49,7 @@ func setupTestDirectory(t *testing.T) string {
func createUploadManager(t *testing.T, shouldFailUpload bool) *DirectoryUploadManager {
rootDirectory := setupTestDirectory(t)
uploader := NewMockUploader(shouldFailUpload)
logger := logrus.New()
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
shutdownC := make(chan struct{})
return NewDirectoryUploadManager(logger, uploader, rootDirectory, 1*time.Second, shutdownC)
}

29
buffer/pool.go Normal file
View File

@ -0,0 +1,29 @@
package buffer
import (
"sync"
)
type Pool struct {
// A Pool must not be copied after first use.
// https://golang.org/pkg/sync/#Pool
buffers sync.Pool
}
func NewPool(bufferSize int) *Pool {
return &Pool{
buffers: sync.Pool{
New: func() interface{} {
return make([]byte, bufferSize)
},
},
}
}
func (p *Pool) Get() []byte {
return p.buffers.Get().([]byte)
}
func (p *Pool) Put(buf []byte) {
p.buffers.Put(buf)
}

View File

@ -11,9 +11,9 @@ import (
"strings"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
cloudflaredWebsocket "github.com/cloudflare/cloudflared/websocket"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
)
type StartOptions struct {
@ -21,6 +21,15 @@ type StartOptions struct {
Headers http.Header
}
// Connection wraps up all the needed functions to forward over the tunnel
type Connection interface {
// ServeStream is used to forward data from the client to the edge
ServeStream(*StartOptions, io.ReadWriter) error
// StartServer is used to listen for incoming connections from the edge to the origin
StartServer(net.Listener, string, <-chan struct{}) error
}
// StdinoutStream is empty struct for wrapping stdin/stdout
// into a single ReadWriter
type StdinoutStream struct {
@ -44,86 +53,57 @@ func closeRespBody(resp *http.Response) {
}
}
// StartClient will copy the data from stdin/stdout over a WebSocket connection
// to the edge (originURL)
func StartClient(logger *logrus.Logger, stream io.ReadWriter, options *StartOptions) error {
return serveStream(logger, stream, options)
}
// StartServer will setup a listener on a specified address/port and then
// StartForwarder will setup a listener on a specified address/port and then
// forward connections to the origin by calling `Serve()`.
func StartServer(logger *logrus.Logger, address string, shutdownC <-chan struct{}, options *StartOptions) error {
func StartForwarder(conn Connection, address string, shutdownC <-chan struct{}, options *StartOptions) error {
listener, err := net.Listen("tcp", address)
if err != nil {
logger.WithError(err).Error("failed to start forwarding server")
return err
return errors.Wrap(err, "failed to start forwarding server")
}
logger.Info("Started listening on ", address)
return Serve(logger, listener, shutdownC, options)
return Serve(conn, listener, shutdownC, options)
}
// StartClient will copy the data from stdin/stdout over a WebSocket connection
// to the edge (originURL)
func StartClient(conn Connection, stream io.ReadWriter, options *StartOptions) error {
return conn.ServeStream(options, stream)
}
// Serve accepts incoming connections on the specified net.Listener.
// Each connection is handled in a new goroutine: its data is copied over a
// WebSocket connection to the edge (originURL).
// `Serve` always closes `listener`.
func Serve(logger *logrus.Logger, listener net.Listener, shutdownC <-chan struct{}, options *StartOptions) error {
func Serve(remoteConn Connection, listener net.Listener, shutdownC <-chan struct{}, options *StartOptions) error {
defer listener.Close()
for {
select {
case <-shutdownC:
return nil
default:
errChan := make(chan error)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return err
// don't block if parent goroutine quit early
select {
case errChan <- err:
default:
}
return
}
go serveConnection(logger, conn, options)
go serveConnection(remoteConn, conn, options)
}
}()
select {
case <-shutdownC:
return nil
case err := <-errChan:
return err
}
}
// serveConnection handles connections for the Serve() call
func serveConnection(logger *logrus.Logger, c net.Conn, options *StartOptions) {
func serveConnection(remoteConn Connection, c net.Conn, options *StartOptions) {
defer c.Close()
serveStream(logger, c, options)
}
// serveStream will serve the data over the WebSocket stream
func serveStream(logger *logrus.Logger, conn io.ReadWriter, options *StartOptions) error {
wsConn, err := createWebsocketStream(options)
if err != nil {
logger.WithError(err).Errorf("failed to connect to %s\n", options.OriginURL)
return err
}
defer wsConn.Close()
cloudflaredWebsocket.Stream(wsConn, conn)
return nil
}
// 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) (*cloudflaredWebsocket.Conn, error) {
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
req.Header = options.Headers
wsConn, resp, err := cloudflaredWebsocket.ClientConnect(req, nil)
defer closeRespBody(resp)
if err != nil && IsAccessResponse(resp) {
wsConn, err = createAccessAuthenticatedStream(options)
if err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
return &cloudflaredWebsocket.Conn{Conn: wsConn}, nil
remoteConn.ServeStream(options, c)
}
// IsAccessResponse checks the http Response to see if the url location
@ -144,56 +124,14 @@ func IsAccessResponse(resp *http.Response) bool {
return false
}
// 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
// happens it deletes the token and runs the connection again, so the user can
// login again and generate a new one.
func createAccessAuthenticatedStream(options *StartOptions) (*websocket.Conn, error) {
wsConn, resp, err := createAccessWebSocketStream(options)
defer closeRespBody(resp)
if err == nil {
return wsConn, nil
}
if !IsAccessResponse(resp) {
return nil, err
}
// Access Token is invalid for some reason. Go through regen flow
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
return nil, err
}
wsConn, resp, err = createAccessWebSocketStream(options)
defer closeRespBody(resp)
if err != nil {
return nil, err
}
return wsConn, nil
}
// createAccessWebSocketStream builds an Access request and makes a connection
func createAccessWebSocketStream(options *StartOptions) (*websocket.Conn, *http.Response, error) {
req, err := BuildAccessRequest(options)
if err != nil {
return nil, nil, err
}
return cloudflaredWebsocket.ClientConnect(req, nil)
}
// buildAccessRequest builds an HTTP request with the Access token set
func BuildAccessRequest(options *StartOptions) (*http.Request, error) {
// BuildAccessRequest builds an HTTP request with the Access token set
func BuildAccessRequest(options *StartOptions, logger logger.Service) (*http.Request, error) {
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
token, err := token.FetchToken(req.URL)
token, err := token.FetchTokenWithRedirect(req.URL, logger)
if err != nil {
return nil, err
}
@ -204,7 +142,7 @@ func BuildAccessRequest(options *StartOptions) (*http.Request, error) {
if err != nil {
return nil, err
}
originRequest.Header.Set("cf-access-token", token)
originRequest.Header.Set(h2mux.CFAccessTokenHeader, token)
for k, v := range options.Headers {
if len(v) >= 1 {

View File

@ -9,8 +9,8 @@ import (
"sync"
"testing"
"github.com/cloudflare/cloudflared/logger"
ws "github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
@ -43,7 +43,8 @@ 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."
logger := logrus.New()
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
wsConn := NewWSConnection(logger, false)
ts := newTestWebSocketServer()
defer ts.Close()
@ -52,7 +53,7 @@ func TestStartClient(t *testing.T) {
OriginURL: "http://" + ts.Listener.Addr().String(),
Headers: nil,
}
err := StartClient(logger, buf, options)
err := StartClient(wsConn, buf, options)
assert.NoError(t, err)
buf.Write([]byte(message))
@ -67,8 +68,9 @@ func TestStartServer(t *testing.T) {
t.Fatalf("Error starting listener: %v", err)
}
message := "Good morning Austin! Time for another sunny day in the great state of Texas."
logger := logrus.New()
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
shutdownC := make(chan struct{})
wsConn := NewWSConnection(logger, false)
ts := newTestWebSocketServer()
defer ts.Close()
options := &StartOptions{
@ -77,7 +79,7 @@ func TestStartServer(t *testing.T) {
}
go func() {
err := Serve(logger, listener, shutdownC, options)
err := Serve(wsConn, listener, shutdownC, options)
if err != nil {
t.Fatalf("Error running server: %v", err)
}

145
carrier/websocket.go Normal file
View File

@ -0,0 +1,145 @@
package carrier
import (
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/socks"
cfwebsocket "github.com/cloudflare/cloudflared/websocket"
"github.com/gorilla/websocket"
)
// Websocket is used to carry data via WS binary frames over the tunnel from client to the origin
// This implements the functions for glider proxy (sock5) and the carrier interface
type Websocket struct {
logger logger.Service
isSocks bool
}
type wsdialer struct {
conn *cfwebsocket.Conn
}
func (d *wsdialer) Dial(address string) (io.ReadWriteCloser, *socks.AddrSpec, error) {
local, ok := d.conn.LocalAddr().(*net.TCPAddr)
if !ok {
return nil, nil, fmt.Errorf("not a tcp connection")
}
addr := socks.AddrSpec{IP: local.IP, Port: local.Port}
return d.conn, &addr, nil
}
// NewWSConnection returns a new connection object
func NewWSConnection(logger logger.Service, isSocks bool) Connection {
return &Websocket{
logger: logger,
isSocks: isSocks,
}
}
// ServeStream will create a Websocket client stream connection to the edge
// it blocks and writes the raw data from conn over the tunnel
func (ws *Websocket) ServeStream(options *StartOptions, conn io.ReadWriter) error {
wsConn, err := createWebsocketStream(options, ws.logger)
if err != nil {
ws.logger.Errorf("failed to connect to %s with error: %s", options.OriginURL, err)
return err
}
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)
}
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.logger, listener, remote, shutdownC, cfwebsocket.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, logger logger.Service) (*cfwebsocket.Conn, error) {
req, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
req.Header = options.Headers
dump, err := httputil.DumpRequest(req, false)
logger.Debugf("Websocket request: %s", string(dump))
wsConn, resp, err := cfwebsocket.ClientConnect(req, nil)
defer closeRespBody(resp)
if err != nil && IsAccessResponse(resp) {
wsConn, err = createAccessAuthenticatedStream(options, logger)
if err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
return &cfwebsocket.Conn{Conn: wsConn}, nil
}
// 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
// happens it deletes the token and runs the connection again, so the user can
// login again and generate a new one.
func createAccessAuthenticatedStream(options *StartOptions, logger logger.Service) (*websocket.Conn, error) {
wsConn, resp, err := createAccessWebSocketStream(options, logger)
defer closeRespBody(resp)
if err == nil {
return wsConn, nil
}
if !IsAccessResponse(resp) {
return nil, err
}
// Access Token is invalid for some reason. Go through regen flow
originReq, err := http.NewRequest(http.MethodGet, options.OriginURL, nil)
if err != nil {
return nil, err
}
if err := token.RemoveTokenIfExists(originReq.URL); err != nil {
return nil, err
}
wsConn, resp, err = createAccessWebSocketStream(options, logger)
defer closeRespBody(resp)
if err != nil {
return nil, err
}
return wsConn, nil
}
// createAccessWebSocketStream builds an Access request and makes a connection
func createAccessWebSocketStream(options *StartOptions, logger logger.Service) (*websocket.Conn, *http.Response, error) {
req, err := BuildAccessRequest(options, logger)
if err != nil {
return nil, nil, err
}
dump, err := httputil.DumpRequest(req, false)
logger.Debugf("Access Websocket request: %s", string(dump))
return cfwebsocket.ClientConnect(req, nil)
}

94
certutil/certutil.go Normal file
View File

@ -0,0 +1,94 @@
package certutil
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"strings"
)
type namedTunnelToken struct {
ZoneID string `json:"zoneID"`
AccountID string `json:"accountID"`
ServiceKey string `json:"serviceKey"`
}
type OriginCert struct {
PrivateKey interface{}
Cert *x509.Certificate
ZoneID string
ServiceKey string
AccountID string
}
func DecodeOriginCert(blocks []byte) (*OriginCert, error) {
if len(blocks) == 0 {
return nil, fmt.Errorf("Cannot decode empty certificate")
}
originCert := OriginCert{}
block, rest := pem.Decode(blocks)
for {
if block == nil {
break
}
switch block.Type {
case "PRIVATE KEY":
if originCert.PrivateKey != nil {
return nil, fmt.Errorf("Found multiple private key in the certificate")
}
// RSA private key
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("Cannot parse private key")
}
originCert.PrivateKey = privateKey
case "CERTIFICATE":
if originCert.Cert != nil {
return nil, fmt.Errorf("Found multiple certificates in the certificate")
}
cert, err := x509.ParseCertificates(block.Bytes)
if err != nil {
return nil, fmt.Errorf("Cannot parse certificate")
} else if len(cert) > 1 {
return nil, fmt.Errorf("Found multiple certificates in the certificate")
}
originCert.Cert = cert[0]
case "WARP TOKEN", "ARGO TUNNEL TOKEN":
if originCert.ZoneID != "" || originCert.ServiceKey != "" {
return nil, fmt.Errorf("Found multiple tokens in the certificate")
}
// The token is a string,
// Try the newer JSON format
ntt := namedTunnelToken{}
if err := json.Unmarshal(block.Bytes, &ntt); err == nil {
originCert.ZoneID = ntt.ZoneID
originCert.ServiceKey = ntt.ServiceKey
originCert.AccountID = ntt.AccountID
} else {
// Try the older format, where the zoneID and service key are seperated by
// a new line character
token := string(block.Bytes)
s := strings.Split(token, "\n")
if len(s) != 2 {
return nil, fmt.Errorf("Cannot parse token")
}
originCert.ZoneID = s[0]
originCert.ServiceKey = s[1]
}
default:
return nil, fmt.Errorf("Unknown block %s in the certificate", block.Type)
}
block, rest = pem.Decode(rest)
}
if originCert.PrivateKey == nil {
return nil, fmt.Errorf("Missing private key in the certificate")
} else if originCert.Cert == nil {
return nil, fmt.Errorf("Missing certificate in the certificate")
} else if originCert.ZoneID == "" || originCert.ServiceKey == "" {
return nil, fmt.Errorf("Missing token in the certificate")
}
return &originCert, nil
}

67
certutil/certutil_test.go Normal file
View File

@ -0,0 +1,67 @@
package certutil
import (
"fmt"
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoadOriginCert(t *testing.T) {
cert, err := DecodeOriginCert([]byte{})
assert.Equal(t, fmt.Errorf("Cannot decode empty certificate"), err)
assert.Nil(t, cert)
blocks, err := ioutil.ReadFile("test-cert-no-key.pem")
assert.Nil(t, err)
cert, err = DecodeOriginCert(blocks)
assert.Equal(t, fmt.Errorf("Missing private key in the certificate"), err)
assert.Nil(t, cert)
blocks, err = ioutil.ReadFile("test-cert-two-certificates.pem")
assert.Nil(t, err)
cert, err = DecodeOriginCert(blocks)
assert.Equal(t, fmt.Errorf("Found multiple certificates in the certificate"), err)
assert.Nil(t, cert)
blocks, err = ioutil.ReadFile("test-cert-unknown-block.pem")
assert.Nil(t, err)
cert, err = DecodeOriginCert(blocks)
assert.Equal(t, fmt.Errorf("Unknown block RSA PRIVATE KEY in the certificate"), err)
assert.Nil(t, cert)
blocks, err = ioutil.ReadFile("test-cert.pem")
assert.Nil(t, err)
cert, err = DecodeOriginCert(blocks)
assert.Nil(t, err)
assert.NotNil(t, cert)
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
key := "v1.0-58bd4f9e28f7b3c28e05a35ff3e80ab4fd9644ef3fece537eb0d12e2e9258217-183442fbb0bbdb3e571558fec9b5589ebd77aafc87498ee3f09f64a4ad79ffe8791edbae08b36c1d8f1d70a8670de56922dff92b15d214a524f4ebfa1958859e-7ce80f79921312a6022c5d25e2d380f82ceaefe3fbdc43dd13b080e3ef1e26f7"
assert.Equal(t, key, cert.ServiceKey)
}
func TestNewlineArgoTunnelToken(t *testing.T) {
ArgoTunnelTokenTest(t, "test-argo-tunnel-cert.pem")
}
func TestJSONArgoTunnelToken(t *testing.T) {
// The given cert's Argo Tunnel Token was generated by base64 encoding this JSON:
// {
// "zoneID": "7b0a4d77dfb881c1a3b7d61ea9443e19",
// "serviceKey": "test-service-key",
// "accountID": "abcdabcdabcdabcd1234567890abcdef"
// }
ArgoTunnelTokenTest(t, "test-argo-tunnel-cert-json.pem")
}
func ArgoTunnelTokenTest(t *testing.T, path string) {
blocks, err := ioutil.ReadFile(path)
assert.Nil(t, err)
cert, err := DecodeOriginCert(blocks)
assert.Nil(t, err)
assert.NotNil(t, cert)
assert.Equal(t, "7b0a4d77dfb881c1a3b7d61ea9443e19", cert.ZoneID)
key := "test-service-key"
assert.Equal(t, key, cert.ServiceKey)
}

View File

@ -0,0 +1,57 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN ARGO TUNNEL TOKEN-----
eyJ6b25lSUQiOiAiN2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkiLCAi
c2VydmljZUtleSI6ICJ0ZXN0LXNlcnZpY2Uta2V5IiwgImFjY291bnRJRCI6ICJh
YmNkYWJjZGFiY2RhYmNkMTIzNDU2Nzg5MGFiY2RlZiJ9
-----END ARGO TUNNEL TOKEN-----

View File

@ -0,0 +1,56 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN ARGO TUNNEL TOKEN-----
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdGVzdC1zZXJ2aWNlLWtl
eQ==
-----END ARGO TUNNEL TOKEN-----

View File

@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN WARP TOKEN-----
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
ZWYxZTI2Zjc=
-----END WARP TOKEN-----

View File

@ -0,0 +1,85 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN WARP TOKEN-----
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
ZWYxZTI2Zjc=
-----END WARP TOKEN-----

View File

@ -0,0 +1,89 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN WARP TOKEN-----
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
ZWYxZTI2Zjc=
-----END WARP TOKEN-----
-----BEGIN RSA PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END RSA PRIVATE KEY-----

61
certutil/test-cert.pem Normal file
View File

@ -0,0 +1,61 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfGswL16Fz9Ei3
sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng6yHR1H5oX1Lg
1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bxtG0uyrXYh7Mt
z0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyXPE6SuDvMHIeX
6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZAzNOxVKrUsyS
x7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOglHJ2n0sMcZ+Ja
1Y649mPVAgMBAAECggEAEbPF0ah9fH0IzTU/CPbIeh3flyY8GDuMpR1HvwUurSWB
IFI9bLyVAXKb8vYP1TMaTnXi5qmFof+/JShgyZc3+1tZtWTfoaiC8Y1bRfE2yk+D
xmwddhDmijYGG7i8uEaeddSdFEh2GKAqkbV/QgBvN2Nl4EVmIOAJXXNe9l5LFyjy
sR10aNVJRYV1FahrCTwZ3SovHP4d4AUvHh/3FFZDukHc37CFA0+CcR4uehp5yedi
2UdqaszXqunFo/3h+Tn9dW2C7gTTZx4+mfyaws3p3YOmdYArXvpejxHIc0FGwLBm
sb9K7wGVUiF0Bt0ch+C1mdYrCaFNHnPuDswjmm3FwQKBgQDYtxOwwSLA6ZyppozX
Doyx9a7PhiMHCFKSdVB4l8rpK545a+AmpG6LRScTtBsMTHBhT3IQ3QPWlVm1AhjF
AvXMa1rOeaGbCbDn1xqEoEVPtj4tys8eTfyWmtU73jWTFauOt4/xpf/urEpg91xj
m+Gl/8qgBrpm5rQxV5Y4MysRlQKBgQC78jzzlhocXGNvw0wT/K2NsknyeoZXqpIE
QYL60FMl4geZn6w9hwxaL1r+g/tUjTnpBPQtS1r2Ed2gXby5zspN1g/PW8U3t3to
P7zHIJ/sLBXrCh5RJko3hUgGhDNOOCIQj4IaKUfvHYvEIbIxlyI0vdsXsgXgMuQ8
pb9Yifn5QQKBgQCmGu0EtYQlyOlDP10EGSrN3Dm45l9CrKZdi326cN4eCkikSoLs
G2x/YumouItiydP5QiNzuXOPrbmse4bwumwb2s0nJSMw6iSmDsFMlmuJxW2zO5e0
6qGH7fUyhgcaTanJIfk6hrm7/mKkH/S4hGpYCc8NCRsmc/35M+D4AoAoYQKBgQC0
LWpZaxDlF30MbAHHN3l6We2iU+vup0sMYXGb2ZOcwa/fir+ozIr++l8VmJmdWTan
OWSM96zgMghx8Os4hhJTxF+rvqK242OfcVsc2x31X94zUaP2z+peh5uhA6Pb3Nxr
W+iyA9k+Vujiwhr+h5D3VvtvH++aG6/KpGtoCf5nAQKBgQDXX2+d7bd5CLNLLFNd
M2i4QoOFcSKIG+v4SuvgEJHgG8vGvxh2qlSxnMWuPV+7/1P5ATLqDj1PlKms+BNR
y7sc5AT9PclkL3Y9MNzOu0LXyBkGYcl8M0EQfLv9VPbWT+NXiMg/O2CHiT02pAAz
uQicoQq3yzeQh20wtrtaXzTNmA==
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIID+jCCA6CgAwIBAgIUJhFxUKEGvTRc3CjCok6dbPGH/P4wCgYIKoZIzj0EAwIw
gagxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTgwNgYD
VQQLEy9DbG91ZEZsYXJlIE9yaWdpbiBTU0wgRUNDIENlcnRpZmljYXRlIEF1dGhv
cml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5p
YTEXMBUGA1UEAxMOKGRldiB1c2Ugb25seSkwHhcNMTcxMDEzMTM1OTAwWhcNMzIx
MDA5MTM1OTAwWjBiMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMR0wGwYDVQQL
ExRDbG91ZEZsYXJlIE9yaWdpbiBDQTEmMCQGA1UEAxMdQ2xvdWRGbGFyZSBPcmln
aW4gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCf
GswL16Fz9Ei3sAg5AmBizoN2nZdyXHP8T57UxUMcrlJXEEXCVS5RR4m9l+EmK0ng
6yHR1H5oX1Lg1WKyXgWwr0whwmdTD+qWFJW2M8HyefyBKLrsGPuxw4CVYT0h72bx
tG0uyrXYh7Mtz0lHjGV90qrFpq5o0jx0sLbDlDvpFPbIO58uYzKG4Sn2VTC4rOyX
PE6SuDvMHIeX6Ekw4wSVQ9eTbksLQqTyxSqM3zp2ygc56SjGjy1nGQT8ZBGFzSbZ
AzNOxVKrUsySx7LzZVl+zCGCPlQwaYLKObKXadZJmrqSFmErC5jcbVgBz7oJQOgl
HJ2n0sMcZ+Ja1Y649mPVAgMBAAGjggEgMIIBHDAOBgNVHQ8BAf8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUzA6f2Ajq
zhX67c6piY2a1uTiUkwwHwYDVR0jBBgwFoAU2qfBlqxKMZnf0QeTeYiMelfqJfgw
RAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5jbG91ZGZs
YXJlLmNvbS9vcmlnaW5fZWNjX2NhMCMGA1UdEQQcMBqCDCouYXJub2xkLmNvbYIK
YXJub2xkLmNvbTA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmNsb3VkZmxh
cmUuY29tL29yaWdpbl9lY2NfY2EuY3JsMAoGCCqGSM49BAMCA0gAMEUCIDV7HoMj
K5rShE/l+90YAOzHC89OH/wUz3I5KYOFuehoAiEA8e92aIf9XBkr0K6EvFCiSsD+
x+Yo/cL8fGfVpPt4UM8=
-----END CERTIFICATE-----
-----BEGIN WARP TOKEN-----
N2IwYTRkNzdkZmI4ODFjMWEzYjdkNjFlYTk0NDNlMTkKdjEuMC01OGJkNGY5ZTI4
ZjdiM2MyOGUwNWEzNWZmM2U4MGFiNGZkOTY0NGVmM2ZlY2U1MzdlYjBkMTJlMmU5
MjU4MjE3LTE4MzQ0MmZiYjBiYmRiM2U1NzE1NThmZWM5YjU1ODllYmQ3N2FhZmM4
NzQ5OGVlM2YwOWY2NGE0YWQ3OWZmZTg3OTFlZGJhZTA4YjM2YzFkOGYxZDcwYTg2
NzBkZTU2OTIyZGZmOTJiMTVkMjE0YTUyNGY0ZWJmYTE5NTg4NTllLTdjZTgwZjc5
OTIxMzEyYTYwMjJjNWQyNWUyZDM4MGY4MmNlYWVmZTNmYmRjNDNkZDEzYjA4MGUz
ZWYxZTI2Zjc=
-----END WARP TOKEN-----

View File

@ -1,4 +1,4 @@
pinned_go: &pinned_go go=1.12.7-1
pinned_go: &pinned_go go=1.12.9-1
build_dir: &build_dir /cfsetup_build
default-flavor: stretch
stretch: &stretch
@ -31,6 +31,18 @@ stretch: &stretch
- export GOOS=linux
- export GOARCH=amd64
- make release
github-release-linux-amd64:
build_dir: *build_dir
builddeps:
- *pinned_go
- build-essential
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- export GOOS=linux
- export GOARCH=amd64
- make github-release
release-linux-armv6:
build_dir: *build_dir
builddeps:
@ -53,6 +65,20 @@ stretch: &stretch
- export GOARCH=arm64
- export CC=gcc-aarch64-linux-gnu
- make release
github-release-linux-armv6:
build_dir: *build_dir
builddeps:
- *pinned_go
- crossbuild-essential-armhf
- gcc-arm-linux-gnueabihf
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- export GOOS=linux
- export GOARCH=arm
- export CC=arm-linux-gnueabihf-gcc
- make github-release
release-linux-386:
build_dir: *build_dir
builddeps:
@ -62,6 +88,18 @@ stretch: &stretch
- export GOOS=linux
- export GOARCH=386
- make release
github-release-linux-386:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-multilib
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- export GOOS=linux
- export GOARCH=386
- make github-release
release-windows-amd64:
build_dir: *build_dir
builddeps:
@ -72,6 +110,19 @@ stretch: &stretch
- export GOARCH=amd64
- export CC=x86_64-w64-mingw32-gcc
- make release
github-release-windows-amd64:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-mingw-w64
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- export GOOS=windows
- export GOARCH=amd64
- export CC=x86_64-w64-mingw32-gcc
- make github-release
release-windows-386:
build_dir: *build_dir
builddeps:
@ -82,6 +133,19 @@ stretch: &stretch
- export GOARCH=386
- export CC=i686-w64-mingw32-gcc-win32
- make release
github-release-windows-386:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-mingw-w64
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- export GOOS=windows
- export GOARCH=386
- export CC=i686-w64-mingw32-gcc-win32
- make github-release
test:
build_dir: *build_dir
builddeps:
@ -94,9 +158,17 @@ stretch: &stretch
- (cd / && go get github.com/BurntSushi/go-sumtype)
- export PATH="$HOME/go/bin:$PATH"
- make test
update-homebrew:
builddeps:
- openssh-client
- s3cmd
post-cache:
- .teamcity/update-homebrew.sh
jessie: *stretch
buster: *stretch
# cfsetup compose
default-stack: test_dbconnect
test_dbconnect:

View File

@ -6,17 +6,75 @@ import (
"strings"
"github.com/cloudflare/cloudflared/carrier"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
)
// StartForwarder starts a client side websocket forward
func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, logger logger.Service) error {
validURLString, err := validation.ValidateUrl(forwarder.Listener)
if err != nil {
return errors.Wrap(err, "error validating origin URL")
}
validURL, err := url.Parse(validURLString)
if err != nil {
return errors.Wrap(err, "error parsing origin URL")
}
// get the headers from the config file and add to the request
headers := make(http.Header)
if forwarder.TokenClientID != "" {
headers.Set(h2mux.CFAccessClientIDHeader, forwarder.TokenClientID)
}
if forwarder.TokenSecret != "" {
headers.Set(h2mux.CFAccessClientSecretHeader, forwarder.TokenSecret)
}
if forwarder.Destination != "" {
headers.Add(h2mux.CFJumpDestinationHeader, forwarder.Destination)
}
options := &carrier.StartOptions{
OriginURL: forwarder.URL,
Headers: headers, //TODO: TUN-2688 support custom headers from config file
}
// 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(logger, false)
logger.Infof("Start Websocket listener on: %s", validURL.Host)
return carrier.StartForwarder(wsConn, validURL.Host, shutdown, options)
}
// ssh will start a WS proxy server for server mode
// or copy from stdin/stdout for client mode
// useful for proxying other protocols (like ssh) over websockets
// (which you can put Access in front of)
func ssh(c *cli.Context) error {
logDirectory, logLevel := config.FindLogSettings()
flagLogDirectory := c.String(sshLogDirectoryFlag)
if flagLogDirectory != "" {
logDirectory = flagLogDirectory
}
flagLogLevel := c.String(sshLogLevelFlag)
if flagLogLevel != "" {
logLevel = flagLogLevel
}
logger, err := logger.New(logger.DefaultFile(logDirectory), logger.LogLevelString(logLevel))
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
// get the hostname from the cmdline and error out if its not provided
rawHostName := c.String(sshHostnameFlag)
hostname, err := validation.ValidateHostname(rawHostName)
@ -28,15 +86,15 @@ func ssh(c *cli.Context) error {
// get the headers from the cmdline and add them
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
if c.IsSet(sshTokenIDFlag) {
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
headers.Set(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
}
if c.IsSet(sshTokenSecretFlag) {
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
headers.Set(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
}
destination := c.String(sshDestinationFlag)
if destination != "" {
headers.Add("CF-Access-SSH-Destination", destination)
headers.Add(h2mux.CFJumpDestinationHeader, destination)
}
options := &carrier.StartOptions{
@ -44,21 +102,30 @@ func ssh(c *cli.Context) error {
Headers: headers,
}
// 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(logger, false)
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
localForwarder, err := config.ValidateUrl(c)
if err != nil {
logger.WithError(err).Error("Error validating origin URL")
logger.Errorf("Error validating origin URL: %s", err)
return errors.Wrap(err, "error validating origin URL")
}
forwarder, err := url.Parse(localForwarder)
if err != nil {
logger.WithError(err).Error("Error validating origin URL")
logger.Errorf("Error validating origin URL: %s", err)
return errors.Wrap(err, "error validating origin URL")
}
return carrier.StartServer(logger, forwarder.Host, shutdownC, options)
logger.Infof("Start Websocket listener on: %s", forwarder.Host)
err = carrier.StartForwarder(wsConn, forwarder.Host, shutdownC, options)
if err != nil {
logger.Errorf("Error on Websocket listener: %s", err)
}
return err
}
return carrier.StartClient(logger, &carrier.StdinoutStream{}, options)
return carrier.StartClient(wsConn, &carrier.StdinoutStream{}, options)
}
func buildRequestHeaders(values []string) http.Header {

View File

@ -10,27 +10,31 @@ import (
"time"
"github.com/cloudflare/cloudflared/carrier"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
"github.com/cloudflare/cloudflared/cmd/cloudflared/token"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/sshgen"
"github.com/cloudflare/cloudflared/validation"
"github.com/pkg/errors"
"golang.org/x/net/idna"
"github.com/cloudflare/cloudflared/log"
raven "github.com/getsentry/raven-go"
cli "gopkg.in/urfave/cli.v2"
"github.com/getsentry/raven-go"
"gopkg.in/urfave/cli.v2"
)
const (
sshHostnameFlag = "hostname"
sshDestinationFlag = "destination"
sshURLFlag = "url"
sshHeaderFlag = "header"
sshTokenIDFlag = "service-token-id"
sshTokenSecretFlag = "service-token-secret"
sshGenCertFlag = "short-lived-cert"
sshConfigTemplate = `
sshHostnameFlag = "hostname"
sshDestinationFlag = "destination"
sshURLFlag = "url"
sshHeaderFlag = "header"
sshTokenIDFlag = "service-token-id"
sshTokenSecretFlag = "service-token-secret"
sshGenCertFlag = "short-lived-cert"
sshLogDirectoryFlag = "log-directory"
sshLogLevelFlag = "log-level"
sshConfigTemplate = `
Add to your {{.Home}}/.ssh/config:
Host {{.Hostname}}
@ -51,7 +55,6 @@ Host cfpipe-{{.Hostname}}
const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878"
var (
logger = log.CreateLogger()
shutdownC chan struct{}
graceShutdownC chan struct{}
)
@ -71,17 +74,17 @@ func Commands() []*cli.Command {
return []*cli.Command{
{
Name: "access",
Category: "Access (BETA)",
Aliases: []string{"forward"},
Category: "Access",
Usage: "access <subcommand>",
Description: `(BETA) Cloudflare Access protects internal resources by securing, authenticating and monitoring access
Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access
per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are
able to reach sensitive resources. The commands provided here allow you to interact with Access protected
applications from the command line. This feature is considered beta. Your feedback is greatly appreciated!
https://cfl.re/CLIAuthBeta`,
applications from the command line.`,
Subcommands: []*cli.Command{
{
Name: "login",
Action: login,
Action: cliutil.ErrorHandler(login),
Usage: "login <url of access application>",
Description: `The login subcommand initiates an authentication flow with your identity provider.
The subcommand will launch a browser. For headless systems, a url is provided.
@ -97,7 +100,7 @@ func Commands() []*cli.Command {
},
{
Name: "curl",
Action: curl,
Action: cliutil.ErrorHandler(curl),
Usage: "curl [--allow-request, -ar] <url> [<curl args>...]",
Description: `The curl subcommand wraps curl and automatically injects the JWT into a cf-access-token
header when using curl to reach an application behind Access.`,
@ -106,7 +109,7 @@ func Commands() []*cli.Command {
},
{
Name: "token",
Action: generateToken,
Action: cliutil.ErrorHandler(generateToken),
Usage: "token -app=<url of access application>",
ArgsUsage: "url of Access application",
Description: `The token subcommand produces a JWT which can be used to authenticate requests.`,
@ -117,45 +120,57 @@ func Commands() []*cli.Command {
},
},
{
Name: "ssh",
Action: ssh,
Aliases: []string{"rdp"},
Name: "tcp",
Action: cliutil.ErrorHandler(ssh),
Aliases: []string{"rdp", "ssh", "smb"},
Usage: "",
ArgsUsage: "",
Description: `The ssh subcommand sends data over a proxy to the Cloudflare edge.`,
Description: `The tcp subcommand sends data over a proxy to the Cloudflare edge.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: sshHostnameFlag,
Usage: "specify the hostname of your application.",
Name: sshHostnameFlag,
Aliases: []string{"tunnel-host", "T"},
Usage: "specify the hostname of your application.",
},
&cli.StringFlag{
Name: sshDestinationFlag,
Usage: "specify the destination address of your SSH server.",
},
&cli.StringFlag{
Name: sshURLFlag,
Usage: "specify the host:port to forward data to Cloudflare edge.",
Name: sshURLFlag,
Aliases: []string{"listener", "L"},
Usage: "specify the host:port to forward data to Cloudflare edge.",
},
&cli.StringSliceFlag{
Name: sshHeaderFlag,
Aliases: []string{"H"},
Usage: "specify additional headers you wish to send.",
},
&cli.StringSliceFlag{
&cli.StringFlag{
Name: sshTokenIDFlag,
Aliases: []string{"id"},
Usage: "specify an Access service token ID you wish to use.",
},
&cli.StringSliceFlag{
&cli.StringFlag{
Name: sshTokenSecretFlag,
Aliases: []string{"secret"},
Usage: "specify an Access service token secret you wish to use.",
},
&cli.StringFlag{
Name: sshLogDirectoryFlag,
Aliases: []string{"logfile"}, //added to match the tunnel side
Usage: "Save application log to this directory for reporting issues.",
},
&cli.StringFlag{
Name: sshLogLevelFlag,
Aliases: []string{"loglevel"}, //added to match the tunnel side
Usage: "Application logging level {fatal, error, info, debug}. ",
},
},
},
{
Name: "ssh-config",
Action: sshConfig,
Action: cliutil.ErrorHandler(sshConfig),
Usage: "",
Description: `Prints an example configuration ~/.ssh/config`,
Flags: []cli.Flag{
@ -171,7 +186,7 @@ func Commands() []*cli.Command {
},
{
Name: "ssh-gen",
Action: sshGen,
Action: cliutil.ErrorHandler(sshGen),
Usage: "",
Description: `Generates a short lived certificate for given hostname`,
Flags: []cli.Flag{
@ -188,8 +203,15 @@ func Commands() []*cli.Command {
// login pops up the browser window to do the actual login and JWT generation
func login(c *cli.Context) error {
raven.SetDSN(sentryDSN)
logger := log.CreateLogger()
if err := raven.SetDSN(sentryDSN); err != nil {
return err
}
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
args := c.Args()
rawURL := ensureURLScheme(args.First())
appURL, err := url.Parse(rawURL)
@ -197,17 +219,20 @@ func login(c *cli.Context) error {
logger.Errorf("Please provide the url of the Access application\n")
return err
}
if err := verifyTokenAtEdge(appURL, c); err != nil {
logger.WithError(err).Error("Could not verify token")
if err := verifyTokenAtEdge(appURL, c, logger); err != nil {
logger.Errorf("Could not verify token: %s", err)
return err
}
token, err := token.GetTokenIfExists(appURL)
if err != nil || token == "" {
cfdToken, err := token.GetTokenIfExists(appURL)
if err != nil {
fmt.Fprintln(os.Stderr, "Unable to find token for provided application.")
return err
} else if cfdToken == "" {
fmt.Fprintln(os.Stderr, "token for provided application was empty.")
return errors.New("empty application token")
}
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", string(token))
fmt.Fprintf(os.Stdout, "Successfully fetched your token:\n\n%s\n\n", cfdToken)
return nil
}
@ -224,8 +249,14 @@ func ensureURLScheme(url string) string {
// curl provides a wrapper around curl, passing Access JWT along in request
func curl(c *cli.Context) error {
raven.SetDSN(sentryDSN)
logger := log.CreateLogger()
if err := raven.SetDSN(sentryDSN); err != nil {
return err
}
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
args := c.Args()
if args.Len() < 1 {
logger.Error("Please provide the access app and command you wish to run.")
@ -233,7 +264,7 @@ func curl(c *cli.Context) error {
}
cmdArgs, allowRequest := parseAllowRequest(args.Slice())
appURL, err := getAppURL(cmdArgs)
appURL, err := getAppURL(cmdArgs, logger)
if err != nil {
return err
}
@ -241,24 +272,26 @@ func curl(c *cli.Context) error {
tok, err := token.GetTokenIfExists(appURL)
if err != nil || tok == "" {
if allowRequest {
logger.Warn("You don't have an Access token set. Please run access token <access application> to fetch one.")
logger.Info("You don't have an Access token set. Please run access token <access application> to fetch one.")
return shell.Run("curl", cmdArgs...)
}
tok, err = token.FetchToken(appURL)
tok, err = token.FetchToken(appURL, logger)
if err != nil {
logger.Error("Failed to refresh token: ", err)
logger.Errorf("Failed to refresh token: %s", err)
return err
}
}
cmdArgs = append(cmdArgs, "-H")
cmdArgs = append(cmdArgs, fmt.Sprintf("cf-access-token: %s", tok))
cmdArgs = append(cmdArgs, fmt.Sprintf("%s: %s", h2mux.CFAccessTokenHeader, tok))
return shell.Run("curl", cmdArgs...)
}
// token dumps provided token to stdout
func generateToken(c *cli.Context) error {
raven.SetDSN(sentryDSN)
if err := raven.SetDSN(sentryDSN); err != nil {
return err
}
appURL, err := url.Parse(c.String("app"))
if err != nil || c.NumFlags() < 1 {
fmt.Fprintln(os.Stderr, "Please provide a url.")
@ -298,6 +331,11 @@ func sshConfig(c *cli.Context) error {
// sshGen generates a short lived certificate for provided hostname
func sshGen(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
// get the hostname from the cmdline and error out if its not provided
rawHostName := c.String(sshHostnameFlag)
hostname, err := validation.ValidateHostname(rawHostName)
@ -313,12 +351,12 @@ func sshGen(c *cli.Context) error {
// this fetchToken function mutates the appURL param. We should refactor that
fetchTokenURL := &url.URL{}
*fetchTokenURL = *originURL
token, err := token.FetchToken(fetchTokenURL)
cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, logger)
if err != nil {
return err
}
if err := sshgen.GenerateShortLivedCertificate(originURL, token); err != nil {
if err := sshgen.GenerateShortLivedCertificate(originURL, cfdToken); err != nil {
return err
}
@ -326,7 +364,7 @@ func sshGen(c *cli.Context) error {
}
// getAppURL will pull the appURL needed for fetching a user's Access token
func getAppURL(cmdArgs []string) (*url.URL, error) {
func getAppURL(cmdArgs []string, logger logger.Service) (*url.URL, error) {
if len(cmdArgs) < 1 {
logger.Error("Please provide a valid URL as the first argument to curl.")
return nil, errors.New("not a valid url")
@ -400,17 +438,17 @@ func isFileThere(candidate string) bool {
// verifyTokenAtEdge checks for a token on disk, or generates a new one.
// Then makes a request to to the origin with the token to ensure it is valid.
// Returns nil if token is valid.
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context, logger logger.Service) error {
headers := buildRequestHeaders(c.StringSlice(sshHeaderFlag))
if c.IsSet(sshTokenIDFlag) {
headers.Add("CF-Access-Client-Id", c.String(sshTokenIDFlag))
headers.Add(h2mux.CFAccessClientIDHeader, c.String(sshTokenIDFlag))
}
if c.IsSet(sshTokenSecretFlag) {
headers.Add("CF-Access-Client-Secret", c.String(sshTokenSecretFlag))
headers.Add(h2mux.CFAccessClientSecretHeader, c.String(sshTokenSecretFlag))
}
options := &carrier.StartOptions{OriginURL: appUrl.String(), Headers: headers}
if valid, err := isTokenValid(options); err != nil {
if valid, err := isTokenValid(options, logger); err != nil {
return err
} else if valid {
return nil
@ -420,7 +458,7 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
return err
}
if valid, err := isTokenValid(options); err != nil {
if valid, err := isTokenValid(options, logger); err != nil {
return err
} else if !valid {
return errors.New("failed to verify token")
@ -430,8 +468,8 @@ func verifyTokenAtEdge(appUrl *url.URL, c *cli.Context) error {
}
// isTokenValid makes a request to the origin and returns true if the response was not a 302.
func isTokenValid(options *carrier.StartOptions) (bool, error) {
req, err := carrier.BuildAccessRequest(options)
func isTokenValid(options *carrier.StartOptions, logger logger.Service) (bool, error) {
req, err := carrier.BuildAccessRequest(options, logger)
if err != nil {
return false, errors.Wrap(err, "Could not create access request")
}

View File

@ -0,0 +1,50 @@
package main
import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
)
// ForwardServiceType is used to identify what kind of overwatch service this is
const ForwardServiceType = "forward"
// ForwarderService is used to wrap the access package websocket forwarders
// into a service model for the overwatch package.
// it also holds a reference to the config object that represents its state
type ForwarderService struct {
forwarder config.Forwarder
shutdown chan struct{}
logger logger.Service
}
// NewForwardService creates a new forwarder service
func NewForwardService(f config.Forwarder, logger logger.Service) *ForwarderService {
return &ForwarderService{forwarder: f, shutdown: make(chan struct{}, 1), logger: logger}
}
// Name is used to figure out this service is related to the others (normally the addr it binds to)
// e.g. localhost:78641 or 127.0.0.1:2222 since this is a websocket forwarder
func (s *ForwarderService) Name() string {
return s.forwarder.Listener
}
// Type is used to identify what kind of overwatch service this is
func (s *ForwarderService) Type() string {
return ForwardServiceType
}
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
func (s *ForwarderService) Hash() string {
return s.forwarder.Hash()
}
// Shutdown stops the websocket listener
func (s *ForwarderService) Shutdown() {
s.shutdown <- struct{}{}
}
// Run is the run loop that is started by the overwatch service
func (s *ForwarderService) Run() error {
return access.StartForwarder(s.forwarder, s.shutdown, s.logger)
}

View File

@ -0,0 +1,73 @@
package main
import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunneldns"
)
// ResolverServiceType is used to identify what kind of overwatch service this is
const ResolverServiceType = "resolver"
// ResolverService is used to wrap the tunneldns package's DNS over HTTP
// into a service model for the overwatch package.
// it also holds a reference to the config object that represents its state
type ResolverService struct {
resolver config.DNSResolver
shutdown chan struct{}
logger logger.Service
}
// NewResolverService creates a new resolver service
func NewResolverService(r config.DNSResolver, logger logger.Service) *ResolverService {
return &ResolverService{resolver: r,
shutdown: make(chan struct{}),
logger: logger,
}
}
// Name is used to figure out this service is related to the others (normally the addr it binds to)
// this is just "resolver" since there can only be one DNS resolver running
func (s *ResolverService) Name() string {
return ResolverServiceType
}
// Type is used to identify what kind of overwatch service this is
func (s *ResolverService) Type() string {
return ResolverServiceType
}
// Hash is used to figure out if this forwarder is the unchanged or not from the config file updates
func (s *ResolverService) Hash() string {
return s.resolver.Hash()
}
// Shutdown stops the tunneldns listener
func (s *ResolverService) Shutdown() {
s.shutdown <- struct{}{}
}
// Run is the run loop that is started by the overwatch service
func (s *ResolverService) Run() error {
// create a listener
l, err := tunneldns.CreateListener(s.resolver.AddressOrDefault(), s.resolver.PortOrDefault(),
s.resolver.UpstreamsOrDefault(), s.resolver.BootstrapsOrDefault(), s.logger)
if err != nil {
return err
}
// start the listener.
readySignal := make(chan struct{})
err = l.Start(readySignal)
if err != nil {
l.Stop()
return err
}
<-readySignal
s.logger.Infof("start resolver on: %s:%d", s.resolver.AddressOrDefault(), s.resolver.PortOrDefault())
// wait for shutdown signal
<-s.shutdown
s.logger.Infof("shutdown on: %s:%d", s.resolver.AddressOrDefault(), s.resolver.PortOrDefault())
return l.Stop()
}

View File

@ -0,0 +1,90 @@
package main
import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/overwatch"
)
// AppService is the main service that runs when no command lines flags are passed to cloudflared
// it manages all the running services such as tunnels, forwarders, DNS resolver, etc
type AppService struct {
configManager config.Manager
serviceManager overwatch.Manager
shutdownC chan struct{}
configUpdateChan chan config.Root
logger logger.Service
}
// NewAppService creates a new AppService with needed supporting services
func NewAppService(configManager config.Manager, serviceManager overwatch.Manager, shutdownC chan struct{}, logger logger.Service) *AppService {
return &AppService{
configManager: configManager,
serviceManager: serviceManager,
shutdownC: shutdownC,
configUpdateChan: make(chan config.Root),
logger: logger,
}
}
// Run starts the run loop to handle config updates and run forwarders, tunnels, etc
func (s *AppService) Run() error {
go s.actionLoop()
return s.configManager.Start(s)
}
// Shutdown kills all the running services
func (s *AppService) Shutdown() error {
s.configManager.Shutdown()
s.shutdownC <- struct{}{}
return nil
}
// ConfigDidUpdate is a delegate notification from the config manager
// it is trigger when the config file has been updated and now the service needs
// to update its services accordingly
func (s *AppService) ConfigDidUpdate(c config.Root) {
s.configUpdateChan <- c
}
// actionLoop handles the actions from running processes
func (s *AppService) actionLoop() {
for {
select {
case c := <-s.configUpdateChan:
s.handleConfigUpdate(c)
case <-s.shutdownC:
for _, service := range s.serviceManager.Services() {
service.Shutdown()
}
return
}
}
}
func (s *AppService) handleConfigUpdate(c config.Root) {
// handle the client forward listeners
activeServices := map[string]struct{}{}
for _, f := range c.Forwarders {
service := NewForwardService(f, s.logger)
s.serviceManager.Add(service)
activeServices[service.Name()] = struct{}{}
}
// handle resolver changes
if c.Resolver.Enabled {
service := NewResolverService(c.Resolver, s.logger)
s.serviceManager.Add(service)
activeServices[service.Name()] = struct{}{}
}
// TODO: TUN-1451 - tunnels
// remove any services that are no longer active
for _, service := range s.serviceManager.Services() {
if _, ok := activeServices[service.Name()]; !ok {
s.serviceManager.Remove(service.Name())
}
}
}

View File

@ -3,7 +3,7 @@ package buildinfo
import (
"runtime"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
)
type BuildInfo struct {
@ -22,7 +22,7 @@ func GetBuildInfo(cloudflaredVersion string) *BuildInfo {
}
}
func (bi *BuildInfo) Log(logger *logrus.Logger) {
func (bi *BuildInfo) Log(logger logger.Service) {
logger.Infof("Version %s", bi.CloudflaredVersion)
logger.Infof("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
}

View File

@ -0,0 +1,55 @@
package cliutil
import (
"fmt"
"log"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
)
type usageError string
func (ue usageError) Error() string {
return string(ue)
}
func UsageError(format string, args ...interface{}) error {
if len(args) == 0 {
return usageError(format)
} else {
msg := fmt.Sprintf(format, args...)
return usageError(msg)
}
}
// Ensures exit with error code if actionFunc returns an error
func ErrorHandler(actionFunc cli.ActionFunc) cli.ActionFunc {
return func(ctx *cli.Context) error {
err := actionFunc(ctx)
if err != nil {
if _, ok := err.(usageError); ok {
msg := fmt.Sprintf("%s\nSee 'cloudflared %s --help'.", err.Error(), ctx.Command.FullName())
return cli.Exit(msg, -1)
}
// os.Exits with error code if err is cli.ExitCoder or cli.MultiError
cli.HandleExitCoder(err)
err = cli.Exit(err.Error(), 1)
}
logger.SharedWriteManager.Shutdown()
return err
}
}
// PrintLoggerSetupError returns an error to stdout to notify when a logger can't start
func PrintLoggerSetupError(msg string, err error) error {
l, le := logger.New()
if le != nil {
log.Printf("%s: %s", msg, err)
} else {
l.Errorf("%s: %s", msg, err)
}
return errors.Wrap(err, msg)
}

View File

@ -4,24 +4,64 @@ import (
"errors"
"os"
"path/filepath"
"runtime"
"github.com/cloudflare/cloudflared/validation"
homedir "github.com/mitchellh/go-homedir"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"gopkg.in/yaml.v2"
)
var (
// File names from which we attempt to read configuration.
// DefaultConfigFiles is the file names from which we attempt to read configuration.
DefaultConfigFiles = []string{"config.yml", "config.yaml"}
// DefaultUnixConfigLocation is the primary location to find a config file
DefaultUnixConfigLocation = "/usr/local/etc/cloudflared"
// DefaultUnixLogLocation is the primary location to find log files
DefaultUnixLogLocation = "/var/log/cloudflared"
// Launchd doesn't set root env variables, so there is default
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/usr/local/etc/cloudflared", "/etc/cloudflared"}
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/etc/cloudflared", DefaultUnixConfigLocation}
)
const DefaultCredentialFile = "cert.pem"
// DefaultConfigDirectory returns the default directory of the config file
func DefaultConfigDirectory() string {
if runtime.GOOS == "windows" {
path := os.Getenv("CFDPATH")
if path == "" {
path = filepath.Join(os.Getenv("ProgramFiles(x86)"), "cloudflared")
if _, err := os.Stat(path); os.IsNotExist(err) { //doesn't exist, so return an empty failure string
return ""
}
}
return path
}
return DefaultUnixConfigLocation
}
// DefaultLogDirectory returns the default directory for log files
func DefaultLogDirectory() string {
if runtime.GOOS == "windows" {
return DefaultConfigDirectory()
}
return DefaultUnixLogLocation
}
// DefaultConfigPath returns the default location of a config file
func DefaultConfigPath() string {
dir := DefaultConfigDirectory()
if dir == "" {
return DefaultConfigFiles[0]
}
return filepath.Join(dir, DefaultConfigFiles[0])
}
// FileExists checks to see if a file exist at the provided path.
func FileExists(path string) (bool, error) {
f, err := os.Open(path)
@ -63,6 +103,68 @@ func FindDefaultConfigPath() string {
return ""
}
// FindOrCreateConfigPath returns the first path that contains a config file
// or creates one in the primary default path if it doesn't exist
func FindOrCreateConfigPath() string {
path := FindDefaultConfigPath()
if path == "" {
// create the default directory if it doesn't exist
path = DefaultConfigPath()
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return ""
}
// write a new config file out
file, err := os.Create(path)
if err != nil {
return ""
}
defer file.Close()
logDir := DefaultLogDirectory()
os.MkdirAll(logDir, os.ModePerm) //try and create it. Doesn't matter if it succeed or not, only byproduct will be no logs
c := Root{
LogDirectory: logDir,
}
if err := yaml.NewEncoder(file).Encode(&c); err != nil {
return ""
}
}
return path
}
// FindLogSettings gets the log directory and level from the config file
func FindLogSettings() (string, string) {
configPath := FindOrCreateConfigPath()
defaultDirectory := DefaultLogDirectory()
defaultLevel := "info"
file, err := os.Open(configPath)
if err != nil {
return defaultDirectory, defaultLevel
}
defer file.Close()
var config Root
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
return defaultDirectory, defaultLevel
}
directory := defaultDirectory
if config.LogDirectory != "" {
directory = config.LogDirectory
}
level := defaultLevel
if config.LogLevel != "" {
level = config.LogLevel
}
return directory, level
}
// ValidateUnixSocket ensures --unix-socket param is used exclusively
// i.e. it fails if a user specifies both --url and --unix-socket
func ValidateUnixSocket(c *cli.Context) (string, error) {

View File

@ -0,0 +1,106 @@
package config
import (
"errors"
"os"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/watcher"
"gopkg.in/yaml.v2"
)
// Notifier sends out config updates
type Notifier interface {
ConfigDidUpdate(Root)
}
// Manager is the base functions of the config manager
type Manager interface {
Start(Notifier) error
Shutdown()
}
// FileManager watches the yaml config for changes
// sends updates to the service to reconfigure to match the updated config
type FileManager struct {
watcher watcher.Notifier
notifier Notifier
configPath string
logger logger.Service
ReadConfig func(string) (Root, error)
}
// NewFileManager creates a config manager
func NewFileManager(watcher watcher.Notifier, configPath string, logger logger.Service) (*FileManager, error) {
m := &FileManager{
watcher: watcher,
configPath: configPath,
logger: logger,
ReadConfig: readConfigFromPath,
}
err := watcher.Add(configPath)
return m, err
}
// Start starts the runloop to watch for config changes
func (m *FileManager) Start(notifier Notifier) error {
m.notifier = notifier
// update the notifier with a fresh config on start
config, err := m.GetConfig()
if err != nil {
return err
}
notifier.ConfigDidUpdate(config)
m.watcher.Start(m)
return nil
}
// GetConfig reads the yaml file from the disk
func (m *FileManager) GetConfig() (Root, error) {
return m.ReadConfig(m.configPath)
}
// Shutdown stops the watcher
func (m *FileManager) Shutdown() {
m.watcher.Shutdown()
}
func readConfigFromPath(configPath string) (Root, error) {
if configPath == "" {
return Root{}, errors.New("unable to find config file")
}
file, err := os.Open(configPath)
if err != nil {
return Root{}, err
}
defer file.Close()
var config Root
if err := yaml.NewDecoder(file).Decode(&config); err != nil {
return Root{}, err
}
return config, nil
}
// File change notifications from the watcher
// WatcherItemDidChange triggers when the yaml config is updated
// sends the updated config to the service to reload its state
func (m *FileManager) WatcherItemDidChange(filepath string) {
config, err := m.GetConfig()
if err != nil {
m.logger.Errorf("Failed to read new config: %s", err)
return
}
m.logger.Info("Config file has been updated")
m.notifier.ConfigDidUpdate(config)
}
// WatcherDidError notifies of errors with the file watcher
func (m *FileManager) WatcherDidError(err error) {
m.logger.Errorf("Config watcher encountered an error: %s", err)
}

View File

@ -0,0 +1,87 @@
package config
import (
"os"
"testing"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/watcher"
"github.com/stretchr/testify/assert"
)
type mockNotifier struct {
configs []Root
}
func (n *mockNotifier) ConfigDidUpdate(c Root) {
n.configs = append(n.configs, c)
}
type mockFileWatcher struct {
path string
notifier watcher.Notification
ready chan struct{}
}
func (w *mockFileWatcher) Start(n watcher.Notification) {
w.notifier = n
w.ready <- struct{}{}
}
func (w *mockFileWatcher) Add(string) error {
return nil
}
func (w *mockFileWatcher) Shutdown() {
}
func (w *mockFileWatcher) TriggerChange() {
w.notifier.WatcherItemDidChange(w.path)
}
func TestConfigChanged(t *testing.T) {
filePath := "config.yaml"
f, err := os.Create(filePath)
assert.NoError(t, err)
defer func() {
f.Close()
os.Remove(filePath)
}()
c := &Root{
Forwarders: []Forwarder{
{
URL: "test.daltoniam.com",
Listener: "127.0.0.1:8080",
},
},
}
configRead := func(configPath string) (Root, error) {
return *c, nil
}
wait := make(chan struct{})
w := &mockFileWatcher{path: filePath, ready: wait}
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
service, err := NewFileManager(w, filePath, logger)
service.ReadConfig = configRead
assert.NoError(t, err)
n := &mockNotifier{}
go service.Start(n)
<-wait
c.Forwarders = append(c.Forwarders, Forwarder{URL: "add.daltoniam.com", Listener: "127.0.0.1:8081"})
w.TriggerChange()
service.Shutdown()
assert.Len(t, n.configs, 2, "did not get 2 config updates as expected")
assert.Len(t, n.configs[0].Forwarders, 1, "not the amount of forwarders expected")
assert.Len(t, n.configs[1].Forwarders, 2, "not the amount of forwarders expected")
assert.Equal(t, n.configs[0].Forwarders[0].Hash(), c.Forwarders[0].Hash(), "forwarder hashes don't match")
assert.Equal(t, n.configs[1].Forwarders[0].Hash(), c.Forwarders[0].Hash(), "forwarder hashes don't match")
assert.Equal(t, n.configs[1].Forwarders[1].Hash(), c.Forwarders[1].Hash(), "forwarder hashes don't match")
}

View File

@ -0,0 +1,101 @@
package config
import (
"crypto/md5"
"fmt"
"io"
"strings"
)
// Forwarder represents a client side listener to forward traffic to the edge
type Forwarder struct {
URL string `json:"url"`
Listener string `json:"listener"`
TokenClientID string `json:"service_token_id" yaml:"serviceTokenID"`
TokenSecret string `json:"secret_token_id" yaml:"serviceTokenSecret"`
Destination string `json:"destination"`
}
// Tunnel represents a tunnel that should be started
type Tunnel struct {
URL string `json:"url"`
Origin string `json:"origin"`
ProtocolType string `json:"type"`
}
// DNSResolver represents a client side DNS resolver
type DNSResolver struct {
Enabled bool `json:"enabled"`
Address string `json:"address,omitempty"`
Port uint16 `json:"port,omitempty"`
Upstreams []string `json:"upstreams,omitempty"`
Bootstraps []string `json:"bootstraps,omitempty"`
}
// Root is the base options to configure the service
type Root struct {
LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"`
LogLevel string `json:"log_level" yaml:"logLevel,omitempty"`
Forwarders []Forwarder `json:"forwarders,omitempty" yaml:"forwarders,omitempty"`
Tunnels []Tunnel `json:"tunnels,omitempty" yaml:"tunnels,omitempty"`
Resolver DNSResolver `json:"resolver,omitempty" yaml:"resolver,omitempty"`
}
// Hash returns the computed values to see if the forwarder values change
func (f *Forwarder) Hash() string {
h := md5.New()
io.WriteString(h, f.URL)
io.WriteString(h, f.Listener)
io.WriteString(h, f.TokenClientID)
io.WriteString(h, f.TokenSecret)
io.WriteString(h, f.Destination)
return fmt.Sprintf("%x", h.Sum(nil))
}
// Hash returns the computed values to see if the forwarder values change
func (r *DNSResolver) Hash() string {
h := md5.New()
io.WriteString(h, r.Address)
io.WriteString(h, strings.Join(r.Bootstraps, ","))
io.WriteString(h, strings.Join(r.Upstreams, ","))
io.WriteString(h, fmt.Sprintf("%d", r.Port))
io.WriteString(h, fmt.Sprintf("%v", r.Enabled))
return fmt.Sprintf("%x", h.Sum(nil))
}
// EnabledOrDefault returns the enabled property
func (r *DNSResolver) EnabledOrDefault() bool {
return r.Enabled
}
// AddressOrDefault returns the address or returns the default if empty
func (r *DNSResolver) AddressOrDefault() string {
if r.Address != "" {
return r.Address
}
return "localhost"
}
// PortOrDefault return the port or returns the default if 0
func (r *DNSResolver) PortOrDefault() uint16 {
if r.Port > 0 {
return r.Port
}
return 53
}
// UpstreamsOrDefault returns the upstreams or returns the default if empty
func (r *DNSResolver) UpstreamsOrDefault() []string {
if len(r.Upstreams) > 0 {
return r.Upstreams
}
return []string{"https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"}
}
// BootstrapsOrDefault returns the bootstraps or returns the default if empty
func (r *DNSResolver) BootstrapsOrDefault() []string {
if len(r.Bootstraps) > 0 {
return r.Bootstraps
}
return []string{"https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"}
}

View File

@ -8,6 +8,8 @@ import (
"path/filepath"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
)
@ -118,10 +120,6 @@ case "$1" in
echo "Starting $name"
$cmd >> "$stdout_log" 2>> "$stderr_log" &
echo $! > "$pid_file"
if ! is_running; then
echo "Unable to start, see $stdout_log and $stderr_log"
exit 1
fi
fi
;;
stop)
@ -182,24 +180,37 @@ func isSystemd() bool {
return false
}
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string) error {
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, logger logger.Service) error {
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
return err
}
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
return err
if srcCredentialPath != destCredentialPath {
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
return err
}
}
srcConfigPath := filepath.Join(userConfigDir, userConfigFile)
destConfigPath := filepath.Join(serviceConfigDir, serviceConfigFile)
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
return err
if srcConfigPath != destConfigPath {
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
return err
}
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
}
return nil
}
func installLinuxService(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
etPath, err := os.Executable()
if err != nil {
return fmt.Errorf("error determining executable path: %v", err)
@ -209,8 +220,8 @@ func installLinuxService(c *cli.Context) error {
userConfigDir := filepath.Dir(c.String("config"))
userConfigFile := filepath.Base(c.String("config"))
userCredentialFile := config.DefaultCredentialFile
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile); err != nil {
logger.WithError(err).Infof("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, logger); err != nil {
logger.Errorf("Failed to copy user configuration: %s. Before running the service, ensure that %s contains two files, %s and %s", err,
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
return err
}
@ -218,41 +229,41 @@ func installLinuxService(c *cli.Context) error {
switch {
case isSystemd():
logger.Infof("Using Systemd")
return installSystemd(&templateArgs)
return installSystemd(&templateArgs, logger)
default:
logger.Infof("Using Sysv")
return installSysv(&templateArgs)
return installSysv(&templateArgs, logger)
}
}
func installSystemd(templateArgs *ServiceTemplateArgs) error {
func installSystemd(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
for _, serviceTemplate := range systemdTemplates {
err := serviceTemplate.Generate(templateArgs)
if err != nil {
logger.WithError(err).Infof("error generating service template")
logger.Errorf("error generating service template: %s", err)
return err
}
}
if err := runCommand("systemctl", "enable", "cloudflared.service"); err != nil {
logger.WithError(err).Infof("systemctl enable cloudflared.service error")
logger.Errorf("systemctl enable cloudflared.service error: %s", err)
return err
}
if err := runCommand("systemctl", "start", "cloudflared-update.timer"); err != nil {
logger.WithError(err).Infof("systemctl start cloudflared-update.timer error")
logger.Errorf("systemctl start cloudflared-update.timer error: %s", err)
return err
}
logger.Infof("systemctl daemon-reload")
return runCommand("systemctl", "daemon-reload")
}
func installSysv(templateArgs *ServiceTemplateArgs) error {
func installSysv(templateArgs *ServiceTemplateArgs, logger logger.Service) error {
confPath, err := sysvTemplate.ResolvePath()
if err != nil {
logger.WithError(err).Infof("error resolving system path")
logger.Errorf("error resolving system path: %s", err)
return err
}
if err := sysvTemplate.Generate(templateArgs); err != nil {
logger.WithError(err).Infof("error generating system template")
logger.Errorf("error generating system template: %s", err)
return err
}
for _, i := range [...]string{"2", "3", "4", "5"} {
@ -269,28 +280,33 @@ func installSysv(templateArgs *ServiceTemplateArgs) error {
}
func uninstallLinuxService(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
switch {
case isSystemd():
logger.Infof("Using Systemd")
return uninstallSystemd()
return uninstallSystemd(logger)
default:
logger.Infof("Using Sysv")
return uninstallSysv()
return uninstallSysv(logger)
}
}
func uninstallSystemd() error {
func uninstallSystemd(logger logger.Service) error {
if err := runCommand("systemctl", "disable", "cloudflared.service"); err != nil {
logger.WithError(err).Infof("systemctl disable cloudflared.service error")
logger.Errorf("systemctl disable cloudflared.service error: %s", err)
return err
}
if err := runCommand("systemctl", "stop", "cloudflared-update.timer"); err != nil {
logger.WithError(err).Infof("systemctl stop cloudflared-update.timer error")
logger.Errorf("systemctl stop cloudflared-update.timer error: %s", err)
return err
}
for _, serviceTemplate := range systemdTemplates {
if err := serviceTemplate.Remove(); err != nil {
logger.WithError(err).Infof("error removing service template")
logger.Errorf("error removing service template: %s", err)
return err
}
}
@ -298,9 +314,9 @@ func uninstallSystemd() error {
return nil
}
func uninstallSysv() error {
func uninstallSysv(logger logger.Service) error {
if err := sysvTemplate.Remove(); err != nil {
logger.WithError(err).Infof("error removing service template")
logger.Errorf("error removing service template: %s", err)
return err
}
for _, i := range [...]string{"2", "3", "4", "5"} {

View File

@ -8,6 +8,7 @@ import (
"gopkg.in/urfave/cli.v2"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
)
@ -60,7 +61,7 @@ func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemp
<false/>
</dict>
<key>ThrottleInterval</key>
<integer>20</integer>
<integer>5</integer>
</dict>
</plist>`, launchdIdentifier, stdoutPath, stderrPath),
}
@ -105,6 +106,11 @@ func stderrPath() (string, error) {
}
func installLaunchd(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
if isRootUser() {
logger.Infof("Installing Argo Tunnel client as a system launch daemon. " +
"Argo Tunnel client will run at boot")
@ -116,35 +122,38 @@ func installLaunchd(c *cli.Context) error {
}
etPath, err := os.Executable()
if err != nil {
logger.WithError(err).Errorf("Error determining executable path")
logger.Errorf("Error determining executable path: %s", err)
return fmt.Errorf("Error determining executable path: %v", err)
}
installPath, err := installPath()
if err != nil {
logger.Errorf("Error determining install path: %s", err)
return errors.Wrap(err, "Error determining install path")
}
stdoutPath, err := stdoutPath()
if err != nil {
logger.Errorf("error determining stdout path: %s", err)
return errors.Wrap(err, "error determining stdout path")
}
stderrPath, err := stderrPath()
if err != nil {
logger.Errorf("error determining stderr path: %s", err)
return errors.Wrap(err, "error determining stderr path")
}
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
if err != nil {
logger.WithError(err).Errorf("error creating launchd template")
logger.Errorf("error creating launchd template: %s", err)
return errors.Wrap(err, "error creating launchd template")
}
templateArgs := ServiceTemplateArgs{Path: etPath}
err = launchdTemplate.Generate(&templateArgs)
if err != nil {
logger.WithError(err).Errorf("error generating launchd template")
logger.Errorf("error generating launchd template: %s", err)
return err
}
plistPath, err := launchdTemplate.ResolvePath()
if err != nil {
logger.WithError(err).Infof("error resolving launchd template path")
logger.Errorf("error resolving launchd template path: %s", err)
return err
}
@ -153,6 +162,11 @@ func installLaunchd(c *cli.Context) error {
}
func uninstallLaunchd(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
if isRootUser() {
logger.Infof("Uninstalling Argo Tunnel as a system launch daemon")
} else {
@ -176,12 +190,12 @@ func uninstallLaunchd(c *cli.Context) error {
}
plistPath, err := launchdTemplate.ResolvePath()
if err != nil {
logger.WithError(err).Infof("error resolving launchd template path")
logger.Errorf("error resolving launchd template path: %s", err)
return err
}
err = runCommand("launchctl", "unload", plistPath)
if err != nil {
logger.WithError(err).Infof("error unloading")
logger.Errorf("error unloading: %s", err)
return err
}

View File

@ -6,10 +6,14 @@ import (
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/access"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/log"
log "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/overwatch"
"github.com/cloudflare/cloudflared/watcher"
raven "github.com/getsentry/raven-go"
homedir "github.com/mitchellh/go-homedir"
@ -25,7 +29,6 @@ const (
var (
Version = "DEV"
BuildTime = "unknown"
logger = log.CreateLogger()
// Mostly network errors that we don't want reported back to Sentry, this is done by substring match.
ignoredErrors = []string{
"connection reset by peer",
@ -59,7 +62,7 @@ func main() {
app := &cli.App{}
app.Name = "cloudflared"
app.Usage = "Cloudflare's command-line tool and agent"
app.ArgsUsage = "origin-url"
app.UsageText = "cloudflared [global options] [command] [command options]"
app.Copyright = fmt.Sprintf(
`(c) %d Cloudflare Inc.
Your installation of cloudflared software constitutes a symbol of your signature indicating that you accept
@ -121,12 +124,12 @@ func isEmptyInvocation(c *cli.Context) bool {
func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionFunc {
return func(c *cli.Context) (err error) {
if isEmptyInvocation(c) {
cli.ShowAppHelpAndExit(c, 1)
return handleServiceMode(shutdownC)
}
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC) }, nil)
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC, nil) }, nil)
exitCode := 0
if err != nil {
handleError(err)
@ -145,7 +148,6 @@ func userHomeDir() (string, error) {
// use with sudo.
homeDir, err := homedir.Dir()
if err != nil {
logger.WithError(err).Error("Cannot determine home directory for the user")
return "", errors.Wrap(err, "Cannot determine home directory for the user")
}
return homeDir, nil
@ -161,3 +163,43 @@ func handleError(err error) {
}
raven.CaptureError(err, nil)
}
// cloudflared was started without any flags
func handleServiceMode(shutdownC chan struct{}) error {
defer log.SharedWriteManager.Shutdown()
logDirectory, logLevel := config.FindLogSettings()
logger, err := log.New(log.DefaultFile(logDirectory), log.LogLevelString(logLevel))
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
logger.Infof("logging to directory: %s", logDirectory)
// start the main run loop that reads from the config file
f, err := watcher.NewFile()
if err != nil {
logger.Errorf("Cannot load config file: %s", err)
return err
}
configPath := config.FindOrCreateConfigPath()
configManager, err := config.NewFileManager(f, configPath, logger)
if err != nil {
logger.Errorf("Cannot setup config file for monitoring: %s", err)
return err
}
serviceCallback := func(t string, name string, err error) {
if err != nil {
logger.Errorf("%s service: %s encountered an error: %s", t, name, err)
}
}
serviceManager := overwatch.NewAppManager(serviceCallback)
appService := NewAppService(configManager, serviceManager, shutdownC, logger)
if err := appService.Run(); err != nil {
logger.Errorf("Failed to start app service: %s", err)
return err
}
return nil
}

View File

@ -73,21 +73,16 @@ func runCommand(command string, args ...string) error {
cmd := exec.Command(command, args...)
stderr, err := cmd.StderrPipe()
if err != nil {
logger.WithError(err).Infof("error getting stderr pipe")
return fmt.Errorf("error getting stderr pipe: %v", err)
}
err = cmd.Start()
if err != nil {
logger.WithError(err).Infof("error starting %s", command)
return fmt.Errorf("error starting %s: %v", command, err)
}
commandErr, _ := ioutil.ReadAll(stderr)
if len(commandErr) > 0 {
logger.Errorf("%s: %s", command, commandErr)
}
ioutil.ReadAll(stderr)
err = cmd.Wait()
if err != nil {
logger.WithError(err).Infof("%s returned error", command)
return fmt.Errorf("%s returned with error: %v", command, err)
}
return nil
@ -148,8 +143,7 @@ func copyConfig(srcConfigPath, destConfigPath string) error {
// Copy or create config
destFile, exists, err := openFile(destConfigPath, true)
if err != nil {
logger.WithError(err).Infof("cannot open %s", destConfigPath)
return err
return fmt.Errorf("cannot open %s with error: %s", destConfigPath, err)
} else if exists {
// config already exists, do nothing
return nil
@ -173,7 +167,6 @@ func copyConfig(srcConfigPath, destConfigPath string) error {
if err != nil {
return fmt.Errorf("unable to copy %s to %s: %v", srcConfigPath, destConfigPath, err)
}
logger.Infof("Copied %s to %s", srcConfigPath, destConfigPath)
}
return nil

View File

@ -2,17 +2,19 @@ package token
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/signal"
"syscall"
"time"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/path"
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
"github.com/cloudflare/cloudflared/log"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/coreos/go-oidc/jose"
)
@ -21,8 +23,6 @@ const (
keyName = "token"
)
var logger = log.CreateLogger()
type lock struct {
lockFilePath string
backoff *origin.BackoffHandler
@ -34,6 +34,21 @@ type signalHandler struct {
signals []os.Signal
}
type jwtPayload struct {
Aud []string `json:"aud"`
Email string `json:"email"`
Exp int `json:"exp"`
Iat int `json:"iat"`
Nbf int `json:"nbf"`
Iss string `json:"iss"`
Type string `json:"type"`
Subt string `json:"sub"`
}
func (p jwtPayload) isExpired() bool {
return int(time.Now().Unix()) > p.Exp
}
func (s *signalHandler) register(handler func()) {
s.sigChannel = make(chan os.Signal, 1)
signal.Notify(s.sigChannel, s.signals...)
@ -112,8 +127,20 @@ func isTokenLocked(lockFilePath string) bool {
return exists && err == nil
}
// FetchTokenWithRedirect will either load a stored token or generate a new one
// it appends a redirect URL to the access cli request if opening the browser
func FetchTokenWithRedirect(appURL *url.URL, logger logger.Service) (string, error) {
return getToken(appURL, true, logger)
}
// FetchToken will either load a stored token or generate a new one
func FetchToken(appURL *url.URL) (string, error) {
// it doesn't append a redirect URL to the access cli request if opening the browser
func FetchToken(appURL *url.URL, logger logger.Service) (string, error) {
return getToken(appURL, false, logger)
}
// getToken will either load a stored token or generate a new one
func getToken(appURL *url.URL, shouldRedirect bool, logger logger.Service) (string, error) {
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
return token, nil
}
@ -139,7 +166,7 @@ func FetchToken(appURL *url.URL) (string, error) {
// this weird parameter is the resource name (token) and the key/value
// we want to send to the transfer service. the key is token and the value
// is blank (basically just the id generated in the transfer service)
token, err := transfer.Run(appURL, keyName, keyName, "", path, true)
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, shouldRedirect, logger)
if err != nil {
return "", err
}
@ -147,7 +174,7 @@ func FetchToken(appURL *url.URL) (string, error) {
return string(token), nil
}
// GetTokenIfExists will return the token from local storage if it exists
// GetTokenIfExists will return the token from local storage if it exists and not expired
func GetTokenIfExists(url *url.URL) (string, error) {
path, err := path.GenerateFilePathFromURL(url, keyName)
if err != nil {
@ -162,6 +189,17 @@ func GetTokenIfExists(url *url.URL) (string, error) {
return "", err
}
var payload jwtPayload
err = json.Unmarshal(token.Payload, &payload)
if err != nil {
return "", err
}
if payload.isExpired() {
err := os.Remove(path)
return "", err
}
return token.Encode(), nil
}

View File

@ -14,7 +14,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/encrypter"
"github.com/cloudflare/cloudflared/cmd/cloudflared/shell"
"github.com/cloudflare/cloudflared/log"
"github.com/cloudflare/cloudflared/logger"
)
const (
@ -22,20 +22,18 @@ const (
clientTimeout = time.Second * 60
)
var logger = log.CreateLogger()
// Run does the transfer "dance" with the end result downloading the supported resource.
// The expanded description is run is encapsulation of shared business logic needed
// to request a resource (token/cert/etc) from the transfer service (loginhelper).
// The "dance" we refer to is building a HTTP request, opening that in a browser waiting for
// the user to complete an action, while it long polls in the background waiting for an
// action to be completed to download the resource.
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool) ([]byte, error) {
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool, shouldRedirect bool, logger logger.Service) ([]byte, error) {
encrypterClient, err := encrypter.New("cloudflared_priv.pem", "cloudflared_pub.pem")
if err != nil {
return nil, err
}
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt)
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt, shouldRedirect)
if err != nil {
return nil, err
}
@ -51,7 +49,7 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
var resourceData []byte
if shouldEncrypt {
buf, key, err := transferRequest(baseStoreURL + "transfer/" + encrypterClient.PublicKey())
buf, key, err := transferRequest(baseStoreURL+"transfer/"+encrypterClient.PublicKey(), logger)
if err != nil {
return nil, err
}
@ -67,7 +65,7 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
resourceData = decrypted
} else {
buf, _, err := transferRequest(baseStoreURL + encrypterClient.PublicKey())
buf, _, err := transferRequest(baseStoreURL+encrypterClient.PublicKey(), logger)
if err != nil {
return nil, err
}
@ -84,7 +82,7 @@ func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncr
// BuildRequestURL creates a request suitable for a resource transfer.
// it will return a constructed url based off the base url and query key/value provided.
// cli will build a url for cli transfer request.
func buildRequestURL(baseURL *url.URL, key, value string, cli bool) (string, error) {
func buildRequestURL(baseURL *url.URL, key, value string, cli, shouldRedirect bool) (string, error) {
q := baseURL.Query()
q.Set(key, value)
baseURL.RawQuery = q.Encode()
@ -92,24 +90,26 @@ func buildRequestURL(baseURL *url.URL, key, value string, cli bool) (string, err
return baseURL.String(), nil
}
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url
baseURL.RawQuery = q.Encode() // and this actual baseURL.
if shouldRedirect {
q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url
}
baseURL.RawQuery = q.Encode() // and this actual baseURL.
baseURL.Path = "cdn-cgi/access/cli"
return baseURL.String(), nil
}
// transferRequest downloads the requested resource from the request URL
func transferRequest(requestURL string) ([]byte, string, error) {
func transferRequest(requestURL string, logger logger.Service) ([]byte, string, error) {
client := &http.Client{Timeout: clientTimeout}
const pollAttempts = 10
// we do "long polling" on the endpoint to get the resource.
for i := 0; i < pollAttempts; i++ {
buf, key, err := poll(client, requestURL)
buf, key, err := poll(client, requestURL, logger)
if err != nil {
return nil, "", err
} else if len(buf) > 0 {
if err := putSuccess(client, requestURL); err != nil {
logger.WithError(err).Error("Failed to update resource success")
logger.Errorf("Failed to update resource success: %s", err)
}
return buf, key, nil
}
@ -118,7 +118,7 @@ func transferRequest(requestURL string) ([]byte, string, error) {
}
// poll the endpoint for the request resource, waiting for the user interaction
func poll(client *http.Client, requestURL string) ([]byte, string, error) {
func poll(client *http.Client, requestURL string, logger logger.Service) ([]byte, string, error) {
resp, err := client.Get(requestURL)
if err != nil {
return nil, "", err

View File

@ -1,34 +1,38 @@
package tunnel
import (
"bufio"
"context"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"reflect"
"runtime"
"runtime/trace"
"strings"
"sync"
"time"
"github.com/cloudflare/cloudflared/awsuploader"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/dbconnect"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/socks"
"github.com/cloudflare/cloudflared/sshlog"
"github.com/cloudflare/cloudflared/sshserver"
"github.com/cloudflare/cloudflared/supervisor"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/websocket"
"github.com/coreos/go-systemd/daemon"
@ -36,6 +40,7 @@ import (
"github.com/getsentry/raven-go"
"github.com/gliderlabs/ssh"
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
@ -76,7 +81,19 @@ const (
// hostKeyPath is the path of the dir to save SSH host keys too
hostKeyPath = "host-key-path"
noIntentMsg = "The --intent argument is required. Cloudflared looks up an Intent to determine what configuration to use (i.e. which tunnels to start). If you don't have any Intents yet, you can use a placeholder Intent Label for now. Then, when you make an Intent with that label, cloudflared will get notified and open the tunnels you specified in that Intent."
//sshServerFlag enables cloudflared ssh proxy server
sshServerFlag = "ssh-server"
// socks5Flag is to enable the socks server to deframe
socks5Flag = "socks5"
// bastionFlag is to enable bastion, or jump host, operation
bastionFlag = "bastion"
logDirectoryFlag = "log-directory"
debugLevelWarning = "At debug level, request URL, method, protocol, content legnth and header will be logged. " +
"Response status, content length and header will also be logged in debug level."
)
var (
@ -93,7 +110,7 @@ func Commands() []*cli.Command {
cmds := []*cli.Command{
{
Name: "login",
Action: login,
Action: cliutil.ErrorHandler(login),
Usage: "Generate a configuration file with your login details",
ArgsUsage: " ",
Flags: []cli.Flag{
@ -106,7 +123,7 @@ func Commands() []*cli.Command {
},
{
Name: "proxy-dns",
Action: tunneldns.Run,
Action: cliutil.ErrorHandler(tunneldns.Run),
Usage: "Run a DNS over HTTPS proxy server.",
Flags: []cli.Flag{
&cli.StringFlag{
@ -133,6 +150,12 @@ func Commands() []*cli.Command {
Value: cli.NewStringSlice("https://1.1.1.1/dns-query", "https://1.0.0.1/dns-query"),
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
},
&cli.StringSliceFlag{
Name: "bootstrap",
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
},
},
ArgsUsage: " ", // can't be the empty string or we get the default output
Hidden: false,
@ -146,14 +169,18 @@ func Commands() []*cli.Command {
c.Hidden = false
subcommands = append(subcommands, &c)
}
subcommands = append(subcommands, buildCreateCommand())
subcommands = append(subcommands, buildListCommand())
subcommands = append(subcommands, buildDeleteCommand())
subcommands = append(subcommands, buildRunCommand())
cmds = append(cmds, &cli.Command{
Name: "tunnel",
Action: tunnel,
Action: cliutil.ErrorHandler(tunnel),
Before: Before,
Category: "Tunnel",
Usage: "Make a locally-running web service accessible over the internet using Argo Tunnel.",
ArgsUsage: "[origin-url]",
ArgsUsage: " ",
Description: `Argo Tunnel asks you to specify a hostname on a Cloudflare-powered
domain you control and a local address. Traffic from that hostname is routed
(optionally via a Cloudflare Load Balancer) to this machine and appears on the
@ -180,14 +207,43 @@ func Commands() []*cli.Command {
}
func tunnel(c *cli.Context) error {
return StartServer(c, version, shutdownC, graceShutdownC)
return StartServer(c, version, shutdownC, graceShutdownC, nil)
}
func Init(v string, s, g chan struct{}) {
version, shutdownC, graceShutdownC = v, s, g
}
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}) error {
func createLogger(c *cli.Context, isTransport bool) (logger.Service, error) {
loggerOpts := []logger.Option{}
logPath := c.String("logfile")
if logPath == "" {
logPath = c.String(logDirectoryFlag)
}
if logPath != "" {
loggerOpts = append(loggerOpts, logger.DefaultFile(logPath))
}
logLevel := c.String("loglevel")
if isTransport {
logLevel = c.String("transport-loglevel")
if logLevel == "" {
logLevel = "fatal"
}
}
loggerOpts = append(loggerOpts, logger.LogLevelString(logLevel))
return logger.New(loggerOpts...)
}
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}, namedTunnel *origin.NamedTunnelConfig) error {
logger, err := createLogger(c, false)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
_ = raven.SetDSN(sentryDSN)
var wg sync.WaitGroup
listeners := gracenet.Net{}
@ -196,64 +252,45 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
dnsReadySignal := make(chan struct{})
if c.String("config") == "" {
logger.Warnf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
}
if err := configMainLogger(c); err != nil {
return errors.Wrap(err, "Error configuring logger")
}
transportLogger, err := configTransportLogger(c)
if err != nil {
return errors.Wrap(err, "Error configuring transport logger")
logger.Infof("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
}
if c.IsSet("trace-output") {
tmpTraceFile, err := ioutil.TempFile("", "trace")
if err != nil {
logger.WithError(err).Error("Failed to create new temporary file to save trace output")
logger.Errorf("Failed to create new temporary file to save trace output: %s", err)
}
defer func() {
if err := tmpTraceFile.Close(); err != nil {
logger.WithError(err).Errorf("Failed to close trace output file %s", tmpTraceFile.Name())
logger.Errorf("Failed to close trace output file %s with error: %s", tmpTraceFile.Name(), err)
}
if err := os.Rename(tmpTraceFile.Name(), c.String("trace-output")); err != nil {
logger.WithError(err).Errorf("Failed to rename temporary trace output file %s to %s", tmpTraceFile.Name(), c.String("trace-output"))
logger.Errorf("Failed to rename temporary trace output file %s to %s with error: %s", tmpTraceFile.Name(), c.String("trace-output"), err)
} else {
err := os.Remove(tmpTraceFile.Name())
if err != nil {
logger.WithError(err).Errorf("Failed to remove the temporary trace file %s", tmpTraceFile.Name())
logger.Errorf("Failed to remove the temporary trace file %s with error: %s", tmpTraceFile.Name(), err)
}
}
}()
if err := trace.Start(tmpTraceFile); err != nil {
logger.WithError(err).Error("Failed to start trace")
logger.Errorf("Failed to start trace: %s", err)
return errors.Wrap(err, "Error starting tracing")
}
defer trace.Stop()
}
if c.String("logfile") != "" {
if err := initLogFile(c, logger, transportLogger); err != nil {
logger.Error(err)
}
}
if err := handleDeprecatedOptions(c); err != nil {
return err
}
buildInfo := buildinfo.GetBuildInfo(version)
buildInfo.Log(logger)
logClientOptions(c)
logClientOptions(c, logger)
if c.IsSet("proxy-dns") {
wg.Add(1)
go func() {
defer wg.Done()
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC)
errC <- runDNSProxyServer(c, dnsReadySignal, shutdownC, logger)
}()
} else {
close(dnsReadySignal)
@ -264,7 +301,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
metricsListener, err := listeners.Listen("tcp", c.String("metrics"))
if err != nil {
logger.WithError(err).Error("Error opening metrics server listener")
logger.Errorf("Error opening metrics server listener: %s", err)
return errors.Wrap(err, "Error opening metrics server listener")
}
defer metricsListener.Close()
@ -276,12 +313,12 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
go notifySystemd(connectedSignal)
if c.IsSet("pidfile") {
go writePidFile(connectedSignal, c.String("pidfile"))
go writePidFile(connectedSignal, c.String("pidfile"), logger)
}
cloudflaredID, err := uuid.NewRandom()
if err != nil {
logger.WithError(err).Error("Cannot generate cloudflared ID")
logger.Errorf("Cannot generate cloudflared ID: %s", err)
return err
}
@ -291,17 +328,13 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
cancel()
}()
if c.IsSet("use-declarative-tunnels") {
return startDeclarativeTunnel(ctx, c, cloudflaredID, buildInfo, &listeners)
}
// update needs to be after DNS proxy is up to resolve equinox server address
if updater.IsAutoupdateEnabled(c) {
if updater.IsAutoupdateEnabled(c, logger) {
logger.Infof("Autoupdate frequency is set to %v", c.Duration("autoupdate-freq"))
wg.Add(1)
go func() {
defer wg.Done()
autoupdater := updater.NewAutoUpdater(c.Duration("autoupdate-freq"), &listeners)
autoupdater := updater.NewAutoUpdater(c.Duration("autoupdate-freq"), &listeners, logger)
errC <- autoupdater.Run(ctx)
}()
}
@ -310,14 +343,14 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
if dnsProxyStandAlone(c) {
connectedSignal.Notify()
// no grace period, handle SIGINT/SIGTERM immediately
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0)
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, 0, logger)
}
if c.IsSet("hello-world") {
logger.Infof("hello-world set")
helloListener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil {
logger.WithError(err).Error("Cannot start Hello World Server")
logger.Errorf("Cannot start Hello World Server: %s", err)
return errors.Wrap(err, "Cannot start Hello World Server")
}
defer helloListener.Close()
@ -329,12 +362,11 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
c.Set("url", "https://"+helloListener.Addr().String())
}
if c.IsSet("ssh-server") {
if c.IsSet(sshServerFlag) {
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
msg := fmt.Sprintf("--ssh-server is not supported on %s", runtime.GOOS)
logger.Error(msg)
return errors.New(msg)
}
logger.Infof("ssh-server set")
@ -345,13 +377,13 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
c.String(accessKeyIDFlag), c.String(secretIDFlag), c.String(sessionTokenIDFlag), c.String(s3URLFlag))
if err != nil {
msg := "Cannot create uploader for SSH Server"
logger.WithError(err).Error(msg)
logger.Errorf("%s: %s", msg, err)
return errors.Wrap(err, msg)
}
if err := os.MkdirAll(sshLogFileDirectory, 0700); err != nil {
msg := fmt.Sprintf("Cannot create SSH log file directory %s", sshLogFileDirectory)
logger.WithError(err).Errorf(msg)
logger.Errorf("%s: %s", msg, err)
return errors.Wrap(err, msg)
}
@ -365,14 +397,14 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
server, err := sshserver.New(logManager, logger, version, localServerAddress, c.String("hostname"), c.Path(hostKeyPath), shutdownC, c.Duration(sshIdleTimeoutFlag), c.Duration(sshMaxTimeoutFlag))
if err != nil {
msg := "Cannot create new SSH Server"
logger.WithError(err).Error(msg)
logger.Errorf("%s: %s", msg, err)
return errors.Wrap(err, msg)
}
wg.Add(1)
go func() {
defer wg.Done()
if err = server.Start(); err != nil && err != ssh.ErrServerClosed {
logger.WithError(err).Error("SSH server error")
logger.Errorf("SSH server error: %s", err)
// TODO: remove when declarative tunnels are implemented.
close(shutdownC)
}
@ -380,46 +412,92 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
c.Set("url", "ssh://"+localServerAddress)
}
if host := hostnameFromURI(c.String("url")); host != "" {
url := c.String("url")
hostname := c.String("hostname")
if url == hostname && url != "" && hostname != "" {
errText := "hostname and url shouldn't match. See --help for more information"
logger.Error(errText)
return fmt.Errorf(errText)
}
if staticHost := hostnameFromURI(c.String("url")); isProxyDestinationConfigured(staticHost, c) {
listener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
logger.WithError(err).Error("Cannot start Websocket Proxy Server")
logger.Errorf("Cannot start Websocket Proxy Server: %s", err)
return errors.Wrap(err, "Cannot start Websocket Proxy Server")
}
wg.Add(1)
go func() {
defer wg.Done()
errC <- websocket.StartProxyServer(logger, listener, host, shutdownC)
streamHandler := websocket.DefaultStreamHandler
if c.IsSet(socks5Flag) {
logger.Info("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)
socksServer.Serve(wsConn)
}
} else if c.IsSet(sshServerFlag) {
streamHandler = func(wsConn *websocket.Conn, remoteConn net.Conn, requestHeaders http.Header) {
if finalDestination := requestHeaders.Get(h2mux.CFJumpDestinationHeader); finalDestination != "" {
token := requestHeaders.Get(h2mux.CFAccessTokenHeader)
if err := websocket.SendSSHPreamble(remoteConn, finalDestination, token); err != nil {
logger.Errorf("Failed to send SSH preamble: %s", err)
return
}
}
websocket.DefaultStreamHandler(wsConn, remoteConn, requestHeaders)
}
}
errC <- websocket.StartProxyServer(logger, listener, staticHost, shutdownC, streamHandler)
}()
c.Set("url", "http://"+listener.Addr().String())
}
transportLogger, err := createLogger(c, true)
if err != nil {
return errors.Wrap(err, "error setting up transport logger")
}
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, logger, transportLogger)
if err != nil {
return err
}
reconnectCh := make(chan origin.ReconnectSignal, 1)
if c.IsSet("stdin-control") {
logger.Info("Enabling control through stdin")
go stdinControl(reconnectCh, logger)
}
wg.Add(1)
go func() {
defer wg.Done()
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID)
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID, reconnectCh, namedTunnel)
}()
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"))
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"), logger)
}
func Before(c *cli.Context) error {
logger, err := createLogger(c, false)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
if c.String("config") == "" {
logger.Debugf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
}
inputSource, err := config.FindInputSourceContext(c)
if err != nil {
logger.WithError(err).Errorf("Cannot load configuration from %s", c.String("config"))
logger.Errorf("Cannot load configuration from %s: %s", c.String("config"), err)
return err
} else if inputSource != nil {
err := altsrc.ApplyInputSourceValues(c, inputSource, c.App.Flags)
if err != nil {
logger.WithError(err).Errorf("Cannot apply configuration from %s", c.String("config"))
logger.Errorf("Cannot apply configuration from %s: %s", c.String("config"), err)
return err
}
logger.Debugf("Applied configuration from %s", c.String("config"))
@ -427,138 +505,27 @@ func Before(c *cli.Context) error {
return nil
}
func startDeclarativeTunnel(ctx context.Context,
c *cli.Context,
cloudflaredID uuid.UUID,
buildInfo *buildinfo.BuildInfo,
listeners *gracenet.Net,
) error {
reverseProxyOrigin, err := defaultOriginConfig(c)
if err != nil {
logger.WithError(err)
return err
}
reverseProxyConfig, err := pogs.NewReverseProxyConfig(
c.String("hostname"),
reverseProxyOrigin,
c.Uint64("retries"),
c.Duration("proxy-connection-timeout"),
c.Uint64("compression-quality"),
)
if err != nil {
logger.WithError(err).Error("Cannot initialize default client config because reverse proxy config is invalid")
return err
}
defaultClientConfig := &pogs.ClientConfig{
Version: pogs.InitVersion(),
SupervisorConfig: &pogs.SupervisorConfig{
AutoUpdateFrequency: c.Duration("autoupdate-freq"),
MetricsUpdateFrequency: c.Duration("metrics-update-freq"),
GracePeriod: c.Duration("grace-period"),
},
EdgeConnectionConfig: &pogs.EdgeConnectionConfig{
NumHAConnections: uint8(c.Int("ha-connections")),
HeartbeatInterval: c.Duration("heartbeat-interval"),
Timeout: c.Duration("dial-edge-timeout"),
MaxFailedHeartbeats: c.Uint64("heartbeat-count"),
},
DoHProxyConfigs: []*pogs.DoHProxyConfig{},
ReverseProxyConfigs: []*pogs.ReverseProxyConfig{reverseProxyConfig},
}
autoupdater := updater.NewAutoUpdater(defaultClientConfig.SupervisorConfig.AutoUpdateFrequency, listeners)
originCert, err := getOriginCert(c)
if err != nil {
logger.WithError(err).Error("error getting origin cert")
return err
}
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
if err != nil {
logger.WithError(err).Error("unable to create TLS config to connect with edge")
return err
}
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
if err != nil {
logger.WithError(err).Error("unable to parse tag")
return err
}
intentLabel := c.String("intent")
if intentLabel == "" {
logger.Error("--intent was empty")
return fmt.Errorf(noIntentMsg)
}
cloudflaredConfig := &connection.CloudflaredConfig{
BuildInfo: buildInfo,
CloudflaredID: cloudflaredID,
IntentLabel: intentLabel,
Tags: tags,
}
serviceDiscoverer, err := serviceDiscoverer(c, logger)
if err != nil {
logger.WithError(err).Error("unable to create service discoverer")
return err
}
supervisor, err := supervisor.NewSupervisor(defaultClientConfig, originCert, toEdgeTLSConfig,
serviceDiscoverer, cloudflaredConfig, autoupdater, updater.SupportAutoUpdate(), logger)
if err != nil {
logger.WithError(err).Error("unable to create Supervisor")
return err
}
return supervisor.Run(ctx)
}
func defaultOriginConfig(c *cli.Context) (pogs.OriginConfig, error) {
if c.IsSet("hello-world") {
return &pogs.HelloWorldOriginConfig{}, nil
}
originConfig := &pogs.HTTPOriginConfig{
TCPKeepAlive: c.Duration("proxy-tcp-keepalive"),
DialDualStack: !c.Bool("proxy-no-happy-eyeballs"),
TLSHandshakeTimeout: c.Duration("proxy-tls-timeout"),
TLSVerify: !c.Bool("no-tls-verify"),
OriginCAPool: c.String("origin-ca-pool"),
OriginServerName: c.String("origin-server-name"),
MaxIdleConnections: c.Uint64("proxy-keepalive-connections"),
IdleConnectionTimeout: c.Duration("proxy-keepalive-timeout"),
ProxyConnectionTimeout: c.Duration("proxy-connection-timeout"),
ExpectContinueTimeout: c.Duration("proxy-expect-continue-timeout"),
ChunkedEncoding: c.Bool("no-chunked-encoding"),
}
if c.IsSet("unix-socket") {
unixSocket, err := config.ValidateUnixSocket(c)
if err != nil {
return nil, errors.Wrap(err, "error validating --unix-socket")
}
originConfig.URLString = unixSocket
}
originAddr, err := config.ValidateUrl(c)
if err != nil {
return nil, errors.Wrap(err, "error validating origin URL")
}
originConfig.URLString = originAddr
return originConfig, nil
// isProxyDestinationConfigured returns true if there is a static host set or if bastion mode is set.
func isProxyDestinationConfigured(staticHost string, c *cli.Context) bool {
return staticHost != "" || c.IsSet(bastionFlag)
}
func waitToShutdown(wg *sync.WaitGroup,
errC chan error,
shutdownC, graceShutdownC chan struct{},
gracePeriod time.Duration,
logger logger.Service,
) error {
var err error
if gracePeriod > 0 {
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceShutdownC, gracePeriod)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceShutdownC, gracePeriod, logger)
} else {
err = waitForSignal(errC, shutdownC)
err = waitForSignal(errC, shutdownC, logger)
close(graceShutdownC)
}
if err != nil {
logger.WithError(err).Error("Quitting due to error")
logger.Errorf("Quitting due to error: %s", err)
} else {
logger.Info("Quitting...")
}
@ -576,11 +543,17 @@ func notifySystemd(waitForSignal *signal.Signal) {
daemon.SdNotify(false, "READY=1")
}
func writePidFile(waitForSignal *signal.Signal, pidFile string) {
func writePidFile(waitForSignal *signal.Signal, pidFile string, logger logger.Service) {
<-waitForSignal.Wait()
file, err := os.Create(pidFile)
expandedPath, err := homedir.Expand(pidFile)
if err != nil {
logger.WithError(err).Errorf("Unable to write pid to %s", pidFile)
logger.Errorf("Unable to expand %s, try to use absolute path in --pidfile: %s", pidFile, err)
return
}
file, err := os.Create(expandedPath)
if err != nil {
logger.Errorf("Unable to write pid to %s: %s", expandedPath, err)
return
}
defer file.Close()
fmt.Fprintf(file, "%d", os.Getpid())
@ -596,6 +569,10 @@ func hostnameFromURI(uri string) string {
return addPortIfMissing(u, 22)
case "rdp":
return addPortIfMissing(u, 3389)
case "smb":
return addPortIfMissing(u, 445)
case "tcp":
return addPortIfMissing(u, 7864) // just a random port since there isn't a default in this case
}
return ""
}
@ -623,13 +600,13 @@ func dbConnectCmd() *cli.Command {
}
// Override action to setup the Proxy, then if successful, start the tunnel daemon.
cmd.Action = func(c *cli.Context) error {
cmd.Action = cliutil.ErrorHandler(func(c *cli.Context) error {
err := dbconnect.CmdAction(c)
if err == nil {
err = tunnel(c)
}
return err
}
})
return cmd
}
@ -690,7 +667,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "edge",
Usage: "Address of the Cloudflare tunnel server.",
Usage: "Address of the Cloudflare tunnel server. Only works in Cloudflare's internal testing environment.",
EnvVars: []string{"TUNNEL_EDGE"},
Hidden: true,
}),
@ -780,6 +757,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_API_CA_KEY"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "api-url",
Usage: "Base URL for Cloudflare API v4",
EnvVars: []string{"TUNNEL_API_URL"},
Value: "https://api.cloudflare.com/client/v4",
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "metrics",
Value: "localhost:",
@ -815,15 +799,15 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
altsrc.NewStringFlag(&cli.StringFlag{
Name: "loglevel",
Value: "info",
Usage: "Application logging level {panic, fatal, error, warn, info, debug}. " + debugLevelWarning,
Usage: "Application logging level {fatal, error, info, debug}. " + debugLevelWarning,
EnvVars: []string{"TUNNEL_LOGLEVEL"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "transport-loglevel",
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
Value: "warn",
Usage: "Transport logging level(previously called protocol logging level) {panic, fatal, error, warn, info, debug}",
Value: "fatal",
Usage: "Transport logging level(previously called protocol logging level) {fatal, error, info, debug}",
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"},
Hidden: shouldHide,
}),
@ -842,7 +826,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "ssh-server",
Name: sshServerFlag,
Value: false,
Usage: "Run an SSH Server",
EnvVars: []string{"TUNNEL_SSH_SERVER"},
@ -860,6 +844,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_LOGFILE"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: logDirectoryFlag,
Usage: "Save application log to this directory for reporting issues.",
EnvVars: []string{"TUNNEL_LOGDIRECTORY"},
Hidden: shouldHide,
}),
altsrc.NewIntFlag(&cli.IntFlag{
Name: "ha-connections",
Value: 4,
@ -939,6 +929,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_DNS_UPSTREAM"},
Hidden: shouldHide,
}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "proxy-dns-bootstrap",
Usage: "bootstrap endpoint URL, you can specify multiple endpoints for redundancy.",
Value: cli.NewStringSlice("https://162.159.36.1/dns-query", "https://162.159.46.1/dns-query", "https://[2606:4700:4700::1111]/dns-query", "https://[2606:4700:4700::1001]/dns-query"),
EnvVars: []string{"TUNNEL_DNS_BOOTSTRAP"},
Hidden: shouldHide,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "grace-period",
Usage: "Duration to accept new requests after cloudflared receives first SIGINT/SIGTERM. A second SIGINT/SIGTERM will force cloudflared to shutdown immediately.",
@ -966,21 +963,17 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "use-declarative-tunnels",
Usage: "Test establishing connections with declarative tunnel methods.",
EnvVars: []string{"TUNNEL_USE_DECLARATIVE"},
Hidden: true,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "intent",
Usage: "The label of an Intent from which `cloudflared` should gets its tunnels from. Intents can be created in the Origin Registry UI.",
EnvVars: []string{"TUNNEL_INTENT"},
Name: "use-reconnect-token",
Usage: "Test reestablishing connections with the new 'reconnect token' flow.",
Value: true,
EnvVars: []string{"TUNNEL_USE_RECONNECT_TOKEN"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "use-reconnect-token",
Usage: "Test reestablishing connections with the new 'reconnect token' flow.",
EnvVars: []string{"TUNNEL_USE_RECONNECT_TOKEN"},
Name: "use-quick-reconnects",
Usage: "Test reestablishing connections with the new 'connection digest' flow.",
Value: true,
EnvVars: []string{"TUNNEL_USE_QUICK_RECONNECTS"},
Hidden: true,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
@ -1051,5 +1044,59 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"HOST_KEY_PATH"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "stdin-control",
Usage: "Control the process using commands sent through stdin",
EnvVars: []string{"STDIN-CONTROL"},
Hidden: true,
Value: false,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: socks5Flag,
Usage: "specify if this tunnel is running as a SOCK5 Server",
EnvVars: []string{"TUNNEL_SOCKS"},
Value: false,
Hidden: shouldHide,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: bastionFlag,
Value: false,
Usage: "Runs as jump host",
EnvVars: []string{"TUNNEL_BASTION"},
Hidden: shouldHide,
}),
}
}
func stdinControl(reconnectCh chan origin.ReconnectSignal, logger logger.Service) {
for {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
command := scanner.Text()
parts := strings.SplitN(command, " ", 2)
switch parts[0] {
case "":
break
case "reconnect":
var reconnect origin.ReconnectSignal
if len(parts) > 1 {
var err error
if reconnect.Delay, err = time.ParseDuration(parts[1]); err != nil {
logger.Error(err.Error())
continue
}
}
logger.Infof("Sending reconnect signal %+v", reconnect)
reconnectCh <- reconnect
default:
logger.Infof("Unknown command: %s", command)
fallthrough
case "help":
logger.Info(`Supported command:
reconnect [delay]
- restarts one randomly chosen connection with optional delay before reconnect`)
}
}
}
}

View File

@ -14,7 +14,7 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tlsconfig"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
@ -23,7 +23,6 @@ import (
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh/terminal"
"gopkg.in/urfave/cli.v2"
)
@ -47,25 +46,16 @@ func findDefaultOriginCertPath() string {
return ""
}
func generateRandomClientID(logger *logrus.Logger) (string, error) {
func generateRandomClientID(logger logger.Service) (string, error) {
u, err := uuid.NewRandom()
if err != nil {
logger.WithError(err).Error("couldn't create UUID for client ID")
logger.Errorf("couldn't create UUID for client ID %s", err)
return "", err
}
return u.String(), nil
}
func handleDeprecatedOptions(c *cli.Context) error {
// Fail if the user provided an old authentication method
if c.IsSet("api-key") || c.IsSet("api-email") || c.IsSet("api-ca-key") {
logger.Error("You don't need to give us your api-key anymore. Please use the new login method. Just run cloudflared login")
return fmt.Errorf("Client provided deprecated options")
}
return nil
}
func logClientOptions(c *cli.Context) {
func logClientOptions(c *cli.Context, logger logger.Service) {
flags := make(map[string]interface{})
for _, flag := range c.LocalFlagNames() {
flags[flag] = c.Generic(flag)
@ -79,7 +69,7 @@ func logClientOptions(c *cli.Context) {
}
if len(flags) > 0 {
logger.WithFields(flags).Info("Flags")
logger.Infof("Environment variables %v", flags)
}
envs := make(map[string]string)
@ -102,27 +92,29 @@ func dnsProxyStandAlone(c *cli.Context) bool {
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world"))
}
func getOriginCert(c *cli.Context) ([]byte, error) {
if c.String("origincert") == "" {
logger.Warnf("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
func findOriginCert(c *cli.Context, logger logger.Service) (string, error) {
originCertPath := c.String("origincert")
if originCertPath == "" {
logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
if isRunningFromTerminal() {
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
return nil, fmt.Errorf("Client didn't specify origincert path when running from terminal")
return "", fmt.Errorf("Client didn't specify origincert path when running from terminal")
} else {
logger.Errorf("You need to specify the origin certificate path by specifying the origincert option in the configuration file, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", serviceUrl)
return nil, fmt.Errorf("Client didn't specify origincert path")
return "", fmt.Errorf("Client didn't specify origincert path")
}
}
// Check that the user has acquired a certificate using the login command
originCertPath, err := homedir.Expand(c.String("origincert"))
var err error
originCertPath, err = homedir.Expand(originCertPath)
if err != nil {
logger.WithError(err).Errorf("Cannot resolve path %s", c.String("origincert"))
return nil, fmt.Errorf("Cannot resolve path %s", c.String("origincert"))
logger.Errorf("Cannot resolve path %s: %s", originCertPath, err)
return "", fmt.Errorf("Cannot resolve path %s", originCertPath)
}
// Check that the user has acquired a certificate using the login command
ok, err := config.FileExists(originCertPath)
if err != nil {
logger.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
return nil, fmt.Errorf("Cannot check if origin cert exists at path %s", c.String("origincert"))
logger.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
return "", fmt.Errorf("Cannot check if origin cert exists at path %s", originCertPath)
}
if !ok {
logger.Errorf(`Cannot find a valid certificate for your origin at the path:
@ -134,26 +126,42 @@ If you don't have a certificate signed by Cloudflare, run the command:
%s login
`, originCertPath, os.Args[0])
return nil, fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
return "", fmt.Errorf("Cannot find a valid certificate at the path %s", originCertPath)
}
return originCertPath, nil
}
func readOriginCert(originCertPath string, logger logger.Service) ([]byte, error) {
logger.Debugf("Reading origin cert from %s", originCertPath)
// Easier to send the certificate as []byte via RPC than decoding it at this point
originCert, err := ioutil.ReadFile(originCertPath)
if err != nil {
logger.WithError(err).Errorf("Cannot read %s to load origin certificate", originCertPath)
logger.Errorf("Cannot read %s to load origin certificate: %s", originCertPath, err)
return nil, fmt.Errorf("Cannot read %s to load origin certificate", originCertPath)
}
return originCert, nil
}
func getOriginCert(c *cli.Context, logger logger.Service) ([]byte, error) {
if originCertPath, err := findOriginCert(c, logger); err != nil {
return nil, err
} else {
return readOriginCert(originCertPath, logger)
}
}
func prepareTunnelConfig(
c *cli.Context,
buildInfo *buildinfo.BuildInfo,
version string, logger,
transportLogger *logrus.Logger,
version string,
logger logger.Service,
transportLogger logger.Service,
) (*origin.TunnelConfig, error) {
hostname, err := validation.ValidateHostname(c.String("hostname"))
if err != nil {
logger.WithError(err).Error("Invalid hostname")
logger.Errorf("Invalid hostname: %s", err)
return nil, errors.Wrap(err, "Invalid hostname")
}
isFreeTunnel := hostname == ""
@ -167,7 +175,7 @@ func prepareTunnelConfig(
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
if err != nil {
logger.WithError(err).Error("Tag parse failure")
logger.Errorf("Tag parse failure: %s", err)
return nil, errors.Wrap(err, "Tag parse failure")
}
@ -175,13 +183,13 @@ func prepareTunnelConfig(
originURL, err := config.ValidateUrl(c)
if err != nil {
logger.WithError(err).Error("Error validating origin URL")
logger.Errorf("Error validating origin URL: %s", err)
return nil, errors.Wrap(err, "Error validating origin URL")
}
var originCert []byte
if !isFreeTunnel {
originCert, err = getOriginCert(c)
originCert, err = getOriginCert(c, logger)
if err != nil {
return nil, errors.Wrap(err, "Error getting origin cert")
}
@ -189,7 +197,7 @@ func prepareTunnelConfig(
originCertPool, err := tlsconfig.LoadOriginCA(c, logger)
if err != nil {
logger.WithError(err).Error("Error loading cert pool")
logger.Errorf("Error loading cert pool: %s", err)
return nil, errors.Wrap(err, "Error loading cert pool")
}
@ -216,7 +224,7 @@ func prepareTunnelConfig(
if c.IsSet("unix-socket") {
unixSocket, err := config.ValidateUnixSocket(c)
if err != nil {
logger.WithError(err).Error("Error validating --unix-socket")
logger.Errorf("Error validating --unix-socket: %s", err)
return nil, errors.Wrap(err, "Error validating --unix-socket")
}
@ -233,16 +241,16 @@ func prepareTunnelConfig(
if !c.IsSet("hello-world") && c.IsSet("origin-server-name") {
httpTransport.TLSClientConfig.ServerName = c.String("origin-server-name")
}
err = validation.ValidateHTTPService(originURL, hostname, httpTransport)
if err != nil {
logger.WithError(err).Error("unable to connect to the origin")
return nil, errors.Wrap(err, "unable to connect to the origin")
// If tunnel running in bastion mode, a connection to origin will not exist until initiated by the client.
if !c.IsSet(bastionFlag) {
if err = validation.ValidateHTTPService(originURL, hostname, httpTransport); err != nil {
logger.Errorf("unable to connect to the origin: %s", err)
}
}
toEdgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c)
if err != nil {
logger.WithError(err).Error("unable to create TLS config to connect with edge")
logger.Errorf("unable to create TLS config to connect with edge: %s", err)
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
}
@ -263,6 +271,7 @@ func prepareTunnelConfig(
IsFreeTunnel: isFreeTunnel,
LBPool: c.String("lb-pool"),
Logger: logger,
TransportLogger: transportLogger,
MaxHeartbeats: c.Uint64("heartbeat-count"),
Metrics: tunnelMetrics,
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
@ -274,21 +283,12 @@ func prepareTunnelConfig(
RunFromTerminal: isRunningFromTerminal(),
Tags: tags,
TlsConfig: toEdgeTLSConfig,
TransportLogger: transportLogger,
UseDeclarativeTunnel: c.Bool("use-declarative-tunnels"),
UseReconnectToken: c.Bool("use-reconnect-token"),
UseQuickReconnects: c.Bool("use-quick-reconnects"),
}, nil
}
func serviceDiscoverer(c *cli.Context, logger *logrus.Logger) (*edgediscovery.Edge, error) {
// If --edge is specfied, resolve edge server addresses
if len(c.StringSlice("edge")) > 0 {
return edgediscovery.StaticEdge(logger, c.StringSlice("edge"))
}
// Otherwise lookup edge server addresses through service discovery
return edgediscovery.ResolveEdge(logger)
}
func isRunningFromTerminal() bool {
return terminal.IsTerminal(int(os.Stdout.Fd()))
}

View File

@ -1,75 +0,0 @@
package tunnel
import (
"fmt"
"os"
"github.com/cloudflare/cloudflared/log"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
"gopkg.in/urfave/cli.v2"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
)
const debugLevelWarning = "At debug level, request URL, method, protocol, content legnth and header will be logged. " +
"Response status, content length and header will also be logged in debug level."
var logger = log.CreateLogger()
func configMainLogger(c *cli.Context) error {
logLevel, err := logrus.ParseLevel(c.String("loglevel"))
if err != nil {
logger.WithError(err).Error("Unknown logging level specified")
return errors.Wrap(err, "Unknown logging level specified")
}
logger.SetLevel(logLevel)
if logLevel == logrus.DebugLevel {
logger.Warn(debugLevelWarning)
}
return nil
}
func configTransportLogger(c *cli.Context) (*logrus.Logger, error) {
transportLogLevel, err := logrus.ParseLevel(c.String("transport-loglevel"))
if err != nil {
logger.WithError(err).Fatal("Unknown transport logging level specified")
return nil, errors.Wrap(err, "Unknown transport logging level specified")
}
transportLogger := logrus.New()
transportLogger.Level = transportLogLevel
return transportLogger, nil
}
func initLogFile(c *cli.Context, loggers ...*logrus.Logger) error {
filePath, err := homedir.Expand(c.String("logfile"))
if err != nil {
return errors.Wrap(err, "Cannot resolve logfile path")
}
fileMode := os.O_WRONLY | os.O_APPEND | os.O_CREATE | os.O_TRUNC
// do not truncate log file if the client has been autoupdated
if c.Bool("is-autoupdated") {
fileMode = os.O_WRONLY | os.O_APPEND | os.O_CREATE
}
f, err := os.OpenFile(filePath, fileMode, 0664)
if err != nil {
errors.Wrap(err, fmt.Sprintf("Cannot open file %s", filePath))
}
defer f.Close()
pathMap := lfshook.PathMap{
logrus.DebugLevel: filePath,
logrus.WarnLevel: filePath,
logrus.InfoLevel: filePath,
logrus.ErrorLevel: filePath,
logrus.FatalLevel: filePath,
logrus.PanicLevel: filePath,
}
for _, l := range loggers {
l.Hooks.Add(lfshook.NewHook(pathMap, &logrus.JSONFormatter{}))
}
return nil
}

View File

@ -9,7 +9,9 @@ import (
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/transfer"
"github.com/cloudflare/cloudflared/logger"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
)
@ -19,6 +21,11 @@ const (
)
func login(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
path, ok, err := checkForExistingCert()
if ok {
fmt.Fprintf(os.Stdout, "You have an existing certificate at %s which login would overwrite.\nIf this is intentional, please move or delete that file then run this command again.\n", path)
@ -33,7 +40,7 @@ func login(c *cli.Context) error {
return err
}
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false)
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, true, logger)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write the certificate due to the following error:\n%v\n\nYour browser will download the certificate instead. You will have to manually\ncopy it to the following path:\n\n%s\n", err, path)
return err

View File

@ -1,6 +1,7 @@
package tunnel
import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunneldns"
"gopkg.in/urfave/cli.v2"
@ -8,23 +9,20 @@ import (
"github.com/pkg/errors"
)
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}) error {
func runDNSProxyServer(c *cli.Context, dnsReadySignal, shutdownC chan struct{}, logger logger.Service) error {
port := c.Int("proxy-dns-port")
if port <= 0 || port > 65535 {
logger.Errorf("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
return errors.New("The 'proxy-dns-port' must be a valid port number in <1, 65535> range.")
}
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"))
listener, err := tunneldns.CreateListener(c.String("proxy-dns-address"), uint16(port), c.StringSlice("proxy-dns-upstream"), c.StringSlice("proxy-dns-bootstrap"), logger)
if err != nil {
close(dnsReadySignal)
listener.Stop()
logger.WithError(err).Error("Cannot create the DNS over HTTPS proxy server")
return errors.Wrap(err, "Cannot create the DNS over HTTPS proxy server")
}
err = listener.Start(dnsReadySignal)
if err != nil {
logger.WithError(err).Error("Cannot start the DNS over HTTPS proxy server")
return errors.Wrap(err, "Cannot start the DNS over HTTPS proxy server")
}
<-shutdownC

View File

@ -5,21 +5,25 @@ import (
"os/signal"
"syscall"
"time"
"github.com/cloudflare/cloudflared/logger"
)
// waitForSignal notifies all routines to shutdownC immediately by closing the
// shutdownC when one of the routines in main exits, or when this process receives
// SIGTERM/SIGINT
func waitForSignal(errC chan error, shutdownC chan struct{}) error {
func waitForSignal(errC chan error, shutdownC chan struct{}, logger logger.Service) error {
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(signals)
select {
case err := <-errC:
logger.Infof("terminating due to error: %v", err)
close(shutdownC)
return err
case <-signals:
case s := <-signals:
logger.Infof("terminating due to signal %s", s)
close(shutdownC)
case <-shutdownC:
}
@ -37,6 +41,7 @@ func waitForSignal(errC chan error, shutdownC chan struct{}) error {
func waitForSignalWithGraceShutdown(errC chan error,
shutdownC, graceShutdownC chan struct{},
gracePeriod time.Duration,
logger logger.Service,
) error {
signals := make(chan os.Signal, 10)
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
@ -44,14 +49,16 @@ func waitForSignalWithGraceShutdown(errC chan error,
select {
case err := <-errC:
logger.Infof("Initiating graceful shutdown due to %v ...", err)
close(graceShutdownC)
close(shutdownC)
return err
case <-signals:
case s := <-signals:
logger.Infof("Initiating graceful shutdown due to signal %s ...", s)
close(graceShutdownC)
waitForGracePeriod(signals, errC, shutdownC, gracePeriod)
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
case <-graceShutdownC:
waitForGracePeriod(signals, errC, shutdownC, gracePeriod)
waitForGracePeriod(signals, errC, shutdownC, gracePeriod, logger)
case <-shutdownC:
close(graceShutdownC)
}
@ -63,8 +70,8 @@ func waitForGracePeriod(signals chan os.Signal,
errC chan error,
shutdownC chan struct{},
gracePeriod time.Duration,
logger logger.Service,
) {
logger.Infof("Initiating graceful shutdown...")
// Unregister signal handler early, so the client can send a second SIGTERM/SIGINT
// to force shutdown cloudflared
signal.Stop(signals)

View File

@ -6,6 +6,7 @@ import (
"testing"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/stretchr/testify/assert"
)
@ -27,6 +28,8 @@ func testChannelClosed(t *testing.T, c chan struct{}) {
}
func TestWaitForSignal(t *testing.T) {
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
// Test handling server error
errC := make(chan error)
shutdownC := make(chan struct{})
@ -36,7 +39,7 @@ func TestWaitForSignal(t *testing.T) {
}()
// received error, shutdownC should be closed
err := waitForSignal(errC, shutdownC)
err := waitForSignal(errC, shutdownC, logger)
assert.Equal(t, serverErr, err)
testChannelClosed(t, shutdownC)
@ -56,7 +59,7 @@ func TestWaitForSignal(t *testing.T) {
syscall.Kill(syscall.Getpid(), sig)
}(sig)
err = waitForSignal(errC, shutdownC)
err = waitForSignal(errC, shutdownC, logger)
assert.Equal(t, nil, err)
assert.Equal(t, shutdownErr, <-errC)
testChannelClosed(t, shutdownC)
@ -73,8 +76,10 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
errC <- serverErr
}()
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
// received error, both shutdownC and graceshutdownC should be closed
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
err := waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
assert.Equal(t, serverErr, err)
testChannelClosed(t, shutdownC)
testChannelClosed(t, graceshutdownC)
@ -84,7 +89,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
shutdownC = make(chan struct{})
graceshutdownC = make(chan struct{})
close(shutdownC)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
assert.NoError(t, err)
testChannelClosed(t, shutdownC)
testChannelClosed(t, graceshutdownC)
@ -94,7 +99,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
shutdownC = make(chan struct{})
graceshutdownC = make(chan struct{})
close(graceshutdownC)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
assert.NoError(t, err)
testChannelClosed(t, shutdownC)
testChannelClosed(t, graceshutdownC)
@ -117,7 +122,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
syscall.Kill(syscall.Getpid(), sig)
}(sig)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
assert.Equal(t, nil, err)
assert.Equal(t, graceShutdownErr, <-errC)
testChannelClosed(t, shutdownC)
@ -143,7 +148,7 @@ func TestWaitForSignalWithGraceShutdown(t *testing.T) {
syscall.Kill(syscall.Getpid(), sig)
}(sig)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick)
err = waitForSignalWithGraceShutdown(errC, shutdownC, graceshutdownC, tick, logger)
assert.Equal(t, nil, err)
assert.Equal(t, shutdownErr, <-errC)
testChannelClosed(t, shutdownC)

View File

@ -0,0 +1,344 @@
package tunnel
import (
"crypto/rand"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelstore"
)
var (
showDeletedFlag = &cli.BoolFlag{
Name: "show-deleted",
Aliases: []string{"d"},
Usage: "Include deleted tunnels in the list",
}
outputFormatFlag = &cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
}
forceFlag = &cli.StringFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "By default, if a tunnel is currently being run from a cloudflared, you can't " +
"simultaneously rerun it again from a second cloudflared. The --force flag lets you " +
"overwrite the previous tunnel. If you want to use a single hostname with multiple " +
"tunnels, you can do so with Cloudflare's Load Balancer product.",
}
)
const hideSubcommands = true
func buildCreateCommand() *cli.Command {
return &cli.Command{
Name: "create",
Action: cliutil.ErrorHandler(createTunnel),
Usage: "Create a new tunnel with given name",
ArgsUsage: "TUNNEL-NAME",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag},
}
}
// generateTunnelSecret as an array of 32 bytes using secure random number generator
func generateTunnelSecret() ([]byte, error) {
randomBytes := make([]byte, 32)
_, err := rand.Read(randomBytes)
return randomBytes, err
}
func createTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel create" requires exactly 1 argument, the name of tunnel to create.`)
}
name := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
tunnelSecret, err := generateTunnelSecret()
if err != nil {
return err
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
cert, err := getOriginCertFromContext(originCertPath, logger)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
tunnel, err := client.CreateTunnel(name, tunnelSecret)
if err != nil {
return errors.Wrap(err, "Error creating a new tunnel")
}
if writeFileErr := writeTunnelCredentials(tunnel.ID, cert.AccountID, originCertPath, tunnelSecret, logger); err != nil {
var errorLines []string
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil {
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
} else {
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the tunnelfile"))
}
errorMsg := strings.Join(errorLines, "\n")
return errors.New(errorMsg)
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
return renderOutput(outputFormat, &tunnel)
}
logger.Infof("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
return nil
}
func tunnelFilePath(tunnelID, originCertPath string) (string, error) {
fileName := fmt.Sprintf("%v.json", tunnelID)
return filepath.Clean(fmt.Sprintf("%v/../%v", originCertPath, fileName)), nil
}
func writeTunnelCredentials(tunnelID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
filePath, err := tunnelFilePath(tunnelID, originCertPath)
if err != nil {
return err
}
body, err := json.Marshal(pogs.TunnelAuth{
AccountTag: accountID,
TunnelSecret: tunnelSecret,
})
if err != nil {
return errors.Wrap(err, "Unable to marshal tunnel credentials to JSON")
}
logger.Infof("Writing tunnel credentials to %v. cloudflared chose this file based on where your origin certificate was found.", filePath)
logger.Infof("Keep this file secret. To revoke these credentials, delete the tunnel.")
return ioutil.WriteFile(filePath, body, 400)
}
func readTunnelCredentials(tunnelID, originCertPath string) (*pogs.TunnelAuth, error) {
filePath, err := tunnelFilePath(tunnelID, originCertPath)
if err != nil {
return nil, err
}
body, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
}
auth := pogs.TunnelAuth{}
err = json.Unmarshal(body, &auth)
return &auth, errors.Wrap(err, "couldn't parse tunnel credentials from JSON")
}
func buildListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Action: cliutil.ErrorHandler(listTunnels),
Usage: "List existing tunnels",
ArgsUsage: " ",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag},
}
}
func listTunnels(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
cert, err := getOriginCertFromContext(originCertPath, logger)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
allTunnels, err := client.ListTunnels()
if err != nil {
return errors.Wrap(err, "Error listing tunnels")
}
var tunnels []tunnelstore.Tunnel
if c.Bool("show-deleted") {
tunnels = allTunnels
} else {
for _, tunnel := range allTunnels {
if tunnel.DeletedAt.IsZero() {
tunnels = append(tunnels, tunnel)
}
}
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
return renderOutput(outputFormat, tunnels)
}
if len(tunnels) > 0 {
const listFormat = "%-40s%-30s%-30s%s\n"
fmt.Printf(listFormat, "ID", "NAME", "CREATED", "CONNECTIONS")
for _, t := range tunnels {
fmt.Printf(listFormat, t.ID, t.Name, t.CreatedAt.Format(time.RFC3339), fmtConnections(t.Connections))
}
} else {
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
}
return nil
}
func fmtConnections(connections []tunnelstore.Connection) string {
// Count connections per colo
numConnsPerColo := make(map[string]uint, len(connections))
for _, connection := range connections {
numConnsPerColo[connection.ColoName]++
}
// Get sorted list of colos
sortedColos := []string{}
for coloName := range numConnsPerColo {
sortedColos = append(sortedColos, coloName)
}
sort.Strings(sortedColos)
// Map each colo to its frequency, combine into output string.
var output []string
for _, coloName := range sortedColos {
output = append(output, fmt.Sprintf("%dx%s", numConnsPerColo[coloName], coloName))
}
return strings.Join(output, ", ")
}
func buildDeleteCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Action: cliutil.ErrorHandler(deleteTunnel),
Usage: "Delete existing tunnel with given ID",
ArgsUsage: "TUNNEL-ID",
Hidden: hideSubcommands,
}
}
func deleteTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel delete" requires exactly 1 argument, the ID of the tunnel to delete.`)
}
id := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
cert, err := getOriginCertFromContext(originCertPath, logger)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
if err := client.DeleteTunnel(id); err != nil {
return errors.Wrapf(err, "Error deleting tunnel %s", id)
}
return nil
}
func renderOutput(format string, v interface{}) error {
switch format {
case "json":
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(v)
case "yaml":
return yaml.NewEncoder(os.Stdout).Encode(v)
default:
return errors.Errorf("Unknown output format '%s'", format)
}
}
func newTunnelstoreClient(c *cli.Context, cert *certutil.OriginCert, logger logger.Service) tunnelstore.Client {
client := tunnelstore.NewRESTClient(c.String("api-url"), cert.AccountID, cert.ServiceKey, logger)
return client
}
func getOriginCertFromContext(originCertPath string, logger logger.Service) (*certutil.OriginCert, error) {
blocks, err := readOriginCert(originCertPath, logger)
if err != nil {
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
}
cert, err := certutil.DecodeOriginCert(blocks)
if err != nil {
return nil, errors.Wrap(err, "Error decoding origin cert")
}
if cert.AccountID == "" {
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
}
return cert, nil
}
func buildRunCommand() *cli.Command {
return &cli.Command{
Name: "run",
Action: cliutil.ErrorHandler(runTunnel),
Usage: "Proxy a local web server by running the given tunnel",
ArgsUsage: "TUNNEL-ID",
Hidden: hideSubcommands,
Flags: []cli.Flag{forceFlag},
}
}
func runTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID of the tunnel to run.`)
}
id := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
credentials, err := readTunnelCredentials(id, originCertPath)
if err != nil {
return err
}
logger.Debugf("Read credentials for %v", credentials.AccountTag)
return StartServer(c, version, shutdownC, graceShutdownC, &origin.NamedTunnelConfig{Auth: *credentials, ID: id})
}

View File

@ -0,0 +1,79 @@
package tunnel
import (
"testing"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func Test_fmtConnections(t *testing.T) {
type args struct {
connections []tunnelstore.Connection
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
args: args{
connections: []tunnelstore.Connection{},
},
want: "",
},
{
name: "trivial",
args: args{
connections: []tunnelstore.Connection{
{
ColoName: "DFW",
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
},
},
},
want: "1xDFW",
},
{
name: "many colos",
args: args{
connections: []tunnelstore.Connection{
{
ColoName: "YRV",
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
},
{
ColoName: "DFW",
ID: uuid.MustParse("c13c0b3b-0fbf-453c-8169-a1990fced6d0"),
},
{
ColoName: "ATL",
ID: uuid.MustParse("70c90639-e386-4e8d-9a4e-7f046d70e63f"),
},
{
ColoName: "DFW",
ID: uuid.MustParse("30ad6251-0305-4635-a670-d3994f474981"),
},
},
},
want: "1xATL, 2xDFW, 1xYRV",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := fmtConnections(tt.args.connections); got != tt.want {
t.Errorf("fmtConnections() = %v, want %v", got, tt.want)
}
})
}
}
func TestTunnelfilePath(t *testing.T) {
actual, err := tunnelFilePath("tunnel", "~/.cloudflared/cert.pem")
assert.NoError(t, err)
expected := "~/.cloudflared/tunnel.json"
assert.Equal(t, expected, actual)
}

View File

@ -2,6 +2,7 @@ package updater
import (
"context"
"fmt"
"os"
"runtime"
"time"
@ -9,9 +10,10 @@ import (
"golang.org/x/crypto/ssh/terminal"
"gopkg.in/urfave/cli.v2"
"github.com/cloudflare/cloudflared/log"
"github.com/cloudflare/cloudflared/logger"
"github.com/equinox-io/equinox"
"github.com/facebookgo/grace/gracenet"
"github.com/pkg/errors"
)
const (
@ -29,9 +31,35 @@ dsCmJ/QZ6aw0w9qkkwEpne1Lmo6+0pGexZzFZOH6w5amShn+RXt7qkSid9iWlzGq
EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
-----END ECDSA PUBLIC KEY-----
`)
logger = log.CreateLogger()
)
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
// https://pkg.go.dev/gopkg.in/urfave/cli.v2?tab=doc#ExitCoder
type statusSuccess struct {
newVersion string
}
func (u *statusSuccess) Error() string {
return fmt.Sprintf("cloudflared has been updated to version %s", u.newVersion)
}
func (u *statusSuccess) ExitCode() int {
return 11
}
// UpdateErr implements ExitCoder interface, the app will exit with status code 10
type statusErr struct {
err error
}
func (e *statusErr) Error() string {
return fmt.Sprintf("failed to update cloudflared: %v", e.err)
}
func (e *statusErr) ExitCode() int {
return 10
}
type UpdateOutcome struct {
Updated bool
Version string
@ -39,7 +67,7 @@ type UpdateOutcome struct {
}
func (uo *UpdateOutcome) noUpdate() bool {
return uo.Error != nil && uo.Updated == false
return uo.Error == nil && uo.Updated == false
}
func checkForUpdateAndApply() UpdateOutcome {
@ -65,26 +93,32 @@ func checkForUpdateAndApply() UpdateOutcome {
}
func Update(_ *cli.Context) error {
updateOutcome := loggedUpdate()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
updateOutcome := loggedUpdate(logger)
if updateOutcome.Error != nil {
os.Exit(10)
return &statusErr{updateOutcome.Error}
}
if updateOutcome.noUpdate() {
logger.Infof("cloudflared is up to date (%s)", updateOutcome.Version)
return nil
}
return updateOutcome.Error
return &statusSuccess{newVersion: updateOutcome.Version}
}
// Checks for an update and applies it if one is available
func loggedUpdate() UpdateOutcome {
func loggedUpdate(logger logger.Service) UpdateOutcome {
updateOutcome := checkForUpdateAndApply()
if updateOutcome.Updated {
logger.Infof("cloudflared has been updated to version %s", updateOutcome.Version)
}
if updateOutcome.Error != nil {
logger.WithError(updateOutcome.Error).Error("update check failed")
logger.Errorf("update check failed: %s", updateOutcome.Error)
}
return updateOutcome
@ -95,6 +129,7 @@ type AutoUpdater struct {
configurable *configurable
listeners *gracenet.Net
updateConfigChan chan *configurable
logger logger.Service
}
// AutoUpdaterConfigurable is the attributes of AutoUpdater that can be reconfigured during runtime
@ -103,7 +138,7 @@ type configurable struct {
freq time.Duration
}
func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net) *AutoUpdater {
func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net, logger logger.Service) *AutoUpdater {
updaterConfigurable := &configurable{
enabled: true,
freq: freq,
@ -116,6 +151,7 @@ func NewAutoUpdater(freq time.Duration, listeners *gracenet.Net) *AutoUpdater {
configurable: updaterConfigurable,
listeners: listeners,
updateConfigChan: make(chan *configurable),
logger: logger,
}
}
@ -123,18 +159,22 @@ func (a *AutoUpdater) Run(ctx context.Context) error {
ticker := time.NewTicker(a.configurable.freq)
for {
if a.configurable.enabled {
updateOutcome := loggedUpdate()
updateOutcome := loggedUpdate(a.logger)
if updateOutcome.Updated {
os.Args = append(os.Args, "--is-autoupdated=true")
pid, err := a.listeners.StartProcess()
if err != nil {
logger.WithError(err).Error("Unable to restart server automatically")
return err
if IsSysV() {
// SysV doesn't have a mechanism to keep service alive, we have to restart the process
a.logger.Info("Restarting service managed by SysV...")
pid, err := a.listeners.StartProcess()
if err != nil {
a.logger.Errorf("Unable to restart server automatically: %s", err)
return &statusErr{err: err}
}
// stop old process after autoupdate. Otherwise we create a new process
// after each update
a.logger.Infof("PID of the new process is %d", pid)
}
// stop old process after autoupdate. Otherwise we create a new process
// after each update
logger.Infof("PID of the new process is %d", pid)
return nil
return &statusSuccess{newVersion: updateOutcome.Version}
}
}
select {
@ -164,14 +204,14 @@ func (a *AutoUpdater) Update(newFreq time.Duration) {
a.updateConfigChan <- newConfigurable
}
func IsAutoupdateEnabled(c *cli.Context) bool {
if !SupportAutoUpdate() {
func IsAutoupdateEnabled(c *cli.Context, l logger.Service) bool {
if !SupportAutoUpdate(l) {
return false
}
return !c.Bool("no-autoupdate") && c.Duration("autoupdate-freq") != 0
}
func SupportAutoUpdate() bool {
func SupportAutoUpdate(logger logger.Service) bool {
if runtime.GOOS == "windows" {
logger.Info(noUpdateOnWindowsMessage)
return false
@ -187,3 +227,14 @@ func SupportAutoUpdate() bool {
func isRunningFromTerminal() bool {
return terminal.IsTerminal(int(os.Stdout.Fd()))
}
func IsSysV() bool {
if runtime.GOOS != "linux" {
return false
}
if _, err := os.Stat("/run/systemd/system"); err == nil {
return false
}
return true
}

View File

@ -4,13 +4,15 @@ import (
"context"
"testing"
"github.com/cloudflare/cloudflared/logger"
"github.com/facebookgo/grace/gracenet"
"github.com/stretchr/testify/assert"
)
func TestDisabledAutoUpdater(t *testing.T) {
listeners := &gracenet.Net{}
autoupdater := NewAutoUpdater(0, listeners)
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
autoupdater := NewAutoUpdater(0, listeners, logger)
ctx, cancel := context.WithCancel(context.Background())
errC := make(chan error)
go func() {

View File

@ -12,6 +12,8 @@ import (
"time"
"unsafe"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
"golang.org/x/sys/windows"
@ -65,6 +67,12 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
// 2. get ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
// This involves actually trying to start the service.
logger, err := logger.New()
if err != nil {
os.Exit(1)
return
}
isIntSess, err := svc.IsAnInteractiveSession()
if err != nil {
logger.Fatalf("failed to determine if we are running in an interactive session: %v", err)
@ -97,9 +105,15 @@ type windowsService struct {
// called by the package code at the start of the service
func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeRequest, statusChan chan<- svc.Status) (ssec bool, errno uint32) {
logger, err := logger.New()
if err != nil {
os.Exit(1)
return
}
elog, err := eventlog.Open(windowsServiceName)
if err != nil {
logger.WithError(err).Errorf("Cannot open event log for %s", windowsServiceName)
logger.Errorf("Cannot open event log for %s with error: %s", windowsServiceName, err)
return
}
defer elog.Close()
@ -160,6 +174,11 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
}
func installWindowsService(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
logger.Infof("Installing Argo Tunnel Windows service")
exepath, err := os.Executable()
if err != nil {
@ -168,7 +187,7 @@ func installWindowsService(c *cli.Context) error {
}
m, err := mgr.Connect()
if err != nil {
logger.WithError(err).Errorf("Cannot establish a connection to the service control manager")
logger.Errorf("Cannot establish a connection to the service control manager: %s", err)
return err
}
defer m.Disconnect()
@ -189,18 +208,23 @@ func installWindowsService(c *cli.Context) error {
err = eventlog.InstallAsEventCreate(windowsServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)
if err != nil {
s.Delete()
logger.WithError(err).Errorf("Cannot install event logger")
logger.Errorf("Cannot install event logger: %s", err)
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
}
err = configRecoveryOption(s.Handle)
if err != nil {
logger.WithError(err).Errorf("Cannot set service recovery actions")
logger.Errorf("Cannot set service recovery actions: %s", err)
logger.Infof("See %s to manually configure service recovery actions", windowsServiceUrl)
}
return nil
}
func uninstallWindowsService(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
logger.Infof("Uninstalling Argo Tunnel Windows Service")
m, err := mgr.Connect()
if err != nil {

View File

@ -1,57 +0,0 @@
package connection
import (
"context"
"net"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/h2mux"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
const (
openStreamTimeout = 30 * time.Second
)
type Connection struct {
id uuid.UUID
muxer *h2mux.Muxer
addr *net.TCPAddr
isLongLived bool
longLivedID int
}
func newConnection(muxer *h2mux.Muxer, addr *net.TCPAddr) (*Connection, error) {
id, err := uuid.NewRandom()
if err != nil {
return nil, err
}
return &Connection{
id: id,
muxer: muxer,
addr: addr,
}, nil
}
func (c *Connection) Serve(ctx context.Context) error {
// Serve doesn't return until h2mux is shutdown
return c.muxer.Serve(ctx)
}
// Connect is used to establish connections with cloudflare's edge network
func (c *Connection) Connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters, logger *logrus.Entry) (tunnelpogs.ConnectResult, error) {
tsClient, err := NewRPCClient(ctx, c.muxer, logger.WithField("rpc", "connect"), openStreamTimeout)
if err != nil {
return nil, errors.Wrap(err, "cannot create new RPC connection")
}
defer tsClient.Close()
return tsClient.Connect(ctx, parameters)
}
func (c *Connection) Shutdown() {
c.muxer.Shutdown()
}

View File

@ -1,302 +0,0 @@
package connection
import (
"context"
"crypto/tls"
"fmt"
"sync"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/streamhandler"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
const (
quickStartLink = "https://developers.cloudflare.com/argo-tunnel/quickstart/"
faqLink = "https://developers.cloudflare.com/argo-tunnel/faq/"
defaultRetryAfter = time.Second * 5
packageNamespace = "connection"
edgeManagerSubsystem = "edgemanager"
)
// EdgeManager manages connections with the edge
type EdgeManager struct {
// streamHandler handles stream opened by the edge
streamHandler *streamhandler.StreamHandler
// TLSConfig is the TLS configuration to connect with edge
tlsConfig *tls.Config
// cloudflaredConfig is the cloudflared configuration that is determined when the process first starts
cloudflaredConfig *CloudflaredConfig
// serviceDiscoverer returns the next edge addr to connect to
serviceDiscoverer *edgediscovery.Edge
// state is attributes of ConnectionManager that can change during runtime.
state *edgeManagerState
logger *logrus.Entry
metrics *metrics
}
type metrics struct {
// activeStreams is a gauge shared by all muxers of this process to expose the total number of active streams
activeStreams prometheus.Gauge
}
func newMetrics(namespace, subsystem string) *metrics {
return &metrics{
activeStreams: h2mux.NewActiveStreamsMetrics(namespace, subsystem),
}
}
// EdgeManagerConfigurable is the configurable attributes of a EdgeConnectionManager
type EdgeManagerConfigurable struct {
TunnelHostnames []h2mux.TunnelHostname
*tunnelpogs.EdgeConnectionConfig
}
type CloudflaredConfig struct {
CloudflaredID uuid.UUID
Tags []tunnelpogs.Tag
BuildInfo *buildinfo.BuildInfo
IntentLabel string
}
func NewEdgeManager(
streamHandler *streamhandler.StreamHandler,
edgeConnMgrConfigurable *EdgeManagerConfigurable,
userCredential []byte,
tlsConfig *tls.Config,
serviceDiscoverer *edgediscovery.Edge,
cloudflaredConfig *CloudflaredConfig,
logger *logrus.Logger,
) *EdgeManager {
return &EdgeManager{
streamHandler: streamHandler,
tlsConfig: tlsConfig,
cloudflaredConfig: cloudflaredConfig,
serviceDiscoverer: serviceDiscoverer,
state: newEdgeConnectionManagerState(edgeConnMgrConfigurable, userCredential),
logger: logger.WithField("subsystem", "connectionManager"),
metrics: newMetrics(packageNamespace, edgeManagerSubsystem),
}
}
func (em *EdgeManager) Run(ctx context.Context) error {
defer em.shutdown()
// Currently, declarative tunnels don't have any concept of a stable connection
// Each edge connection is transient and when it dies, it is replaced by a different one,
// not restarted.
// So in the future we should really change this so that n connections are stored individually
connIndex := 0
for {
select {
case <-ctx.Done():
return errors.Wrap(ctx.Err(), "EdgeConnectionManager terminated")
default:
time.Sleep(1 * time.Second)
}
// Create/delete connection one at a time, so we don't need to adjust for connections that are being created/deleted
// in shouldCreateConnection or shouldReduceConnection calculation
if em.state.shouldCreateConnection(em.serviceDiscoverer.AvailableAddrs()) {
if connErr := em.newConnection(ctx, connIndex); connErr != nil {
if !connErr.ShouldRetry {
em.logger.WithError(connErr).Error(em.noRetryMessage())
return connErr
}
em.logger.WithError(connErr).Error("cannot create new connection")
} else {
connIndex++
}
} else if em.state.shouldReduceConnection() {
if err := em.closeConnection(ctx); err != nil {
em.logger.WithError(err).Error("cannot close connection")
}
}
}
}
func (em *EdgeManager) UpdateConfigurable(newConfigurable *EdgeManagerConfigurable) {
em.logger.Infof("New edge connection manager configuration %+v", newConfigurable)
em.state.updateConfigurable(newConfigurable)
}
func (em *EdgeManager) newConnection(ctx context.Context, index int) *tunnelpogs.ConnectError {
edgeTCPAddr, err := em.serviceDiscoverer.GetAddr(index)
if err != nil {
return retryConnection(fmt.Sprintf("edge address discovery error: %v", err))
}
configurable := em.state.getConfigurable()
edgeConn, err := DialEdge(ctx, configurable.Timeout, em.tlsConfig, edgeTCPAddr)
if err != nil {
return retryConnection(fmt.Sprintf("dial edge error: %v", err))
}
// Establish a muxed connection with the edge
// Client mux handshake with agent server
muxer, err := h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{
Timeout: configurable.Timeout,
Handler: em.streamHandler,
IsClient: true,
HeartbeatInterval: configurable.HeartbeatInterval,
MaxHeartbeats: configurable.MaxFailedHeartbeats,
Logger: em.logger.WithField("subsystem", "muxer"),
}, em.metrics.activeStreams)
if err != nil {
retryConnection(fmt.Sprintf("couldn't perform handshake with edge: %v", err))
}
h2muxConn, err := newConnection(muxer, edgeTCPAddr)
if err != nil {
return retryConnection(fmt.Sprintf("couldn't create h2mux connection: %v", err))
}
go em.serveConn(ctx, h2muxConn)
connResult, err := h2muxConn.Connect(ctx, &tunnelpogs.ConnectParameters{
CloudflaredID: em.cloudflaredConfig.CloudflaredID,
CloudflaredVersion: em.cloudflaredConfig.BuildInfo.CloudflaredVersion,
NumPreviousAttempts: 0,
OriginCert: em.state.getUserCredential(),
IntentLabel: em.cloudflaredConfig.IntentLabel,
Tags: em.cloudflaredConfig.Tags,
}, em.logger)
if err != nil {
h2muxConn.Shutdown()
return retryConnection(fmt.Sprintf("couldn't connect to edge: %v", err))
}
if connErr := connResult.ConnectError(); connErr != nil {
return connErr
}
em.state.newConnection(h2muxConn)
em.logger.Infof("connected to %s", connResult.ConnectedTo())
if connResult.ClientConfig() != nil {
em.streamHandler.UseConfiguration(ctx, connResult.ClientConfig())
}
return nil
}
func (em *EdgeManager) closeConnection(ctx context.Context) error {
conn := em.state.getFirstConnection()
if conn == nil {
return fmt.Errorf("no connection to close")
}
conn.Shutdown()
// teardown will be handled by EdgeManager.serveConn in another goroutine
return nil
}
func (em *EdgeManager) serveConn(ctx context.Context, conn *Connection) {
err := conn.Serve(ctx)
em.logger.WithError(err).Warn("Connection closed")
em.state.closeConnection(conn)
em.serviceDiscoverer.GiveBack(conn.addr)
}
func (em *EdgeManager) noRetryMessage() string {
messageTemplate := "cloudflared could not register an Argo Tunnel on your account. Please confirm the following before trying again:" +
"1. You have Argo Smart Routing enabled in your account, See Enable Argo section of %s." +
"2. Your credential at %s is still valid. See %s."
return fmt.Sprintf(messageTemplate, quickStartLink, em.state.getConfigurable().UserCredentialPath, faqLink)
}
func (em *EdgeManager) shutdown() {
em.state.shutdown()
}
type edgeManagerState struct {
sync.RWMutex
configurable *EdgeManagerConfigurable
userCredential []byte
conns map[uuid.UUID]*Connection
}
func newEdgeConnectionManagerState(configurable *EdgeManagerConfigurable, userCredential []byte) *edgeManagerState {
return &edgeManagerState{
configurable: configurable,
userCredential: userCredential,
conns: make(map[uuid.UUID]*Connection),
}
}
func (ems *edgeManagerState) shouldCreateConnection(availableEdgeAddrs int) bool {
ems.RLock()
defer ems.RUnlock()
expectedHAConns := int(ems.configurable.NumHAConnections)
if availableEdgeAddrs < expectedHAConns {
expectedHAConns = availableEdgeAddrs
}
return len(ems.conns) < expectedHAConns
}
func (ems *edgeManagerState) shouldReduceConnection() bool {
ems.RLock()
defer ems.RUnlock()
return uint8(len(ems.conns)) > ems.configurable.NumHAConnections
}
func (ems *edgeManagerState) newConnection(conn *Connection) {
ems.Lock()
defer ems.Unlock()
ems.conns[conn.id] = conn
}
func (ems *edgeManagerState) closeConnection(conn *Connection) {
ems.Lock()
defer ems.Unlock()
delete(ems.conns, conn.id)
}
func (ems *edgeManagerState) getFirstConnection() *Connection {
ems.RLock()
defer ems.RUnlock()
for _, conn := range ems.conns {
return conn
}
return nil
}
func (ems *edgeManagerState) shutdown() {
ems.Lock()
defer ems.Unlock()
for _, conn := range ems.conns {
conn.Shutdown()
}
}
func (ems *edgeManagerState) getConfigurable() *EdgeManagerConfigurable {
ems.Lock()
defer ems.Unlock()
return ems.configurable
}
func (ems *edgeManagerState) updateConfigurable(newConfigurable *EdgeManagerConfigurable) {
ems.Lock()
defer ems.Unlock()
ems.configurable = newConfigurable
}
func (ems *edgeManagerState) getUserCredential() []byte {
ems.RLock()
defer ems.RUnlock()
return ems.userCredential
}
func retryConnection(cause string) *tunnelpogs.ConnectError {
return &tunnelpogs.ConnectError{
Cause: cause,
RetryAfter: defaultRetryAfter,
ShouldRetry: true,
}
}

View File

@ -1,77 +0,0 @@
package connection
import (
"net"
"testing"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/streamhandler"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
var (
configurable = &EdgeManagerConfigurable{
[]h2mux.TunnelHostname{
"http.example.com",
"ws.example.com",
"hello.example.com",
},
&pogs.EdgeConnectionConfig{
NumHAConnections: 1,
HeartbeatInterval: 1 * time.Second,
Timeout: 5 * time.Second,
MaxFailedHeartbeats: 3,
UserCredentialPath: "/etc/cloudflared/cert.pem",
},
}
cloudflaredConfig = &CloudflaredConfig{
CloudflaredID: uuid.New(),
Tags: []pogs.Tag{
{Name: "pool", Value: "east-6"},
},
BuildInfo: &buildinfo.BuildInfo{
GoOS: "linux",
GoVersion: "1.12",
GoArch: "amd64",
CloudflaredVersion: "2019.6.0",
},
}
)
func mockEdgeManager() *EdgeManager {
newConfigChan := make(chan<- *pogs.ClientConfig)
useConfigResultChan := make(<-chan *pogs.UseConfigurationResult)
logger := logrus.New()
edge := edgediscovery.MockEdge(logger, []*net.TCPAddr{})
return NewEdgeManager(
streamhandler.NewStreamHandler(newConfigChan, useConfigResultChan, logger),
configurable,
[]byte{},
nil,
edge,
cloudflaredConfig,
logger,
)
}
func TestUpdateConfigurable(t *testing.T) {
m := mockEdgeManager()
newConfigurable := &EdgeManagerConfigurable{
[]h2mux.TunnelHostname{
"second.example.com",
},
&pogs.EdgeConnectionConfig{
NumHAConnections: 2,
},
}
m.UpdateConfigurable(newConfigurable)
assert.Equal(t, newConfigurable, m.state.getConfigurable())
}

View File

@ -5,10 +5,10 @@ import (
"fmt"
"time"
"github.com/sirupsen/logrus"
rpc "zombiezen.com/go/capnproto2/rpc"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
@ -18,7 +18,7 @@ import (
func NewRPCClient(
ctx context.Context,
muxer *h2mux.Muxer,
logger *logrus.Entry,
logger logger.Service,
openStreamTimeout time.Duration,
) (client tunnelpogs.TunnelServer_PogsClient, err error) {
openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout)

View File

@ -15,11 +15,19 @@ import (
// The tunnel package is responsible for appending this to tunnel.Commands().
func Cmd() *cli.Command {
return &cli.Command{
Category: "Database Connect (ALPHA)",
Category: "Database Connect (ALPHA) - Deprecated",
Name: "db-connect",
Usage: "Access your SQL database from Cloudflare Workers or the browser",
Usage: "deprecated: Access your SQL database from Cloudflare Workers or the browser",
ArgsUsage: " ",
Description: `
This feature has been deprecated.
Please see:
cloudflared access tcp --help
for setting up database connections to the cloudflare edge.
Creates a connection between your database and the Cloudflare edge.
Now you can execute SQL commands anywhere you can send HTTPS requests.

View File

@ -11,17 +11,17 @@ import (
"time"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// Proxy is an HTTP server that proxies requests to a Client.
type Proxy struct {
client Client
accessValidator *validation.Access
logger *logrus.Logger
logger logger.Service
}
// NewInsecureProxy creates a Proxy that talks to a Client at an origin.
@ -43,7 +43,12 @@ func NewInsecureProxy(ctx context.Context, origin string) (*Proxy, error) {
return nil, errors.Wrap(err, "could not connect to the database")
}
return &Proxy{client, nil, logrus.New()}, nil
logger, err := logger.New()
if err != nil {
return nil, errors.Wrap(err, "error setting up logger")
}
return &Proxy{client, nil, logger}, nil
}
// NewSecureProxy creates a Proxy that talks to a Client at an origin.
@ -90,7 +95,8 @@ func (proxy *Proxy) IsAllowed(r *http.Request, verbose ...bool) bool {
// Warn administrators that invalid JWTs are being rejected. This is indicative
// of either a misconfiguration of the CLI or a massive failure of upstream systems.
if len(verbose) > 0 {
proxy.httpLog(r, err).Error("Failed JWT authentication")
cfRay := proxy.getRayHeader(r)
proxy.logger.Infof("dbproxy: Failed JWT authentication: cf-ray: %s %s", cfRay, err)
}
return false
@ -234,13 +240,14 @@ func (proxy *Proxy) httpRespondErr(w http.ResponseWriter, r *http.Request, defau
proxy.httpRespond(w, r, status, err.Error())
if len(err.Error()) > 0 {
proxy.httpLog(r, err).Warn("Database proxy error")
cfRay := proxy.getRayHeader(r)
proxy.logger.Infof("dbproxy: Database proxy error: cf-ray: %s %s", cfRay, err)
}
}
// httpLog returns a logrus.Entry that is formatted to output a request Cf-ray.
func (proxy *Proxy) httpLog(r *http.Request, err error) *logrus.Entry {
return proxy.logger.WithContext(r.Context()).WithField("CF-RAY", r.Header.Get("Cf-ray")).WithError(err)
// getRayHeader returns the request's Cf-ray header.
func (proxy *Proxy) getRayHeader(r *http.Request) string {
return r.Header.Get("Cf-ray")
}
// httpError extracts common errors and returns an status code and friendly error.

View File

@ -7,8 +7,8 @@ import (
"net"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
@ -58,15 +58,15 @@ var friendlyDNSErrorLines = []string{
}
// EdgeDiscovery implements HA service discovery lookup.
func edgeDiscovery(logger *logrus.Entry) ([][]*net.TCPAddr, error) {
func edgeDiscovery(logger logger.Service) ([][]*net.TCPAddr, error) {
_, addrs, err := netLookupSRV(srvService, srvProto, srvName)
if err != nil {
_, fallbackAddrs, fallbackErr := fallbackLookupSRV(srvService, srvProto, srvName)
if fallbackErr != nil || len(fallbackAddrs) == 0 {
// use the original DNS error `err` in messages, not `fallbackErr`
logger.Errorln("Error looking up Cloudflare edge IPs: the DNS query failed:", err)
logger.Errorf("Error looking up Cloudflare edge IPs: the DNS query failed: %s", err)
for _, s := range friendlyDNSErrorLines {
logger.Errorln(s)
logger.Error(s)
}
return nil, errors.Wrapf(err, "Could not lookup srv records on _%v._%v.%v", srvService, srvProto, srvName)
}

View File

@ -3,7 +3,7 @@ package allregions
import (
"testing"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
"github.com/stretchr/testify/assert"
)
@ -19,7 +19,8 @@ func TestEdgeDiscovery(t *testing.T) {
}
}
addrLists, err := edgeDiscovery(logrus.New().WithFields(logrus.Fields{}))
l := logger.NewOutputWriter(logger.NewMockWriteManager())
addrLists, err := edgeDiscovery(l)
assert.NoError(t, err)
actualAddrSet := map[string]bool{}
for _, addrs := range addrLists {

View File

@ -56,6 +56,10 @@ func (r Region) GetUnusedIP(excluding *net.TCPAddr) *net.TCPAddr {
// Use the address, assigning it to a proxy connection.
func (r Region) Use(addr *net.TCPAddr, connID int) {
if addr == nil {
//logrus.Errorf("Attempted to use nil address for connection %d", connID)
return
}
r.connFor[addr] = InUse(connID)
}

View File

@ -4,7 +4,7 @@ import (
"fmt"
"net"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
)
// Regions stores Cloudflare edge network IPs, partitioned into two regions.
@ -19,7 +19,7 @@ type Regions struct {
// ------------------------------------
// ResolveEdge resolves the Cloudflare edge, returning all regions discovered.
func ResolveEdge(logger *logrus.Entry) (*Regions, error) {
func ResolveEdge(logger logger.Service) (*Regions, error) {
addrLists, err := edgeDiscovery(logger)
if err != nil {
return nil, err
@ -68,8 +68,8 @@ func NewNoResolve(addrs []*net.TCPAddr) *Regions {
// GetAnyAddress returns an arbitrary address from the larger region.
func (rs *Regions) GetAnyAddress() *net.TCPAddr {
if rs.region1.AvailableAddrs() > rs.region2.AvailableAddrs() {
return rs.region1.GetAnyAddress()
if addr := rs.region1.GetAnyAddress(); addr != nil {
return addr
}
return rs.region2.GetAnyAddress()
}
@ -86,21 +86,28 @@ func (rs *Regions) AddrUsedBy(connID int) *net.TCPAddr {
// GetUnusedAddr gets an unused addr from the edge, excluding the given addr. Prefer to use addresses
// evenly across both regions.
func (rs *Regions) GetUnusedAddr(excluding *net.TCPAddr, connID int) *net.TCPAddr {
var addr *net.TCPAddr
if rs.region1.AvailableAddrs() > rs.region2.AvailableAddrs() {
addr = rs.region1.GetUnusedIP(excluding)
rs.region1.Use(addr, connID)
} else {
addr = rs.region2.GetUnusedIP(excluding)
rs.region2.Use(addr, connID)
return getAddrs(excluding, connID, &rs.region1, &rs.region2)
}
if addr == nil {
return nil
return getAddrs(excluding, connID, &rs.region2, &rs.region1)
}
// getAddrs tries to grab address form `first` region, then `second` region
// this is an unrolled loop over 2 element array
func getAddrs(excluding *net.TCPAddr, connID int, first *Region, second *Region) *net.TCPAddr {
addr := first.GetUnusedIP(excluding)
if addr != nil {
first.Use(addr, connID)
return addr
}
addr = second.GetUnusedIP(excluding)
if addr != nil {
second.Use(addr, connID)
return addr
}
// Mark the address as used and return it
return addr
return nil
}
// AvailableAddrs returns how many edge addresses aren't used.

View File

@ -58,6 +58,7 @@ func TestRegions_Giveback_Region1(t *testing.T) {
rs.GiveBack(&addr0)
assert.Equal(t, &addr0, rs.GetUnusedAddr(nil, 3))
}
func TestRegions_Giveback_Region2(t *testing.T) {
rs := makeRegions()
rs.region1.Use(&addr0, 0)

View File

@ -6,8 +6,7 @@ import (
"sync"
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
)
const (
@ -20,7 +19,7 @@ var errNoAddressesLeft = fmt.Errorf("There are no free edge addresses left")
type Edge struct {
regions *allregions.Regions
sync.Mutex
logger *logrus.Entry
logger logger.Service
}
// ------------------------------------
@ -29,37 +28,34 @@ type Edge struct {
// ResolveEdge runs the initial discovery of the Cloudflare edge, finding Addrs that can be allocated
// to connections.
func ResolveEdge(l *logrus.Logger) (*Edge, error) {
logger := l.WithField("subsystem", subsystem)
regions, err := allregions.ResolveEdge(logger)
func ResolveEdge(l logger.Service) (*Edge, error) {
regions, err := allregions.ResolveEdge(l)
if err != nil {
return new(Edge), err
}
return &Edge{
logger: logger,
logger: l,
regions: regions,
}, nil
}
// StaticEdge creates a list of edge addresses from the list of hostnames. Mainly used for testing connectivity.
func StaticEdge(l *logrus.Logger, hostnames []string) (*Edge, error) {
logger := l.WithField("subsystem", subsystem)
func StaticEdge(l logger.Service, hostnames []string) (*Edge, error) {
regions, err := allregions.StaticEdge(hostnames)
if err != nil {
return new(Edge), err
}
return &Edge{
logger: logger,
logger: l,
regions: regions,
}, nil
}
// MockEdge creates a Cloudflare Edge from arbitrary TCP addresses. Used for testing.
func MockEdge(l *logrus.Logger, addrs []*net.TCPAddr) *Edge {
logger := l.WithField("subsystem", subsystem)
func MockEdge(l logger.Service, addrs []*net.TCPAddr) *Edge {
regions := allregions.NewNoResolve(addrs)
return &Edge{
logger: logger,
logger: l,
regions: regions,
}
}
@ -83,24 +79,20 @@ func (ed *Edge) GetAddrForRPC() (*net.TCPAddr, error) {
func (ed *Edge) GetAddr(connID int) (*net.TCPAddr, error) {
ed.Lock()
defer ed.Unlock()
logger := ed.logger.WithFields(logrus.Fields{
"connID": connID,
"function": "GetAddr",
})
// If this connection has already used an edge addr, return it.
if addr := ed.regions.AddrUsedBy(connID); addr != nil {
logger.Debug("Returning same address back to proxy connection")
ed.logger.Debugf("edgediscovery - GetAddr: Returning same address back to proxy connection: connID: %d", connID)
return addr, nil
}
// Otherwise, give it an unused one
addr := ed.regions.GetUnusedAddr(nil, connID)
if addr == nil {
logger.Debug("No addresses left to give proxy connection")
ed.logger.Debugf("edgediscovery - GetAddr: No addresses left to give proxy connection: connID: %d", connID)
return nil, errNoAddressesLeft
}
logger.Debug("Giving connection its new address")
ed.logger.Debugf("edgediscovery - GetAddr: Giving connection its new address %s: connID: %d", addr, connID)
return addr, nil
}
@ -108,10 +100,6 @@ func (ed *Edge) GetAddr(connID int) (*net.TCPAddr, error) {
func (ed *Edge) GetDifferentAddr(connID int) (*net.TCPAddr, error) {
ed.Lock()
defer ed.Unlock()
logger := ed.logger.WithFields(logrus.Fields{
"connID": connID,
"function": "GetDifferentAddr",
})
oldAddr := ed.regions.AddrUsedBy(connID)
if oldAddr != nil {
@ -119,10 +107,11 @@ func (ed *Edge) GetDifferentAddr(connID int) (*net.TCPAddr, error) {
}
addr := ed.regions.GetUnusedAddr(oldAddr, connID)
if addr == nil {
logger.Debug("No addresses left to give proxy connection")
ed.logger.Debugf("edgediscovery - GetDifferentAddr: No addresses left to give proxy connection: connID: %d", connID)
// note: if oldAddr were not nil, it will become available on the next iteration
return nil, errNoAddressesLeft
}
logger.Debug("Giving connection its new address")
ed.logger.Debugf("edgediscovery - GetDifferentAddr: Giving connection its new address %s: connID: %d", addr, connID)
return addr, nil
}
@ -138,6 +127,6 @@ func (ed *Edge) AvailableAddrs() int {
func (ed *Edge) GiveBack(addr *net.TCPAddr) bool {
ed.Lock()
defer ed.Unlock()
ed.logger.WithField("function", "GiveBack").Debug("Address now unused")
ed.logger.Debug("edgediscovery - GiveBack: Address now unused")
return ed.regions.GiveBack(addr)
}

View File

@ -4,7 +4,7 @@ import (
"net"
"testing"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
"github.com/stretchr/testify/assert"
)
@ -32,7 +32,7 @@ var (
)
func TestGiveBack(t *testing.T) {
l := logrus.New()
l := logger.NewOutputWriter(logger.NewMockWriteManager())
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
// Give this connection an address
@ -47,8 +47,26 @@ func TestGiveBack(t *testing.T) {
edge.GiveBack(addr)
assert.Equal(t, 4, edge.AvailableAddrs())
}
func TestRPCAndProxyShareSingleEdgeIP(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
// Make an edge with a single IP
edge := MockEdge(l, []*net.TCPAddr{&addr0})
tunnelConnID := 0
// Use the IP for a tunnel
addrTunnel, err := edge.GetAddr(tunnelConnID)
assert.NoError(t, err)
// Ensure the IP can be used for RPC too
addrRPC, err := edge.GetAddrForRPC()
assert.NoError(t, err)
assert.Equal(t, addrTunnel, addrRPC)
}
func TestGetAddrForRPC(t *testing.T) {
l := logrus.New()
l := logger.NewOutputWriter(logger.NewMockWriteManager())
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
// Get a connection
@ -65,8 +83,32 @@ func TestGetAddrForRPC(t *testing.T) {
assert.Equal(t, 4, edge.AvailableAddrs())
}
func TestOnePerRegion(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
// Make an edge with only one address
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1})
// Use the only address
const connID = 0
a1, err := edge.GetAddr(connID)
assert.NoError(t, err)
assert.NotNil(t, a1)
// if the first address is bad, get the second one
a2, err := edge.GetDifferentAddr(connID)
assert.NoError(t, err)
assert.NotNil(t, a2)
assert.NotEqual(t, a1, a2)
// now that second one is bad, get the first one again
a3, err := edge.GetDifferentAddr(connID)
assert.NoError(t, err)
assert.Equal(t, a1, a3)
}
func TestOnlyOneAddrLeft(t *testing.T) {
l := logrus.New()
l := logger.NewOutputWriter(logger.NewMockWriteManager())
// Make an edge with only one address
edge := MockEdge(l, []*net.TCPAddr{&addr0})
@ -80,10 +122,15 @@ func TestOnlyOneAddrLeft(t *testing.T) {
// If that edge address is "bad", there's no alternative address.
_, err = edge.GetDifferentAddr(connID)
assert.Error(t, err)
// previously bad address should become available again on next iteration.
addr, err = edge.GetDifferentAddr(connID)
assert.NoError(t, err)
assert.NotNil(t, addr)
}
func TestNoAddrsLeft(t *testing.T) {
l := logrus.New()
l := logger.NewOutputWriter(logger.NewMockWriteManager())
// Make an edge with no addresses
edge := MockEdge(l, []*net.TCPAddr{})
@ -95,7 +142,7 @@ func TestNoAddrsLeft(t *testing.T) {
}
func TestGetAddr(t *testing.T) {
l := logrus.New()
l := logger.NewOutputWriter(logger.NewMockWriteManager())
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
// Give this connection an address
@ -111,7 +158,7 @@ func TestGetAddr(t *testing.T) {
}
func TestGetDifferentAddr(t *testing.T) {
l := logrus.New()
l := logger.NewOutputWriter(logger.NewMockWriteManager())
edge := MockEdge(l, []*net.TCPAddr{&addr0, &addr1, &addr2, &addr3})
// Give this connection an address

128
github_release.py Executable file
View File

@ -0,0 +1,128 @@
#!/usr/bin/python3
"""
Creates Github Releases and uploads assets
"""
import argparse
import logging
import os
from github import Github, GithubException, UnknownObjectException
FORMAT = "%(levelname)s - %(asctime)s: %(message)s"
logging.basicConfig(format=FORMAT)
CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared")
GITHUB_CONFLICT_CODE = "already_exists"
def assert_tag_exists(repo, version):
""" Raise exception if repo does not contain a tag matching version """
tags = repo.get_tags()
if not tags or tags[0].name != version:
raise Exception("Tag {} not found".format(version))
def get_or_create_release(repo, version, dry_run=False):
"""
Get a Github Release matching the version tag or create a new one.
If a conflict occurs on creation, attempt to fetch the Release on last time
"""
try:
release = repo.get_release(version)
logging.info("Release %s found", version)
return release
except UnknownObjectException:
logging.info("Release %s not found", version)
# We dont want to create a new release tag if one doesnt already exist
assert_tag_exists(repo, version)
if dry_run:
logging.info("Skipping Release creation because of dry-run")
return
try:
logging.info("Creating release %s", version)
return repo.create_git_release(version, version, "")
except GithubException as e:
errors = e.data.get("errors", [])
if e.status == 422 and any(
[err.get("code") == GITHUB_CONFLICT_CODE for err in errors]
):
logging.warning(
"Conflict: Release was likely just made by a different build: %s",
e.data,
)
return repo.get_release(version)
raise e
def parse_args():
""" Parse and validate args """
parser = argparse.ArgumentParser(
description="Creates Github Releases and uploads assets."
)
parser.add_argument(
"--api-key", default=os.environ.get("API_KEY"), help="Github API key"
)
parser.add_argument(
"--release-version",
metavar="version",
default=os.environ.get("VERSION"),
help="Release version",
)
parser.add_argument(
"--path", default=os.environ.get("ASSET_PATH"), help="Asset path"
)
parser.add_argument(
"--name", default=os.environ.get("ASSET_NAME"), help="Asset Name"
)
parser.add_argument(
"--dry-run", action="store_true", help="Do not create release or upload asset"
)
args = parser.parse_args()
is_valid = True
if not args.release_version:
logging.error("Missing release version")
is_valid = False
if not args.path:
logging.error("Missing asset path")
is_valid = False
if not args.name:
logging.error("Missing asset name")
is_valid = False
if not args.api_key:
logging.error("Missing API key")
is_valid = False
if is_valid:
return args
parser.print_usage()
exit(1)
def main():
""" Attempts to upload Asset to Github Release. Creates Release if it doesnt exist """
try:
args = parse_args()
client = Github(args.api_key)
repo = client.get_repo(CLOUDFLARED_REPO)
release = get_or_create_release(repo, args.release_version, args.dry_run)
if args.dry_run:
logging.info("Skipping asset upload because of dry-run")
return
release.upload_asset(args.path, name=args.name)
except Exception as e:
logging.exception(e)
exit(1)
main()

28
go.mod
View File

@ -3,12 +3,16 @@ module github.com/cloudflare/cloudflared
go 1.12
require (
github.com/BurntSushi/go-sumtype v0.0.0-20190304192233-fcb4a6205bdc // indirect
github.com/DATA-DOG/go-sqlmock v1.3.3
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/aws/aws-sdk-go v1.25.8
github.com/beorn7/perks v1.0.1 // indirect
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261 // indirect
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93
github.com/cloudflare/cfssl v0.0.0-20141119014638-2f7f44e802e2 // indirect
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
github.com/coredns/coredns v1.2.0
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
@ -21,11 +25,11 @@ require (
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0
github.com/go-sql-driver/mysql v1.4.1
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/google/certificate-transparency-go v1.1.0
github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.0
@ -33,12 +37,13 @@ require (
github.com/jmoiron/sqlx v1.2.0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kshvakov/clickhouse v1.3.11
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lib/pq v1.2.0
github.com/mattn/go-colorable v0.1.4
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mattn/go-sqlite3 v1.11.0
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b // indirect
github.com/miekg/dns v1.1.8
github.com/miekg/dns v1.1.27
github.com/mitchellh/go-homedir v1.1.0
github.com/opentracing/opentracing-go v1.1.0 // indirect
github.com/philhofer/fwd v1.0.0 // indirect
@ -48,22 +53,23 @@ require (
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect
github.com/prometheus/common v0.7.0 // indirect
github.com/prometheus/procfs v0.0.5 // indirect
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/sirupsen/logrus v1.4.2
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
github.com/stretchr/testify v1.3.0
github.com/tinylib/msgp v1.1.0 // indirect
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc
golang.org/x/net v0.0.0-20191007182048-72f939374954
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sys v0.0.0-20191008105621-543471e840be
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2
google.golang.org/appengine v1.5.0 // indirect
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd // indirect
google.golang.org/grpc v1.24.0 // indirect
gopkg.in/coreos/go-oidc.v2 v2.1.0
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8
gopkg.in/yaml.v2 v2.2.4 // indirect
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7
gopkg.in/yaml.v2 v2.2.4
zombiezen.com/go/capnproto2 v2.18.0+incompatible
)
// ../../go/pkg/mod/github.com/coredns/coredns@v1.2.0/plugin/metrics/metrics.go:40:49: too many arguments in call to prometheus.NewProcessCollector

300
go.sum
View File

@ -1,59 +1,56 @@
bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw=
contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
github.com/BurntSushi/go-sumtype v0.0.0-20190304192233-fcb4a6205bdc h1:nvTP+jmloR0+J4YQur/rLRdLcGVEU4SquDgH+Bo7gBY=
github.com/BurntSushi/go-sumtype v0.0.0-20190304192233-fcb4a6205bdc/go.mod h1:7yTWMMG2vOm4ABVciEt4EgNVP7fxwtcKIb/EuiLiKqY=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1 h1:RKnVV4C7qoN/sToLX2y1dqH7T6kKLMHcwRJlgwb9Ggk=
github.com/acmacalister/skittles v0.0.0-20160609003031-7423546701e1/go.mod h1:gI5CyA/CEnS6eqNV22rqs4dG3aGfaSbXgPORIlwr2r0=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.25.8 h1:n7I+HUUXjun2CsX7JK+1hpRIkZrlKhd3nayeb+Xmavs=
github.com/aws/aws-sdk-go v1.25.8/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk=
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261 h1:6/yVvBsKeAw05IUj4AzvrxaCnDjN4nUqKjW9+w5wixg=
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 h1:JLaf/iINcLyjwbtTsCJjc6rtlASgHeIJPrB6QmwURnA=
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY=
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93 h1:QrGfkZDnMxcWHaYDdB7CmqS9i26OAnUj/xcus/abYkY=
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93/go.mod h1:QiTe66jFdP7cUKMCCf/WrvDyYdtdmdZfVcdoLbzaKVY=
github.com/cloudflare/cfssl v0.0.0-20141119014638-2f7f44e802e2 h1:GOj9uxwfkVdMaHYGQqhab3hr1rjMDqfaQI+JjpKXgMM=
github.com/cloudflare/cfssl v0.0.0-20141119014638-2f7f44e802e2/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4=
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc h1:Dvk3ySBsOm5EviLx6VCyILnafPcQinXGP5jbTdHUJgE=
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc/go.mod h1:HlgKKR8V5a1wroIDDIz3/A+T+9Janfq+7n1P5sEFdi0=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo=
github.com/coredns/coredns v1.2.0 h1:YEI38K2BJYzL/SxO2tZFD727T/C68DqVWkBQjT0sWPU=
github.com/coredns/coredns v1.2.0/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73 h1:7CNPV0LWRCa1FNmqg700pbXhzvmoaXKyfxWRkjRym7Q=
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a h1:W8b4lQ4tFF21aspRGoBuCNV6V2fFJBF+pm1J6OY8Lys=
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0 h1:epsH3lb7KVbXHYk7LYGN5EiE0MxcevHU85CKITJ0wUY=
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/equinox-io/equinox v1.2.0 h1:bBS7Ou+Y7Jwgmy8TWSYxEh85WctuFn7FPlgbUzX4DBA=
github.com/equinox-io/equinox v1.2.0/go.mod h1:6s3HJB0PYUNgs0mxmI8fHdfVl3TQ25ieA/PVfr+eyVo=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
@ -66,180 +63,103 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojt
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10 h1:YO10pIIBftO/kkTFdWhctH96grJ7qiy7bMdiZcIvPKs=
github.com/getsentry/raven-go v0.0.0-20180517221441-ed7bcb39ff10/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0 h1:gF8ngtda767ddth2SH0YSAhswhz6qUkvyI9EZFYCWJA=
github.com/gliderlabs/ssh v0.0.0-20191009160644-63518b5243e0/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU=
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
github.com/golangci/go-tools v0.0.0-20190318055746-e32c54105b7c/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
github.com/golangci/golangci-lint v1.17.2-0.20190910081718-bad04bb7378f/go.mod h1:kaqo8l0OZKYPtjNmG4z4HrWLgcYNIJ9B9q3LWri9uLg=
github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg=
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/certificate-transparency-go v1.1.0 h1:10MlrYzh5wfkToxWI4yJzffsxLfxcEDlOATMx/V9Kzw=
github.com/google/certificate-transparency-go v1.1.0/go.mod h1:i+Q7XY+ArBveOUT36jiHGfuSK1fHICIg6sUkRxPAbCs=
github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/monologue v0.0.0-20190606152607-4b11a32b5934/go.mod h1:6NTfaQoUpg5QmPsCUWLR3ig33FHrKXhTtWzF0DVdmuk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/trillian v1.2.2-0.20190612132142-05461f4df60a/go.mod h1:YPmUVn5NGwgnDUgqlVyFGMTgaWlnSvH7W5p+NdOG8UA=
github.com/google/trillian-examples v0.0.0-20190603134952-4e75ba15216c/go.mod h1:WgL3XZ3pA8/9cm7yxqWrZE6iZkESB2ItGxy5Fo6k2lk=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo=
github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kshvakov/clickhouse v1.3.11 h1:dtzTJY0fCA+MWkLyuKZaNPkmSwdX4gh8+Klic9NB1Lw=
github.com/kshvakov/clickhouse v1.3.11/go.mod h1:/SVBAcqF3u7rxQ9sTWCZwf8jzzvxiZGeQvtmSF2BBEc=
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/letsencrypt/pkcs11key v2.0.1-0.20170608213348-396559074696+incompatible/go.mod h1:iGYXKqDXt0cpBthCHdr9ZdsQwyGlYFh/+8xa4WzIQ34=
github.com/lib/pq v0.0.0-20180201184707-88edab080323/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b h1:/BbY4n99iMazlr2igipph+hj0MwlZIWpcsP8Iy+na+s=
github.com/mholt/caddy v0.0.0-20180807230124-d3b731e9255b/go.mod h1:Wb1PlT4DAYSqOEd03MsqkdkXnTxA8v9pKjdpxbqM1kY=
github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI=
github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -255,191 +175,117 @@ github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLy
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8=
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ultraware/funlen v0.0.1/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0 h1:6DtWz8hNS4qbq0OCRPhdBMG9E2qKTSDKlwnP3dmZvuA=
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0/go.mod h1:A47W3pdWONaZmXuLZgfKLAVgUY0qvfTRM5vVDKS40S4=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v3.3.13+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e/go.mod h1:w7kd3qXHh8FNaczNjslXqvFQiv5mMWRXlL9klTUAHc8=
github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb/go.mod h1:29UiAJNsiVdvTBFCJW8e3q6dcDbOoPkhMgttOSCIMMY=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954 h1:JGZucVF/L/TotR719NbujzadOZ2AgnYlqphQGHDCKaU=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 h1:p9xBe/w/OzkeYVKm234g55gMdD1nSIooTir5kV11kfA=
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2 h1:nq114VpM8lsSlP+lyUbANecYHYiFcSNFtqcBlxRV+gA=
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190221204921-83362c3779f5/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190909030654-5b82db07426d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190605220351-eb0b1bdb6ae6/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd h1:84VQPzup3IpKLxuIAZjHMhVjJ8fZ4/i3yUnj3k6fUdw=
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s=
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/coreos/go-oidc.v2 v2.1.0 h1:E8PjVFdj/SLDKB0hvb70KTbMbYVHjqztiQdSkIg8E+I=
gopkg.in/coreos/go-oidc.v2 v2.1.0/go.mod h1:fYaTe2FS96wZZwR17YTDHwG+Mw6fmyqJNxN2eNCGPCI=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.4.0 h1:0kXPskUMGAXXWJlP05ktEMOV0vmzFQUWw6d+aZJQU8A=
gopkg.in/square/go-jose.v2 v2.4.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8 h1:/pLAskKF+d5SawboKd8GB8ew4ClHDbt2c3K9EBFeRGU=
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8/go.mod h1:cKXr3E0k4aosgycml1b5z33BVV6hai1Kh7uDgFOkbcs=
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.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7 h1:CZoOFlTPbKfAShKYrMuUfYbnXexFT1rYRUX1SPnrdE4=
zombiezen.com/go/capnproto2 v0.0.0-20180616160808-7cfd211c19c7/go.mod h1:TMGa8HWGJkXiq4nHe9Zu/JgRF5oUtg4XizFC+Vexbec=
zombiezen.com/go/capnproto2 v2.18.0+incompatible h1:mwfXZniffG5mXokQGHUJWGnqIBggoPfT/CEwon9Yess=
zombiezen.com/go/capnproto2 v2.18.0+incompatible/go.mod h1:XO5Pr2SbXgqZwn0m0Ru54QBqpOf4K5AYBO+8LAOBQEQ=

View File

@ -7,8 +7,8 @@ import (
"sync"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
"golang.org/x/sync/errgroup"
@ -48,7 +48,7 @@ type MuxerConfig struct {
// The minimum number of heartbeats to send before terminating the connection.
MaxHeartbeats uint64
// Logger to use
Logger *log.Entry
Logger logger.Service
CompressionQuality CompressionSetting
// Initial size for HTTP2 flow control windows
DefaultWindowSize uint32
@ -136,10 +136,10 @@ func Handshake(
handshakeSetting := http2.Setting{ID: SettingMuxerMagic, Val: MuxerMagicEdge}
compressionSetting := http2.Setting{ID: SettingCompression, Val: config.CompressionQuality.toH2Setting()}
if CompressionIsSupported() {
log.Debug("Compression is supported")
config.Logger.Debug("muxer: Compression is supported")
m.compressionQuality = config.CompressionQuality.getPreset()
} else {
log.Debug("Compression is not supported")
config.Logger.Debug("muxer: Compression is not supported")
compressionSetting = http2.Setting{ID: SettingCompression, Val: 0}
}
@ -176,12 +176,12 @@ func Handshake(
// Sanity check to enusre idelDuration is sane
if idleDuration == 0 || idleDuration < defaultTimeout {
idleDuration = defaultTimeout
config.Logger.Warn("Minimum idle time has been adjusted to ", defaultTimeout)
config.Logger.Infof("muxer: Minimum idle time has been adjusted to %d", defaultTimeout)
}
maxRetries := config.MaxHeartbeats
if maxRetries == 0 {
maxRetries = defaultRetries
config.Logger.Warn("Minimum number of unacked heartbeats to send before closing the connection has been adjusted to ", maxRetries)
config.Logger.Infof("muxer: Minimum number of unacked heartbeats to send before closing the connection has been adjusted to %d", maxRetries)
}
compBytesBefore, compBytesAfter := NewAtomicCounter(0), NewAtomicCounter(0)
@ -206,9 +206,9 @@ func Handshake(
initialStreamWindow: m.config.DefaultWindowSize,
streamWindowMax: m.config.MaxWindowSize,
streamWriteBufferMaxLen: m.config.StreamWriteBufferMaxLen,
r: m.r,
metricsUpdater: m.muxMetricsUpdater,
bytesRead: inBoundCounter,
r: m.r,
metricsUpdater: m.muxMetricsUpdater,
bytesRead: inBoundCounter,
}
m.muxWriter = &MuxWriter{
f: m.f,
@ -321,24 +321,63 @@ func joinErrorsWithTimeout(errChan <-chan error, receiveCount int, timeout time.
func (m *Muxer) Serve(ctx context.Context) error {
errGroup, _ := errgroup.WithContext(ctx)
errGroup.Go(func() error {
err := m.muxReader.run(m.config.Logger)
m.explicitShutdown.Fuse(false)
m.r.Close()
m.abort()
return err
ch := make(chan error)
go func() {
err := m.muxReader.run(m.config.Logger)
m.explicitShutdown.Fuse(false)
m.r.Close()
m.abort()
// don't block if parent goroutine quit early
select {
case ch <- err:
default:
}
}()
select {
case err := <-ch:
return err
case <-ctx.Done():
return ctx.Err()
}
})
errGroup.Go(func() error {
err := m.muxWriter.run(m.config.Logger)
m.explicitShutdown.Fuse(false)
m.w.Close()
m.abort()
return err
ch := make(chan error)
go func() {
err := m.muxWriter.run(m.config.Logger)
m.explicitShutdown.Fuse(false)
m.w.Close()
m.abort()
// don't block if parent goroutine quit early
select {
case ch <- err:
default:
}
}()
select {
case err := <-ch:
return err
case <-ctx.Done():
return ctx.Err()
}
})
errGroup.Go(func() error {
err := m.muxMetricsUpdater.run(m.config.Logger)
return err
ch := make(chan error)
go func() {
err := m.muxMetricsUpdater.run(m.config.Logger)
// don't block if parent goroutine quit early
select {
case ch <- err:
default:
}
}()
select {
case err := <-ch:
return err
case <-ctx.Done():
return ctx.Err()
}
})
err := errGroup.Wait()

View File

@ -15,8 +15,8 @@ import (
"testing"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup"
)
@ -28,7 +28,7 @@ const (
func TestMain(m *testing.M) {
if os.Getenv("VERBOSE") == "1" {
log.SetLevel(log.DebugLevel)
//TODO: set log level
}
os.Exit(m.Run())
}
@ -51,7 +51,7 @@ func NewDefaultMuxerPair(t assert.TestingT, testName string, f MuxedStreamFunc)
Handler: f,
IsClient: true,
Name: "origin",
Logger: log.NewEntry(log.New()),
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
DefaultWindowSize: (1 << 8) - 1,
MaxWindowSize: (1 << 15) - 1,
StreamWriteBufferMaxLen: 1024,
@ -63,7 +63,7 @@ func NewDefaultMuxerPair(t assert.TestingT, testName string, f MuxedStreamFunc)
Timeout: testHandshakeTimeout,
IsClient: false,
Name: "edge",
Logger: log.NewEntry(log.New()),
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
DefaultWindowSize: (1 << 8) - 1,
MaxWindowSize: (1 << 15) - 1,
StreamWriteBufferMaxLen: 1024,
@ -86,7 +86,7 @@ func NewCompressedMuxerPair(t assert.TestingT, testName string, quality Compress
IsClient: true,
Name: "origin",
CompressionQuality: quality,
Logger: log.NewEntry(log.New()),
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
HeartbeatInterval: defaultTimeout,
MaxHeartbeats: defaultRetries,
},
@ -96,7 +96,7 @@ func NewCompressedMuxerPair(t assert.TestingT, testName string, quality Compress
IsClient: false,
Name: "edge",
CompressionQuality: quality,
Logger: log.NewEntry(log.New()),
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
HeartbeatInterval: defaultTimeout,
MaxHeartbeats: defaultRetries,
},
@ -301,6 +301,7 @@ func TestSingleStreamLargeResponseBody(t *testing.T) {
}
func TestMultipleStreams(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
f := MuxedStreamFunc(func(stream *MuxedStream) error {
if len(stream.Headers) != 1 {
t.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
@ -308,13 +309,13 @@ func TestMultipleStreams(t *testing.T) {
if stream.Headers[0].Name != "client-token" {
t.Fatalf("expected header name %s, got %s", "client-token", stream.Headers[0].Name)
}
log.Debugf("Got request for stream %s", stream.Headers[0].Value)
l.Debugf("Got request for stream %s", stream.Headers[0].Value)
stream.WriteHeaders([]Header{
{Name: "response-token", Value: stream.Headers[0].Value},
})
log.Debugf("Wrote headers for stream %s", stream.Headers[0].Value)
l.Debugf("Wrote headers for stream %s", stream.Headers[0].Value)
stream.Write([]byte("OK"))
log.Debugf("Wrote body for stream %s", stream.Headers[0].Value)
l.Debugf("Wrote body for stream %s", stream.Headers[0].Value)
return nil
})
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
@ -332,7 +333,7 @@ func TestMultipleStreams(t *testing.T) {
[]Header{{Name: "client-token", Value: tokenString}},
nil,
)
log.Debugf("Got headers for stream %d", tokenId)
l.Debugf("Got headers for stream %d", tokenId)
if err != nil {
errorsC <- err
return
@ -370,7 +371,7 @@ func TestMultipleStreams(t *testing.T) {
testFail := false
for err := range errorsC {
testFail = true
log.Error(err)
l.Errorf("%s", err)
}
if testFail {
t.Fatalf("TestMultipleStreams failed")
@ -448,6 +449,8 @@ func TestMultipleStreamsFlowControl(t *testing.T) {
}
func TestGracefulShutdown(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
sendC := make(chan struct{})
responseBuf := bytes.Repeat([]byte("Hello world"), 65536)
@ -456,17 +459,17 @@ func TestGracefulShutdown(t *testing.T) {
{Name: "response-header", Value: "responseValue"},
})
<-sendC
log.Debugf("Writing %d bytes", len(responseBuf))
l.Debugf("Writing %d bytes", len(responseBuf))
stream.Write(responseBuf)
stream.CloseWrite()
log.Debugf("Wrote %d bytes", len(responseBuf))
l.Debugf("Wrote %d bytes", len(responseBuf))
// Reading from the stream will block until the edge closes its end of the stream.
// Otherwise, we'll close the whole connection before receiving the 'stream closed'
// message from the edge.
// Graceful shutdown works if you omit this, it just gives spurious errors for now -
// TODO ignore errors when writing 'stream closed' and we're shutting down.
stream.Read([]byte{0})
log.Debugf("Handler ends")
l.Debugf("Handler ends")
return nil
})
muxPair := NewDefaultMuxerPair(t, t.Name(), f)
@ -483,7 +486,7 @@ func TestGracefulShutdown(t *testing.T) {
muxPair.EdgeMux.Shutdown()
close(sendC)
responseBody := make([]byte, len(responseBuf))
log.Debugf("Waiting for %d bytes", len(responseBuf))
l.Debugf("Waiting for %d bytes", len(responseBuf))
n, err := io.ReadFull(stream, responseBody)
if err != nil {
t.Fatalf("error from (*MuxedStream).Read with %d bytes read: %s", n, err)
@ -676,6 +679,7 @@ func AssertIfPipeReadable(t *testing.T, pipe io.ReadCloser) {
}
func TestMultipleStreamsWithDictionaries(t *testing.T) {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
for q := CompressionNone; q <= CompressionMax; q++ {
htmlBody := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"` +
@ -812,7 +816,7 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
testFail := false
for err := range errorsC {
testFail = true
log.Error(err)
l.Errorf("%s", err)
}
if testFail {
t.Fatalf("TestMultipleStreams failed")
@ -826,6 +830,8 @@ func TestMultipleStreamsWithDictionaries(t *testing.T) {
}
func sampleSiteHandler(files map[string][]byte) MuxedStreamFunc {
l := logger.NewOutputWriter(logger.NewMockWriteManager())
return func(stream *MuxedStream) error {
var contentType string
var pathHeader Header
@ -853,13 +859,13 @@ func sampleSiteHandler(files map[string][]byte) MuxedStreamFunc {
stream.WriteHeaders([]Header{
Header{Name: "content-type", Value: contentType},
})
log.Debugf("Wrote headers for stream %s", pathHeader.Value)
l.Debugf("Wrote headers for stream %s", pathHeader.Value)
file, ok := files[pathHeader.Value]
if !ok {
return fmt.Errorf("%s content is not preloaded", pathHeader.Value)
}
stream.Write(file)
log.Debugf("Wrote body for stream %s", pathHeader.Value)
l.Debugf("Wrote body for stream %s", pathHeader.Value)
return nil
}
}

View File

@ -1,14 +1,15 @@
package h2mux
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/pkg/errors"
)
type Header struct {
@ -17,12 +18,31 @@ type Header struct {
var headerEncoding = base64.RawStdEncoding
// H2RequestHeadersToH1Request converts the HTTP/2 headers to an HTTP/1 Request
// object. This includes conversion of the pseudo-headers into their closest
const (
RequestUserHeadersField = "cf-cloudflared-request-headers"
ResponseUserHeadersField = "cf-cloudflared-response-headers"
ResponseMetaHeaderField = "cf-cloudflared-response-meta"
ResponseSourceCloudflared = "cloudflared"
ResponseSourceOrigin = "origin"
CFAccessTokenHeader = "cf-access-token"
CFJumpDestinationHeader = "CF-Access-Jump-Destination"
CFAccessClientIDHeader = "CF-Access-Client-Id"
CFAccessClientSecretHeader = "CF-Access-Client-Secret"
)
// H2RequestHeadersToH1Request converts the HTTP/2 headers coming from origintunneld
// to an HTTP/1 Request object destined for the local origin web service.
// This operation includes conversion of the pseudo-headers into their closest
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
for _, header := range h2 {
switch header.Name {
if !IsControlHeader(header.Name) {
continue
}
switch strings.ToLower(header.Name) {
case ":method":
h1.Method = header.Value
case ":scheme":
@ -55,40 +75,100 @@ func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
base := h1.URL.String()
base = strings.TrimRight(base, "/")
// But we know :path begins with '/', because we handled '*' above - see RFC7540
url, err := url.Parse(base + header.Value)
requestURL, err := url.Parse(base + header.Value)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value))
}
h1.URL = url
h1.URL = requestURL
case "content-length":
contentLength, err := strconv.ParseInt(header.Value, 10, 64)
if err != nil {
return fmt.Errorf("unparseable content length")
}
h1.ContentLength = contentLength
case RequestUserHeadersField:
// Do not forward the serialized headers to the origin -- deserialize them, and ditch the serialized version
// Find and parse user headers serialized into a single one
userHeaders, err := ParseUserHeaders(RequestUserHeadersField, h2)
if err != nil {
return errors.Wrap(err, "Unable to parse user headers")
}
for _, userHeader := range userHeaders {
h1.Header.Add(http.CanonicalHeaderKey(userHeader.Name), userHeader.Value)
}
default:
// All other control headers shall just be proxied transparently
h1.Header.Add(http.CanonicalHeaderKey(header.Name), header.Value)
}
}
return nil
}
func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
h2 = []Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}}
for headerName, headerValues := range h1.Header {
for _, headerValue := range headerValues {
h2 = append(h2, Header{Name: strings.ToLower(headerName), Value: headerValue})
func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header, error) {
for _, header := range headers {
if header.Name == headerNameToParseFrom {
return DeserializeHeaders(header.Value)
}
}
return nil, fmt.Errorf("%v header not found", RequestUserHeadersField)
}
func IsControlHeader(headerName string) bool {
headerName = strings.ToLower(headerName)
return headerName == "content-length" ||
headerName == "connection" || headerName == "upgrade" || // Websocket headers
strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-")
}
// IsWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
func IsWebsocketClientHeader(headerName string) bool {
headerName = strings.ToLower(headerName)
return headerName == "sec-websocket-accept" ||
headerName == "connection" ||
headerName == "upgrade"
}
func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
h2 = []Header{
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
}
userHeaders := http.Header{}
for header, values := range h1.Header {
for _, value := range values {
if strings.ToLower(header) == "content-length" {
// This header has meaning in HTTP/2 and will be used by the edge,
// so it should be sent as an HTTP/2 response header.
// Since these are http2 headers, they're required to be lowercase
h2 = append(h2, Header{Name: strings.ToLower(header), Value: value})
} else if !IsControlHeader(header) || IsWebsocketClientHeader(header) {
// User headers, on the other hand, must all be serialized so that
// HTTP/2 header validation won't be applied to HTTP/1 header values
if _, ok := userHeaders[header]; ok {
userHeaders[header] = append(userHeaders[header], value)
} else {
userHeaders[header] = []string{value}
}
}
}
}
// Perform user header serialization and set them in the single header
h2 = append(h2, CreateSerializedHeaders(ResponseUserHeadersField, userHeaders)...)
return h2
}
// Serialize HTTP1.x headers by base64-encoding each header name and value,
// and then joining them in the format of [key:value;]
func SerializeHeaders(h1 *http.Request) []byte {
var serializedHeaders [][]byte
for headerName, headerValues := range h1.Header {
func SerializeHeaders(h1Headers http.Header) string {
var serializedHeaders []string
for headerName, headerValues := range h1Headers {
for _, headerValue := range headerValues {
encodedName := make([]byte, headerEncoding.EncodedLen(len(headerName)))
headerEncoding.Encode(encodedName, []byte(headerName))
@ -98,28 +178,28 @@ func SerializeHeaders(h1 *http.Request) []byte {
serializedHeaders = append(
serializedHeaders,
bytes.Join(
[][]byte{encodedName, encodedValue},
[]byte(":"),
strings.Join(
[]string{string(encodedName), string(encodedValue)},
":",
),
)
}
}
return bytes.Join(serializedHeaders, []byte(";"))
return strings.Join(serializedHeaders, ";")
}
// Deserialize headers serialized by `SerializeHeader`
func DeserializeHeaders(serializedHeaders []byte) (http.Header, error) {
func DeserializeHeaders(serializedHeaders string) ([]Header, error) {
const unableToDeserializeErr = "Unable to deserialize headers"
deserialized := http.Header{}
for _, serializedPair := range bytes.Split(serializedHeaders, []byte(";")) {
var deserialized []Header
for _, serializedPair := range strings.Split(serializedHeaders, ";") {
if len(serializedPair) == 0 {
continue
}
serializedHeaderParts := bytes.Split(serializedPair, []byte(":"))
serializedHeaderParts := strings.Split(serializedPair, ":")
if len(serializedHeaderParts) != 2 {
return nil, errors.New(unableToDeserializeErr)
}
@ -129,15 +209,46 @@ func DeserializeHeaders(serializedHeaders []byte) (http.Header, error) {
deserializedName := make([]byte, headerEncoding.DecodedLen(len(serializedName)))
deserializedValue := make([]byte, headerEncoding.DecodedLen(len(serializedValue)))
if _, err := headerEncoding.Decode(deserializedName, serializedName); err != nil {
if _, err := headerEncoding.Decode(deserializedName, []byte(serializedName)); err != nil {
return nil, errors.Wrap(err, unableToDeserializeErr)
}
if _, err := headerEncoding.Decode(deserializedValue, serializedValue); err != nil {
if _, err := headerEncoding.Decode(deserializedValue, []byte(serializedValue)); err != nil {
return nil, errors.Wrap(err, unableToDeserializeErr)
}
deserialized.Add(string(deserializedName), string(deserializedValue))
deserialized = append(deserialized, Header{
Name: string(deserializedName),
Value: string(deserializedValue),
})
}
return deserialized, nil
}
func CreateSerializedHeaders(headersField string, headers ...http.Header) []Header {
var serializedHeaderChunks []string
for _, headerChunk := range headers {
serializedHeaderChunks = append(serializedHeaderChunks, SerializeHeaders(headerChunk))
}
return []Header{{
headersField,
strings.Join(serializedHeaderChunks, ";"),
}}
}
type ResponseMetaHeader struct {
Source string `json:"src"`
}
func CreateResponseMetaHeader(headerName, source string) Header {
jsonResponseMetaHeader, err := json.Marshal(ResponseMetaHeader{Source: source})
if err != nil {
panic(err)
}
return Header{
Name: headerName,
Value: string(jsonResponseMetaHeader),
}
}

View File

@ -7,6 +7,7 @@ import (
"net/url"
"reflect"
"regexp"
"sort"
"strings"
"testing"
"testing/quick"
@ -15,29 +16,30 @@ import (
"github.com/stretchr/testify/require"
)
type ByName []Header
func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool {
if a[i].Name == a[j].Name {
return a[i].Value < a[j].Value
}
return a[i].Name < a[j].Name
}
func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
headersConversionErr := H2RequestHeadersToH1Request(
[]Header{
{
Name: "Mock header 1",
Value: "Mock value 1",
},
{
Name: "Mock header 2",
Value: "Mock value 2",
},
},
request,
)
mockHeaders := http.Header{
"Mock header 1": {"Mock value 1"},
"Mock header 2": {"Mock value 2"},
}
assert.Equal(t, http.Header{
"Mock header 1": []string{"Mock value 1"},
"Mock header 2": []string{"Mock value 2"},
}, request.Header)
headersConversionErr := H2RequestHeadersToH1Request(CreateSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
assert.NoError(t, headersConversionErr)
}
@ -46,12 +48,14 @@ func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
assert.NoError(t, err)
headersConversionErr := H2RequestHeadersToH1Request(
[]Header{},
[]Header{{
RequestUserHeadersField,
SerializeHeaders(http.Header{}),
}},
request,
)
assert.Equal(t, http.Header{}, request.Header)
assert.True(t, reflect.DeepEqual(http.Header{}, request.Header))
assert.NoError(t, headersConversionErr)
}
@ -59,19 +63,12 @@ func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
headersConversionErr := H2RequestHeadersToH1Request(
[]Header{
{
Name: ":path",
Value: "//bad_path/",
},
{
Name: "Mock header",
Value: "Mock value",
},
},
request,
)
mockRequestHeaders := []Header{
{Name: ":path", Value: "//bad_path/"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
@ -86,19 +83,12 @@ func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
assert.NoError(t, err)
headersConversionErr := H2RequestHeadersToH1Request(
[]Header{
{
Name: ":path",
Value: "/?query=mock%20value",
},
{
Name: "Mock header",
Value: "Mock value",
},
},
request,
)
mockRequestHeaders := []Header{
{Name: ":path", Value: "/?query=mock%20value"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
@ -113,19 +103,12 @@ func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil)
assert.NoError(t, err)
headersConversionErr := H2RequestHeadersToH1Request(
[]Header{
{
Name: ":path",
Value: "/mock%20path",
},
{
Name: "Mock header",
Value: "Mock value",
},
},
request,
)
mockRequestHeaders := []Header{
{Name: ":path", Value: "/mock%20path"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.Equal(t, http.Header{
"Mock header": []string{"Mock value"},
@ -276,19 +259,13 @@ func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, requestURL, nil)
assert.NoError(t, err)
headersConversionErr := H2RequestHeadersToH1Request(
[]Header{
{
Name: ":path",
Value: testCase.path,
},
{
Name: "Mock header",
Value: "Mock value",
},
},
request,
)
mockRequestHeaders := []Header{
{Name: ":path", Value: testCase.path},
{Name: RequestUserHeadersField, Value: SerializeHeaders(http.Header{"Mock header": {"Mock value"}})},
}
headersConversionErr := H2RequestHeadersToH1Request(mockRequestHeaders, request)
assert.NoError(t, headersConversionErr)
assert.Equal(t,
@ -358,6 +335,7 @@ func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) {
{Name: ":scheme", Value: testScheme},
{Name: ":authority", Value: expectedHostname},
{Name: ":path", Value: testPath},
{Name: RequestUserHeadersField, Value: ""},
}
h1, err := http.NewRequest("GET", testOrigin.url, nil)
require.NoError(t, err)
@ -439,11 +417,21 @@ func randomHTTP2Path(t *testing.T, rand *rand.Rand) string {
return result
}
func stdlibHeaderToH2muxHeader(headers http.Header) (h2muxHeaders []Header) {
for name, values := range headers {
for _, value := range values {
h2muxHeaders = append(h2muxHeaders, Header{name, value})
}
}
return h2muxHeaders
}
func TestSerializeHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
mockHeaders := map[string][]string{
mockHeaders := http.Header{
"Mock-Header-One": {"Mock header one value", "three"},
"Mock-Header-Two-Long": {"Mock header two value\nlong"},
":;": {":;", ";:"},
@ -465,7 +453,7 @@ func TestSerializeHeaders(t *testing.T) {
}
}
serializedHeaders := SerializeHeaders(request)
serializedHeaders := SerializeHeaders(request.Header)
// Sanity check: the headers serialized to something that's not an empty string
assert.NotEqual(t, "", serializedHeaders)
@ -474,18 +462,24 @@ func TestSerializeHeaders(t *testing.T) {
deserializedHeaders, err := DeserializeHeaders(serializedHeaders)
assert.NoError(t, err)
assert.Equal(t, len(mockHeaders), len(deserializedHeaders))
for header, value := range deserializedHeaders {
assert.NotEqual(t, "", value)
assert.Equal(t, mockHeaders[header], value)
}
assert.Equal(t, 13, len(deserializedHeaders))
h2muxExpectedHeaders := stdlibHeaderToH2muxHeader(mockHeaders)
sort.Sort(ByName(deserializedHeaders))
sort.Sort(ByName(h2muxExpectedHeaders))
assert.True(
t,
reflect.DeepEqual(h2muxExpectedHeaders, deserializedHeaders),
fmt.Sprintf("got = %#v, want = %#v\n", deserializedHeaders, h2muxExpectedHeaders),
)
}
func TestSerializeNoHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err)
serializedHeaders := SerializeHeaders(request)
serializedHeaders := SerializeHeaders(request.Header)
deserializedHeaders, err := DeserializeHeaders(serializedHeaders)
assert.NoError(t, err)
assert.Equal(t, 0, len(deserializedHeaders))
@ -502,7 +496,157 @@ func TestDeserializeMalformed(t *testing.T) {
}
for _, malformedValue := range malformedData {
_, err = DeserializeHeaders([]byte(malformedValue))
_, err = DeserializeHeaders(malformedValue)
assert.Error(t, err)
}
}
func TestParseHeaders(t *testing.T) {
mockUserHeadersToSerialize := http.Header{
"Mock-Header-One": {"1", "1.5"},
"Mock-Header-Two": {"2"},
"Mock-Header-Three": {"3"},
}
mockHeaders := []Header{
{Name: "One", Value: "1"},
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
}
expectedHeaders := []Header{
{Name: "Mock-Header-One", Value: "1"},
{Name: "Mock-Header-One", Value: "1.5"},
{Name: "Mock-Header-Two", Value: "2"},
{Name: "Mock-Header-Three", Value: "3"},
}
parsedHeaders, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
assert.NoError(t, err)
assert.ElementsMatch(t, expectedHeaders, parsedHeaders)
}
func TestParseHeadersNoSerializedHeader(t *testing.T) {
mockHeaders := []Header{
{Name: "One", Value: "1"},
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
}
_, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
assert.EqualError(t, err, fmt.Sprintf("%s header not found", RequestUserHeadersField))
}
func TestIsControlHeader(t *testing.T) {
controlHeaders := []string{
// Anything that begins with cf-
"cf-sample-header",
"CF-SAMPLE-HEADER",
"Cf-Sample-Header",
// Any http2 pseudoheader
":sample-pseudo-header",
// content-length is a special case, it has to be there
// for some requests to work (per the HTTP2 spec)
"content-length",
}
for _, header := range controlHeaders {
assert.True(t, IsControlHeader(header))
}
}
func TestIsNotControlHeader(t *testing.T) {
notControlHeaders := []string{
"Mock-header",
"Another-sample-header",
}
for _, header := range notControlHeaders {
assert.False(t, IsControlHeader(header))
}
}
func TestH1ResponseToH2ResponseHeaders(t *testing.T) {
mockHeaders := http.Header{
"User-header-one": {""},
"User-header-two": {"1", "2"},
"cf-header": {"cf-value"},
"Content-Length": {"123"},
}
mockResponse := http.Response{
StatusCode: 200,
Header: mockHeaders,
}
headers := H1ResponseToH2ResponseHeaders(&mockResponse)
serializedHeadersIndex := -1
for i, header := range headers {
if header.Name == ResponseUserHeadersField {
serializedHeadersIndex = i
break
}
}
assert.NotEqual(t, -1, serializedHeadersIndex)
actualControlHeaders := append(
headers[:serializedHeadersIndex],
headers[serializedHeadersIndex+1:]...,
)
expectedControlHeaders := []Header{
{Name: ":status", Value: "200"},
{Name: "content-length", Value: "123"},
}
assert.ElementsMatch(t, expectedControlHeaders, actualControlHeaders)
actualUserHeaders, err := DeserializeHeaders(headers[serializedHeadersIndex].Value)
expectedUserHeaders := []Header{
{Name: "User-header-one", Value: ""},
{Name: "User-header-two", Value: "1"},
{Name: "User-header-two", Value: "2"},
}
assert.NoError(t, err)
assert.ElementsMatch(t, expectedUserHeaders, actualUserHeaders)
}
// The purpose of this test is to check that our code and the http.Header
// implementation don't throw validation errors about header size
func TestHeaderSize(t *testing.T) {
largeValue := randSeq(5 * 1024 * 1024) // 5Mb
largeHeaders := http.Header{
"User-header": {largeValue},
}
mockResponse := http.Response{
StatusCode: 200,
Header: largeHeaders,
}
serializedHeaders := H1ResponseToH2ResponseHeaders(&mockResponse)
request, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
assert.NoError(t, err)
for _, header := range serializedHeaders {
request.Header.Set(header.Name, header.Value)
}
for _, header := range serializedHeaders {
if header.Name != ResponseUserHeadersField {
continue
}
deserializedHeaders, err := DeserializeHeaders(header.Value)
assert.NoError(t, err)
assert.Equal(t, largeValue, deserializedHeaders[0].Value)
}
}
func randSeq(n int) string {
randomizer := rand.New(rand.NewSource(17))
var letters = []rune(":;,+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[randomizer.Intn(len(letters))]
}
return string(b)
}

View File

@ -4,10 +4,9 @@ import (
"sync"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/golang-collections/collections/queue"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
)
// data points used to compute average receive window and send window size
@ -22,7 +21,7 @@ type muxMetricsUpdater interface {
// metrics returns the latest metrics
metrics() *MuxerMetrics
// run is a blocking call to start the event loop
run(logger *log.Entry) error
run(logger logger.Service) error
// updateRTTChan is called by muxReader to report new RTT measurements
updateRTT(rtt *roundTripMeasurement)
//updateReceiveWindowChan is called by muxReader and muxWriter when receiveWindow size is updated
@ -139,34 +138,30 @@ func (updater *muxMetricsUpdaterImpl) metrics() *MuxerMetrics {
return m
}
func (updater *muxMetricsUpdaterImpl) run(parentLogger *log.Entry) error {
logger := parentLogger.WithFields(log.Fields{
"subsystem": "mux",
"dir": "metrics",
})
defer logger.Debug("event loop finished")
func (updater *muxMetricsUpdaterImpl) run(logger logger.Service) error {
defer logger.Debug("mux - metrics: event loop finished")
for {
select {
case <-updater.abortChan:
logger.Infof("Stopping mux metrics updater")
logger.Infof("mux - metrics: Stopping mux metrics updater")
return nil
case roundTripMeasurement := <-updater.updateRTTChan:
go updater.rttData.update(roundTripMeasurement)
logger.Debug("Update rtt")
logger.Debug("mux - metrics: Update rtt")
case receiveWindow := <-updater.updateReceiveWindowChan:
go updater.receiveWindowData.update(receiveWindow)
logger.Debug("Update receive window")
logger.Debug("mux - metrics: Update receive window")
case sendWindow := <-updater.updateSendWindowChan:
go updater.sendWindowData.update(sendWindow)
logger.Debug("Update send window")
logger.Debug("mux - metrics: Update send window")
case inBoundBytes := <-updater.updateInBoundBytesChan:
// inBoundBytes is bytes/sec because the update interval is 1 sec
go updater.inBoundRate.update(inBoundBytes)
logger.Debugf("Inbound bytes %d", inBoundBytes)
logger.Debugf("mux - metrics: Inbound bytes %d", inBoundBytes)
case outBoundBytes := <-updater.updateOutBoundBytesChan:
// outBoundBytes is bytes/sec because the update interval is 1 sec
go updater.outBoundRate.update(outBoundBytes)
logger.Debugf("Outbound bytes %d", outBoundBytes)
logger.Debugf("mux - metrics: Outbound bytes %d", outBoundBytes)
}
}
}

View File

@ -5,7 +5,7 @@ import (
"testing"
"time"
log "github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
"github.com/stretchr/testify/assert"
)
@ -91,7 +91,7 @@ func TestMuxMetricsUpdater(t *testing.T) {
abortChan := make(chan struct{})
compBefore, compAfter := NewAtomicCounter(0), NewAtomicCounter(0)
m := newMuxMetricsUpdater(abortChan, compBefore, compAfter)
logger := log.NewEntry(log.New())
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
go func() {
errChan <- m.run(logger)

View File

@ -3,11 +3,12 @@ package h2mux
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"net/url"
"time"
log "github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
"golang.org/x/net/http2"
)
@ -67,12 +68,8 @@ func (r *MuxReader) Shutdown() <-chan struct{} {
return done
}
func (r *MuxReader) run(parentLogger *log.Entry) error {
logger := parentLogger.WithFields(log.Fields{
"subsystem": "mux",
"dir": "read",
})
defer logger.Debug("event loop finished")
func (r *MuxReader) run(logger logger.Service) error {
defer logger.Debug("mux - read: event loop finished")
// routine to periodically update bytesRead
go func() {
@ -90,13 +87,13 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
for {
frame, err := r.f.ReadFrame()
if err != nil {
errLogger := logger.WithError(err)
errorString := fmt.Sprintf("mux - read: %s", err)
if errorDetail := r.f.ErrorDetail(); errorDetail != nil {
errLogger = errLogger.WithField("errorDetail", errorDetail)
errorString = fmt.Sprintf("%s: errorDetail: %s", errorString, errorDetail)
}
switch e := err.(type) {
case http2.StreamError:
errLogger.Warn("stream error")
logger.Infof("%s: stream error", errorString)
// Ideally we wouldn't return here, since that aborts the muxer.
// We should communicate the error to the relevant MuxedStream
// data structure, so that callers of MuxedStream.Read() and
@ -104,25 +101,25 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
// and keep the muxer going.
return r.streamError(e.StreamID, e.Code)
case http2.ConnectionError:
errLogger.Warn("connection error")
logger.Infof("%s: stream error", errorString)
return r.connectionError(err)
default:
if isConnectionClosedError(err) {
if r.streams.Len() == 0 {
// don't log the error here -- that would just be extra noise
logger.Debug("shutting down")
logger.Debug("mux - read: shutting down")
return nil
}
errLogger.Warn("connection closed unexpectedly")
logger.Infof("%s: connection closed unexpectedly", errorString)
return err
} else {
errLogger.Warn("frame read error")
logger.Infof("%s: frame read error", errorString)
return r.connectionError(err)
}
}
}
r.connActive.Signal()
logger.WithField("data", frame).Debug("read frame")
logger.Debugf("mux - read: read frame: data %v", frame)
switch f := frame.(type) {
case *http2.DataFrame:
err = r.receiveFrameData(f, logger)
@ -158,7 +155,7 @@ func (r *MuxReader) run(parentLogger *log.Entry) error {
err = ErrUnexpectedFrameType
}
if err != nil {
logger.WithField("data", frame).WithError(err).Debug("frame error")
logger.Debugf("mux - read: read error: data %v", frame)
return r.connectionError(err)
}
}
@ -279,8 +276,7 @@ func (r *MuxReader) handleStream(stream *MuxedStream) {
}
// Receives a data frame from a stream. A non-nil error is a connection error.
func (r *MuxReader) receiveFrameData(frame *http2.DataFrame, parentLogger *log.Entry) error {
logger := parentLogger.WithField("stream", frame.Header().StreamID)
func (r *MuxReader) receiveFrameData(frame *http2.DataFrame, logger logger.Service) error {
stream, err := r.getStreamForFrame(frame)
if err != nil {
return r.defaultStreamErrorHandler(err, frame.Header())
@ -296,9 +292,9 @@ func (r *MuxReader) receiveFrameData(frame *http2.DataFrame, parentLogger *log.E
if frame.Header().Flags.Has(http2.FlagDataEndStream) {
if stream.receiveEOF() {
r.streams.Delete(stream.streamID)
logger.Debug("stream closed")
logger.Debugf("mux - read: stream closed: streamID: %d", frame.Header().StreamID)
} else {
logger.Debug("shutdown receive side")
logger.Debugf("mux - read: shutdown receive side: streamID: %d", frame.Header().StreamID)
}
return nil
}

View File

@ -6,7 +6,7 @@ import (
"io"
"time"
log "github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/logger"
"golang.org/x/net/http2"
"golang.org/x/net/http2/hpack"
)
@ -72,12 +72,8 @@ func tsToPingData(ts int64) [8]byte {
return pingData
}
func (w *MuxWriter) run(parentLogger *log.Entry) error {
logger := parentLogger.WithFields(log.Fields{
"subsystem": "mux",
"dir": "write",
})
defer logger.Debug("event loop finished")
func (w *MuxWriter) run(logger logger.Service) error {
defer logger.Debug("mux - write: event loop finished")
// routine to periodically communicate bytesWrote
go func() {
@ -95,17 +91,17 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
for {
select {
case <-w.abortChan:
logger.Debug("aborting writer thread")
logger.Debug("mux - write: aborting writer thread")
return nil
case errCode := <-w.goAwayChan:
logger.Debug("sending GOAWAY code ", errCode)
logger.Debugf("mux - write: sending GOAWAY code %v", errCode)
err := w.f.WriteGoAway(w.streams.LastPeerStreamID(), errCode, []byte{})
if err != nil {
return err
}
w.idleTimer.MarkActive()
case <-w.pingTimestamp.GetUpdateChan():
logger.Debug("sending PING ACK")
logger.Debug("mux - write: sending PING ACK")
err := w.f.WritePing(true, tsToPingData(w.pingTimestamp.Get()))
if err != nil {
return err
@ -115,7 +111,7 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
if !w.idleTimer.Retry() {
return ErrConnectionDropped
}
logger.Debug("sending PING")
logger.Debug("mux - write: sending PING")
err := w.f.WritePing(false, tsToPingData(time.Now().UnixNano()))
if err != nil {
return err
@ -125,7 +121,7 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
w.idleTimer.MarkActive()
case <-w.streamErrors.GetSignalChan():
for streamID, errCode := range w.streamErrors.GetErrors() {
logger.WithField("stream", streamID).WithField("code", errCode).Debug("resetting stream")
logger.Debugf("mux - write: resetting stream with code: %v streamID: %d", errCode, streamID)
err := w.f.WriteRSTStream(streamID, errCode)
if err != nil {
return err
@ -145,19 +141,17 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
if streamRequest.body != nil {
go streamRequest.flushBody()
}
streamLogger := logger.WithField("stream", streamID)
err := w.writeStreamData(streamRequest.stream, streamLogger)
err := w.writeStreamData(streamRequest.stream, logger)
if err != nil {
return err
}
w.idleTimer.MarkActive()
case streamID := <-w.readyStreamChan:
streamLogger := logger.WithField("stream", streamID)
stream, ok := w.streams.Get(streamID)
if !ok {
continue
}
err := w.writeStreamData(stream, streamLogger)
err := w.writeStreamData(stream, logger)
if err != nil {
return err
}
@ -165,7 +159,7 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
case useDict := <-w.useDictChan:
err := w.writeUseDictionary(useDict)
if err != nil {
logger.WithError(err).Warn("error writing use dictionary")
logger.Errorf("mux - write: error writing use dictionary: %s", err)
return err
}
w.idleTimer.MarkActive()
@ -173,18 +167,18 @@ func (w *MuxWriter) run(parentLogger *log.Entry) error {
}
}
func (w *MuxWriter) writeStreamData(stream *MuxedStream, logger *log.Entry) error {
logger.Debug("writable")
func (w *MuxWriter) writeStreamData(stream *MuxedStream, logger logger.Service) error {
logger.Debugf("mux - write: writable: streamID: %d", stream.streamID)
chunk := stream.getChunk()
w.metricsUpdater.updateReceiveWindow(stream.getReceiveWindow())
w.metricsUpdater.updateSendWindow(stream.getSendWindow())
if chunk.sendHeadersFrame() {
err := w.writeHeaders(chunk.streamID, chunk.headers)
if err != nil {
logger.WithError(err).Warn("error writing headers")
logger.Errorf("mux - write: error writing headers: %s: streamID: %d", err, stream.streamID)
return err
}
logger.Debug("output headers")
logger.Debugf("mux - write: output headers: streamID: %d", stream.streamID)
}
if chunk.sendWindowUpdateFrame() {
@ -195,22 +189,22 @@ func (w *MuxWriter) writeStreamData(stream *MuxedStream, logger *log.Entry) erro
// window, unless the receiver treats this as a connection error"
err := w.f.WriteWindowUpdate(chunk.streamID, chunk.windowUpdate)
if err != nil {
logger.WithError(err).Warn("error writing window update")
logger.Errorf("mux - write: error writing window update: %s: streamID: %d", err, stream.streamID)
return err
}
logger.Debugf("increment receive window by %d", chunk.windowUpdate)
logger.Debugf("mux - write: increment receive window by %d streamID: %d", chunk.windowUpdate, stream.streamID)
}
for chunk.sendDataFrame() {
payload, sentEOF := chunk.nextDataFrame(int(w.maxFrameSize))
err := w.f.WriteData(chunk.streamID, sentEOF, payload)
if err != nil {
logger.WithError(err).Warn("error writing data")
logger.Errorf("mux - write: error writing data: %s: streamID: %d", err, stream.streamID)
return err
}
// update the amount of data wrote
w.bytesWrote.IncrementBy(uint64(len(payload)))
logger.WithField("len", len(payload)).Debug("output data")
logger.Debugf("mux - write: output data: %d: streamID: %d", len(payload), stream.streamID)
if sentEOF {
if stream.readBuffer.Closed() {
@ -218,15 +212,15 @@ func (w *MuxWriter) writeStreamData(stream *MuxedStream, logger *log.Entry) erro
if !stream.gotReceiveEOF() {
// the peer may send data that we no longer want to receive. Force them into the
// closed state.
logger.Debug("resetting stream")
logger.Debugf("mux - write: resetting stream: streamID: %d", stream.streamID)
w.f.WriteRSTStream(chunk.streamID, http2.ErrCodeNo)
} else {
// Half-open stream transitioned into closed
logger.Debug("closing stream")
logger.Debugf("mux - write: closing stream: streamID: %d", stream.streamID)
}
w.streams.Delete(chunk.streamID)
} else {
logger.Debug("closing stream write side")
logger.Debugf("mux - write: closing stream write side: streamID: %d", stream.streamID)
}
}
}
@ -250,28 +244,52 @@ func (w *MuxWriter) encodeHeaders(headers []Header) ([]byte, error) {
// writeHeaders writes a block of encoded headers, splitting it into multiple frames if necessary.
func (w *MuxWriter) writeHeaders(streamID uint32, headers []Header) error {
encodedHeaders, err := w.encodeHeaders(headers)
if err != nil {
if err != nil || len(encodedHeaders) == 0 {
return err
}
blockSize := int(w.maxFrameSize)
endHeaders := len(encodedHeaders) == 0
for !endHeaders && err == nil {
blockFragment := encodedHeaders
if len(encodedHeaders) > blockSize {
blockFragment = blockFragment[:blockSize]
encodedHeaders = encodedHeaders[blockSize:]
// Send CONTINUATION frame if the headers can't be fit into 1 frame
err = w.f.WriteContinuation(streamID, endHeaders, blockFragment)
} else {
endHeaders = true
err = w.f.WriteHeaders(http2.HeadersFrameParam{
StreamID: streamID,
EndHeaders: endHeaders,
BlockFragment: blockFragment,
})
// CONTINUATION is unnecessary; the headers fit within the blockSize
if len(encodedHeaders) < blockSize {
return w.f.WriteHeaders(http2.HeadersFrameParam{
StreamID: streamID,
EndHeaders: true,
BlockFragment: encodedHeaders,
})
}
choppedHeaders := chopEncodedHeaders(encodedHeaders, blockSize)
// len(choppedHeaders) is at least 2
if err := w.f.WriteHeaders(http2.HeadersFrameParam{StreamID: streamID, EndHeaders: false, BlockFragment: choppedHeaders[0]}); err != nil {
return err
}
for i := 1; i < len(choppedHeaders)-1; i++ {
if err := w.f.WriteContinuation(streamID, false, choppedHeaders[i]); err != nil {
return err
}
}
return err
if err := w.f.WriteContinuation(streamID, true, choppedHeaders[len(choppedHeaders)-1]); err != nil {
return err
}
return nil
}
// Partition a slice of bytes into `len(slice) / blockSize` slices of length `blockSize`
func chopEncodedHeaders(headers []byte, chunkSize int) [][]byte {
var divided [][]byte
for i := 0; i < len(headers); i += chunkSize {
end := i + chunkSize
if end > len(headers) {
end = len(headers)
}
divided = append(divided, headers[i:end])
}
return divided
}
func (w *MuxWriter) writeUseDictionary(dictRequest useDictRequest) error {

26
h2mux/muxwriter_test.go Normal file
View File

@ -0,0 +1,26 @@
package h2mux
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestChopEncodedHeaders(t *testing.T) {
mockEncodedHeaders := make([]byte, 5)
for i := range mockEncodedHeaders {
mockEncodedHeaders[i] = byte(i)
}
chopped := chopEncodedHeaders(mockEncodedHeaders, 4)
assert.Equal(t, 2, len(chopped))
assert.Equal(t, []byte{0, 1, 2, 3}, chopped[0])
assert.Equal(t, []byte{4}, chopped[1])
}
func TestChopEncodedEmptyHeaders(t *testing.T) {
mockEncodedHeaders := make([]byte, 0)
chopped := chopEncodedHeaders(mockEncodedHeaders, 3)
assert.Equal(t, 0, len(chopped))
}

View File

@ -80,7 +80,7 @@ ghost.init({
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
<meta class="swiftype" name="language" data-type="string" content="en" />
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
<script src="https://blog.cloudflare.com/assets/js/index.js"></script>
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
<script>
var trackRecruitingLink = function(role, url) {
@ -128,7 +128,7 @@ HTMLAttrToAdd.setAttribute("lang", "en");
overflow-y: scroll;
}
</style>
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
<link href="https://blog.cloudflare.com/assets/css/screen.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
<style>

View File

@ -113,7 +113,7 @@ ghost.init({
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
<meta class="swiftype" name="language" data-type="string" content="en" />
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
<script src="https://blog.cloudflare.com/assets/js/index.js"></script>
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
<script>
var trackRecruitingLink = function(role, url) {
@ -161,7 +161,7 @@ HTMLAttrToAdd.setAttribute("lang", "en");
overflow-y: scroll;
}
</style>
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
<link href="https://blog.cloudflare.com/assets/css/screen.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
<style>

View File

@ -109,7 +109,7 @@ ghost.init({
<link rel="alternate" type="application/rss+xml" title="Cloudflare Blog" href="https://blog.cloudflare.com/rss/" />
<meta name="msvalidate.01" content="CF295E1604697F9CAD18B5A232E871F6" />
<meta class="swiftype" name="language" data-type="string" content="en" />
<script src="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/js/index.js"></script>
<script src="https://blog.cloudflare.com/assets/js/index.js"></script>
<script type="text/javascript" src="//cdn.bizible.com/scripts/bizible.js" async=""></script>
<script>
var trackRecruitingLink = function(role, url) {
@ -157,7 +157,7 @@ HTMLAttrToAdd.setAttribute("lang", "en");
overflow-y: scroll;
}
</style>
<link href="https://s3-us-west-1.amazonaws.com/cf-ghost-assets-hotfix/css/screen.css" rel="stylesheet">
<link href="https://blog.cloudflare.com/assets/css/screen.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.8.1/themes/prism.min.css" rel="stylesheet">
<style>

View File

@ -12,8 +12,8 @@ import (
"os"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/tlsconfig"
)
@ -56,7 +56,7 @@ const indexTemplate = `
<path class="st2" d="M93.6 12.8h-.5c-.1 0-.2.1-.3.2l-.7 2.4c-.3 1-.2 2 .3 2.6.5.6 1.2 1 2.1 1.1l3.7.2c.1 0 .2.1.3.1.1.1.1.2 0 .3-.1.2-.2.3-.4.3l-3.8.2c-2.1.1-4.3 1.8-5.1 3.8l-.2.9c-.1.1 0 .3.2.3h13.2c.2 0 .3-.1.3-.3.2-.8.4-1.7.4-2.6 0-5.2-4.3-9.5-9.5-9.5"/>
<path class="st3" d="M104.4 30.8c-.5 0-.9-.4-.9-.9s.4-.9.9-.9.9.4.9.9-.4.9-.9.9m0-1.6c-.4 0-.7.3-.7.7 0 .4.3.7.7.7.4 0 .7-.3.7-.7 0-.4-.3-.7-.7-.7m.4 1.2h-.2l-.2-.3h-.2v.3h-.2v-.9h.5c.2 0 .3.1.3.3 0 .1-.1.2-.2.3l.2.3zm-.3-.5c.1 0 .1 0 .1-.1s-.1-.1-.1-.1h-.3v.3h.3zM14.8 29H17v6h3.8v1.9h-6zM23.1 32.9c0-2.3 1.8-4.1 4.3-4.1s4.2 1.8 4.2 4.1-1.8 4.1-4.3 4.1c-2.4 0-4.2-1.8-4.2-4.1m6.3 0c0-1.2-.8-2.2-2-2.2s-2 1-2 2.1.8 2.1 2 2.1c1.2.2 2-.8 2-2M34.3 33.4V29h2.2v4.4c0 1.1.6 1.7 1.5 1.7s1.5-.5 1.5-1.6V29h2.2v4.4c0 2.6-1.5 3.7-3.7 3.7-2.3-.1-3.7-1.2-3.7-3.7M45 29h3.1c2.8 0 4.5 1.6 4.5 3.9s-1.7 4-4.5 4h-3V29zm3.1 5.9c1.3 0 2.2-.7 2.2-2s-.9-2-2.2-2h-.9v4h.9zM55.7 29H62v1.9h-4.1v1.3h3.7V34h-3.7v2.9h-2.2zM65.1 29h2.2v6h3.8v1.9h-6zM76.8 28.9H79l3.4 8H80l-.6-1.4h-3.1l-.6 1.4h-2.3l3.4-8zm2 4.9l-.9-2.2-.9 2.2h1.8zM85.2 29h3.7c1.2 0 2 .3 2.6.9.5.5.7 1.1.7 1.8 0 1.2-.6 2-1.6 2.4l1.9 2.8H90l-1.6-2.4h-1v2.4h-2.2V29zm3.6 3.8c.7 0 1.2-.4 1.2-.9 0-.6-.5-.9-1.2-.9h-1.4v1.9h1.4zM95.3 29h6.4v1.8h-4.2V32h3.8v1.8h-3.8V35h4.3v1.9h-6.5zM10 33.9c-.3.7-1 1.2-1.8 1.2-1.2 0-2-1-2-2.1s.8-2.1 2-2.1c.9 0 1.6.6 1.9 1.3h2.3c-.4-1.9-2-3.3-4.2-3.3-2.4 0-4.3 1.8-4.3 4.1s1.8 4.1 4.2 4.1c2.1 0 3.7-1.4 4.2-3.2H10z"/>
</svg>
<h1 class="f4 f2-ns mt5 fw5">Congrats! You created your first tunnel!</h1>
<h1 class="f4 f2-ns mt5 fw5">Congrats! You created a tunnel!</h1>
<p class="f6 f5-m f4-l measure lh-copy fw3">
Argo Tunnel exposes locally running applications to the internet by
running an encrypted, virtual tunnel from your laptop or server to
@ -91,7 +91,7 @@ const indexTemplate = `
</html>
`
func StartHelloWorldServer(logger *logrus.Logger, listener net.Listener, shutdownC <-chan struct{}) error {
func StartHelloWorldServer(logger logger.Service, listener net.Listener, shutdownC <-chan struct{}) error {
logger.Infof("Starting Hello World server at %s", listener.Addr())
serverName := defaultServerName
if hostname, err := os.Hostname(); err == nil {
@ -148,7 +148,7 @@ func uptimeHandler(startTime time.Time) http.HandlerFunc {
}
// This handler will echo message
func websocketHandler(logger *logrus.Logger, upgrader websocket.Upgrader) http.HandlerFunc {
func websocketHandler(logger logger.Service, upgrader websocket.Upgrader) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
@ -158,12 +158,12 @@ func websocketHandler(logger *logrus.Logger, upgrader websocket.Upgrader) http.H
for {
mt, message, err := conn.ReadMessage()
if err != nil {
logger.WithError(err).Error("websocket read message error")
logger.Errorf("websocket read message error: %s", err)
break
}
if err := conn.WriteMessage(mt, message); err != nil {
logger.WithError(err).Error("websocket write message error")
logger.Errorf("websocket write message error: %s", err)
break
}
}

View File

@ -1,85 +0,0 @@
// this forks the logrus json formatter to rename msg -> message as that's the
// expected field. Ideally the logger should make it easier for us.
package log
import (
"encoding/json"
"fmt"
"runtime"
"time"
"github.com/mattn/go-colorable"
"github.com/sirupsen/logrus"
)
var (
DefaultTimestampFormat = time.RFC3339Nano
)
type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps.
TimestampFormat string
}
func CreateLogger() *logrus.Logger {
logger := logrus.New()
logger.Out = colorable.NewColorableStderr()
logger.Formatter = &logrus.TextFormatter{ForceColors: runtime.GOOS == "windows"}
return logger
}
func (f *JSONFormatter) Format(entry *logrus.Entry) ([]byte, error) {
data := make(logrus.Fields, len(entry.Data)+3)
for k, v := range entry.Data {
switch v := v.(type) {
case error:
// Otherwise errors are ignored by `encoding/json`
// https://github.com/sirupsen/logrus/issues/137
data[k] = v.Error()
default:
data[k] = v
}
}
prefixFieldClashes(data)
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = DefaultTimestampFormat
}
data["time"] = entry.Time.Format(timestampFormat)
data["message"] = entry.Message
data["level"] = entry.Level.String()
serialized, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
}
return append(serialized, '\n'), nil
}
// This is to not silently overwrite `time`, `msg` and `level` fields when
// dumping it. If this code wasn't there doing:
//
// logrus.WithField("level", 1).Info("hello")
//
// Would just silently drop the user provided level. Instead with this code
// it'll logged as:
//
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
//
// It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters.
func prefixFieldClashes(data logrus.Fields) {
if t, ok := data["time"]; ok {
data["fields.time"] = t
}
if m, ok := data["msg"]; ok {
data["fields.msg"] = m
}
if l, ok := data["level"]; ok {
data["fields.level"] = l
}
}

150
logger/create.go Normal file
View File

@ -0,0 +1,150 @@
package logger
import (
"fmt"
"os"
"strings"
"time"
"github.com/alecthomas/units"
)
// Option is to encaspulate actions that will be called by Parse and run later to build an Options struct
type Option func(*Options) error
// Options is use to set logging configuration data
type Options struct {
logFileDirectory string
maxFileSize units.Base2Bytes
maxFileCount uint
terminalOutputDisabled bool
supportedFileLevels []Level
supportedTerminalLevels []Level
}
// DisableTerminal stops terminal output for the logger
func DisableTerminal(disable bool) Option {
return func(c *Options) error {
c.terminalOutputDisabled = disable
return nil
}
}
// File sets a custom file to log events
func File(path string, size units.Base2Bytes, count uint) Option {
return func(c *Options) error {
c.logFileDirectory = path
c.maxFileSize = size
c.maxFileCount = count
return nil
}
}
// DefaultFile configures the log options will the defaults
func DefaultFile(directoryPath string) Option {
return func(c *Options) error {
size, err := units.ParseBase2Bytes("1MB")
if err != nil {
return err
}
c.logFileDirectory = directoryPath
c.maxFileSize = size
c.maxFileCount = 5
return nil
}
}
// SupportedFileLevels sets the supported logging levels for the log file
func SupportedFileLevels(supported []Level) Option {
return func(c *Options) error {
c.supportedFileLevels = supported
return nil
}
}
// SupportedTerminalevels sets the supported logging levels for the terminal output
func SupportedTerminalevels(supported []Level) Option {
return func(c *Options) error {
c.supportedTerminalLevels = supported
return nil
}
}
// LogLevelString sets the supported logging levels from a command line flag
func LogLevelString(level string) Option {
return func(c *Options) error {
supported, err := ParseLevelString(level)
if err != nil {
return err
}
c.supportedFileLevels = supported
c.supportedTerminalLevels = supported
return nil
}
}
// Parse builds the Options struct so the caller knows what actions should be run
func Parse(opts ...Option) (*Options, error) {
options := &Options{}
for _, opt := range opts {
if err := opt(options); err != nil {
return nil, err
}
}
return options, nil
}
// New setups a new logger based on the options.
// The default behavior is to write to standard out
func New(opts ...Option) (Service, error) {
config, err := Parse(opts...)
if err != nil {
return nil, err
}
l := NewOutputWriter(SharedWriteManager)
if config.logFileDirectory != "" {
l.Add(NewFileRollingWriter(config.logFileDirectory,
"cloudflared",
int64(config.maxFileSize),
config.maxFileCount),
NewDefaultFormatter(time.RFC3339Nano), config.supportedFileLevels...)
}
if !config.terminalOutputDisabled {
if len(config.supportedTerminalLevels) == 0 {
l.Add(os.Stdout, NewTerminalFormatter(""), InfoLevel)
l.Add(os.Stderr, NewTerminalFormatter(""), ErrorLevel, FatalLevel)
} else {
errLevels := []Level{}
outLevels := []Level{}
for _, level := range config.supportedTerminalLevels {
if level == ErrorLevel || level == FatalLevel {
errLevels = append(errLevels, level)
} else {
outLevels = append(outLevels, level)
}
}
l.Add(os.Stdout, NewTerminalFormatter(""), outLevels...)
l.Add(os.Stderr, NewTerminalFormatter(""), errLevels...)
}
}
return l, nil
}
// ParseLevelString returns the expected log levels based on the cmd flag
func ParseLevelString(lvl string) ([]Level, error) {
switch strings.ToLower(lvl) {
case "fatal":
return []Level{FatalLevel}, nil
case "error":
return []Level{FatalLevel, ErrorLevel}, nil
case "info", "warn":
return []Level{FatalLevel, ErrorLevel, InfoLevel}, nil
case "debug":
return []Level{FatalLevel, ErrorLevel, InfoLevel, DebugLevel}, nil
}
return []Level{}, fmt.Errorf("not a valid log level: %q", lvl)
}

125
logger/file_writer.go Normal file
View File

@ -0,0 +1,125 @@
package logger
import (
"fmt"
"os"
"path/filepath"
)
// FileRollingWriter maintains a set of log files numbered in order
// to keep a subset of log data to ensure it doesn't grow pass defined limits
type FileRollingWriter struct {
baseFileName string
directory string
maxFileSize int64
maxFileCount uint
fileHandle *os.File
}
// NewFileRollingWriter creates a new rolling file writer.
// directory is the working directory for the files
// baseFileName is the log file name. This writer appends .log to the name for the file name
// maxFileSize is the size in bytes of how large each file can be. Not a hard limit, general limit based after each write
// maxFileCount is the number of rolled files to keep.
func NewFileRollingWriter(directory, baseFileName string, maxFileSize int64, maxFileCount uint) *FileRollingWriter {
return &FileRollingWriter{
directory: directory,
baseFileName: baseFileName,
maxFileSize: maxFileSize,
maxFileCount: maxFileCount,
}
}
// Write is an implementation of io.writer the rolls the file once it reaches its max size
// It is expected the caller to Write is doing so in a thread safe manner (as WriteManager does).
func (w *FileRollingWriter) Write(p []byte) (n int, err error) {
logFile, isSingleFile := buildPath(w.directory, w.baseFileName)
if w.fileHandle == nil {
h, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664)
if err != nil {
return 0, err
}
w.fileHandle = h
}
// get size for rolling check
info, err := w.fileHandle.Stat()
if err != nil {
// failed to stat the file. Close the file handle and attempt to open a new handle on the next write
w.Close()
w.fileHandle = nil
return 0, err
}
// write to the file
written, err := w.fileHandle.Write(p)
// check if the file needs to be rolled
if err == nil && info.Size()+int64(written) > w.maxFileSize && !isSingleFile {
// close the file handle than do the renaming. A new one will be opened on the next write
w.Close()
w.rename(logFile, 1)
}
return written, err
}
// Close closes the file handle if it is open
func (w *FileRollingWriter) Close() {
if w.fileHandle != nil {
w.fileHandle.Close()
w.fileHandle = nil
}
}
// rename is how the files are rolled. It works recursively to move the base log file to the rolled ones
// e.g. cloudflared.log -> cloudflared-1.log,
// but if cloudflared-1.log already exists, it is renamed to cloudflared-2.log,
// then the other files move in to their postion
func (w *FileRollingWriter) rename(sourcePath string, index uint) {
destinationPath, isSingleFile := buildPath(w.directory, fmt.Sprintf("%s-%d", w.baseFileName, index))
if isSingleFile {
return //don't need to rename anything, it is a single file
}
// rolled to the max amount of files allowed on disk
if index >= w.maxFileCount {
os.Remove(destinationPath)
}
// if the rolled path already exist, rename it to cloudflared-2.log, then do this one.
// recursive call since the oldest one needs to be renamed, before the newer ones can be moved
if exists(destinationPath) {
w.rename(destinationPath, index+1)
}
os.Rename(sourcePath, destinationPath)
}
// return the path to the log file and if it is a single file or not.
// true means a single file. false means a rolled file
func buildPath(directory, fileName string) (string, bool) {
if !isDirectory(directory) { // not a directory, so try and treat it as a single file for backwards compatibility sake
return directory, true
}
return filepath.Join(directory, fileName+".log"), false
}
func exists(filePath string) bool {
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return false
}
return true
}
func isDirectory(path string) bool {
if path == "" {
return true
}
fileInfo, err := os.Stat(path)
if err != nil {
return false
}
return fileInfo.IsDir()
}

108
logger/file_writer_test.go Normal file
View File

@ -0,0 +1,108 @@
package logger
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFileWrite(t *testing.T) {
fileName := "test_file"
fileLog := fileName + ".log"
testData := []byte(string("hello Dalton, how are you doing?"))
defer func() {
os.Remove(fileLog)
}()
w := NewFileRollingWriter("", fileName, 1000, 2)
defer w.Close()
l, err := w.Write(testData)
assert.NoError(t, err)
assert.Equal(t, l, len(testData), "expected write length and data length to match")
d, err := ioutil.ReadFile(fileLog)
assert.FileExists(t, fileLog, "file doesn't exist at expected path")
assert.Equal(t, d, testData, "expected data in file to match test data")
}
func TestRolling(t *testing.T) {
dirName := "testdir"
err := os.Mkdir(dirName, 0755)
assert.NoError(t, err)
fileName := "test_file"
firstFile := filepath.Join(dirName, fileName+".log")
secondFile := filepath.Join(dirName, fileName+"-1.log")
thirdFile := filepath.Join(dirName, fileName+"-2.log")
defer func() {
os.RemoveAll(dirName)
os.Remove(firstFile)
os.Remove(secondFile)
os.Remove(thirdFile)
}()
w := NewFileRollingWriter(dirName, fileName, 1000, 2)
defer w.Close()
for i := 99; i >= 1; i-- {
testData := []byte(fmt.Sprintf("%d bottles of beer on the wall...", i))
w.Write(testData)
}
assert.FileExists(t, firstFile, "first file doesn't exist as expected")
assert.FileExists(t, secondFile, "second file doesn't exist as expected")
assert.FileExists(t, thirdFile, "third file doesn't exist as expected")
assert.False(t, exists(filepath.Join(dirName, fileName+"-3.log")), "limited to two files and there is more")
}
func TestSingleFile(t *testing.T) {
fileName := "test_file"
testData := []byte(string("hello Dalton, how are you doing?"))
defer func() {
os.Remove(fileName)
}()
w := NewFileRollingWriter(fileName, fileName, 1000, 2)
defer w.Close()
l, err := w.Write(testData)
assert.NoError(t, err)
assert.Equal(t, l, len(testData), "expected write length and data length to match")
d, err := ioutil.ReadFile(fileName)
assert.FileExists(t, fileName, "file doesn't exist at expected path")
assert.Equal(t, d, testData, "expected data in file to match test data")
}
func TestSingleFileInDirectory(t *testing.T) {
dirName := "testdir"
err := os.Mkdir(dirName, 0755)
assert.NoError(t, err)
fileName := "test_file"
fullPath := filepath.Join(dirName, fileName+".log")
testData := []byte(string("hello Dalton, how are you doing?"))
defer func() {
os.Remove(fullPath)
os.RemoveAll(dirName)
}()
w := NewFileRollingWriter(fullPath, fileName, 1000, 2)
defer w.Close()
l, err := w.Write(testData)
assert.NoError(t, err)
assert.Equal(t, l, len(testData), "expected write length and data length to match")
d, err := ioutil.ReadFile(fullPath)
assert.FileExists(t, fullPath, "file doesn't exist at expected path")
assert.Equal(t, d, testData, "expected data in file to match test data")
}

102
logger/formatter.go Normal file
View File

@ -0,0 +1,102 @@
package logger
import (
"fmt"
"runtime"
"time"
"github.com/acmacalister/skittles"
)
// Level of logging
type Level int
const (
// InfoLevel is for standard log messages
InfoLevel Level = iota
// DebugLevel is for messages that are intended for purposes debugging only
DebugLevel
// ErrorLevel is for error message to indicte something has gone wrong
ErrorLevel
// FatalLevel is for error message that log and kill the program with an os.exit(1)
FatalLevel
)
// Formatter is the base interface for formatting logging messages before writing them out
type Formatter interface {
Timestamp(Level, time.Time) string // format the timestamp string
Content(Level, string) string // format content string (color for terminal, etc)
}
// DefaultFormatter writes a simple structure timestamp and the message per log line
type DefaultFormatter struct {
format string
}
// NewDefaultFormatter creates the standard log formatter
// format is the time format to use for timestamp formatting
func NewDefaultFormatter(format string) Formatter {
return &DefaultFormatter{
format: format,
}
}
// Timestamp formats a log line timestamp with a brackets around them
func (f *DefaultFormatter) Timestamp(l Level, d time.Time) string {
if f.format == "" {
return ""
}
return fmt.Sprintf("[%s]: ", d.Format(f.format))
}
// Content just writes the log line straight to the sources
func (f *DefaultFormatter) Content(l Level, c string) string {
return c
}
// TerminalFormatter is setup for colored output
type TerminalFormatter struct {
format string
supportsColor bool
}
// NewTerminalFormatter creates a Terminal formatter for colored output
// format is the time format to use for timestamp formatting
func NewTerminalFormatter(format string) Formatter {
supportsColor := (runtime.GOOS != "windows")
return &TerminalFormatter{
format: format,
supportsColor: supportsColor,
}
}
// Timestamp returns the log level with a matching color to the log type
func (f *TerminalFormatter) Timestamp(l Level, d time.Time) string {
t := ""
switch l {
case InfoLevel:
t = f.output("[INFO] ", skittles.Cyan)
case ErrorLevel:
t = f.output("[ERROR] ", skittles.Red)
case DebugLevel:
t = f.output("[DEBUG] ", skittles.Yellow)
case FatalLevel:
t = f.output("[FATAL] ", skittles.Red)
}
return t
}
// Content just writes the log line straight to the sources
func (f *TerminalFormatter) Content(l Level, c string) string {
return c
}
func (f *TerminalFormatter) output(msg string, colorFunc func(interface{}) string) string {
if f.supportsColor {
return colorFunc(msg)
}
return msg
}

59
logger/manager.go Normal file
View File

@ -0,0 +1,59 @@
package logger
import "sync"
// SharedWriteManager is a package level variable to allows multiple loggers to use the same write manager.
// This is useful when multiple loggers will write to the same file to ensure they don't clobber each other.
var SharedWriteManager = NewWriteManager()
type writeData struct {
writeFunc func([]byte)
data []byte
}
// WriteManager is a logging service that handles managing multiple writing streams
type WriteManager struct {
shutdown chan struct{}
writeChan chan writeData
writers map[string]Service
wg sync.WaitGroup
}
// NewWriteManager creates a write manager that implements OutputManager
func NewWriteManager() OutputManager {
m := &WriteManager{
shutdown: make(chan struct{}),
writeChan: make(chan writeData, 1000),
}
go m.run()
return m
}
// Append adds a message to the writer runloop
func (m *WriteManager) Append(data []byte, callback func([]byte)) {
m.wg.Add(1)
m.writeChan <- writeData{data: data, writeFunc: callback}
}
// Shutdown stops the sync manager service
func (m *WriteManager) Shutdown() {
m.wg.Wait()
close(m.shutdown)
close(m.writeChan)
}
// run is the main runloop that schedules log messages
func (m *WriteManager) run() {
for {
select {
case event, ok := <-m.writeChan:
if ok {
event.writeFunc(event.data)
m.wg.Done()
}
case <-m.shutdown:
return
}
}
}

18
logger/manager_test.go Normal file
View File

@ -0,0 +1,18 @@
package logger
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWriteManger(t *testing.T) {
testData := []byte(string("hello Austin, how are you doing?"))
waitChan := make(chan []byte)
m := NewWriteManager()
m.Append(testData, func(b []byte) {
waitChan <- b
})
resp := <-waitChan
assert.Equal(t, testData, resp)
}

18
logger/mock_manager.go Normal file
View File

@ -0,0 +1,18 @@
package logger
// MockWriteManager does nothing and is provided for testing purposes
type MockWriteManager struct {
}
// NewMockWriteManager creates an OutputManager that does nothing for testing purposes
func NewMockWriteManager() OutputManager {
return &MockWriteManager{}
}
// Append is a mock stub
func (m *MockWriteManager) Append(data []byte, callback func([]byte)) {
}
// Shutdown is a mock stub
func (m *MockWriteManager) Shutdown() {
}

132
logger/output.go Normal file
View File

@ -0,0 +1,132 @@
package logger
import (
"fmt"
"io"
"os"
"time"
)
// provided for testing
var osExit = os.Exit
// OutputManager is used to sync data of Output
type OutputManager interface {
Append([]byte, func([]byte))
Shutdown()
}
// Service is the logging service that is either a group or single log writer
type Service interface {
Error(message string)
Info(message string)
Debug(message string)
Fatal(message string)
Errorf(format string, args ...interface{})
Infof(format string, args ...interface{})
Debugf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
}
type sourceGroup struct {
writer io.Writer
formatter Formatter
levelsSupported []Level
}
// OutputWriter is the standard logging implementation
type OutputWriter struct {
groups []sourceGroup
syncWriter OutputManager
}
// NewOutputWriter create a new logger
func NewOutputWriter(syncWriter OutputManager) *OutputWriter {
return &OutputWriter{
syncWriter: syncWriter,
groups: make([]sourceGroup, 0),
}
}
// Add a writer and formatter to output to
func (s *OutputWriter) Add(writer io.Writer, formatter Formatter, levels ...Level) {
s.groups = append(s.groups, sourceGroup{writer: writer, formatter: formatter, levelsSupported: levels})
}
// Error writes an error to the logging sources
func (s *OutputWriter) Error(message string) {
s.output(ErrorLevel, message)
}
// Info writes an info string to the logging sources
func (s *OutputWriter) Info(message string) {
s.output(InfoLevel, message)
}
// Debug writes a debug string to the logging sources
func (s *OutputWriter) Debug(message string) {
s.output(DebugLevel, message)
}
// Fatal writes a error string to the logging sources and runs does an os.exit()
func (s *OutputWriter) Fatal(message string) {
s.output(FatalLevel, message)
s.syncWriter.Shutdown() // waits for the pending logging to finish
osExit(1)
}
// Errorf writes a formatted error to the logging sources
func (s *OutputWriter) Errorf(format string, args ...interface{}) {
s.output(ErrorLevel, fmt.Sprintf(format, args...))
}
// Infof writes a formatted info statement to the logging sources
func (s *OutputWriter) Infof(format string, args ...interface{}) {
s.output(InfoLevel, fmt.Sprintf(format, args...))
}
// Debugf writes a formatted debug statement to the logging sources
func (s *OutputWriter) Debugf(format string, args ...interface{}) {
s.output(DebugLevel, fmt.Sprintf(format, args...))
}
// Fatalf writes a writes a formatted error statement and runs does an os.exit()
func (s *OutputWriter) Fatalf(format string, args ...interface{}) {
s.output(FatalLevel, fmt.Sprintf(format, args...))
s.syncWriter.Shutdown() // waits for the pending logging to finish
osExit(1)
}
// output does the actual write to the sync manager
func (s *OutputWriter) output(l Level, content string) {
for _, group := range s.groups {
if isSupported(group, l) {
logLine := fmt.Sprintf("%s%s\n", group.formatter.Timestamp(l, time.Now()),
group.formatter.Content(l, content))
s.append(group, []byte(logLine))
}
}
}
func (s *OutputWriter) append(group sourceGroup, logLine []byte) {
s.syncWriter.Append(logLine, func(b []byte) {
group.writer.Write(b)
})
}
// isSupported checks if the log level is supported
func isSupported(group sourceGroup, l Level) bool {
for _, level := range group.levelsSupported {
if l == level {
return true
}
}
return false
}
// Write implements io.Writer to support SetOutput of the log package
func (s *OutputWriter) Write(p []byte) (n int, err error) {
s.Info(string(p))
return len(p), nil
}

104
logger/output_test.go Normal file
View File

@ -0,0 +1,104 @@
package logger
import (
"bufio"
"bytes"
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestLogLevel(t *testing.T) {
timeFormat := "2006-01-02"
f := NewDefaultFormatter(timeFormat)
m := NewWriteManager()
var testBuffer bytes.Buffer
logger := NewOutputWriter(m)
logger.Add(&testBuffer, f, InfoLevel, DebugLevel)
testTime := f.Timestamp(InfoLevel, time.Now())
testInfo := "hello Dalton, how are you doing?"
logger.Info(testInfo)
tesErr := "hello Austin, how did it break today?"
logger.Error(tesErr)
testDebug := "hello Bill, who are you?"
logger.Debug(testDebug)
m.Shutdown()
lines := strings.Split(testBuffer.String(), "\n")
assert.Len(t, lines, 3, "only expected two strings in the buffer")
infoLine := lines[0]
debugLine := lines[1]
compareInfo := fmt.Sprintf("%s%s", testTime, testInfo)
assert.Equal(t, compareInfo, infoLine, "expect the strings to match")
compareDebug := fmt.Sprintf("%s%s", testTime, testDebug)
assert.Equal(t, compareDebug, debugLine, "expect the strings to match")
}
func TestOutputWrite(t *testing.T) {
timeFormat := "2006-01-02"
f := NewDefaultFormatter(timeFormat)
m := NewWriteManager()
var testBuffer bytes.Buffer
logger := NewOutputWriter(m)
logger.Add(&testBuffer, f, InfoLevel)
testData := "hello Bob Bork, how are you doing?"
logger.Info(testData)
testTime := f.Timestamp(InfoLevel, time.Now())
m.Shutdown()
scanner := bufio.NewScanner(&testBuffer)
scanner.Scan()
line := scanner.Text()
assert.NoError(t, scanner.Err())
compareLine := fmt.Sprintf("%s%s", testTime, testData)
assert.Equal(t, compareLine, line, "expect the strings to match")
}
func TestFatalWrite(t *testing.T) {
timeFormat := "2006-01-02"
f := NewDefaultFormatter(timeFormat)
m := NewWriteManager()
var testBuffer bytes.Buffer
logger := NewOutputWriter(m)
logger.Add(&testBuffer, f, FatalLevel)
oldOsExit := osExit
defer func() { osExit = oldOsExit }()
var got int
myExit := func(code int) {
got = code
}
osExit = myExit
testData := "so long y'all"
logger.Fatal(testData)
testTime := f.Timestamp(FatalLevel, time.Now())
scanner := bufio.NewScanner(&testBuffer)
scanner.Scan()
line := scanner.Text()
assert.NoError(t, scanner.Err())
compareLine := fmt.Sprintf("%s%s", testTime, testData)
assert.Equal(t, compareLine, line, "expect the strings to match")
assert.Equal(t, got, 1, "exit code should be one for a fatal log")
}

31
make-mac-pkg.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
TARGET_DIRECTORY=".build"
BINARY_NAME="cloudflared"
VERSION=$(git describe --tags --always --dirty="-dev")
PRODUCT="cloudflared"
echo "building cloudflared"
make cloudflared
echo "creating build directory"
mkdir ${TARGET_DIRECTORY}
mkdir ${TARGET_DIRECTORY}/contents
cp -r .mac_resources/scripts ${TARGET_DIRECTORY}/scripts
echo "move cloudflared into the build directory"
mv $BINARY_NAME {$TARGET_DIRECTORY}/contents/${PRODUCT}
echo "build the installer package"
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
${PRODUCT}.pkg
# TODO: our iOS/Mac account doesn't have this installer certificate type.
# need to find how we can get it --sign "Developer ID Installer: Cloudflare" \
echo "cleaning up the build directory"
rm -rf $TARGET_DIRECTORY

View File

@ -12,9 +12,9 @@ import (
"golang.org/x/net/trace"
"github.com/cloudflare/cloudflared/logger"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sirupsen/logrus"
)
const (
@ -22,7 +22,7 @@ const (
startupTime = time.Millisecond * 500
)
func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, logger *logrus.Logger) (err error) {
func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, logger logger.Service) (err error) {
var wg sync.WaitGroup
// Metrics port is privileged, so no need for further access control
trace.AuthRequest = func(*http.Request) (bool, bool) { return true, true }
@ -43,7 +43,7 @@ func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, logger *logrus.Logg
defer wg.Done()
err = server.Serve(l)
}()
logger.WithField("addr", l.Addr()).Info("Starting metrics server")
logger.Infof("Starting metrics server on %s", fmt.Sprintf("%v/metrics", l.Addr()))
// server.Serve will hang if server.Shutdown is called before the server is
// fully started up. So add artificial delay.
time.Sleep(startupTime)
@ -58,7 +58,7 @@ func ServeMetrics(l net.Listener, shutdownC <-chan struct{}, logger *logrus.Logg
logger.Info("Metrics server stopped")
return nil
}
logger.WithError(err).Error("Metrics server quit with error")
logger.Errorf("Metrics server quit with error: %s", err)
return err
}

View File

@ -0,0 +1,21 @@
package origin
import (
"time"
)
type ReconnectSignal struct {
// wait this many seconds before re-establish the connection
Delay time.Duration
}
// Error allows us to use ReconnectSignal as a special error to force connection abort
func (r *ReconnectSignal) Error() string {
return "reconnect signal"
}
func (r *ReconnectSignal) DelayBeforeReconnect() {
if r.Delay > 0 {
time.Sleep(r.Delay)
}
}

View File

@ -9,11 +9,12 @@ import (
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/cloudflare/cloudflared/buffer"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/signal"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
@ -55,13 +56,20 @@ type Supervisor struct {
nextConnectedIndex int
nextConnectedSignal chan struct{}
logger *logrus.Entry
logger logger.Service
jwtLock *sync.RWMutex
jwtLock sync.RWMutex
jwt []byte
eventDigestLock *sync.RWMutex
eventDigestLock sync.RWMutex
eventDigest []byte
connDigestLock sync.RWMutex
connDigest map[uint8][]byte
bufferPool *buffer.Pool
namedTunnel *NamedTunnelConfig
}
type resolveResult struct {
@ -74,7 +82,7 @@ type tunnelError struct {
err error
}
func NewSupervisor(config *TunnelConfig, u uuid.UUID) (*Supervisor, error) {
func NewSupervisor(config *TunnelConfig, cloudflaredUUID uuid.UUID, namedTunnel *NamedTunnelConfig) (*Supervisor, error) {
var (
edgeIPs *edgediscovery.Edge
err error
@ -88,20 +96,21 @@ func NewSupervisor(config *TunnelConfig, u uuid.UUID) (*Supervisor, error) {
return nil, err
}
return &Supervisor{
cloudflaredUUID: u,
cloudflaredUUID: cloudflaredUUID,
config: config,
edgeIPs: edgeIPs,
tunnelErrors: make(chan tunnelError),
tunnelsConnecting: map[int]chan struct{}{},
logger: config.Logger.WithField("subsystem", "supervisor"),
jwtLock: &sync.RWMutex{},
eventDigestLock: &sync.RWMutex{},
logger: config.Logger,
connDigest: make(map[uint8][]byte),
bufferPool: buffer.NewPool(512 * 1024),
namedTunnel: namedTunnel,
}, nil
}
func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal) error {
func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, reconnectCh chan ReconnectSignal) error {
logger := s.config.Logger
if err := s.initialize(ctx, connectedSignal); err != nil {
if err := s.initialize(ctx, connectedSignal, reconnectCh); err != nil {
return err
}
var tunnelsWaiting []int
@ -117,7 +126,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal) er
if timer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate); err == nil {
refreshAuthBackoffTimer = timer
} else {
logger.WithError(err).Errorf("initial refreshAuth failed, retrying in %v", refreshAuthRetryDuration)
logger.Errorf("supervisor: initial refreshAuth failed, retrying in %v: %s", refreshAuthRetryDuration, err)
refreshAuthBackoffTimer = time.After(refreshAuthRetryDuration)
}
}
@ -136,7 +145,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal) er
case tunnelError := <-s.tunnelErrors:
tunnelsActive--
if tunnelError.err != nil {
logger.WithError(tunnelError.err).Warn("Tunnel disconnected due to error")
logger.Infof("supervisor: Tunnel disconnected due to error: %s", tunnelError.err)
tunnelsWaiting = append(tunnelsWaiting, tunnelError.index)
s.waitForNextTunnel(tunnelError.index)
@ -151,7 +160,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal) er
case <-backoffTimer:
backoffTimer = nil
for _, index := range tunnelsWaiting {
go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index))
go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index), reconnectCh)
}
tunnelsActive += len(tunnelsWaiting)
tunnelsWaiting = nil
@ -159,7 +168,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal) er
case <-refreshAuthBackoffTimer:
newTimer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate)
if err != nil {
logger.WithError(err).Error("Authentication failed")
logger.Errorf("supervisor: Authentication failed: %s", err)
// Permanent failure. Leave the `select` without setting the
// channel to be non-null, so we'll never hit this case of the `select` again.
continue
@ -176,26 +185,26 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal) er
s.lastResolve = time.Now()
s.resolverC = nil
if result.err == nil {
logger.Debug("Service discovery refresh complete")
logger.Debug("supervisor: Service discovery refresh complete")
} else {
logger.WithError(result.err).Error("Service discovery error")
logger.Errorf("supervisor: Service discovery error: %s", result.err)
}
}
}
}
// Returns nil if initialization succeeded, else the initialization error.
func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal) error {
func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal, reconnectCh chan ReconnectSignal) error {
logger := s.logger
s.lastResolve = time.Now()
availableAddrs := int(s.edgeIPs.AvailableAddrs())
if s.config.HAConnections > availableAddrs {
logger.Warnf("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, availableAddrs)
logger.Infof("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, availableAddrs)
s.config.HAConnections = availableAddrs
}
go s.startFirstTunnel(ctx, connectedSignal)
go s.startFirstTunnel(ctx, connectedSignal, reconnectCh)
select {
case <-ctx.Done():
<-s.tunnelErrors
@ -207,7 +216,7 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig
// At least one successful connection, so start the rest
for i := 1; i < s.config.HAConnections; i++ {
ch := signal.New(make(chan struct{}))
go s.startTunnel(ctx, i, ch)
go s.startTunnel(ctx, i, ch, reconnectCh)
time.Sleep(registrationInterval)
}
return nil
@ -215,7 +224,7 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig
// startTunnel starts the first tunnel connection. The resulting error will be sent on
// s.tunnelErrors. It will send a signal via connectedSignal if registration succeed
func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *signal.Signal) {
func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *signal.Signal, reconnectCh chan ReconnectSignal) {
var (
addr *net.TCPAddr
err error
@ -230,7 +239,7 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign
return
}
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID)
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
// If the first tunnel disconnects, keep restarting it.
edgeErrors := 0
for s.unusedIPs() {
@ -253,13 +262,13 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign
return
}
}
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID)
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
}
}
// startTunnel starts a new tunnel connection. The resulting error will be sent on
// s.tunnelErrors.
func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal *signal.Signal) {
func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal *signal.Signal, reconnectCh chan ReconnectSignal) {
var (
addr *net.TCPAddr
err error
@ -268,11 +277,11 @@ func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal
s.tunnelErrors <- tunnelError{index: index, addr: addr, err: err}
}()
addr, err = s.edgeIPs.GetAddr(index)
addr, err = s.edgeIPs.GetDifferentAddr(index)
if err != nil {
return
}
err = ServeTunnelLoop(ctx, s, s.config, addr, uint8(index), connectedSignal, s.cloudflaredUUID)
err = ServeTunnelLoop(ctx, s, s.config, addr, uint8(index), connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
}
func (s *Supervisor) newConnectedTunnelSignal(index int) *signal.Signal {
@ -328,17 +337,33 @@ func (s *Supervisor) SetEventDigest(eventDigest []byte) {
s.eventDigest = eventDigest
}
func (s *Supervisor) ConnDigest(connID uint8) ([]byte, error) {
s.connDigestLock.RLock()
defer s.connDigestLock.RUnlock()
digest, ok := s.connDigest[connID]
if !ok {
return nil, fmt.Errorf("no connection digest for connection %v", connID)
}
return digest, nil
}
func (s *Supervisor) SetConnDigest(connID uint8, connDigest []byte) {
s.connDigestLock.Lock()
defer s.connDigestLock.Unlock()
s.connDigest[connID] = connDigest
}
func (s *Supervisor) refreshAuth(
ctx context.Context,
backoff *BackoffHandler,
authenticate func(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error),
) (retryTimer <-chan time.Time, err error) {
logger := s.config.Logger.WithField("subsystem", subsystemRefreshAuth)
logger := s.config.Logger
authOutcome, err := authenticate(ctx, backoff.Retries())
if err != nil {
s.config.Metrics.authFail.WithLabelValues(err.Error()).Inc()
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
logger.WithError(err).Warnf("Retrying in %v", duration)
logger.Debugf("refresh_auth: Retrying in %v: %s", duration, err)
return backoff.BackoffTimer(), nil
}
return nil, err
@ -354,13 +379,13 @@ func (s *Supervisor) refreshAuth(
case tunnelpogs.AuthUnknown:
duration := outcome.RefreshAfter()
s.config.Metrics.authFail.WithLabelValues(outcome.Error()).Inc()
logger.WithError(outcome).Warnf("Retrying in %v", duration)
logger.Debugf("refresh_auth: Retrying in %v: %s", duration, outcome)
return timeAfter(duration), nil
case tunnelpogs.AuthFail:
s.config.Metrics.authFail.WithLabelValues(outcome.Error()).Inc()
return nil, outcome
default:
err := fmt.Errorf("Unexpected outcome type %T", authOutcome)
err := fmt.Errorf("refresh_auth: Unexpected outcome type %T", authOutcome)
s.config.Metrics.authFail.WithLabelValues(err.Error()).Inc()
return nil, err
}
@ -383,7 +408,6 @@ func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int)
return nil // noop
})
muxerConfig := s.config.muxerConfig(handler)
muxerConfig.Logger = muxerConfig.Logger.WithField("subsystem", subsystemRefreshAuth)
muxer, err := h2mux.Handshake(edgeConn, edgeConn, muxerConfig, s.config.Metrics.activeStreams)
if err != nil {
return nil, err
@ -395,7 +419,7 @@ func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int)
<-muxer.Shutdown()
}()
tunnelServer, err := connection.NewRPCClient(ctx, muxer, s.logger.WithField("subsystem", subsystemRefreshAuth), openStreamTimeout)
tunnelServer, err := connection.NewRPCClient(ctx, muxer, s.logger, openStreamTimeout)
if err != nil {
return nil, err
}

View File

@ -9,13 +9,13 @@ import (
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/logger"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
func testConfig(logger *logrus.Logger) *TunnelConfig {
func testConfig(logger logger.Service) *TunnelConfig {
metrics := TunnelMetrics{}
metrics.authSuccess = prometheus.NewCounter(
@ -40,8 +40,7 @@ func testConfig(logger *logrus.Logger) *TunnelConfig {
}
func TestRefreshAuthBackoff(t *testing.T) {
logger := logrus.New()
logger.Level = logrus.ErrorLevel
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
var wait time.Duration
timeAfter = func(d time.Duration) <-chan time.Time {
@ -49,7 +48,7 @@ func TestRefreshAuthBackoff(t *testing.T) {
return time.After(d)
}
s, err := NewSupervisor(testConfig(logger), uuid.New())
s, err := NewSupervisor(testConfig(logger), uuid.New(), nil)
if !assert.NoError(t, err) {
t.FailNow()
}
@ -85,8 +84,7 @@ func TestRefreshAuthBackoff(t *testing.T) {
}
func TestRefreshAuthSuccess(t *testing.T) {
logger := logrus.New()
logger.Level = logrus.ErrorLevel
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
var wait time.Duration
timeAfter = func(d time.Duration) <-chan time.Time {
@ -94,7 +92,7 @@ func TestRefreshAuthSuccess(t *testing.T) {
return time.After(d)
}
s, err := NewSupervisor(testConfig(logger), uuid.New())
s, err := NewSupervisor(testConfig(logger), uuid.New(), nil)
if !assert.NoError(t, err) {
t.FailNow()
}
@ -114,8 +112,7 @@ func TestRefreshAuthSuccess(t *testing.T) {
}
func TestRefreshAuthUnknown(t *testing.T) {
logger := logrus.New()
logger.Level = logrus.ErrorLevel
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
var wait time.Duration
timeAfter = func(d time.Duration) <-chan time.Time {
@ -123,7 +120,7 @@ func TestRefreshAuthUnknown(t *testing.T) {
return time.After(d)
}
s, err := NewSupervisor(testConfig(logger), uuid.New())
s, err := NewSupervisor(testConfig(logger), uuid.New(), nil)
if !assert.NoError(t, err) {
t.FailNow()
}
@ -143,10 +140,9 @@ func TestRefreshAuthUnknown(t *testing.T) {
}
func TestRefreshAuthFail(t *testing.T) {
logger := logrus.New()
logger.Level = logrus.ErrorLevel
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
s, err := NewSupervisor(testConfig(logger), uuid.New())
s, err := NewSupervisor(testConfig(logger), uuid.New(), nil)
if !assert.NoError(t, err) {
t.FailNow()
}

View File

@ -17,15 +17,16 @@ import (
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
"github.com/cloudflare/cloudflared/buffer"
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/streamhandler"
"github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/validation"
"github.com/cloudflare/cloudflared/websocket"
@ -38,6 +39,8 @@ const (
lbProbeUserAgentPrefix = "Mozilla/5.0 (compatible; Cloudflare-Traffic-Manager/1.0; +https://www.cloudflare.com/traffic-manager/;"
TagHeaderNamePrefix = "Cf-Warp-Tag-"
DuplicateConnectionError = "EDUPCONN"
FeatureSerializedHeaders = "serialized_headers"
FeatureQuickReconnects = "quick_reconnects"
)
type registerRPCName string
@ -65,7 +68,8 @@ type TunnelConfig struct {
IsAutoupdated bool
IsFreeTunnel bool
LBPool string
Logger *log.Logger
Logger logger.Service
TransportLogger logger.Service
MaxHeartbeats uint64
Metrics *TunnelMetrics
MetricsUpdateFreq time.Duration
@ -76,7 +80,6 @@ type TunnelConfig struct {
RunFromTerminal bool
Tags []tunnelpogs.Tag
TlsConfig *tls.Config
TransportLogger *log.Logger
UseDeclarativeTunnel bool
WSGI bool
// OriginUrl may not be used if a user specifies a unix socket.
@ -84,6 +87,8 @@ type TunnelConfig struct {
// feature-flag to use new edge reconnect tokens
UseReconnectToken bool
// feature-flag for using ConnectionDigest
UseQuickReconnects bool
}
// ReconnectTunnelCredentialManager is invoked by functions in this file to
@ -92,6 +97,8 @@ type ReconnectTunnelCredentialManager interface {
ReconnectToken() ([]byte, error)
EventDigest() ([]byte, error)
SetEventDigest(eventDigest []byte)
ConnDigest(connID uint8) ([]byte, error)
SetConnDigest(connID uint8, connDigest []byte)
}
type dupConnRegisterTunnelError struct{}
@ -137,7 +144,7 @@ func (c *TunnelConfig) muxerConfig(handler h2mux.MuxedStreamHandler) h2mux.Muxer
IsClient: true,
HeartbeatInterval: c.HeartbeatInterval,
MaxHeartbeats: c.MaxHeartbeats,
Logger: c.TransportLogger.WithFields(log.Fields{}),
Logger: c.TransportLogger,
CompressionQuality: h2mux.CompressionSetting(c.CompressionQuality),
}
}
@ -160,15 +167,29 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
RunFromTerminal: c.RunFromTerminal,
CompressionQuality: c.CompressionQuality,
UUID: uuid.String(),
Features: c.SupportedFeatures(),
}
}
func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID) error {
s, err := NewSupervisor(config, cloudflaredID)
func (c *TunnelConfig) SupportedFeatures() []string {
basic := []string{FeatureSerializedHeaders}
if c.UseQuickReconnects {
basic = append(basic, FeatureQuickReconnects)
}
return basic
}
type NamedTunnelConfig struct {
Auth pogs.TunnelAuth
ID string
}
func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID, reconnectCh chan ReconnectSignal, namedTunnel *NamedTunnelConfig) error {
s, err := NewSupervisor(config, cloudflaredID, namedTunnel)
if err != nil {
return err
}
return s.Run(ctx, connectedSignal)
return s.Run(ctx, connectedSignal, reconnectCh)
}
func ServeTunnelLoop(ctx context.Context,
@ -177,9 +198,10 @@ func ServeTunnelLoop(ctx context.Context,
addr *net.TCPAddr,
connectionID uint8,
connectedSignal *signal.Signal,
u uuid.UUID,
cloudflaredUUID uuid.UUID,
bufferPool *buffer.Pool,
reconnectCh chan ReconnectSignal,
) error {
connectionLogger := config.Logger.WithField("connectionID", connectionID)
config.Metrics.incrementHaConnections()
defer config.Metrics.decrementHaConnections()
backoff := BackoffHandler{MaxRetries: config.Retries}
@ -196,15 +218,17 @@ func ServeTunnelLoop(ctx context.Context,
ctx,
credentialManager,
config,
connectionLogger,
config.Logger,
addr, connectionID,
connectedFuse,
&backoff,
u,
cloudflaredUUID,
bufferPool,
reconnectCh,
)
if recoverable {
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
connectionLogger.Infof("Retrying in %s seconds", duration)
config.Logger.Infof("Retrying in %s seconds: connectionID: %d", duration, connectionID)
backoff.Backoff(ctx)
continue
}
@ -217,12 +241,14 @@ func ServeTunnel(
ctx context.Context,
credentialManager ReconnectTunnelCredentialManager,
config *TunnelConfig,
logger *log.Entry,
logger logger.Service,
addr *net.TCPAddr,
connectionID uint8,
connectedFuse *h2mux.BooleanFuse,
backoff *BackoffHandler,
u uuid.UUID,
cloudflaredUUID uuid.UUID,
bufferPool *buffer.Pool,
reconnectCh chan ReconnectSignal,
) (err error, recoverable bool) {
// Treat panics as recoverable errors
defer func() {
@ -243,16 +269,15 @@ func ServeTunnel(
tags["ha"] = connectionTag
// Returns error from parsing the origin URL or handshake errors
handler, originLocalIP, err := NewTunnelHandler(ctx, config, addr, connectionID)
handler, originLocalIP, err := NewTunnelHandler(ctx, config, addr, connectionID, bufferPool)
if err != nil {
errLog := logger.WithError(err)
switch err.(type) {
case connection.DialError:
errLog.Error("Unable to dial edge")
logger.Errorf("Unable to dial edge: %s connectionID: %d", err, connectionID)
case h2mux.MuxerHandshakeError:
errLog.Error("Handshake failed with edge server")
logger.Errorf("Handshake failed with edge server: %s connectionID: %d", err, connectionID)
default:
errLog.Error("Tunnel creation failure")
logger.Errorf("Tunnel creation failure: %s connectionID: %d", err, connectionID)
return err, false
}
return err, true
@ -273,17 +298,25 @@ func ServeTunnel(
eventDigest, eventDigestErr := credentialManager.EventDigest()
// if we have both credentials, we can reconnect
if tokenErr == nil && eventDigestErr == nil {
return ReconnectTunnel(serveCtx, token, eventDigest, handler.muxer, config, logger, connectionID, originLocalIP, u)
var connDigest []byte
// check if we can use Quick Reconnects
if config.UseQuickReconnects {
if digest, connDigestErr := credentialManager.ConnDigest(connectionID); connDigestErr == nil {
connDigest = digest
}
}
return ReconnectTunnel(serveCtx, token, eventDigest, connDigest, handler.muxer, config, logger, connectionID, originLocalIP, cloudflaredUUID, credentialManager)
}
// log errors and proceed to RegisterTunnel
if tokenErr != nil {
logger.WithError(tokenErr).Error("Couldn't get reconnect token")
logger.Errorf("Couldn't get reconnect token: %s", tokenErr)
}
if eventDigestErr != nil {
logger.WithError(eventDigestErr).Error("Couldn't get event digest")
logger.Errorf("Couldn't get event digest: %s", eventDigestErr)
}
}
return RegisterTunnel(serveCtx, credentialManager, handler.muxer, config, logger, connectionID, originLocalIP, u)
return RegisterTunnel(serveCtx, credentialManager, handler.muxer, config, logger, connectionID, originLocalIP, cloudflaredUUID)
})
errGroup.Go(func() error {
@ -292,7 +325,10 @@ func ServeTunnel(
select {
case <-serveCtx.Done():
// UnregisterTunnel blocks until the RPC call returns
err := UnregisterTunnel(handler.muxer, config.GracePeriod, config.TransportLogger)
var err error
if connectedFuse.Value() {
err = UnregisterTunnel(handler.muxer, config.GracePeriod, config.TransportLogger)
}
handler.muxer.Shutdown()
return err
case <-updateMetricsTickC:
@ -301,6 +337,17 @@ func ServeTunnel(
}
})
errGroup.Go(func() error {
for {
select {
case reconnect := <-reconnectCh:
return &reconnect
case <-serveCtx.Done():
return nil
}
}
})
errGroup.Go(func() error {
// All routines should stop when muxer finish serving. When muxer is shutdown
// gracefully, it doesn't return an error, so we need to return errMuxerShutdown
@ -321,7 +368,7 @@ func ServeTunnel(
logger.Info("Already connected to this server, selecting a different one")
return err, true
case serverRegisterTunnelError:
logger.WithError(castedErr.cause).Error("Register tunnel error from server side")
logger.Errorf("Register tunnel error from server side: %s", castedErr.cause)
// Don't send registration error return from server to Sentry. They are
// logged on server side
if incidents := config.IncidentLookup.ActiveIncidents(); len(incidents) > 0 {
@ -329,13 +376,17 @@ func ServeTunnel(
}
return castedErr.cause, !castedErr.permanent
case clientRegisterTunnelError:
logger.WithError(castedErr.cause).Error("Register tunnel error on client side")
logger.Errorf("Register tunnel error on client side: %s", castedErr.cause)
return err, true
case muxerShutdownError:
logger.Infof("Muxer shutdown")
logger.Info("Muxer shutdown")
return err, true
case *ReconnectSignal:
logger.Infof("Restarting due to reconnect signal in %d seconds", castedErr.Delay)
castedErr.DelayBeforeReconnect()
return err, true
default:
logger.WithError(err).Error("Serve tunnel error")
logger.Errorf("Serve tunnel error: %s", err)
return err, true
}
}
@ -347,13 +398,13 @@ func RegisterTunnel(
credentialManager ReconnectTunnelCredentialManager,
muxer *h2mux.Muxer,
config *TunnelConfig,
logger *log.Entry,
logger logger.Service,
connectionID uint8,
originLocalIP string,
uuid uuid.UUID,
) error {
config.TransportLogger.Debug("initiating RPC stream to register")
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger.WithField("subsystem", "rpc-register"), openStreamTimeout)
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, register)
@ -375,22 +426,23 @@ func RegisterTunnel(
return processRegisterTunnelError(registrationErr, config.Metrics, register)
}
credentialManager.SetEventDigest(registration.EventDigest)
return processRegistrationSuccess(config, logger, connectionID, registration, register)
return processRegistrationSuccess(config, logger, connectionID, registration, register, credentialManager)
}
func ReconnectTunnel(
ctx context.Context,
token []byte,
eventDigest []byte,
eventDigest, connDigest []byte,
muxer *h2mux.Muxer,
config *TunnelConfig,
logger *log.Entry,
logger logger.Service,
connectionID uint8,
originLocalIP string,
uuid uuid.UUID,
credentialManager ReconnectTunnelCredentialManager,
) error {
config.TransportLogger.Debug("initiating RPC stream to reconnect")
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger.WithField("subsystem", "rpc-reconnect"), openStreamTimeout)
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, reconnect)
@ -405,6 +457,7 @@ func ReconnectTunnel(
ctx,
token,
eventDigest,
connDigest,
config.Hostname,
config.RegistrationOptions(connectionID, originLocalIP, uuid),
)
@ -412,10 +465,17 @@ func ReconnectTunnel(
// ReconnectTunnel RPC failure
return processRegisterTunnelError(registrationErr, config.Metrics, reconnect)
}
return processRegistrationSuccess(config, logger, connectionID, registration, reconnect)
return processRegistrationSuccess(config, logger, connectionID, registration, reconnect, credentialManager)
}
func processRegistrationSuccess(config *TunnelConfig, logger *log.Entry, connectionID uint8, registration *tunnelpogs.TunnelRegistration, name registerRPCName) error {
func processRegistrationSuccess(
config *TunnelConfig,
logger logger.Service,
connectionID uint8,
registration *tunnelpogs.TunnelRegistration,
name registerRPCName,
credentialManager ReconnectTunnelCredentialManager,
) error {
for _, logLine := range registration.LogLines {
logger.Info(logLine)
}
@ -429,14 +489,15 @@ func processRegistrationSuccess(config *TunnelConfig, logger *log.Entry, connect
if isTrialTunnel := config.Hostname == ""; isTrialTunnel {
if url, err := url.Parse(registration.Url); err == nil {
for _, line := range asciiBox(trialZoneMsg(url.String()), 2) {
logger.Infoln(line)
logger.Info(line)
}
} else {
logger.Errorln("Failed to connect tunnel, please try again.")
logger.Error("Failed to connect tunnel, please try again.")
return fmt.Errorf("empty URL in response from Cloudflare edge")
}
}
credentialManager.SetConnDigest(connectionID, registration.ConnDigest)
config.Metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc()
logger.Infof("Route propagating, it may take up to 1 minute for your new route to become functional")
@ -456,14 +517,16 @@ func processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, metrics
}
}
func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log.Logger) error {
func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger logger.Service) error {
logger.Debug("initiating RPC stream to unregister")
ctx := context.Background()
tunnelServer, err := connection.NewRPCClient(ctx, muxer, logger.WithField("subsystem", "rpc-unregister"), openStreamTimeout)
tunnelServer, err := connection.NewRPCClient(ctx, muxer, logger, openStreamTimeout)
if err != nil {
// RPC stream open error
return err
}
defer tunnelServer.Close()
// gracePeriod is encoded in int64 using capnproto
return tunnelServer.UnregisterTunnel(ctx, gracePeriod.Nanoseconds())
}
@ -472,16 +535,16 @@ func LogServerInfo(
promise tunnelrpc.ServerInfo_Promise,
connectionID uint8,
metrics *TunnelMetrics,
logger *log.Entry,
logger logger.Service,
) {
serverInfoMessage, err := promise.Struct()
if err != nil {
logger.WithError(err).Warn("Failed to retrieve server information")
logger.Errorf("Failed to retrieve server information: %s", err)
return
}
serverInfo, err := tunnelpogs.UnmarshalServerInfo(serverInfoMessage)
if err != nil {
logger.WithError(err).Warn("Failed to retrieve server information")
logger.Errorf("Failed to retrieve server information: %s", err)
return
}
logger.Infof("Connected to %s", serverInfo.LocationName)
@ -498,8 +561,10 @@ type TunnelHandler struct {
metrics *TunnelMetrics
// connectionID is only used by metrics, and prometheus requires labels to be string
connectionID string
logger *log.Logger
logger logger.Service
noChunkedEncoding bool
bufferPool *buffer.Pool
}
// NewTunnelHandler returns a TunnelHandler, origin LAN IP and error
@ -507,6 +572,7 @@ func NewTunnelHandler(ctx context.Context,
config *TunnelConfig,
addr *net.TCPAddr,
connectionID uint8,
bufferPool *buffer.Pool,
) (*TunnelHandler, string, error) {
originURL, err := validation.ValidateUrl(config.OriginUrl)
if err != nil {
@ -522,6 +588,7 @@ func NewTunnelHandler(ctx context.Context,
connectionID: uint8ToString(connectionID),
logger: config.Logger,
noChunkedEncoding: config.NoChunkedEncoding,
bufferPool: bufferPool,
}
if h.httpClient == nil {
h.httpClient = http.DefaultTransport
@ -552,12 +619,12 @@ func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
req, reqErr := h.createRequest(stream)
if reqErr != nil {
h.logError(stream, reqErr)
h.writeErrorResponse(stream, reqErr)
return reqErr
}
cfRay := streamhandler.FindCfRayHeader(req)
lbProbe := streamhandler.IsLBProbeRequest(req)
cfRay := findCfRayHeader(req)
lbProbe := isLBProbeRequest(req)
h.logRequest(req, cfRay, lbProbe)
var resp *http.Response
@ -568,7 +635,7 @@ func (h *TunnelHandler) ServeStream(stream *h2mux.MuxedStream) error {
resp, respErr = h.serveHTTP(stream, req)
}
if respErr != nil {
h.logError(stream, respErr)
h.writeErrorResponse(stream, respErr)
return respErr
}
h.logResponseOk(resp, cfRay, lbProbe)
@ -606,6 +673,7 @@ func (h *TunnelHandler) serveWebsocket(stream *h2mux.MuxedStream, req *http.Requ
// Copy to/from stream to the undelying connection. Use the underlying
// connection because cloudflared doesn't operate on the message themselves
websocket.Stream(conn.UnderlyingConn(), stream)
return response, nil
}
@ -633,7 +701,9 @@ func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request)
}
defer response.Body.Close()
err = stream.WriteHeaders(h2mux.H1ResponseToH2ResponseHeaders(response))
headers := h2mux.H1ResponseToH2ResponseHeaders(response)
headers = append(headers, h2mux.CreateResponseMetaHeader(h2mux.ResponseMetaHeaderField, h2mux.ResponseSourceOrigin))
err = stream.WriteHeaders(headers)
if err != nil {
return nil, errors.Wrap(err, "Error writing response header")
}
@ -642,7 +712,9 @@ func (h *TunnelHandler) serveHTTP(stream *h2mux.MuxedStream, req *http.Request)
} else {
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
// compression generates dictionary on first write
io.CopyBuffer(stream, response.Body, make([]byte, 512*1024))
buf := h.bufferPool.Get()
defer h.bufferPool.Put(buf)
io.CopyBuffer(stream, response.Body, buf)
}
return response, nil
}
@ -666,49 +738,50 @@ func (h *TunnelHandler) isEventStream(response *http.Response) bool {
return false
}
func (h *TunnelHandler) logError(stream *h2mux.MuxedStream, err error) {
h.logger.WithError(err).Error("HTTP request error")
stream.WriteHeaders([]h2mux.Header{{Name: ":status", Value: "502"}})
func (h *TunnelHandler) writeErrorResponse(stream *h2mux.MuxedStream, err error) {
h.logger.Errorf("HTTP request error: %s", err)
stream.WriteHeaders([]h2mux.Header{
{Name: ":status", Value: "502"},
h2mux.CreateResponseMetaHeader(h2mux.ResponseMetaHeaderField, h2mux.ResponseSourceCloudflared),
})
stream.Write([]byte("502 Bad Gateway"))
h.metrics.incrementResponses(h.connectionID, "502")
}
func (h *TunnelHandler) logRequest(req *http.Request, cfRay string, lbProbe bool) {
logger := log.NewEntry(h.logger)
logger := h.logger
if cfRay != "" {
logger = logger.WithField("CF-RAY", cfRay)
logger.Debugf("%s %s %s", req.Method, req.URL, req.Proto)
logger.Debugf("CF-RAY: %s %s %s %s", cfRay, req.Method, req.URL, req.Proto)
} else if lbProbe {
logger.Debugf("Load Balancer health check %s %s %s", req.Method, req.URL, req.Proto)
logger.Debugf("CF-RAY: %s Load Balancer health check %s %s %s", cfRay, req.Method, req.URL, req.Proto)
} else {
logger.Warnf("All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", req.Method, req.URL, req.Proto)
logger.Infof("CF-RAY: %s All requests should have a CF-RAY header. Please open a support ticket with Cloudflare. %s %s %s ", cfRay, req.Method, req.URL, req.Proto)
}
logger.Debugf("Request Headers %+v", req.Header)
logger.Debugf("CF-RAY: %s Request Headers %+v", cfRay, req.Header)
if contentLen := req.ContentLength; contentLen == -1 {
logger.Debugf("Request Content length unknown")
logger.Debugf("CF-RAY: %s Request Content length unknown", cfRay)
} else {
logger.Debugf("Request content length %d", contentLen)
logger.Debugf("CF-RAY: %s Request content length %d", cfRay, contentLen)
}
}
func (h *TunnelHandler) logResponseOk(r *http.Response, cfRay string, lbProbe bool) {
h.metrics.incrementResponses(h.connectionID, "200")
logger := log.NewEntry(h.logger)
logger := h.logger
if cfRay != "" {
logger = logger.WithField("CF-RAY", cfRay)
logger.Debugf("%s", r.Status)
logger.Debugf("CF-RAY: %s %s", cfRay, r.Status)
} else if lbProbe {
logger.Debugf("Response to Load Balancer health check %s", r.Status)
} else {
logger.Infof("%s", r.Status)
}
logger.Debugf("Response Headers %+v", r.Header)
logger.Debugf("CF-RAY: %s Response Headers %+v", cfRay, r.Header)
if contentLen := r.ContentLength; contentLen == -1 {
logger.Debugf("Response content length unknown")
logger.Debugf("CF-RAY: %s Response content length unknown", cfRay)
} else {
logger.Debugf("Response content length %d", contentLen)
logger.Debugf("CF-RAY: %s Response content length %d", cfRay, contentLen)
}
}
@ -765,3 +838,11 @@ func activeIncidentsMsg(incidents []Incident) string {
return preamble + " " + strings.Join(incidentStrings, "; ")
}
func findCfRayHeader(h1 *http.Request) string {
return h1.Header.Get("Cf-Ray")
}
func isLBProbeRequest(req *http.Request) bool {
return strings.HasPrefix(req.UserAgent(), lbProbeUserAgentPrefix)
}

View File

@ -1,235 +0,0 @@
// Package client defines and implements interface to proxy to HTTP, websocket and hello world origins
package originservice
import (
"bufio"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/hello"
"github.com/cloudflare/cloudflared/log"
"github.com/cloudflare/cloudflared/websocket"
"github.com/pkg/errors"
)
// OriginService is an interface to proxy requests to different type of origins
type OriginService interface {
Proxy(stream *h2mux.MuxedStream, req *http.Request) (resp *http.Response, err error)
URL() *url.URL
Summary() string
Shutdown()
}
// HTTPService talks to origin using HTTP/HTTPS
type HTTPService struct {
client http.RoundTripper
originURL *url.URL
chunkedEncoding bool
}
func NewHTTPService(transport http.RoundTripper, url *url.URL, chunkedEncoding bool) OriginService {
return &HTTPService{
client: transport,
originURL: url,
chunkedEncoding: chunkedEncoding,
}
}
func (hc *HTTPService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
// Support for WSGI Servers by switching transfer encoding from chunked to gzip/deflate
if !hc.chunkedEncoding {
req.TransferEncoding = []string{"gzip", "deflate"}
cLength, err := strconv.Atoi(req.Header.Get("Content-Length"))
if err == nil {
req.ContentLength = int64(cLength)
}
}
// Request origin to keep connection alive to improve performance
req.Header.Set("Connection", "keep-alive")
resp, err := hc.client.RoundTrip(req)
if err != nil {
return nil, errors.Wrap(err, "error proxying request to HTTP origin")
}
defer resp.Body.Close()
err = stream.WriteHeaders(h1ResponseToH2Response(resp))
if err != nil {
return nil, errors.Wrap(err, "error writing response header to HTTP origin")
}
if isEventStream(resp) {
writeEventStream(stream, resp.Body)
} else {
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
// compression generates dictionary on first write
io.CopyBuffer(stream, resp.Body, make([]byte, 512*1024))
}
return resp, nil
}
func (hc *HTTPService) URL() *url.URL {
return hc.originURL
}
func (hc *HTTPService) Summary() string {
return fmt.Sprintf("HTTP service listening on %s", hc.originURL)
}
func (hc *HTTPService) Shutdown() {}
// WebsocketService talks to origin using WS/WSS
type WebsocketService struct {
tlsConfig *tls.Config
originURL *url.URL
shutdownC chan struct{}
}
func NewWebSocketService(tlsConfig *tls.Config, url *url.URL) (OriginService, error) {
listener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
return nil, errors.Wrap(err, "cannot start Websocket Proxy Server")
}
shutdownC := make(chan struct{})
go func() {
websocket.StartProxyServer(log.CreateLogger(), listener, url.String(), shutdownC)
}()
return &WebsocketService{
tlsConfig: tlsConfig,
originURL: url,
shutdownC: shutdownC,
}, nil
}
func (wsc *WebsocketService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
if !websocket.IsWebSocketUpgrade(req) {
return nil, fmt.Errorf("request is not a websocket connection")
}
conn, response, err := websocket.ClientConnect(req, wsc.tlsConfig)
if err != nil {
return nil, err
}
defer conn.Close()
err = stream.WriteHeaders(h1ResponseToH2Response(response))
if err != nil {
return nil, errors.Wrap(err, "error writing response header to websocket origin")
}
// Copy to/from stream to the undelying connection. Use the underlying
// connection because cloudflared doesn't operate on the message themselves
websocket.Stream(conn.UnderlyingConn(), stream)
return response, nil
}
func (wsc *WebsocketService) URL() *url.URL {
return wsc.originURL
}
func (wsc *WebsocketService) Summary() string {
return fmt.Sprintf("Websocket listening on %s", wsc.originURL)
}
func (wsc *WebsocketService) Shutdown() {
close(wsc.shutdownC)
}
// HelloWorldService talks to the hello world example origin
type HelloWorldService struct {
client http.RoundTripper
listener net.Listener
originURL *url.URL
shutdownC chan struct{}
}
func NewHelloWorldService(transport http.RoundTripper) (OriginService, error) {
listener, err := hello.CreateTLSListener("127.0.0.1:")
if err != nil {
return nil, errors.Wrap(err, "cannot start Hello World Server")
}
shutdownC := make(chan struct{})
go func() {
hello.StartHelloWorldServer(log.CreateLogger(), listener, shutdownC)
}()
return &HelloWorldService{
client: transport,
listener: listener,
originURL: &url.URL{
Scheme: "https",
Host: listener.Addr().String(),
},
shutdownC: shutdownC,
}, nil
}
func (hwc *HelloWorldService) Proxy(stream *h2mux.MuxedStream, req *http.Request) (*http.Response, error) {
// Request origin to keep connection alive to improve performance
req.Header.Set("Connection", "keep-alive")
resp, err := hwc.client.RoundTrip(req)
if err != nil {
return nil, errors.Wrap(err, "error proxying request to Hello World origin")
}
defer resp.Body.Close()
err = stream.WriteHeaders(h1ResponseToH2Response(resp))
if err != nil {
return nil, errors.Wrap(err, "error writing response header to Hello World origin")
}
// Use CopyBuffer, because Copy only allocates a 32KiB buffer, and cross-stream
// compression generates dictionary on first write
io.CopyBuffer(stream, resp.Body, make([]byte, 512*1024))
return resp, nil
}
func (hwc *HelloWorldService) URL() *url.URL {
return hwc.originURL
}
func (hwc *HelloWorldService) Summary() string {
return fmt.Sprintf("Hello World service listening on %s", hwc.originURL)
}
func (hwc *HelloWorldService) Shutdown() {
hwc.listener.Close()
}
func isEventStream(resp *http.Response) bool {
// Check if content-type is text/event-stream. We need to check if the header value starts with text/event-stream
// because text/event-stream; charset=UTF-8 is also valid
// Ref: https://tools.ietf.org/html/rfc7231#section-3.1.1.1
for _, contentType := range resp.Header["content-type"] {
if strings.HasPrefix(strings.ToLower(contentType), "text/event-stream") {
return true
}
}
return false
}
func writeEventStream(stream *h2mux.MuxedStream, respBody io.ReadCloser) {
reader := bufio.NewReader(respBody)
for {
line, err := reader.ReadBytes('\n')
if err != nil {
break
}
stream.Write(line)
}
}
func h1ResponseToH2Response(h1 *http.Response) (h2 []h2mux.Header) {
h2 = []h2mux.Header{{Name: ":status", Value: fmt.Sprintf("%d", h1.StatusCode)}}
for headerName, headerValues := range h1.Header {
for _, headerValue := range headerValues {
h2 = append(h2, h2mux.Header{Name: strings.ToLower(headerName), Value: headerValue})
}
}
return
}

View File

@ -1,60 +0,0 @@
package originservice
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsEventStream(t *testing.T) {
tests := []struct {
resp *http.Response
isEventStream bool
}{
{
resp: &http.Response{},
isEventStream: false,
},
{
// isEventStream checks all headers
resp: &http.Response{
Header: http.Header{
"accept": []string{"text/html"},
"content-type": []string{"text/event-stream"},
},
},
isEventStream: true,
},
{
// Content-Type and text/event-stream are case-insensitive. text/event-stream can be followed by OWS parameter
resp: &http.Response{
Header: http.Header{
"content-type": []string{"Text/event-stream;charset=utf-8"},
},
},
isEventStream: true,
},
{
// Content-Type and text/event-stream are case-insensitive. text/event-stream can be followed by OWS parameter
resp: &http.Response{
Header: http.Header{
"content-type": []string{"appication/json", "text/html", "Text/event-stream;charset=utf-8"},
},
},
isEventStream: true,
},
{
// Not an event stream because the content-type value doesn't start with text/event-stream
resp: &http.Response{
Header: http.Header{
"content-type": []string{" text/event-stream"},
},
},
isEventStream: false,
},
}
for _, test := range tests {
assert.Equal(t, test.isEventStream, isEventStream(test.resp), "Header: %v", test.resp.Header)
}
}

Some files were not shown because too many files have changed in this diff Show More