mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20466dacb7 | |||
| 5b3b592108 | |||
| 9952ce0069 | |||
| 1cbc8fb8ac | |||
| edc69694cb | |||
| bd15c6b8c3 | |||
| 42fe2e7266 | |||
| 44e3be2c88 | |||
| 61d5461138 | |||
| b696ca8b1c | |||
| 6a4d020c27 | |||
| 4791ba3b87 | |||
| cf1c9a3083 | |||
| d61e3fb130 | |||
| ca7d6797e1 | |||
| af0d04d0f3 | |||
| 6274567e16 | |||
| 8836ee1dda | |||
| 7afde79600 | |||
| 3d782f7162 | |||
| 058598ea58 | |||
| 8e617df914 | |||
| 7260f3e487 | |||
| a42b66e8bd | |||
| 28d556b8d4 | |||
| 33701f50ec | |||
| abfeebf67d | |||
| 3b293048f4 | |||
| ac3638f6b1 | |||
| f7ff41f1dc | |||
| f5c8ff77e9 | |||
| 87e06100df | |||
| 1ed9e0fceb |
@@ -2,8 +2,6 @@ images:
|
||||
- name: cloudflared
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
versions:
|
||||
- latest
|
||||
registries:
|
||||
- name: docker.io/cloudflare
|
||||
user: env:DOCKER_USER
|
||||
|
||||
@@ -4,7 +4,8 @@ VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DAT
|
||||
|
||||
IMPORT_PATH := github.com/cloudflare/cloudflared
|
||||
PACKAGE_DIR := $(CURDIR)/packaging
|
||||
INSTALL_BINDIR := usr/local/bin
|
||||
INSTALL_BINDIR := /usr/bin/
|
||||
MAN_DIR := /usr/share/man/man1/
|
||||
|
||||
EQUINOX_FLAGS = --version="$(VERSION)" \
|
||||
--platforms="$(EQUINOX_BUILD_PLATFORMS)" \
|
||||
@@ -48,6 +49,12 @@ else
|
||||
EXECUTABLE_PATH=./cloudflared
|
||||
endif
|
||||
|
||||
ifeq ($(FLAVOR), centos-7)
|
||||
TARGET_PUBLIC_REPO ?= el7
|
||||
else
|
||||
TARGET_PUBLIC_REPO ?= $(FLAVOR)
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: cloudflared test
|
||||
|
||||
@@ -74,31 +81,39 @@ test-ssh-server:
|
||||
define publish_package
|
||||
for HOST in $(CF_PKG_HOSTS); do \
|
||||
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
|
||||
scp -4 cloudflared_$(VERSION)_amd64.deb cfsync@$$HOST:/state/cf-pkg/staging/$(1)/$(FLAVOR)/cloudflared/; \
|
||||
scp -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
|
||||
done
|
||||
endef
|
||||
|
||||
.PHONY: publish-deb
|
||||
publish-deb: cloudflared-deb
|
||||
$(call publish_package,apt)
|
||||
$(call publish_package,deb,apt)
|
||||
|
||||
.PHONY: publish-rpm
|
||||
publish-rpm: cloudflared-rpm
|
||||
$(call publish_package,yum)
|
||||
$(call publish_package,rpm,yum)
|
||||
|
||||
define build_package
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
cat cloudflared_man_template | sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' > $(PACKAGE_DIR)/cloudflared.1
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) --$(1)-compression bzip2 \
|
||||
--description 'Cloudflare Argo tunnel daemon' \
|
||||
--vendor 'Cloudflare' \
|
||||
--license 'Cloudflare Service Agreement' \
|
||||
--url 'https://github.com/cloudflare/cloudflared' \
|
||||
-m 'Cloudflare <support@cloudflare.com>' \
|
||||
-a $(TARGET_ARCH) -v $(VERSION) -n cloudflared --after-install postinst.sh --after-remove postrm.sh \
|
||||
cloudflared=$(INSTALL_BINDIR) cloudflared.1=$(MAN_DIR)
|
||||
endef
|
||||
|
||||
.PHONY: cloudflared-deb
|
||||
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 $(TARGET_ARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
|
||||
$(call build_package,deb)
|
||||
|
||||
.PHONY: cloudflared-rpm
|
||||
cloudflared-rpm: cloudflared
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t rpm --rpm-compression bzip2 \
|
||||
-a $(TARGET_ARCH) -v $(VERSION) -n cloudflared cloudflared=/usr/local/bin/
|
||||
$(call build_package,rpm)
|
||||
|
||||
.PHONY: cloudflared-darwin-amd64.tgz
|
||||
cloudflared-darwin-amd64.tgz: cloudflared
|
||||
@@ -122,6 +137,10 @@ release: bin/equinox
|
||||
github-release: cloudflared
|
||||
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
|
||||
|
||||
.PHONY: github-message
|
||||
github-message:
|
||||
python3 github_message.py --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/
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
2020.8.0
|
||||
- 2020-07-30 TUN-3220: tunnel route reports created route
|
||||
- 2020-07-31 TUN-3221: ConnectionOptions tracks numPreviousAttempts.
|
||||
- 2020-07-20 TUN-3190: Initialize logger using command line flags in tunnels subcommands
|
||||
- 2020-07-21 TUN-3192: Use zone ID in tunnelstore request path; improve debug logging
|
||||
- 2020-07-23 TUN-3194: Don't render log output when level is not enabled
|
||||
- 2020-07-22 AUTH-2016 adds sha256 hashes to releases
|
||||
- 2020-07-27 Removes centos 6 build
|
||||
- 2020-07-27 TUN-3209: Add benchmark for header serialization
|
||||
- 2020-07-24 TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2
|
||||
- 2020-07-26 TUN-3208: Add benchmark for large response write
|
||||
- 2020-07-27 TUN-3208: Reduce copies and allocations on h2mux write path. Pre-allocate 16KB write buffer on the first write if possible. Use explicit byte array for chunks on write thread to avoid copying through intermediate buffer due to io.CopyN.
|
||||
- 2020-07-28 AUTH-2714: Adds arm64 cloudflared build
|
||||
- 2020-07-29 AUTH-2927 run message update after all github builds are done
|
||||
- 2020-07-17 AUTH-2902 redirect with just the root host on curl commands
|
||||
|
||||
2020.7.4
|
||||
- 2020-07-20 Build cloudflared for arm64 on native agents
|
||||
- 2020-07-10 TUN-3048: Handle error when user tries to delete active tunnel
|
||||
- 2020-07-14 AUTH-2890: adds error handler to cli actions
|
||||
- 2020-07-06 TUN-3156: Add route subcommand under tunnel
|
||||
|
||||
2020.7.3
|
||||
- 2020-07-13 Change scp command to use file glob that matches both cloudflared rpms and debs
|
||||
|
||||
2020.7.2
|
||||
- 2020-07-02 AUTH-2644: Change install location and add man page
|
||||
- 2020-07-02 TUN-3131: Allow user to specify tunnel credentials path, and remove it in tunnel delete command
|
||||
- 2020-07-03 TUN-3008: Implement cloudflared tunnel cleanup command
|
||||
- 2020-07-02 TUN-3150: cloudflared tunnel list's table should use intelligent column width
|
||||
- 2020-07-07 TUN-3169: Move on to the next address when edge returns duplicate connection. There's no point in trying to connect to the same address since it will be hashed to the same metal. Improve logging of errors from serve tunnel loop, hide useless context cancelled error.
|
||||
- 2020-07-08 beautify package meta information generated by fpm (#218)
|
||||
- 2020-07-06 AUTH-2871: fix rpm builds
|
||||
- 2020-07-08 AUTH-2858: Set file to disable autoupdate
|
||||
- 2020-07-09 AUTH-2872: Adds centos-6 build
|
||||
|
||||
2020.7.1
|
||||
- 2020-07-02 DEVTOOLS-7321: Push GitHub homebrew updates to master
|
||||
- 2020-07-06 TUN-3161: Upgrade golang.org/x/ deps
|
||||
|
||||
+46
-1
@@ -22,6 +22,17 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- make cloudflared-deb
|
||||
publish-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -147,6 +158,20 @@ stretch: &stretch
|
||||
- export GOARCH=386
|
||||
- export CC=i686-w64-mingw32-gcc-win32
|
||||
- make github-release
|
||||
github-release-linux-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- crossbuild-essential-armhf
|
||||
- g++-aarch64-linux-gnu
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- export GOOS=linux
|
||||
- export GOARCH=arm64
|
||||
- export CC=aarch64-linux-gnu-gcc
|
||||
- make github-release
|
||||
test:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
@@ -165,6 +190,15 @@ stretch: &stretch
|
||||
- s3cmd
|
||||
post-cache:
|
||||
- .teamcity/update-homebrew.sh
|
||||
github-message-release:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
post-cache:
|
||||
- pip3 install pygithub
|
||||
- make github-message
|
||||
|
||||
jessie: *stretch
|
||||
|
||||
@@ -173,7 +207,7 @@ buster: *stretch
|
||||
centos-7:
|
||||
publish-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
builddeps: &el7_builddeps
|
||||
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
|
||||
post-cache:
|
||||
- sudo yum install -y fakeroot
|
||||
@@ -183,6 +217,17 @@ centos-7:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-rpm
|
||||
build-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: *el7_builddeps
|
||||
post-cache:
|
||||
- sudo yum install -y fakeroot
|
||||
- wget https://golang.org/dl/go1.12.9.linux-amd64.tar.gz -P /tmp/
|
||||
- sudo tar -C /usr/local -xzf /tmp/go1.12.9.linux-amd64.tar.gz
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-rpm
|
||||
|
||||
# cfsetup compose
|
||||
default-stack: test_dbconnect
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.\" Manpage for cloudflared.
|
||||
.TH man 1 ${DATE} "${VERSION}" "cloudflared man page"
|
||||
.SH NAME
|
||||
cloudflared \- creates a connection to the cloudflare edge network
|
||||
.SH DESCRIPTION
|
||||
cloudflared creates a persistent connection between a local service and the Cloudflare network. Once the daemon is running and the Tunnel has been configured, the local service can be locked down to only allow connections from Cloudflare.
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
@@ -21,12 +22,12 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
&cli.Command{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as a system service",
|
||||
Action: installLinuxService,
|
||||
Action: cliutil.ErrorHandler(installLinuxService),
|
||||
},
|
||||
&cli.Command{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel service",
|
||||
Action: uninstallLinuxService,
|
||||
Action: cliutil.ErrorHandler(uninstallLinuxService),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@@ -24,12 +25,12 @@ func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
|
||||
{
|
||||
Name: "install",
|
||||
Usage: "Install Argo Tunnel as an user launch agent",
|
||||
Action: installLaunchd,
|
||||
Action: cliutil.ErrorHandler(installLaunchd),
|
||||
},
|
||||
{
|
||||
Name: "uninstall",
|
||||
Usage: "Uninstall the Argo Tunnel launch agent",
|
||||
Action: uninstallLaunchd,
|
||||
Action: cliutil.ErrorHandler(uninstallLaunchd),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -88,7 +88,7 @@ func commands(version func(c *cli.Context)) []*cli.Command {
|
||||
cmds := []*cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Action: updater.Update,
|
||||
Action: cliutil.ErrorHandler(updater.Update),
|
||||
Usage: "Update the agent if a new version exists",
|
||||
ArgsUsage: " ",
|
||||
Description: `Looks for a new version on the official download server.
|
||||
|
||||
@@ -128,19 +128,19 @@ func isTokenLocked(lockFilePath string) bool {
|
||||
}
|
||||
|
||||
// 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
|
||||
// it appends the full url as the 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
|
||||
// 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)
|
||||
}
|
||||
|
||||
// FetchToken will either load a stored token or generate a new one
|
||||
// it appends the host of the appURL as the redirect URL to the access cli request if opening the browser
|
||||
func FetchToken(appURL *url.URL, logger logger.Service) (string, error) {
|
||||
return getToken(appURL, true, 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) {
|
||||
func getToken(appURL *url.URL, useHostOnly bool, logger logger.Service) (string, error) {
|
||||
if token, err := GetTokenIfExists(appURL); token != "" && err == nil {
|
||||
return token, nil
|
||||
}
|
||||
@@ -166,7 +166,7 @@ func getToken(appURL *url.URL, shouldRedirect bool, logger logger.Service) (stri
|
||||
// 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, shouldRedirect, logger)
|
||||
token, err := transfer.Run(appURL, keyName, keyName, "", path, true, useHostOnly, logger)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@ const (
|
||||
// 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, shouldRedirect bool, logger logger.Service) ([]byte, error) {
|
||||
func Run(transferURL *url.URL, resourceName, key, value, path string, shouldEncrypt bool, useHostOnly 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, shouldRedirect)
|
||||
requestURL, err := buildRequestURL(transferURL, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -82,18 +82,18 @@ 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, shouldRedirect bool) (string, error) {
|
||||
func buildRequestURL(baseURL *url.URL, key, value string, cli, useHostOnly bool) (string, error) {
|
||||
q := baseURL.Query()
|
||||
q.Set(key, value)
|
||||
baseURL.RawQuery = q.Encode()
|
||||
if useHostOnly {
|
||||
baseURL.Path = ""
|
||||
}
|
||||
if !cli {
|
||||
return baseURL.String(), nil
|
||||
}
|
||||
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
@@ -173,6 +173,8 @@ func Commands() []*cli.Command {
|
||||
subcommands = append(subcommands, buildListCommand())
|
||||
subcommands = append(subcommands, buildDeleteCommand())
|
||||
subcommands = append(subcommands, buildRunCommand())
|
||||
subcommands = append(subcommands, buildCleanupCommand())
|
||||
subcommands = append(subcommands, buildRouteCommand())
|
||||
|
||||
cmds = append(cmds, &cli.Command{
|
||||
Name: "tunnel",
|
||||
|
||||
@@ -40,7 +40,7 @@ func login(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, true, logger)
|
||||
_, err = transfer.Run(loginURL, "cert", "callback", callbackStoreURL, path, false, false, 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
|
||||
|
||||
@@ -9,21 +9,28 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"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/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
const (
|
||||
credFileFlagAlias = "cred-file"
|
||||
)
|
||||
|
||||
var (
|
||||
showDeletedFlag = &cli.BoolFlag{
|
||||
Name: "show-deleted",
|
||||
@@ -43,6 +50,16 @@ var (
|
||||
"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.",
|
||||
}
|
||||
credentialsFileFlag = &cli.StringFlag{
|
||||
Name: "credentials-file",
|
||||
Aliases: []string{credFileFlagAlias},
|
||||
Usage: "File path of tunnel credentials",
|
||||
}
|
||||
forceDeleteFlag = &cli.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Allows you to delete a tunnel, even if it has active connections.",
|
||||
}
|
||||
)
|
||||
|
||||
const hideSubcommands = true
|
||||
@@ -71,7 +88,7 @@ func createTunnel(c *cli.Context) error {
|
||||
}
|
||||
name := c.Args().First()
|
||||
|
||||
logger, err := logger.New()
|
||||
logger, err := createLogger(c, false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
@@ -81,11 +98,7 @@ func createTunnel(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
originCertPath, err := findOriginCert(c, logger)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
cert, err := getOriginCertFromContext(originCertPath, logger)
|
||||
cert, originCertPath, err := getOriginCertFromContext(c, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -118,13 +131,15 @@ func createTunnel(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func tunnelFilePath(tunnelID, originCertPath string) (string, error) {
|
||||
func tunnelFilePath(tunnelID uuid.UUID, directory string) (string, error) {
|
||||
fileName := fmt.Sprintf("%v.json", tunnelID)
|
||||
return filepath.Clean(fmt.Sprintf("%v/../%v", originCertPath, fileName)), nil
|
||||
filePath := filepath.Clean(fmt.Sprintf("%s/%s", directory, fileName))
|
||||
return homedir.Expand(filePath)
|
||||
}
|
||||
|
||||
func writeTunnelCredentials(tunnelID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
|
||||
filePath, err := tunnelFilePath(tunnelID, originCertPath)
|
||||
func writeTunnelCredentials(tunnelID uuid.UUID, accountID, originCertPath string, tunnelSecret []byte, logger logger.Service) error {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
filePath, err := tunnelFilePath(tunnelID, originCertDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -140,8 +155,8 @@ func writeTunnelCredentials(tunnelID, accountID, originCertPath string, tunnelSe
|
||||
return ioutil.WriteFile(filePath, body, 400)
|
||||
}
|
||||
|
||||
func readTunnelCredentials(tunnelID, originCertPath string) (*pogs.TunnelAuth, error) {
|
||||
filePath, err := tunnelFilePath(tunnelID, originCertPath)
|
||||
func readTunnelCredentials(c *cli.Context, tunnelID uuid.UUID, logger logger.Service) (*pogs.TunnelAuth, error) {
|
||||
filePath, err := tunnelCredentialsPath(c, tunnelID, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -157,6 +172,42 @@ func readTunnelCredentials(tunnelID, originCertPath string) (*pogs.TunnelAuth, e
|
||||
return &auth, nil
|
||||
}
|
||||
|
||||
func tunnelCredentialsPath(c *cli.Context, tunnelID uuid.UUID, logger logger.Service) (string, error) {
|
||||
if filePath := c.String("credentials-file"); filePath != "" {
|
||||
if validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to look for tunnel credentials in the origin cert directory
|
||||
if originCertPath, err := findOriginCert(c, logger); err == nil {
|
||||
originCertDir := filepath.Dir(originCertPath)
|
||||
if filePath, err := tunnelFilePath(tunnelID, originCertDir); err == nil {
|
||||
if validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort look under default config directories
|
||||
for _, configDir := range config.DefaultConfigDirs {
|
||||
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
|
||||
if validFilePath(filePath) {
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("Tunnel credentials file not found")
|
||||
}
|
||||
|
||||
func validFilePath(path string) bool {
|
||||
fileStat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !fileStat.IsDir()
|
||||
}
|
||||
|
||||
func buildListCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
@@ -169,16 +220,12 @@ func buildListCommand() *cli.Command {
|
||||
}
|
||||
|
||||
func listTunnels(c *cli.Context) error {
|
||||
logger, err := logger.New()
|
||||
logger, err := createLogger(c, false)
|
||||
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)
|
||||
cert, _, err := getOriginCertFromContext(c, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -203,12 +250,9 @@ func listTunnels(c *cli.Context) error {
|
||||
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))
|
||||
}
|
||||
fmtAndPrintTunnelList(tunnels)
|
||||
} else {
|
||||
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
|
||||
}
|
||||
@@ -216,6 +260,30 @@ func listTunnels(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fmtAndPrintTunnelList(tunnels []tunnelstore.Tunnel) {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
padding = 1
|
||||
padChar = ' '
|
||||
flags = 0
|
||||
)
|
||||
|
||||
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
||||
|
||||
// Print column headers with tabbed columns
|
||||
fmt.Fprintln(writer, "ID\tNAME\tCREATED\tCONNECTIONS\t")
|
||||
|
||||
// Loop through tunnels, create formatted string for each, and print using tabwriter
|
||||
for _, t := range tunnels {
|
||||
formattedStr := fmt.Sprintf("%s\t%s\t%s\t%s\t", t.ID, t.Name, t.CreatedAt.Format(time.RFC3339), fmtConnections(t.Connections))
|
||||
fmt.Fprintln(writer, formattedStr)
|
||||
}
|
||||
|
||||
// Write data buffered in tabwriter to output
|
||||
writer.Flush()
|
||||
}
|
||||
|
||||
func fmtConnections(connections []tunnelstore.Connection) string {
|
||||
|
||||
// Count connections per colo
|
||||
@@ -246,6 +314,7 @@ func buildDeleteCommand() *cli.Command {
|
||||
Usage: "Delete existing tunnel with given ID",
|
||||
ArgsUsage: "TUNNEL-ID",
|
||||
Hidden: hideSubcommands,
|
||||
Flags: []cli.Flag{credentialsFileFlag, forceDeleteFlag},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,25 +322,56 @@ 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()
|
||||
tunnelID, err := uuid.Parse(c.Args().First())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing tunnel ID")
|
||||
}
|
||||
|
||||
logger, err := logger.New()
|
||||
logger, err := createLogger(c, false)
|
||||
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)
|
||||
cert, _, err := getOriginCertFromContext(c, 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)
|
||||
forceFlagSet := c.Bool("force")
|
||||
|
||||
tunnel, err := client.GetTunnel(tunnelID)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Can't get tunnel information. Please check tunnel id: %s", tunnelID)
|
||||
}
|
||||
|
||||
// Check if tunnel DeletedAt field has already been set
|
||||
if !tunnel.DeletedAt.IsZero() {
|
||||
return errors.New("This tunnel has already been deleted.")
|
||||
}
|
||||
// Check if tunnel has existing connections and if force flag is set, cleanup connections
|
||||
if len(tunnel.Connections) > 0 {
|
||||
if !forceFlagSet {
|
||||
return errors.New("You can not delete this tunnel because it has active connections. To see connections run the 'list' command. If you believe the tunnel is not active, you can use a -f / --force flag with this command.")
|
||||
}
|
||||
|
||||
if err := client.CleanupConnections(tunnelID); err != nil {
|
||||
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnelID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.DeleteTunnel(tunnelID); err != nil {
|
||||
return errors.Wrapf(err, "Error deleting tunnel %s", tunnelID)
|
||||
}
|
||||
|
||||
tunnelCredentialsPath, err := tunnelCredentialsPath(c, tunnelID, logger)
|
||||
if err != nil {
|
||||
logger.Infof("Cannot locate tunnel credentials to delete, error: %v. Please delete the file manually", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = os.Remove(tunnelCredentialsPath); err != nil {
|
||||
logger.Infof("Cannot delete tunnel credentials, error: %v. Please delete the file manually", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -291,26 +391,29 @@ func renderOutput(format string, v interface{}) error {
|
||||
}
|
||||
|
||||
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)
|
||||
client := tunnelstore.NewRESTClient(c.String("api-url"), cert.AccountID, cert.ZoneID, cert.ServiceKey, logger)
|
||||
return client
|
||||
}
|
||||
|
||||
func getOriginCertFromContext(originCertPath string, logger logger.Service) (*certutil.OriginCert, error) {
|
||||
|
||||
func getOriginCertFromContext(c *cli.Context, logger logger.Service) (cert *certutil.OriginCert, originCertPath string, err error) {
|
||||
originCertPath, err = findOriginCert(c, logger)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, "Error locating origin cert")
|
||||
}
|
||||
blocks, err := readOriginCert(originCertPath, logger)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
|
||||
return nil, "", errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
|
||||
}
|
||||
|
||||
cert, err := certutil.DecodeOriginCert(blocks)
|
||||
cert, err = certutil.DecodeOriginCert(blocks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error decoding origin cert")
|
||||
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 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
|
||||
return cert, originCertPath, nil
|
||||
}
|
||||
|
||||
func buildRunCommand() *cli.Command {
|
||||
@@ -320,7 +423,7 @@ func buildRunCommand() *cli.Command {
|
||||
Usage: "Proxy a local web server by running the given tunnel",
|
||||
ArgsUsage: "TUNNEL-ID",
|
||||
Hidden: hideSubcommands,
|
||||
Flags: []cli.Flag{forceFlag},
|
||||
Flags: []cli.Flag{forceFlag, credentialsFileFlag},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,25 +431,161 @@ 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()
|
||||
tunnelID, err := uuid.Parse(id)
|
||||
|
||||
tunnelID, err := uuid.Parse(c.Args().First())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing tunnel ID")
|
||||
}
|
||||
|
||||
logger, err := logger.New()
|
||||
logger, err := createLogger(c, false)
|
||||
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)
|
||||
credentials, err := readTunnelCredentials(c, tunnelID, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debugf("Read credentials for %v", credentials.AccountTag)
|
||||
return StartServer(c, version, shutdownC, graceShutdownC, &origin.NamedTunnelConfig{Auth: *credentials, ID: tunnelID})
|
||||
}
|
||||
|
||||
func buildCleanupCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "cleanup",
|
||||
Action: cliutil.ErrorHandler(cleanupConnections),
|
||||
Usage: "Cleanup connections for the tunnel with given IDs",
|
||||
ArgsUsage: "TUNNEL-IDS",
|
||||
Hidden: hideSubcommands,
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupConnections(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel cleanup" requires at least 1 argument, the IDs of the tunnels to cleanup connections.`)
|
||||
}
|
||||
|
||||
logger, err := createLogger(c, false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
cert, _, err := getOriginCertFromContext(c, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := newTunnelstoreClient(c, cert, logger)
|
||||
|
||||
for i := 0; i < c.NArg(); i++ {
|
||||
tunnelID, err := uuid.Parse(c.Args().Get(i))
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to parse argument %d as tunnelID, error :%v", i, err)
|
||||
continue
|
||||
}
|
||||
logger.Infof("Cleanup connection for tunnel %s", tunnelID)
|
||||
if err := client.CleanupConnections(tunnelID); err != nil {
|
||||
logger.Errorf("Error cleaning up connections for tunnel %v, error :%v", tunnelID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRouteCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "route",
|
||||
Action: cliutil.ErrorHandler(routeTunnel),
|
||||
Usage: "Define what hostname or load balancer can route to this tunnel",
|
||||
Description: `The route defines what hostname or load balancer can route to this tunnel.
|
||||
To route a hostname: cloudflared tunnel route dns <tunnel ID> <hostname>
|
||||
To route a load balancer: cloudflared tunnel route lb <tunnel ID> <load balancer name> <load balancer pool>
|
||||
If you don't specify a load balancer pool, we will create a new pool called tunnel:<tunnel ID>`,
|
||||
ArgsUsage: "dns|lb TUNNEL-ID HOSTNAME [LB-POOL]",
|
||||
Hidden: hideSubcommands,
|
||||
}
|
||||
}
|
||||
|
||||
func routeTunnel(c *cli.Context) error {
|
||||
if c.NArg() < 2 {
|
||||
return cliutil.UsageError(`"cloudflared tunnel route" requires the first argument to be the route type(dns or lb), followed by the ID of the tunnel`)
|
||||
}
|
||||
const tunnelIDIndex = 1
|
||||
tunnelID, err := uuid.Parse(c.Args().Get(tunnelIDIndex))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing tunnel ID")
|
||||
}
|
||||
|
||||
logger, err := createLogger(c, false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
routeType := c.Args().First()
|
||||
var route tunnelstore.Route
|
||||
switch routeType {
|
||||
case "dns":
|
||||
route, err = dnsRouteFromArg(c, tunnelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "lb":
|
||||
route, err = lbRouteFromArg(c, tunnelID, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return cliutil.UsageError("%s is not a recognized route type. Supported route types are dns and lb", routeType)
|
||||
}
|
||||
|
||||
cert, _, err := getOriginCertFromContext(c, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := newTunnelstoreClient(c, cert, logger)
|
||||
|
||||
if err := client.RouteTunnel(tunnelID, route); err != nil {
|
||||
return errors.Wrap(err, "Failed to route tunnel")
|
||||
}
|
||||
|
||||
logger.Infof(route.SuccessSummary())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dnsRouteFromArg(c *cli.Context, tunnelID uuid.UUID) (tunnelstore.Route, error) {
|
||||
const (
|
||||
userHostnameIndex = 2
|
||||
expectArgs = 3
|
||||
)
|
||||
if c.NArg() != expectArgs {
|
||||
return nil, cliutil.UsageError("Expect %d arguments, got %d", expectArgs, c.NArg())
|
||||
}
|
||||
userHostname := c.Args().Get(userHostnameIndex)
|
||||
if userHostname == "" {
|
||||
return nil, cliutil.UsageError("The third argument should be the hostname")
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(userHostname), nil
|
||||
}
|
||||
|
||||
func lbRouteFromArg(c *cli.Context, tunnelID uuid.UUID, logger logger.Service) (tunnelstore.Route, error) {
|
||||
const (
|
||||
lbNameIndex = 2
|
||||
lbPoolIndex = 3
|
||||
expectMinArgs = 3
|
||||
)
|
||||
if c.NArg() < expectMinArgs {
|
||||
return nil, cliutil.UsageError("Expect at least %d arguments, got %d", expectMinArgs, c.NArg())
|
||||
}
|
||||
lbName := c.Args().Get(lbNameIndex)
|
||||
if lbName == "" {
|
||||
return nil, cliutil.UsageError("The third argument should be the load balancer name")
|
||||
}
|
||||
lbPool := c.Args().Get(lbPoolIndex)
|
||||
if lbPool == "" {
|
||||
lbPool = fmt.Sprintf("tunnel:%v", tunnelID)
|
||||
logger.Infof("Generate pool name %s", lbPool)
|
||||
}
|
||||
|
||||
return tunnelstore.NewLBRoute(lbName, lbPool), nil
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -72,8 +75,13 @@ func Test_fmtConnections(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTunnelfilePath(t *testing.T) {
|
||||
actual, err := tunnelFilePath("tunnel", "~/.cloudflared/cert.pem")
|
||||
tunnelID, err := uuid.Parse("f48d8918-bc23-4647-9d48-082c5b76de65")
|
||||
assert.NoError(t, err)
|
||||
expected := "~/.cloudflared/tunnel.json"
|
||||
originCertDir := filepath.Dir("~/.cloudflared/cert.pem")
|
||||
actual, err := tunnelFilePath(tunnelID, originCertDir)
|
||||
assert.NoError(t, err)
|
||||
homeDir, err := homedir.Dir()
|
||||
assert.NoError(t, err)
|
||||
expected := fmt.Sprintf("%s/.cloudflared/%v.json", homeDir, tunnelID)
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"gopkg.in/urfave/cli.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/equinox-io/equinox"
|
||||
"github.com/facebookgo/grace/gracenet"
|
||||
@@ -17,10 +19,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultCheckUpdateFreq = time.Hour * 24
|
||||
appID = "app_idCzgxYerVD"
|
||||
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
|
||||
DefaultCheckUpdateFreq = time.Hour * 24
|
||||
appID = "app_idCzgxYerVD"
|
||||
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/argo-tunnel/reference/service/"
|
||||
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
|
||||
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
|
||||
isManagedInstallFile = ".installedFromPackageManager"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -98,6 +102,11 @@ func Update(_ *cli.Context) error {
|
||||
return errors.Wrap(err, "error setting up logger")
|
||||
}
|
||||
|
||||
if wasInstalledFromPackageManager() {
|
||||
logger.Error("cloudflared was installed by a package manager. Please update using the same method.")
|
||||
return nil
|
||||
}
|
||||
|
||||
updateOutcome := loggedUpdate(logger)
|
||||
if updateOutcome.Error != nil {
|
||||
return &statusErr{updateOutcome.Error}
|
||||
@@ -217,6 +226,11 @@ func SupportAutoUpdate(logger logger.Service) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if wasInstalledFromPackageManager() {
|
||||
logger.Info(noUpdateManagedPackageMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
if isRunningFromTerminal() {
|
||||
logger.Info(noUpdateInShellMessage)
|
||||
return false
|
||||
@@ -224,6 +238,11 @@ func SupportAutoUpdate(logger logger.Service) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func wasInstalledFromPackageManager() bool {
|
||||
ok, _ := config.FileExists(filepath.Join(config.DefaultUnixConfigLocation, isManagedInstallFile))
|
||||
return ok
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
return terminal.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Creates Github Releases Notes with content hashes
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import hashlib
|
||||
import glob
|
||||
|
||||
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 get_sha256(filename):
|
||||
""" get the sha256 of a file """
|
||||
sha256_hash = hashlib.sha256()
|
||||
with open(filename,"rb") as f:
|
||||
for byte_block in iter(lambda: f.read(4096),b""):
|
||||
sha256_hash.update(byte_block)
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
|
||||
def update_or_add_message(msg, name, sha):
|
||||
"""
|
||||
updates or builds the github version message for each new asset's sha256.
|
||||
Searches the existing message string to update or create.
|
||||
"""
|
||||
new_text = '{0}: {1}\n'.format(name, sha)
|
||||
start = msg.find(name)
|
||||
if (start != -1):
|
||||
end = msg.find("\n", start)
|
||||
if (end != -1):
|
||||
return msg.replace(msg[start:end+1], new_text)
|
||||
back = msg.rfind("```")
|
||||
if (back != -1):
|
||||
return '{0}{1}```'.format(msg[:back], new_text)
|
||||
return '{0} \n### SHA256 Checksums:\n```\n{1}```'.format(msg, new_text)
|
||||
|
||||
def get_release(repo, version):
|
||||
""" Get a Github Release matching the version tag. """
|
||||
try:
|
||||
release = repo.get_release(version)
|
||||
logging.info("Release %s found", version)
|
||||
return release
|
||||
except UnknownObjectException:
|
||||
logging.info("Release %s not found", version)
|
||||
|
||||
|
||||
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(
|
||||
"--dry-run", action="store_true", help="Do not modify the release message"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
is_valid = True
|
||||
if not args.release_version:
|
||||
logging.error("Missing release version")
|
||||
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 update the Github Release message with the github asset's checksums """
|
||||
try:
|
||||
args = parse_args()
|
||||
client = Github(args.api_key)
|
||||
repo = client.get_repo(CLOUDFLARED_REPO)
|
||||
release = get_release(repo, args.release_version)
|
||||
|
||||
msg = release.body
|
||||
|
||||
for filename in glob.glob(".artifacts/*.*"):
|
||||
pkg_hash = get_sha256(filename)
|
||||
# add the sha256 of the new artifact to the release message body
|
||||
msg = update_or_add_message(msg, filename, pkg_hash)
|
||||
|
||||
if args.dry_run:
|
||||
logging.info("Skipping asset upload because of dry-run")
|
||||
return
|
||||
|
||||
# update the release body text
|
||||
release.update_release(version, version, msg)
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
exit(1)
|
||||
|
||||
|
||||
main()
|
||||
+13
-1
@@ -6,6 +6,7 @@ Creates Github Releases and uploads assets
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from github import Github, GithubException, UnknownObjectException
|
||||
|
||||
@@ -15,7 +16,6 @@ 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()
|
||||
@@ -120,6 +120,18 @@ def main():
|
||||
return
|
||||
|
||||
release.upload_asset(args.path, name=args.name)
|
||||
|
||||
# create the artifacts directory if it doesn't exist
|
||||
artifact_path = os.path.join(os.getcwd(), 'artifacts')
|
||||
if not os.path.isdir(artifact_path):
|
||||
os.mkdir(artifact_path)
|
||||
|
||||
# copy the binary to the path
|
||||
copy_path = os.path.join(artifact_path, args.name)
|
||||
shutil.copy(args.path, copy_path)
|
||||
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
exit(1)
|
||||
|
||||
+6
-4
@@ -7,11 +7,12 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/hpack"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,6 +22,7 @@ const (
|
||||
defaultTimeout time.Duration = 5 * time.Second
|
||||
defaultRetries uint64 = 5
|
||||
defaultWriteBufferMaxLen int = 1024 * 1024 // 1mb
|
||||
writeBufferInitialSize int = 16 * 1024 // 16KB
|
||||
|
||||
SettingMuxerMagic http2.SettingID = 0x42db
|
||||
MuxerMagicOrigin uint32 = 0xa2e43c8b
|
||||
@@ -206,9 +208,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,
|
||||
|
||||
+107
-1
@@ -15,10 +15,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1032,3 +1033,108 @@ func openStreams(b *testing.B, muxPair *DefaultMuxerPair, n int) {
|
||||
}
|
||||
assert.NoError(b, errGroup.Wait())
|
||||
}
|
||||
|
||||
func BenchmarkSingleStreamLargeResponseBody(b *testing.B) {
|
||||
const bodySize = 1 << 24
|
||||
|
||||
const writeBufferSize = 16 << 10
|
||||
const writeN = bodySize / writeBufferSize
|
||||
payload := make([]byte, writeBufferSize)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
const readBufferSize = 16 << 10
|
||||
const readN = bodySize / readBufferSize
|
||||
responseBody := make([]byte, readBufferSize)
|
||||
|
||||
f := MuxedStreamFunc(func(stream *MuxedStream) error {
|
||||
if len(stream.Headers) != 1 {
|
||||
b.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
|
||||
}
|
||||
if stream.Headers[0].Name != "test-header" {
|
||||
b.Fatalf("expected header name %s, got %s", "test-header", stream.Headers[0].Name)
|
||||
}
|
||||
if stream.Headers[0].Value != "headerValue" {
|
||||
b.Fatalf("expected header value %s, got %s", "headerValue", stream.Headers[0].Value)
|
||||
}
|
||||
stream.WriteHeaders([]Header{
|
||||
{Name: "response-header", Value: "responseValue"},
|
||||
})
|
||||
for i := 0; i < writeN; i++ {
|
||||
n, err := stream.Write(payload)
|
||||
if err != nil {
|
||||
b.Fatalf("origin write error: %s", err)
|
||||
}
|
||||
if n != len(payload) {
|
||||
b.Fatalf("origin short write: %d/%d bytes", n, len(payload))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
name := fmt.Sprintf("%s_%d", b.Name(), rand.Int())
|
||||
origin, edge := net.Pipe()
|
||||
|
||||
muxPair := &DefaultMuxerPair{
|
||||
OriginMuxConfig: MuxerConfig{
|
||||
Timeout: testHandshakeTimeout,
|
||||
Handler: f,
|
||||
IsClient: true,
|
||||
Name: "origin",
|
||||
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
|
||||
DefaultWindowSize: defaultWindowSize,
|
||||
MaxWindowSize: maxWindowSize,
|
||||
StreamWriteBufferMaxLen: defaultWriteBufferMaxLen,
|
||||
HeartbeatInterval: defaultTimeout,
|
||||
MaxHeartbeats: defaultRetries,
|
||||
},
|
||||
OriginConn: origin,
|
||||
EdgeMuxConfig: MuxerConfig{
|
||||
Timeout: testHandshakeTimeout,
|
||||
IsClient: false,
|
||||
Name: "edge",
|
||||
Logger: logger.NewOutputWriter(logger.NewMockWriteManager()),
|
||||
DefaultWindowSize: defaultWindowSize,
|
||||
MaxWindowSize: maxWindowSize,
|
||||
StreamWriteBufferMaxLen: defaultWriteBufferMaxLen,
|
||||
HeartbeatInterval: defaultTimeout,
|
||||
MaxHeartbeats: defaultRetries,
|
||||
},
|
||||
EdgeConn: edge,
|
||||
doneC: make(chan struct{}),
|
||||
}
|
||||
assert.NoError(b, muxPair.Handshake(name))
|
||||
muxPair.Serve(b)
|
||||
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
stream, err := muxPair.OpenEdgeMuxStream(
|
||||
[]Header{{Name: "test-header", Value: "headerValue"}},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
b.Fatalf("error in OpenStream: %s", err)
|
||||
}
|
||||
if len(stream.Headers) != 1 {
|
||||
b.Fatalf("expected %d headers, got %d", 1, len(stream.Headers))
|
||||
}
|
||||
if stream.Headers[0].Name != "response-header" {
|
||||
b.Fatalf("expected header name %s, got %s", "response-header", stream.Headers[0].Name)
|
||||
}
|
||||
if stream.Headers[0].Value != "responseValue" {
|
||||
b.Fatalf("expected header value %s, got %s", "responseValue", stream.Headers[0].Value)
|
||||
}
|
||||
|
||||
for k := 0; k < readN; k++ {
|
||||
n, err := io.ReadFull(stream, responseBody)
|
||||
if err != nil {
|
||||
b.Fatalf("error from (*MuxedStream).Read: %s", err)
|
||||
}
|
||||
if n != len(responseBody) {
|
||||
b.Fatalf("expected response body to have %d bytes, got %d", len(responseBody), n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+52
-50
@@ -38,11 +38,12 @@ const (
|
||||
// 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 {
|
||||
if !IsControlHeader(header.Name) {
|
||||
name := strings.ToLower(header.Name)
|
||||
if !IsControlHeader(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.ToLower(header.Name) {
|
||||
switch name {
|
||||
case ":method":
|
||||
h1.Method = header.Value
|
||||
case ":scheme":
|
||||
@@ -116,18 +117,14 @@ func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header,
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
|
||||
func isWebsocketClientHeader(headerName string) bool {
|
||||
return headerName == "sec-websocket-accept" ||
|
||||
headerName == "connection" ||
|
||||
headerName == "upgrade"
|
||||
@@ -137,29 +134,24 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
|
||||
h2 = []Header{
|
||||
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
|
||||
}
|
||||
userHeaders := http.Header{}
|
||||
userHeaders := make(http.Header, len(h1.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.
|
||||
h2name := strings.ToLower(header)
|
||||
if h2name == "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}
|
||||
}
|
||||
}
|
||||
// Since these are http2 headers, they're required to be lowercase
|
||||
h2 = append(h2, Header{Name: "content-length", Value: values[0]})
|
||||
} else if !IsControlHeader(h2name) || isWebsocketClientHeader(h2name) {
|
||||
// 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
|
||||
userHeaders[header] = values
|
||||
}
|
||||
}
|
||||
|
||||
// Perform user header serialization and set them in the single header
|
||||
h2 = append(h2, CreateSerializedHeaders(ResponseUserHeadersField, userHeaders)...)
|
||||
h2 = append(h2, Header{ResponseUserHeadersField, SerializeHeaders(userHeaders)})
|
||||
|
||||
return h2
|
||||
}
|
||||
@@ -167,26 +159,48 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
|
||||
// Serialize HTTP1.x headers by base64-encoding each header name and value,
|
||||
// and then joining them in the format of [key:value;]
|
||||
func SerializeHeaders(h1Headers http.Header) string {
|
||||
var serializedHeaders []string
|
||||
// compute size of the fully serialized value and largest temp buffer we will need
|
||||
serializedLen := 0
|
||||
maxTempLen := 0
|
||||
for headerName, headerValues := range h1Headers {
|
||||
for _, headerValue := range headerValues {
|
||||
encodedName := make([]byte, headerEncoding.EncodedLen(len(headerName)))
|
||||
headerEncoding.Encode(encodedName, []byte(headerName))
|
||||
nameLen := headerEncoding.EncodedLen(len(headerName))
|
||||
valueLen := headerEncoding.EncodedLen(len(headerValue))
|
||||
const delims = 2
|
||||
serializedLen += delims + nameLen + valueLen
|
||||
if nameLen > maxTempLen {
|
||||
maxTempLen = nameLen
|
||||
}
|
||||
if valueLen > maxTempLen {
|
||||
maxTempLen = valueLen
|
||||
}
|
||||
}
|
||||
}
|
||||
var buf strings.Builder
|
||||
buf.Grow(serializedLen)
|
||||
|
||||
encodedValue := make([]byte, headerEncoding.EncodedLen(len(headerValue)))
|
||||
headerEncoding.Encode(encodedValue, []byte(headerValue))
|
||||
temp := make([]byte, maxTempLen)
|
||||
writeB64 := func(s string) {
|
||||
n := headerEncoding.EncodedLen(len(s))
|
||||
if n > len(temp) {
|
||||
temp = make([]byte, n)
|
||||
}
|
||||
headerEncoding.Encode(temp[:n], []byte(s))
|
||||
buf.Write(temp[:n])
|
||||
}
|
||||
|
||||
serializedHeaders = append(
|
||||
serializedHeaders,
|
||||
strings.Join(
|
||||
[]string{string(encodedName), string(encodedValue)},
|
||||
":",
|
||||
),
|
||||
)
|
||||
for headerName, headerValues := range h1Headers {
|
||||
for _, headerValue := range headerValues {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte(';')
|
||||
}
|
||||
writeB64(headerName)
|
||||
buf.WriteByte(':')
|
||||
writeB64(headerValue)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(serializedHeaders, ";")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Deserialize headers serialized by `SerializeHeader`
|
||||
@@ -225,18 +239,6 @@ func DeserializeHeaders(serializedHeaders string) ([]Header, error) {
|
||||
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"`
|
||||
}
|
||||
|
||||
+42
-19
@@ -37,12 +37,19 @@ func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
|
||||
"Mock header 2": {"Mock value 2"},
|
||||
}
|
||||
|
||||
headersConversionErr := H2RequestHeadersToH1Request(CreateSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
|
||||
headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
|
||||
|
||||
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
|
||||
assert.NoError(t, headersConversionErr)
|
||||
}
|
||||
|
||||
func createSerializedHeaders(headersField string, headers http.Header) []Header {
|
||||
return []Header{{
|
||||
headersField,
|
||||
SerializeHeaders(headers),
|
||||
}}
|
||||
}
|
||||
|
||||
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
|
||||
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
@@ -509,40 +516,32 @@ func TestParseHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
mockHeaders := []Header{
|
||||
{Name: "One", Value: "1"},
|
||||
{Name: "One", Value: "1"}, // will be dropped
|
||||
{Name: "Cf-Two", Value: "cf-value-1"},
|
||||
{Name: "Cf-Two", Value: "cf-value-2"},
|
||||
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
|
||||
}
|
||||
|
||||
expectedHeaders := []Header{
|
||||
{Name: "Cf-Two", Value: "cf-value-1"},
|
||||
{Name: "Cf-Two", Value: "cf-value-2"},
|
||||
{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"},
|
||||
h1 := &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
_, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders)
|
||||
assert.EqualError(t, err, fmt.Sprintf("%s header not found", RequestUserHeadersField))
|
||||
err := H2RequestHeadersToH1Request(mockHeaders, h1)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -559,8 +558,8 @@ func TestIsControlHeader(t *testing.T) {
|
||||
|
||||
func TestIsNotControlHeader(t *testing.T) {
|
||||
notControlHeaders := []string{
|
||||
"Mock-header",
|
||||
"Another-sample-header",
|
||||
"mock-header",
|
||||
"another-sample-header",
|
||||
}
|
||||
|
||||
for _, header := range notControlHeaders {
|
||||
@@ -650,3 +649,27 @@ func randSeq(n int) string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
|
||||
ser := "eC1mb3J3YXJkZWQtcHJvdG8:aHR0cHM;dXBncmFkZS1pbnNlY3VyZS1yZXF1ZXN0cw:MQ;YWNjZXB0LWxhbmd1YWdl:ZW4tVVMsZW47cT0wLjkscnU7cT0wLjg;YWNjZXB0LWVuY29kaW5n:Z3ppcA;eC1mb3J3YXJkZWQtZm9y:MTczLjI0NS42MC42;dXNlci1hZ2VudA:TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTRfNikgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzg0LjAuNDE0Ny44OSBTYWZhcmkvNTM3LjM2;c2VjLWZldGNoLW1vZGU:bmF2aWdhdGU;Y2RuLWxvb3A:Y2xvdWRmbGFyZQ;c2VjLWZldGNoLWRlc3Q:ZG9jdW1lbnQ;c2VjLWZldGNoLXVzZXI:PzE;c2VjLWZldGNoLXNpdGU:bm9uZQ;Y29va2ll:X19jZmR1aWQ9ZGNkOWZjOGNjNWMxMzE0NTMyYTFkMjhlZDEyOWRhOTYwMTU2OTk1MTYzNDsgX19jZl9ibT1mYzY2MzMzYzAzZmM0MWFiZTZmOWEyYzI2ZDUwOTA0YzIxYzZhMTQ2LTE1OTU2MjIzNDEtMTgwMC1BZTVzS2pIU2NiWGVFM05mMUhrTlNQMG1tMHBLc2pQWkloVnM1Z2g1SkNHQkFhS1UxVDB2b003alBGN3FjMHVSR2NjZGcrWHdhL1EzbTJhQzdDVU4xZ2M9;YWNjZXB0:dGV4dC9odG1sLGFwcGxpY2F0aW9uL3hodG1sK3htbCxhcHBsaWNhdGlvbi94bWw7cT0wLjksaW1hZ2Uvd2VicCxpbWFnZS9hcG5nLCovKjtxPTAuOCxhcHBsaWNhdGlvbi9zaWduZWQtZXhjaGFuZ2U7dj1iMztxPTAuOQ"
|
||||
h2, _ := DeserializeHeaders(ser)
|
||||
h1 := make(http.Header)
|
||||
for _, header := range h2 {
|
||||
h1.Add(header.Name, header.Value)
|
||||
}
|
||||
h1.Add("Content-Length", "200")
|
||||
h1.Add("Cf-Something", "Else")
|
||||
h1.Add("Upgrade", "websocket")
|
||||
|
||||
h1resp := &http.Response{
|
||||
StatusCode: 200,
|
||||
Header: h1,
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = H1ResponseToH2ResponseHeaders(h1resp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+31
-5
@@ -138,6 +138,14 @@ func (s *MuxedStream) Write(p []byte) (int, error) {
|
||||
if s.writeEOF {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// pre-allocate some space in the write buffer if possible
|
||||
if buffer, ok := s.writeBuffer.(*bytes.Buffer); ok {
|
||||
if buffer.Cap() == 0 {
|
||||
buffer.Grow(writeBufferInitialSize)
|
||||
}
|
||||
}
|
||||
|
||||
totalWritten := 0
|
||||
for totalWritten < len(p) {
|
||||
// If the buffer is full, block till there is more room.
|
||||
@@ -367,7 +375,9 @@ type streamChunk struct {
|
||||
// true if data frames should be sent
|
||||
sendData bool
|
||||
eof bool
|
||||
buffer bytes.Buffer
|
||||
|
||||
buffer []byte
|
||||
offset int
|
||||
}
|
||||
|
||||
// getChunk atomically extracts a chunk of data to be written by MuxWriter.
|
||||
@@ -385,8 +395,17 @@ func (s *MuxedStream) getChunk() *streamChunk {
|
||||
eof: s.writeEOF && uint32(s.writeBuffer.Len()) <= s.sendWindow,
|
||||
}
|
||||
// Copy at most s.sendWindow bytes, adjust the sendWindow accordingly
|
||||
writeLen, _ := io.CopyN(&chunk.buffer, s.writeBuffer, int64(s.sendWindow))
|
||||
s.sendWindow -= uint32(writeLen)
|
||||
toCopy := int(s.sendWindow)
|
||||
if toCopy > s.writeBuffer.Len() {
|
||||
toCopy = s.writeBuffer.Len()
|
||||
}
|
||||
|
||||
if toCopy > 0 {
|
||||
buf := make([]byte, toCopy)
|
||||
writeLen, _ := s.writeBuffer.Read(buf)
|
||||
chunk.buffer = buf[:writeLen]
|
||||
s.sendWindow -= uint32(writeLen)
|
||||
}
|
||||
|
||||
// Allow MuxedStream::Write() to continue, if needed
|
||||
if s.writeBuffer.Len() < s.writeBufferMaxLen {
|
||||
@@ -421,8 +440,15 @@ func (c *streamChunk) sendDataFrame() bool {
|
||||
}
|
||||
|
||||
func (c *streamChunk) nextDataFrame(frameSize int) (payload []byte, endStream bool) {
|
||||
payload = c.buffer.Next(frameSize)
|
||||
if c.buffer.Len() == 0 {
|
||||
bytesLeft := len(c.buffer) - c.offset
|
||||
if frameSize > bytesLeft {
|
||||
frameSize = bytesLeft
|
||||
}
|
||||
nextOffset := c.offset + frameSize
|
||||
payload = c.buffer[c.offset:nextOffset]
|
||||
c.offset = nextOffset
|
||||
|
||||
if c.offset == len(c.buffer) {
|
||||
// this is the last data frame in this chunk
|
||||
c.sendData = false
|
||||
if c.eof {
|
||||
|
||||
+6
-6
@@ -8,17 +8,17 @@ import (
|
||||
"github.com/acmacalister/skittles"
|
||||
)
|
||||
|
||||
// Level of logging
|
||||
// Level of logging, lower number means more verbose logging, higher more terse
|
||||
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
|
||||
DebugLevel Level = iota
|
||||
|
||||
// ErrorLevel is for error message to indicte something has gone wrong
|
||||
// InfoLevel is for standard log messages
|
||||
InfoLevel
|
||||
|
||||
// ErrorLevel is for error message to indicate something has gone wrong
|
||||
ErrorLevel
|
||||
|
||||
// FatalLevel is for error message that log and kill the program with an os.exit(1)
|
||||
|
||||
+5
-5
@@ -7,8 +7,8 @@ import "sync"
|
||||
var SharedWriteManager = NewWriteManager()
|
||||
|
||||
type writeData struct {
|
||||
writeFunc func([]byte)
|
||||
data []byte
|
||||
target LogOutput
|
||||
data []byte
|
||||
}
|
||||
|
||||
// WriteManager is a logging service that handles managing multiple writing streams
|
||||
@@ -31,9 +31,9 @@ func NewWriteManager() OutputManager {
|
||||
}
|
||||
|
||||
// Append adds a message to the writer runloop
|
||||
func (m *WriteManager) Append(data []byte, callback func([]byte)) {
|
||||
func (m *WriteManager) Append(data []byte, target LogOutput) {
|
||||
m.wg.Add(1)
|
||||
m.writeChan <- writeData{data: data, writeFunc: callback}
|
||||
m.writeChan <- writeData{data: data, target: target}
|
||||
}
|
||||
|
||||
// Shutdown stops the sync manager service
|
||||
@@ -49,7 +49,7 @@ func (m *WriteManager) run() {
|
||||
select {
|
||||
case event, ok := <-m.writeChan:
|
||||
if ok {
|
||||
event.writeFunc(event.data)
|
||||
event.target.WriteLogLine(event.data)
|
||||
m.wg.Done()
|
||||
}
|
||||
case <-m.shutdown:
|
||||
|
||||
@@ -6,13 +6,19 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type outputFunc func(b []byte)
|
||||
|
||||
func (f outputFunc) WriteLogLine(data []byte) {
|
||||
f(data)
|
||||
}
|
||||
|
||||
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) {
|
||||
m.Append(testData, outputFunc(func(b []byte) {
|
||||
waitChan <- b
|
||||
})
|
||||
}))
|
||||
resp := <-waitChan
|
||||
assert.Equal(t, testData, resp)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ func NewMockWriteManager() OutputManager {
|
||||
}
|
||||
|
||||
// Append is a mock stub
|
||||
func (m *MockWriteManager) Append(data []byte, callback func([]byte)) {
|
||||
func (m *MockWriteManager) Append(data []byte, target LogOutput) {
|
||||
}
|
||||
|
||||
// Shutdown is a mock stub
|
||||
|
||||
+52
-29
@@ -10,9 +10,13 @@ import (
|
||||
// provided for testing
|
||||
var osExit = os.Exit
|
||||
|
||||
type LogOutput interface {
|
||||
WriteLogLine([]byte)
|
||||
}
|
||||
|
||||
// OutputManager is used to sync data of Output
|
||||
type OutputManager interface {
|
||||
Append([]byte, func([]byte))
|
||||
Append([]byte, LogOutput)
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
@@ -35,38 +39,66 @@ type sourceGroup struct {
|
||||
levelsSupported []Level
|
||||
}
|
||||
|
||||
func (s *sourceGroup) WriteLogLine(data []byte) {
|
||||
_, _ = s.writer.Write(data)
|
||||
}
|
||||
|
||||
func (s *sourceGroup) supportsLevel(l Level) bool {
|
||||
for _, level := range s.levelsSupported {
|
||||
if l == level {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OutputWriter is the standard logging implementation
|
||||
type OutputWriter struct {
|
||||
groups []sourceGroup
|
||||
groups []*sourceGroup
|
||||
syncWriter OutputManager
|
||||
minLevel Level
|
||||
}
|
||||
|
||||
// NewOutputWriter create a new logger
|
||||
func NewOutputWriter(syncWriter OutputManager) *OutputWriter {
|
||||
return &OutputWriter{
|
||||
syncWriter: syncWriter,
|
||||
groups: make([]sourceGroup, 0),
|
||||
groups: nil,
|
||||
minLevel: FatalLevel,
|
||||
}
|
||||
}
|
||||
|
||||
// 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})
|
||||
s.groups = append(s.groups, &sourceGroup{writer: writer, formatter: formatter, levelsSupported: levels})
|
||||
|
||||
// track most verbose (lowest) level we need to output
|
||||
for _, level := range levels {
|
||||
if level < s.minLevel {
|
||||
s.minLevel = level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error writes an error to the logging sources
|
||||
func (s *OutputWriter) Error(message string) {
|
||||
s.output(ErrorLevel, message)
|
||||
if s.minLevel <= ErrorLevel {
|
||||
s.output(ErrorLevel, message)
|
||||
}
|
||||
}
|
||||
|
||||
// Info writes an info string to the logging sources
|
||||
func (s *OutputWriter) Info(message string) {
|
||||
s.output(InfoLevel, message)
|
||||
if s.minLevel <= InfoLevel {
|
||||
s.output(InfoLevel, message)
|
||||
}
|
||||
}
|
||||
|
||||
// Debug writes a debug string to the logging sources
|
||||
func (s *OutputWriter) Debug(message string) {
|
||||
s.output(DebugLevel, message)
|
||||
if s.minLevel <= DebugLevel {
|
||||
s.output(DebugLevel, message)
|
||||
}
|
||||
}
|
||||
|
||||
// Fatal writes a error string to the logging sources and runs does an os.exit()
|
||||
@@ -78,17 +110,23 @@ func (s *OutputWriter) Fatal(message string) {
|
||||
|
||||
// Errorf writes a formatted error to the logging sources
|
||||
func (s *OutputWriter) Errorf(format string, args ...interface{}) {
|
||||
s.output(ErrorLevel, fmt.Sprintf(format, args...))
|
||||
if s.minLevel <= ErrorLevel {
|
||||
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...))
|
||||
if s.minLevel <= InfoLevel {
|
||||
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...))
|
||||
if s.minLevel <= DebugLevel {
|
||||
s.output(DebugLevel, fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
|
||||
// Fatalf writes a writes a formatted error statement and runs does an os.exit()
|
||||
@@ -100,31 +138,16 @@ func (s *OutputWriter) Fatalf(format string, args ...interface{}) {
|
||||
|
||||
// output does the actual write to the sync manager
|
||||
func (s *OutputWriter) output(l Level, content string) {
|
||||
now := time.Now()
|
||||
for _, group := range s.groups {
|
||||
if isSupported(group, l) {
|
||||
logLine := fmt.Sprintf("%s%s\n", group.formatter.Timestamp(l, time.Now()),
|
||||
if group.supportsLevel(l) {
|
||||
logLine := fmt.Sprintf("%s%s\n", group.formatter.Timestamp(l, now),
|
||||
group.formatter.Content(l, content))
|
||||
s.append(group, []byte(logLine))
|
||||
s.syncWriter.Append([]byte(logLine), group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
@@ -55,6 +55,8 @@ func TestOutputWrite(t *testing.T) {
|
||||
logger := NewOutputWriter(m)
|
||||
logger.Add(&testBuffer, f, InfoLevel)
|
||||
|
||||
logger.Debugf("debug message not logged here")
|
||||
|
||||
testData := "hello Bob Bork, how are you doing?"
|
||||
logger.Info(testData)
|
||||
testTime := f.Timestamp(InfoLevel, time.Now())
|
||||
|
||||
+26
-20
@@ -105,7 +105,7 @@ type dupConnRegisterTunnelError struct{}
|
||||
var errDuplicationConnection = &dupConnRegisterTunnelError{}
|
||||
|
||||
func (e dupConnRegisterTunnelError) Error() string {
|
||||
return "already connected to this server"
|
||||
return "already connected to this server, trying another address"
|
||||
}
|
||||
|
||||
type muxerShutdownError struct{}
|
||||
@@ -172,16 +172,17 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TunnelConfig) ConnectionOptions(originLocalAddr string) *tunnelpogs.ConnectionOptions {
|
||||
func (c *TunnelConfig) ConnectionOptions(originLocalAddr string, numPreviousAttempts uint8) *tunnelpogs.ConnectionOptions {
|
||||
// attempt to parse out origin IP, but don't fail since it's informational field
|
||||
host, _, _ := net.SplitHostPort(originLocalAddr)
|
||||
originIP := net.ParseIP(host)
|
||||
|
||||
return &tunnelpogs.ConnectionOptions{
|
||||
Client: c.NamedTunnel.Client,
|
||||
OriginLocalIP: originIP,
|
||||
ReplaceExisting: c.ReplaceExisting,
|
||||
CompressionQuality: uint8(c.CompressionQuality),
|
||||
Client: c.NamedTunnel.Client,
|
||||
OriginLocalIP: originIP,
|
||||
ReplaceExisting: c.ReplaceExisting,
|
||||
CompressionQuality: uint8(c.CompressionQuality),
|
||||
NumPreviousAttempts: numPreviousAttempts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +306,7 @@ func ServeTunnel(
|
||||
}()
|
||||
|
||||
if config.NamedTunnel != nil {
|
||||
return RegisterConnection(ctx, handler.muxer, config, connectionIndex, originLocalAddr)
|
||||
return RegisterConnection(ctx, handler.muxer, config, connectionIndex, originLocalAddr, uint8(backoff.retries))
|
||||
}
|
||||
|
||||
if config.UseReconnectToken && connectedFuse.Value() {
|
||||
@@ -376,29 +377,33 @@ func ServeTunnel(
|
||||
|
||||
err = errGroup.Wait()
|
||||
if err != nil {
|
||||
switch castedErr := err.(type) {
|
||||
case dupConnRegisterTunnelError:
|
||||
logger.Info("Already connected to this server, selecting a different one")
|
||||
return err, true
|
||||
case serverRegisterTunnelError:
|
||||
logger.Errorf("Register tunnel error from server side: %s", castedErr.cause)
|
||||
switch err := err.(type) {
|
||||
case *dupConnRegisterTunnelError:
|
||||
// don't retry this connection anymore, let supervisor pick new a address
|
||||
return err, false
|
||||
case *serverRegisterTunnelError:
|
||||
logger.Errorf("Register tunnel error from server side: %s", err.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 {
|
||||
logger.Error(activeIncidentsMsg(incidents))
|
||||
}
|
||||
return castedErr.cause, !castedErr.permanent
|
||||
case clientRegisterTunnelError:
|
||||
logger.Errorf("Register tunnel error on client side: %s", castedErr.cause)
|
||||
return err.cause, !err.permanent
|
||||
case *clientRegisterTunnelError:
|
||||
logger.Errorf("Register tunnel error on client side: %s", err.cause)
|
||||
return err, true
|
||||
case muxerShutdownError:
|
||||
case *muxerShutdownError:
|
||||
logger.Info("Muxer shutdown")
|
||||
return err, true
|
||||
case *ReconnectSignal:
|
||||
logger.Infof("Restarting connection %d due to reconnect signal in %d seconds", connectionIndex, castedErr.Delay)
|
||||
castedErr.DelayBeforeReconnect()
|
||||
logger.Infof("Restarting connection %d due to reconnect signal in %d seconds", connectionIndex, err.Delay)
|
||||
err.DelayBeforeReconnect()
|
||||
return err, true
|
||||
default:
|
||||
if err == context.Canceled {
|
||||
logger.Debugf("Serve tunnel error: %s", err)
|
||||
return err, false
|
||||
}
|
||||
logger.Errorf("Serve tunnel error: %s", err)
|
||||
return err, true
|
||||
}
|
||||
@@ -412,6 +417,7 @@ func RegisterConnection(
|
||||
config *TunnelConfig,
|
||||
connectionIndex uint8,
|
||||
originLocalAddr string,
|
||||
numPreviousAttempts uint8,
|
||||
) error {
|
||||
const registerConnection = "registerConnection"
|
||||
|
||||
@@ -428,7 +434,7 @@ func RegisterConnection(
|
||||
config.NamedTunnel.Auth,
|
||||
config.NamedTunnel.ID,
|
||||
connectionIndex,
|
||||
config.ConnectionOptions(originLocalAddr),
|
||||
config.ConnectionOptions(originLocalAddr, numPreviousAttempts),
|
||||
)
|
||||
if err != nil {
|
||||
if err.Error() == DuplicateConnectionError {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
ln -s /usr/bin/cloudflared /usr/local/bin/cloudflared
|
||||
mkdir -p /usr/local/etc/cloudflared/
|
||||
touch /usr/local/etc/cloudflared/.installedFromPackageManager || true
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
rm /usr/local/bin/cloudflared
|
||||
rm /usr/local/etc/cloudflared/.installedFromPackageManager || true
|
||||
@@ -26,10 +26,11 @@ type ClientInfo struct {
|
||||
}
|
||||
|
||||
type ConnectionOptions struct {
|
||||
Client ClientInfo
|
||||
OriginLocalIP net.IP `capnp:"originLocalIp"`
|
||||
ReplaceExisting bool
|
||||
CompressionQuality uint8
|
||||
Client ClientInfo
|
||||
OriginLocalIP net.IP `capnp:"originLocalIp"`
|
||||
ReplaceExisting bool
|
||||
CompressionQuality uint8
|
||||
NumPreviousAttempts uint8
|
||||
}
|
||||
|
||||
type TunnelAuth struct {
|
||||
|
||||
@@ -98,6 +98,8 @@ struct ConnectionOptions {
|
||||
replaceExisting @2 :Bool;
|
||||
# cross stream compression setting, 0 - off, 3 - high
|
||||
compressionQuality @3 :UInt8;
|
||||
# number of previous attempts to send RegisterConnection
|
||||
numPreviousAttempts @4 :UInt8;
|
||||
}
|
||||
|
||||
struct ConnectionResponse {
|
||||
|
||||
+184
-175
@@ -1109,6 +1109,14 @@ func (s ConnectionOptions) SetCompressionQuality(v uint8) {
|
||||
s.Struct.SetUint8(1, v)
|
||||
}
|
||||
|
||||
func (s ConnectionOptions) NumPreviousAttempts() uint8 {
|
||||
return s.Struct.Uint8(2)
|
||||
}
|
||||
|
||||
func (s ConnectionOptions) SetNumPreviousAttempts(v uint8) {
|
||||
s.Struct.SetUint8(2, v)
|
||||
}
|
||||
|
||||
// ConnectionOptions_List is a list of ConnectionOptions.
|
||||
type ConnectionOptions_List struct{ capnp.List }
|
||||
|
||||
@@ -3363,181 +3371,182 @@ func (p TunnelServer_reconnectTunnel_Results_Promise) Result() TunnelRegistratio
|
||||
return TunnelRegistration_Promise{Pipeline: p.Pipeline.GetPipeline(0)}
|
||||
}
|
||||
|
||||
const schema_db8274f9144abc7e = "x\xda\xccYkl\x1c\xd5\xf5?g\xee\xaeg\xfdb" +
|
||||
"=\x99M\x9cX\xf0\xf7\xbfQ\"\x8a!\x80\xe3R\x85" +
|
||||
"\x14j;\xd8)6yx\xbcq\xc5#A\x8cwo" +
|
||||
"\xecqwg63\xb3\xc6N\x13\x92\x98\xa4\x10Dy" +
|
||||
"\x84\x84G\x0a-\x04\xa9UiiI\xa1j\xa9\xa8J" +
|
||||
"\xfa\x82\x8agP\xa8\x02\xa4jKH[\xa2P\xda@" +
|
||||
"\x85\xa8\x80\xa9\xce\xcc\xce\xc3kc;\xd0\x0f\xfd\xb6:" +
|
||||
"s\xee\xbd\xe7}~\xe7\xec\xf9\xcb\xc56\xa19.V" +
|
||||
"\x03([\xe3\x15\x0eozi\xe3\xfd\x0b\x7f5\x06J" +
|
||||
"\x03\xa2s\xdd\x13\xdd\xa9\xf7\xed\xb1\xd7 \xceD\x80\x16" +
|
||||
"-\xbe\x11\xe5\xcdq\x11@\x1e\x8d\xff\x15\xd0\xb9L:" +
|
||||
"\xef\xaa\xd4\x8b\xcf_\x0fRC\x949F\xcc\xeb*\x9a" +
|
||||
"P\xceW\x10\xb3VA\xcc\x17\xe5_\xd8\xf7\xf9=\xcf" +
|
||||
"l\x07\xa9A\x08\x99\x01[\xfa\xc4\x8d(k\"qr" +
|
||||
"q5\xa0\xf3\xce\xee\xb9\xdf{\xe0\xf9\xa7w\x80t&" +
|
||||
"B\xe9\xed\xcd\xe2\xab\x08(\xdf.\xfe\x10\xd0i]\xfe" +
|
||||
"\xdc\xe3\x0d-w\xee.{W \xc6\xe6D\x13\xca\xed" +
|
||||
"\x09\xba\xed\xe2\xc4\xb5\x80\xce\xfc\xe1\x85\xd7\xfc\xf27\x8f" +
|
||||
"\xde\x05\xca\"D\xe7H\xff\xd9/\xb3\xfb\x1ez\x0d\xfa" +
|
||||
"PD\x01\xa0\xe5\x81\xc4>\xbax\xbf\xcb\xfb\xc29O" +
|
||||
"\xfc\xec\xb6Go\xf8\x06(g\"\x02\xb8\x8a\xcc\xae\xfc" +
|
||||
"71\x9cU\xd9\x0a\xe8\xec>\xfc\xf3U\xf9\xdb\xf7\xee" +
|
||||
"\xf3Ds\xbf+\x95\x82\x001g{\xd7{\xf9\xbe\x07" +
|
||||
"\xd3\x0f\x96\x84&+\xb5tV\x9eD\xc0\x96+*\x1b" +
|
||||
"\x11\xd0\xb9\xe0\xd5c\xabW\xfeh\xfdw\"g\x8bU" +
|
||||
"\x1b\xe9\xec\xc3\xffwY\xe5\xc8\xb1\xe5\x8f\x80\xb4\xc8\xff" +
|
||||
"\xb2\xa1\xaa\x97\xbe\xc4\xd6\xb1\x8f\xd4\xbb\x7f\xf1X\xb9?" +
|
||||
"\\U\xf3U\xfd(o\xab\"U7W\x91\x89o:" +
|
||||
"\xb0\xf7\xec\xc4\xb7\xde\xf9\xf1dvQ\xab\xfbQ.V" +
|
||||
"\x13\xf3\x86j\xd2uv\x17\x1ey\xb29\xf6\x93\xa8\x95" +
|
||||
"\x0fV\x1f']\x8fU\x93\x95\xcfxkY\xad\xfe\xf6" +
|
||||
"\xd8\x93e\xb7\xb9\x8c;j\xbaQ\xbe\xa7\x86n\xdbS" +
|
||||
"C\xcc\xddW\xdd\xb1+~\xec\x8e\xa7H\xd0\x88{\xe3" +
|
||||
"\x09\xd7'\xb5&\xca\x9d\xb5\xf4\xb3\xbd\xb6\x9e\x01:\x0d" +
|
||||
"\x8f|\xe1\x07\xcb\xb2\xaf<3\x89\xa4\xf2\x81\xba\x93\xf2" +
|
||||
"su\xf4\xebwu$\xe8\xd1E\xfb\xbf\xfa\xe6\xd7\x0f" +
|
||||
"\x1e*\x09\x8at\xcdB\xc9u\xca\x85\x129%\xf0i" +
|
||||
"\x99\x91\\\xce+\xa4!\x94\xf3\x92\x1b\x87.\xb7pL" +
|
||||
"\x9d\xb7\xf5\xf7_<\x12qC^z\x1d!\xe6\xac\xfa" +
|
||||
"\xf2UC\x95\x9b\x8f\x1e\x8d>\xa4J\xaeE\x8a\xee\xd1" +
|
||||
"\xbf\x7f\xfb\xf8\xad'\xf2\xd9\xbf\xb8\xa1\xe4\xdbl\x8f\xb4" +
|
||||
"T\x00\x94\x1f\x96\xc8\x03\xf5\x8d\xb5\x9d\xf3\x0f\xf7\x1c\xf7" +
|
||||
"<\xe9]\xb1s\xd62bx`\x16]q\xc15\xed" +
|
||||
"|\xed\x92\xcb\x8f\x83\xd4\xc0\xc6e\xc1\x81YKQ>" +
|
||||
"8\x8b\x0e<7\xeb\x06\x94\x8f\xc9\xf5\x00\xce-\xd7u" +
|
||||
"\xac\xbep\xfe\x81\x93Q\x91^\x96)\xaa\xe47e\xba" +
|
||||
"o\xfd\x92\x13_Zx\xcboO\x96\x19\xd2e\xacL" +
|
||||
"5\xa1</E\xaa\xcfN\xb5\x02\xbe\xbd\xfc\x9b\x87\x1a" +
|
||||
"\x92\x0d\xef\x96\x99\x89r\xb4\xe5\xc2\xd4\x10\xca+\x89\xb7" +
|
||||
"\xa5+\xf5\x14\x85\xeb\xd7^\xbbz\xe4\xa5\xeb\xdf\xf9W" +
|
||||
"\xb9G\xdd\xab/\x9e\xd3\x8b\xb22\x87\xae^9\x87\xfc" +
|
||||
"\x7f\xd7\x9a\xbfm9\xb1g\xce{\x13\xf4zk\xce\x10" +
|
||||
"\xcaXO\x9c\x1f\xce\xb9A^G\xbf\x9c\x17\xc5\x07\x9b" +
|
||||
";\xb6<\xf3~$\xe0;\xeb\xbb)\xe0\xef\x14\xef=" +
|
||||
"\xba\xf5\x8fW\x7f\x10U\xb8\xbd\xfeuR\xb8\xaf\x9e\x14" +
|
||||
"\xde\xf4\xf6=\x97\xde\xba\xf6\xfb\x1fE\xb3\xa8~\x8c\x8e" +
|
||||
"\xdaE]\xe79\xb3\x10\xcb\x9c\xe7\xff\xcc\x9c\x9bQ\x0b" +
|
||||
"zai{\xd1\x1e\xe4\xba\xadeT\x9b\xf7\xf2V\xab" +
|
||||
"`\xe8\x16\xefAT\xeaX\x0c \x86\x00\x92:\x04\xa0" +
|
||||
"\\\xc3P\xc9\x09(!\xa6\xc8\xc1\x92F\xc4A\x86\x8a" +
|
||||
"-\xa0$\x08)\xaa\x18\xd2\x86\xf9\x00J\x8e\xa12\"" +
|
||||
" \xb2\x142\x00\xa9\xb8\x0b@\x19a\xa8l\x17\xd0)" +
|
||||
"p3\xaf\xea\\\x87\xa4\xddi\x9aX\x03\x02\xd6\x00:" +
|
||||
"&\xb7\xcdQ\xb5?\x07I\x1e!\x8bC\xd7\xdaX\x0b" +
|
||||
"\x02\xd6\x02:\x83F\xd1\xb4\xfat\x1b\xb5\\/_o" +
|
||||
"r\x0b\x07\xb1\x02\x04\xac\x00\x0c\xd4c\x13\xd5\xbb$\xa7" +
|
||||
"q\xddNv\xe9\xeb\x8d2\xa5\xba'S\xaa\xbb\xa4\xd4" +
|
||||
"\xf6\x88R\xdb\x96\x01(\x9b\x18*7\x0a(\xb1\x92V" +
|
||||
";\x9a\xa8-0Tn\x16\xd0\xc9\xb8\x8fte\x01 " +
|
||||
"\x90w=W\xed\xa2\xc9-\xa2\x9d\x06\xd8\xc3\xd0U\xeb" +
|
||||
"4\xc0-\xc3\xdc\xb44C\xf7\xd5L\xaaff00" +
|
||||
"\xc5\x14\xae\xea\x1c\xd1,[\xd3\x07\xd6\xb8\xf4\xd6\x1e#" +
|
||||
"\xa7eFI\xab\x1aW\xce3\x96\x02 J\xb3\xaf\x04" +
|
||||
"@A\x92\x96\x01\xb4j\x03\xbaar'\xabY\x19C" +
|
||||
"\xd79\xb0\x8c\xbd\xa5_\xcd\xa9z\x86\x07\x0fUL|" +
|
||||
"\xc8{ \xcd\xcdan\x9e\xabF\x02dA\x8fj\xaa" +
|
||||
",o)5\x81\x1d;\xaf\x04P:\x18*=\x11;" +
|
||||
"\xae$;\xae`\xa8\\\x1e\xb1c\x1f\xd9\xb1\x87\xa1\xb2" +
|
||||
"V@\xc70\xb5\x01M\xbf\x84\x033\xa3>\xb6l]" +
|
||||
"\xcds\xb2Y\xc9\x1e[\x8c\x82\xad\x19\xba\x85ua\x1d" +
|
||||
"\x05\xc4\xba\xa9\xbd\xee)\x90\xa4\xd8&\xfb$\x02i\xcf" +
|
||||
"\"i?\xcbP\xf9\\D\xdaf\x0a\xe5\xf3\x19*\x17" +
|
||||
"\x09\xe8\xa8\x99\x8cQ\xd4\xed5\xc0\xd4\x812\xa7\xa49" +
|
||||
"$3&\x0f\xe5\xf5\x9f\x8dO\x12wd\xef\x0c\x89\xde" +
|
||||
"\xcb\xbd\x9c:\xd7\xe4\x96X\xcc\xd9$M\x8d\xe3x\xe2" +
|
||||
",\x06P\x160T\xce\x17\xb0\x16?r<y\x16\xed" +
|
||||
"\x0a\xe5i\xe4\xa6i\x98X\x17\xd6\x9c\x92\xf6\x99\xd2\x03" +
|
||||
"h\xe8\x1d\xdcV\xb5\x1c\x92\x8d\x826Wf\xa3\xe9\x9c" +
|
||||
"\\\xd4M>\xa0Y67=\xf2\x82V\xf2t\xdeR" +
|
||||
"b\x81\xe9j\xf7\x02(u\x0c\x95\xd3\x05t\x06L5" +
|
||||
"\xc3{\xb8\x89\x9a\x91]\xa5\xeaF\x9a\xf1\x0c\xc6A\xc0" +
|
||||
"x\xe4\xd1\xd3N\xf5\xd1^n\x15s\xb6\x05\xc1\xa9\xa9" +
|
||||
"\xcf\x9b\xbcd\x84\xd2\xf1\x9eFO\xe6T \xf3\xe6\xf9" +
|
||||
"a\xed\x09\xdc\xbd\xad?L\xdd 8wR`\xdc\xc8" +
|
||||
"P\xd9\x1dI\xf2\xdb)\x8coc\xa8\xdc+\xa0\x14\x8b" +
|
||||
"\xa50\x06 \xddCa\xbc\x9b\xa1r\xbf0\xbeB\xf1" +
|
||||
"a\xae\xdb\x1d\xda\x00\x88\xdc\x0a\xa9$b\x876\xc0\x81" +
|
||||
"Y\x9f6\xd0\x13\xd3\xd8\xc3\xe8\xb7\x8c\x1c\xb7y\x07\xcf" +
|
||||
"\xe4TS\xb5\xb5a\xee}/\x05\xa3\xef\xd4\xa9.\xec" +
|
||||
"u=B\x87\x0d}\x82\x9b\xc2\xa0.\xb9\x0a\xad\xa9\xea" +
|
||||
"U\xc8\xbe\xba`k\xa2\xa1[e%x)\x80\xb2\x96" +
|
||||
"\xa12\x18\xf1\x0e7\x01\x94,C\xa5 \x96\x9c\x93" +
|
||||
"\x1f\x0b\xdb\x8a\xc4\xb0\xd4W\xf6\x86\xbem\xf5*0\xd6" +
|
||||
"\x85\xb8\xbbd;\xaf\xce\xac0\xa01\xa3\xe6\xba\x0a\x81" +
|
||||
"\x07L^\xc8\xa9\x19\xde\x89\xa5\x9a\x0a\x88 \xba\x0e" +
|
||||
"\xcb\x17LnY\xa8\x19\xbaRTs\x1a\xb3G't" +
|
||||
"\x9a)\xb5\xa5|\x14\xb5\x9cUVz\x9a\xc2\\\x0f\xb4" +
|
||||
"]D\xa5\xe7\x1c\x86\xca\x12\x01\x93\xc5\xa2\x96\x0d\x04\xcc" +
|
||||
"\x19\x19\xd7\x09\x90\\\xa5\xe6\xf9\x84\xf6P1mn\x8c" +
|
||||
"\xcb\xac\x1e5\xe9\xa6\xc6\xffR\xdd\x9e\x1a\x8c\x90\xea\xe0" +
|
||||
"6\xb7Pd\xca\xe66\x86\xca\x8a\x88\xc8]\x8b#z" +
|
||||
"\xf8\"\xaf\xec\x0f\xf5\x10\xbf\xc2G}\xa9\x1ay\x9e*" +
|
||||
"\xa5o\xcc\x922\xed ^\x16\xf2L%_4;V" +
|
||||
"\x17\x1a]\x0dI\xc6%\xbe\x8c\xf2(v\x03\xa4G\x90" +
|
||||
"az;\x86b\xca\xdbp\x19@z\x13\xd1o\xc4P" +
|
||||
"Ry\x076\x00\xa4\xb7\x12\xfdf\x0c@\x93\xbc\x13\x1f" +
|
||||
"\x02H\xdfL\xe4\xbb\x89=\xc6\xdc\xe2#\xefq\xaf\xdf" +
|
||||
"M\xf4\xfb\x89\x1e\x8f\xa50\x0e \xdf\x87M\x00\xe9\xbb" +
|
||||
"\x89\xfe\x18\xd1+\x84\x14V\x00\xc8\xfbq\x08 \xfd\x08" +
|
||||
"\xd1\x9f \xba\x18O\x11n\x94\x1fG\x13 \xfdS\xa2" +
|
||||
"\xff\x9a\xe8\x89\xb9)L\xd0\x80\xe1\xd2\x9f$\xfa\xb3D" +
|
||||
"\xaf\x9c\x97\xc2J\x1a7p\x0c \xfd4\xd1\x0f\x11\xbd" +
|
||||
"\x0aSX\x05 \x1f\xc4\xbd\x00\xe9CD\xff\x13\xd1\xab" +
|
||||
"+RX\x0d \xff\xc1\x95\xe70\xd1\xdf zM," +
|
||||
"\x855\x00\xf2\x9fq\x1f@\xfa\x0d\xa2\xff\x83\xe8\xb5b" +
|
||||
"\x0ak\x01\xe4\xb7\\\xbdN\x10=!\x94!*?\xa2" +
|
||||
"\xca`\x133\xac\xc0e\xbc\x94\xc5\xe8\x85{\x8f\x91$" +
|
||||
"h\x84\xc9p\xb2\x06\xc4$\xa0S0\x8c\xdc\xaa\xf1\x91" +
|
||||
"\x9a\xb4\xd5\x01\xcb\x87hu\xe1\xf8\x05H\xc4\xa0\xcfB" +
|
||||
"\xd2\xd0\xbb\xb2A!(\xaf+\xbe$\x9a\xd5^\xb4\x8d" +
|
||||
"b\x01\x1a\xb3\xaa\xcd\xb3AU1\x8b\xfar\xd3\xc8\xaf" +
|
||||
"An\xe65]\xcdMSo*A\xc0J(\x95\x04" +
|
||||
"\xffn\xbd\x98\xef1\xf9\xb0\x86F\xd1j\xb7m\x9e\x17" +
|
||||
"\x0b\xb6\x15H\xf4\xf1\x803\x88h\xa1<\xa2\x1b\x0bK" +
|
||||
"\xd7\xa8\x033\xa9S\x8bC\xdc\x94\xd4#\x05\xa9qX" +
|
||||
"\xcd\x15?Iy\x1a\xdf\xba{[\xbd\xd6\x1f\xc5\x1b\xd4" +
|
||||
"\x1d\x12\x0c\x95\x94\x80\xad\xa6\xd7n\xea\xfc9m\xfaR" +
|
||||
"2\x1e\x80%\xfd\xa9\xc6\xbd\x1f#\xab\x0fzG(\xdd" +
|
||||
"?c\xf1\x07\xb8\xed\xfd\xa2\xb9\x82@\xb1\x18m\xab\xa7" +
|
||||
"v\xba\x97[\xc9\x99\xa8\x1e\xce\xb3e\xca\x8b3\xea\xe2" +
|
||||
"\x93\xf4p\x1f\xe3E:r\xd3d\x1d\xb9{\x92\x8e\xdc" +
|
||||
"\x1b\xed\xc8B\xa9#S\xa3(0T6\x09\x98\xa4\xb1" +
|
||||
"\x01\xeb\xc2}\xd48\xa1\xc7\x8fJ\x14\x0a]z\x96\x03" +
|
||||
"\x8e\xf8\xd1\x1ci\x1f\xc1\x9egz443\xb5}\x94" +
|
||||
"9\xad\xc1\x83\xe5\xc9L\x07\x8eV\xefQ\x8a\xb3\xb9," +
|
||||
"\x0e\x10,\x92\xd0_VH\xfb7\x82 }W\xc4p" +
|
||||
"\xd9\x82\xfenE\xba\xcf\x04A\xda#\xa2\x10,\xdb\xd0" +
|
||||
"_\xaaI;o\x02A\xda!\"\x0bve\xe8\x8f\xfb" +
|
||||
"\xcd\xa3U\x08\x82\xb4Y\xc4X\xb0\x1fD\x7fY m" +
|
||||
"\x18\x02A\xd2D\x8c\x07k8\xf4\xb7F\xd2\xba1\x10" +
|
||||
"\xa4>\xd1\xf1\x8d\x04\xad\x9e\x1em\xe8\xf81\x0a\x8dn" +
|
||||
"\x94\xb6\xa1\xe3\xc3@\xf4A\x05@\x1b:>\xe6d\x1f" +
|
||||
"\x07:].\x7f\x86\x84$M\x91m\x04\xbf\xbc\xfc\xc7" +
|
||||
"R\x01\x806Tb\x18\xd9\x95\x00|R\xa8\xd3\xcb\x1b" +
|
||||
"?M)\x99\xc4\xb9\xde;\xc1\x0e!r/\xa1\xb7\x1a" +
|
||||
"\x86\xca\\aZ\xc0\x16\xfb8-\xfc\xa0M\xd2a\xba" +
|
||||
"\xff\xff\x83\xfb\x0f\x12\xe0y\x96\xa1r8\x92\x8e/\x13" +
|
||||
"\xf1E\x86\xca\x91\x08\xe0y\x85r\xf40C\xe5\xddp" +
|
||||
"\xf1\xf2\xcf\x9b\x00\x94w\x19\xf6F\x00\x84\xf4!1~" +
|
||||
"@m\xd6\x85\x0f\xe8\xc1\x878\xee\x02H'\xa8\xfd\xa6" +
|
||||
"\\\xf8\x10\xf3\xe0\x83\x84\xfd\x00\xe9:\xa2\x9f\x1e\x85\x0f" +
|
||||
"\xf3\xf0J\x80\xf4\\\xa2/@\x01E\x1eY\xda\x14\xcd" +
|
||||
"\x10`\xe5\x8c\x81\x15\x9a>iO\xf27Ah/W" +
|
||||
"\xb5\\\xd1\xe4\x10\xb6\xc4R\x91\xe8\x88tioE\xd4" +
|
||||
"\xbe\x9e\xc2/M\xc1\x93E\x0bE\x10P<\xb5\xc9k" +
|
||||
"F\x1d\xa3\xd34\x0d4\xcb\xc0\xe7\xe2\x10|\x06\xd8\x93" +
|
||||
"0\xf4\xa5\x0c\x955\xe4\x8a6\xcf\x15J\x7f\x08\x97\x1b" +
|
||||
"3j\xd1\xe2\x13t\x00\xc6\xcd`Z\xb6\x06\x8db." +
|
||||
"\xdb\xcbA\xb4\xcd\xd12\x13L\x0bB\xd3<\xe9W\x9c" +
|
||||
"\x84[q\xfc=,\xfa\xebV\xa9y/\x08\xd2\"\xaa" +
|
||||
"8\xfej\x11\xfd\xa5\xba\xf4\x99\x87@\x90\xce\x08\x0b\x00" +
|
||||
"\xfa6`\x86>>\xe5\xbd\x0fn\x8c\xb6a\x0f\xe2\x7f" +
|
||||
"c\x86\xf4\xda\xcf)d\xfa\xb8U\x14\x95qq&}" +
|
||||
"3\xf8\xe3\xa6,\xd3+?\xed8\xed7\x92\xff\x04\x00" +
|
||||
"\x00\xff\xff\xed\xc3%H"
|
||||
const schema_db8274f9144abc7e = "x\xda\xccY{l\x1c\xd5\xd5?g\xee\xaeg\xed\xd8" +
|
||||
"\xac\x87\xd9\xc4\x89\x05\x9f\xbf/\x0a\xe2\xc3`\xc0q\xa9" +
|
||||
"\xd2\x94\xd6v\xb0Sl\xf2\xf0x\x93\x96G\x82\x18\xef" +
|
||||
"\xde\xd8\xe3\xee\xce,3\xb3\xc6N\x13\x92\x98\xa4\x10\xc4" +
|
||||
"+!\xe1\x91\x926\x04\xd1\xaa\x14ZR@m*\xaa" +
|
||||
"B_\x80x\xa3P%\x10\xd4\x96\x90>\xa2PJ\xa0" +
|
||||
"BT\x94\xa9\xce\xcc\xce\xc3kc;\xd0?\xfa\xdf\xea" +
|
||||
"\xcc\xb9\xf7\x9es~\xe7\x9e\xf3\xbbg\xcf\xff\x9a\xd8&" +
|
||||
"4\xc7\xc5\x19\x00\xca\xc6x\x85\xc3\x1b_Y\xbb\xe7\x8c" +
|
||||
"_\x8d\x82R\x8f\xe8\\\xfbxw\xeaC{\xf4u\x88" +
|
||||
"3\x11\xa0E\x8b\xafEy}\\\x04\x90G\xe2\x7f\x06" +
|
||||
"t.\x91\xce\xbb\"\xf5\xd2\x0b\xd7\x81T\x1fU\x8e\x91" +
|
||||
"\xf2\xea\x8aF\x94\xf3\x15\xa4\xacU\x90\xf2\x85\xf9\x17\xf7" +
|
||||
"~~\xe7\xb3\x9bA\xaa\x17Be\xc0\x96\x95\xe2Z\x94" +
|
||||
"5\x914\xb9\xb8\x1c\xd0yo\xc7\xec\x1f\xdc\xfb\xc2\xd3" +
|
||||
"[@:\x13\xa1t\xf6z\xf15\x04\x94\xb7\x89?\x02" +
|
||||
"tZ\x17?\xbf\xbf\xbe\xe5\x8e\x1de\xe7\x0a\xa4\xd8\x9c" +
|
||||
"hD\xb9=A\xbb})q\x0d\xa03w\xe8\x8c\xab" +
|
||||
"~\xf9\x9bG\xee\x04\xa5\x09\xd19\xdcw\xf6\xabl\xf7" +
|
||||
"\x03\xaf\xc3J\x14Q\x00h\xb97\xb1\x976\xde\xe7\xea" +
|
||||
"\xbex\xce\xe3?\xbb\xed\x91\xeb\xbf\x05\xca\x99\x88\x00\xae" +
|
||||
"#3+\xffI\x0agU\xb6\x02:;\x0e\xfe|Y" +
|
||||
"~\xdb\xae\xbd\x9ei\xeew\xa5R\x10 \xe6l\xee\xfa" +
|
||||
" \xbf\xf2\xbe\xf4}%\xa3)J-\x9d\x95'\x10\xb0" +
|
||||
"\xe5\xb2\xca\x06\x04t.x\xed\xe8\xf2\xa5?^\xf3\xbd" +
|
||||
"\xc8\xdab\xd5ZZ\xfb\xd0\xff\\R9|t\xf1\xc3" +
|
||||
" 5\xf9_\xae\xae\xea\xa5/\xb1\xd5\xecc\xf5\xae_" +
|
||||
"<Z\x8e\x87\xebj\xbe\xaa\x0f\xe5MUnx\xaa\xdc" +
|
||||
"#n|r\xd7\xd9\x89\xef\xbc\xf7\xd8D\x81\xb9\x7fF" +
|
||||
"\x1f\xca\xfbgP`\x1e\x9bA\xce\xce\xec\xc2\xc3O4" +
|
||||
"\xc7~\x12\x0d\xf3\xcc\xeac\xae\xb3\xd5\x14\xe6\xd3\xdf^" +
|
||||
"T\xa3\xbf3\xfaD\xd9n\xae\xe23\xd5\xdd(\xbfQ" +
|
||||
"M\xbb\x1dr\x95\xbb\xaf\xb8}{\xfc\xe8\xedO\x91\xa5" +
|
||||
"\x11|\xe3\x09\xd7\xcf\x1a\x13\xe5\xad5\xf4sKM\x1d" +
|
||||
"\x03t\xea\x1f\xfe\xe2\x0f\x17e\x0f=;\x81\xa5r\\" +
|
||||
":!K\x12\xfd\xaa\x91\xc8\xd0#M\xfb\xbe\xf1\xd7\x9b" +
|
||||
"_>P2\x14\xdd\\\x94\\T\xd6K\x84J\x00j" +
|
||||
"Y\x94\\\xcd\xdd\xd2 \xca\xfb\xdc\xed\x1er\xb5\x85\xa3" +
|
||||
"\xea\x9c\x8d\xbf\xfb\xf2\xe1\x08\x0e\xfb\xa47\x11b\xce\xb2" +
|
||||
"\xaf^1X\xb9\xfe\xc8\x91\xe8A\xf7KnD\xf6\xbb" +
|
||||
"K\xff\xf6\xddc\xb7\x1e\xcfg\xff\xe4\xe6\x92\x1f\xb3C" +
|
||||
"\xd2B\x01P~W\xa2,\xafk\xa8\xe9\x9c{\xb0\xe7" +
|
||||
"\x98\x07\xa5\xb7\xc5\xf3\xa7.\"\x85\xa3\xa7\xd2\x16\x17\\" +
|
||||
"\xd5\xceW-\xb8\xf4\x18H\xf5l\xcc5\x88\xcb\x0bQ" +
|
||||
"\x9e)\xd3\x02I\xbe\x1e\xe5\xb3Ru\x00\xce-\xd7v" +
|
||||
",\xff\xc2\xdc'ODM\x9a\x93\xa2\xb4\x92\x9bR\xb4" +
|
||||
"\xdf\x9a\x05\xc7\xbfr\xc6-\xbf=Q\x16HWqi" +
|
||||
"\xaa\x11\xe5\xd5)r\xfd2R~g\xf1\xb7\x0f\xd4'" +
|
||||
"\xeb\xdf/\x0bS\x85\x9bA\xa9A\x94\xb7\x91n\xcb\xcd" +
|
||||
"\xa9\xa7(\x99\xbe\xf9\xfa\x95\xc3\xaf\\\xf7\xde?\xca\x11" +
|
||||
"u\xb7\xde4\xab\x17\xe5\x9d\xb3h\xebm\xb3\x08\xff;" +
|
||||
"W\xfce\xc3\xf1\x9d\xb3>\x18\xe7Ws\xdd \xca\x9d" +
|
||||
"u\xa4\xd9^w\xbd|/\xfdr^\x12\xefk\xee\xd8" +
|
||||
"\xf0\xec\x87\x91\x8c\xdfZ\xd7M\x19\x7f\x87x\xcf\x91\x8d" +
|
||||
"\xbf\xbf\xf2\xa3\xa8\xc3[\xea\xde$\x87\xef\xae#\x87\xd7" +
|
||||
"\xbds\xf7\xc5\xb7\xaez\xf0\xe3\x08|\xfb\xebFi\xa9" +
|
||||
"]\xd4u\x9e3\x0b\xb1\xccy\xfe\xcf\xcc\xb9\x19\xb5\xa0" +
|
||||
"\x17\x16\xb6\x17\xed\x01\xae\xdbZF\xb5y/o\xb5\x0a" +
|
||||
"\x86n\xf1\x1eD\xa5\x96\xc5\x00b\x08 \xa9\x83\x00\xca" +
|
||||
"U\x0c\x95\x9c\x80\x12b\x8a\x00\x964\x12\x0e0Tl" +
|
||||
"\x01%AHQ\xc9\x90\xae\x9e\x0b\xa0\xe4\x18*\xc3\x02" +
|
||||
"\"K!\x03\x90\x8a\xdb\x01\x94a\x86\xcaf\x01\x9d\x02" +
|
||||
"7\xf3\xaa\xceuH\xda\x9d\xa6\x89\xd5 `5\xa0c" +
|
||||
"r\xdb\x1cQ\xfbr\x90\xe4\x11\xb18x\x8d\x8d5 " +
|
||||
"`\x0d\xa03`\x14Mk\xa5n\xa3\x96\xeb\xe5kL" +
|
||||
"n\xe1\x00V\x80\x80\x15\x80\x81{l\xbc{\x17\xe54" +
|
||||
"\xae\xdb\xc9.}\x8dQ\xe6T\xf7DNu\x97\x9c\xda" +
|
||||
"\x1cqj\xd3\"\x00e\x1dC\xe5\x06\x01%V\xf2j" +
|
||||
"K#\xf5\x05\x86\xcaM\x02:\x19\xf7\x90\xae,\x00\x04" +
|
||||
"\xf6\xae\xe1\xaa]4\xb9E\xb2S\x00{\x18\xban\x9d" +
|
||||
"\x02\xb8a\x88\x9b\x96f\xe8\xbe\x9bI\xd5\xcc\x0c\x04\xa1" +
|
||||
"\x98\x04\xaa\xcea\xcd\xb25\xbd\x7f\x85+o\xed1r" +
|
||||
"Zf\x84\xbc\xaav\xed<}!\x00\xa24\xf3r\x00" +
|
||||
"\x14$i\x11@\xab\xd6\xaf\x1b&w\xb2\x9a\x951t" +
|
||||
"\x9d\x03\xcb\xd8\x1b\xfa\xd4\x9c\xaagxpP\xc5\xf8\x83" +
|
||||
"\xbc\x03\xd2\xdc\x1c\xe2\xe6\xb9j$A\xe6\xf5\xa8\xa6\xca" +
|
||||
"\xf2\x96R\x1d\xc4\xb1\xf3r\x00\xa5\x83\xa1\xd2\x13\x89\xe3" +
|
||||
"R\x8a\xe3\x12\x86\xca\xa5\x918\xae\xa48\xf60TV" +
|
||||
"\x09\xe8\x18\xa6\xd6\xaf\xe9\x17q`f\x14c\xcb\xd6\xd5" +
|
||||
"<\xa7\x98\x95\xe2\xb1\xc1(\xd8\x9a\xa1[X\x1b\xd6Q" +
|
||||
"@\xac\x9d\x1cu\xcf\x81$\xe56\xc5'\x11X{\x16" +
|
||||
"Y\xfb\xff\x0c\x95\xcfE\xacm\xa6T>\x9f\xa1r\xa1" +
|
||||
"\x80\x8e\x9a\xc9\x18E\xdd^\x01L\xed/\x03%\xcd!" +
|
||||
"\x991yh\xaf\x7fl|\x82\xbc\xa3xg\xc8\xf4^" +
|
||||
"\xee\xdd\xa9sMn\x89\xc5\x9cM\xd6T;\x8eg\xce" +
|
||||
"|\x00e\x1eC\xe5|\x01k\xf0c\xc7\xb3\xa7i{" +
|
||||
"hO\x037M\xc3\xc4\xda\xb0\xe6\x94\xbc\xcf\x94\x0e@" +
|
||||
"C\xef\xe0\xb6\xaa\xe5\x90b\x14\xb4\xb9\xb2\x18M\x05r" +
|
||||
"Q7y\xbff\xd9\xdc\xf4\xc4\xf3Z\x09\xe9\xbc\xa5\xc4" +
|
||||
"\x82\xd0\xd5\xec\x02Pj\x19*\xa7\x09\xe8\xf4\x9bj\x86" +
|
||||
"\xf7p\x135#\xbbL\xd5\x8d4\xe3\x19\x8c\x83\x80\xf1" +
|
||||
"\xc8\xa1\xa7\x9c\xec\xa1\xbd\xdc*\xe6l\x0b\x82U\x93\xaf" +
|
||||
"7y)\x08\xa5\xe5=\x0d\x9e\xcd\xa9\xc0\xe6\xf5s\xc3" +
|
||||
"\xda\x13\xc0\xbd\xa9/\xbc\xbaArn\xa5\xc4\xb8\x81\xa1" +
|
||||
"\xb2#r\xc9\xb7Q\x1a\xdf\xc6P\xb9G@)\x16K" +
|
||||
"a\x0c@\xba\x9b\xd2x\x07Ce\x8f0\xb6B\xf1!" +
|
||||
"\xae\xdb\x1dZ?\x88\xdc\x0a\xa5db\x87\xd6\xcf\x81Y" +
|
||||
"\x9f5\xd1\x13S\xc4\xc3\xe8\xb3\x8c\x1c\xb7y\x07\xcf\xe4" +
|
||||
"TS\xb5\xb5!\xee}/%\xa3\x0f\xead\x1b\xf6\xba" +
|
||||
"\x88\xd0bC\x1f\x07S\x98\xd4%\xa8\xd0\x9a\xac^\x85" +
|
||||
"\xea\xcb\x0b\xb6&\x1a\xbaE\x971\x82\xce\xc2\x89\xd01" +
|
||||
"Ct\xd0\x07g4\x0a\x0e\x96\xc0\xd9\x15\xe2 \xc5\x04" +
|
||||
"\x0f\x9c\xdd{\x01\x94=\x0c\x95\x07\x05l\xf5\xca2\xd6" +
|
||||
"\x86l\xbc\x14P\xaf\xf8,1\xa0!\xa3\xe6\xba\x0a\x01" +
|
||||
",&/\xe4\xd4\x0c\xef\xc4R\xa1\x05D\x10\x10]\x14" +
|
||||
"\xf3\x05\x93[\x16j\x86\xae\x14\xd5\x9c\xc6\xec\x91\xa0\xfd" +
|
||||
"\xe8\xc5|\x8f\xc9\x8744\x8aV\xbbm\xf3\xbcX\xb0" +
|
||||
"\xadq\xcdi\xd2\x00\xd1\x15\x16\xb5\x9cUV\xad\x1a\xc3" +
|
||||
"\xf2\x10\x04\xa8\x89\xaa\xd59\x0c\x95\x05\x02&\x8bE-" +
|
||||
"\x1b\x98\x9f32.n\x90\\\xa6\xe6\xf9\xb8\x8eR1" +
|
||||
"\xe5u\x1as\x19{\xd4\xa4{\x9b\xfe\x9bJ\xfd\xe4\xfc" +
|
||||
"\x85\\\x07\xb7\x1f\x86&S\x01hc\xa8,\x89\x98\xdc" +
|
||||
"5?\xe2\x87o\xf2\xd2\xbe\xd0\x0f\xf1\xeb|\xc4\xb7\xaa" +
|
||||
"\x81\xe7\xa9\xb8\xfa\xc1,9\xd3\x0e\xe2%\xa1\xced\xf6" +
|
||||
"E/\xd4\xf2B\x83\xeb!\xd9\xb8\xc0\xb7Q\x1e\xc1n" +
|
||||
"\x80\xf402Lo\xc6\xd0Ly\x13.\x02H\xaf#" +
|
||||
"\xf9\x0d\x18Z*o\xc1z\x80\xf4F\x92\xdf\x84\x01\xcf" +
|
||||
"\x92\xb7\xe2\x03\x00\xe9\x9bH|\x17\xa9\xc7\x98{%\xe4" +
|
||||
"\x9d\xee\xf6;H\xbe\x87\xe4\xf1X\x0a\xe3\x00\xf2nl" +
|
||||
"\x04H\xdfE\xf2GI^!\xa4\xb0\x02@\xde\x87\x83" +
|
||||
"\x00\xe9\x87I\xfe8\xc9\xc5x\x8a\xa8\xa6\xbc\x1fM\x80" +
|
||||
"\xf4OI\xfek\x92'f\xa70\x01 ?\xe9\xca\x9f" +
|
||||
" \xf9s$\xaf\x9c\x93\xc2J\x00\xf9\x19\x1c\x05H?" +
|
||||
"M\xf2\x03$\xaf\xc2\x14V\x01\xc8/\xe3.\x80\xf4\x01" +
|
||||
"\x92\xff\x81\xe43*R8\x03@~\xc3\xb5\xe7 \xc9" +
|
||||
"\xdf\"yu,\x85\xd5\x00\xf2\x1fq/@\xfa-\x92" +
|
||||
"\xff\x9d\xe45b\x0ak\x00\xe4\xb7]\xbf\x8e\x93<!" +
|
||||
"\x94\x910?\xa3\xca\x98\x163\xac\x002^\xba\xe3\xe8" +
|
||||
"\xa5{\x8f\x91$6\x85\xc9\xf05\x0e\x88I@\xa7`" +
|
||||
"\x18\xb9ec35i\xab\xfd\x96\xcf\xeaj\xc3\x17\x1b" +
|
||||
" \x09\x83\xd6\x0cIC\xef\xca\x06\x85\xa0\xbc\xea\xf8\x96" +
|
||||
"hV{\xd16\x8a\x05h\xc8\xaa6\xcf\x065\xc7," +
|
||||
"\xea\x8bM#\xbf\x02\xb9\x99\xd7t57E5\xaa\x04" +
|
||||
"\x01+\xa1T\x12\xfc\xbd'/M\x9f\xccQ\x83\x8c\x16" +
|
||||
"\xca3\xba\xa1\xb0p\x85\xda?\x9d:5?\xa4ZI" +
|
||||
"=R\x90\x1a\x86\xd4\\\xf1\xd3\x94\xa7\xb1\xdd\xbe\xb7\xd5" +
|
||||
"c\x0bQ\x8aB\x0d%\xc1PI\x09\xd8jz\x1d\xaa" +
|
||||
"\xd6\x7f\xdaM]J\xc6r\xb6\xa4\xff\x10r\xf7\xc7\xc8" +
|
||||
"\xb8\x84\xce\x11J\xfbO\xdb\xfc~n{\xbf\xe8)B" +
|
||||
"<Z\x8cv\xe2\x93[\xdd\xcb\xad\xe4t\\\x0f\x9f\xc0" +
|
||||
"e\xce\x8b\xd3j\xfc\x13\xb4}\x9f\x16F\xdeQ\x84\xfd" +
|
||||
"*\x86\xca@\x04{NM!\xcbP)\x84M<\xdf" +
|
||||
"\x1b\xbe\x0d%&\x94\x1e\x87\xd4(\x0a\x0c\x95u\x02&" +
|
||||
"\xe9\xa5\x81\xb5\xe1\x0ck\x8c\xd1c_W\x94\x0a]z" +
|
||||
"\x96\x03\x0e\xfb\xd9\x1ci\x1f\xc1lhj\x025=\xb7" +
|
||||
"}b:e\xc0\x83y\xcbt\xdf(\xad\xde\xa1\x94g" +
|
||||
"\xb3Y\x1c \x98=\xa1?\xdf\x90\xf6\xad\x05A\xfa\xbe" +
|
||||
"\x88\xe1|\x06\xfdq\x8c\xb4\xdb\x04A\xda)\xa2\x10\x0c" +
|
||||
"\xe8\xd0\x1f\xc4I[o\x04A\xda\"\"\x0b\xe6k\xe8" +
|
||||
"O\x08\x9aG\xaa\x10\x04i\xbd\x88\xb1`\xa6\x88\xfe|" +
|
||||
"A\xbaz\x10\x04I\x131\x1e\x8c\xee\xd0\x1f4I\xab" +
|
||||
"GA\x90V\x8a\x8e\x1f$h\xf5\xfchC\xc7\xcfQ" +
|
||||
"hp\xb3\xb4\x0d\x1d\x9f9\xa2O*\x00\xda\xd0\xf1i" +
|
||||
"*\xfb$\x9e\xeaj\xf9\xcfNH\xd2\xc3\xb3\x8d\xc8\x99" +
|
||||
"w\xff\xb1T\x00\xa0\x0d\x95\x18F\xc6+\x00\x9f\x96\xea" +
|
||||
"\xf4\xf2\x86\xcfRJ&\x00\xd7;'\x18;D\xf6%" +
|
||||
"\xf6V\xcdP\x99-LI\xd8b\x9f\xe4\x85\x9f\xb4I" +
|
||||
"ZL\xfb\xffo\xb0\xff\xcbDx\x9ec\xa8\x1c\x8c\\" +
|
||||
"\xc7WI\xf8\x12C\xe5p\x84\xf0\x1c\xa2;z\x90\xa1" +
|
||||
"\xf2~8\xaby\xf7F\x00\xe5}\x86\xbd\x11\x02!\xfd" +
|
||||
"\x8b\x14?\xa26\xeb\xd2\x07\xf4\xe8C\x1c\xb7\x03\xa4\x13" +
|
||||
"\xd4~S.}\x88y\xf4A\xc2>\x80t-\xc9O" +
|
||||
"\x8b\xd2\x879x9@z6\xc9\xe7\xa1\x80\"\x8f\xcc" +
|
||||
"y\x8afH\xb0rF\xff\x12M\x9f\xb0'\xf9\xc3#" +
|
||||
"\xb4\x17\xabZ\xaehr\x08[b\xa9HtD\xba\xb4" +
|
||||
"7Uj_C\xe9\x97\xa6\xe4\xc9\xa2\x85\"\x08(\x9e" +
|
||||
"\xdccmZ\x1d\xa3\xd34\x0d4\xcb\xc8\xe7\xfc\x90|" +
|
||||
"\x06\xdc\x938\xf4\xc5\x0c\x95\x15\x04E\x9b\x07\x85\xd2\x17" +
|
||||
"\xd2\xe5\x86\x8cZ\xb4\xf88\x1f\x80q3x`[\x03" +
|
||||
"F1\x97\xed\xe5 \xda\xe6HY\x08\xa6$\xa1i\x9e" +
|
||||
"\xf4+N\xc2\xad8\xfe\xe8\x16\xfd\x09\xad\xd4\xbc\x0b\x04" +
|
||||
"\xa9\x89*\x8e?\x8dD\x7f\x10/\xfd\xdf\x03 H\xa7" +
|
||||
"\x87\x05\x00\xfd\x180C\x1f{\xe5\xbd\x0fn\x8e\xb6a" +
|
||||
"\x0f\xe2\x7f\xe2\xd9\xe9\xb5\x9f\x93\xb8\xe9c\xa6WT\xc6" +
|
||||
"\xc5\xe9\xf4\xcd\xe0\xcf\x9e\xb2\x9b^\xf9Y_\xe0~#" +
|
||||
"\xf9w\x00\x00\x00\xff\xffH\xa22\xa3"
|
||||
|
||||
func init() {
|
||||
schemas.Register(schema_db8274f9144abc7e,
|
||||
|
||||
+138
-35
@@ -9,9 +9,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,7 +28,7 @@ var (
|
||||
)
|
||||
|
||||
type Tunnel struct {
|
||||
ID string `json:"id"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
@@ -39,29 +40,108 @@ type Connection struct {
|
||||
ID uuid.UUID `json:"uuid"`
|
||||
}
|
||||
|
||||
// Route represents a record type that can route to a tunnel
|
||||
type Route interface {
|
||||
json.Marshaler
|
||||
RecordType() string
|
||||
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
|
||||
SuccessSummary() string
|
||||
}
|
||||
|
||||
type DNSRoute struct {
|
||||
userHostname string
|
||||
}
|
||||
|
||||
func NewDNSRoute(userHostname string) Route {
|
||||
return &DNSRoute{
|
||||
userHostname: userHostname,
|
||||
}
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
UserHostname string `json:"user_hostname"`
|
||||
}{
|
||||
Type: dr.RecordType(),
|
||||
UserHostname: dr.userHostname,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) RecordType() string {
|
||||
return "dns"
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) SuccessSummary() string {
|
||||
return fmt.Sprintf("%s will route to your tunnel", dr.userHostname)
|
||||
}
|
||||
|
||||
type LBRoute struct {
|
||||
lbName string
|
||||
lbPool string
|
||||
}
|
||||
|
||||
func NewLBRoute(lbName, lbPool string) Route {
|
||||
return &LBRoute{
|
||||
lbName: lbName,
|
||||
lbPool: lbPool,
|
||||
}
|
||||
}
|
||||
|
||||
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
LBName string `json:"lb_name"`
|
||||
LBPool string `json:"lb_pool"`
|
||||
}{
|
||||
Type: lr.RecordType(),
|
||||
LBName: lr.lbName,
|
||||
LBPool: lr.lbPool,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) RecordType() string {
|
||||
return "lb"
|
||||
}
|
||||
|
||||
func (lr *LBRoute) SuccessSummary() string {
|
||||
return fmt.Sprintf("Load balancer %s will route to this tunnel through pool %s", lr.lbName, lr.lbPool)
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
|
||||
GetTunnel(id string) (*Tunnel, error)
|
||||
DeleteTunnel(id string) error
|
||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||
DeleteTunnel(tunnelID uuid.UUID) error
|
||||
ListTunnels() ([]Tunnel, error)
|
||||
CleanupConnections(tunnelID uuid.UUID) error
|
||||
RouteTunnel(tunnelID uuid.UUID, route Route) error
|
||||
}
|
||||
|
||||
type RESTClient struct {
|
||||
baseURL string
|
||||
authToken string
|
||||
client http.Client
|
||||
logger logger.Service
|
||||
baseEndpoints *baseEndpoints
|
||||
authToken string
|
||||
client http.Client
|
||||
logger logger.Service
|
||||
}
|
||||
|
||||
type baseEndpoints struct {
|
||||
accountLevel string
|
||||
zoneLevel string
|
||||
}
|
||||
|
||||
var _ Client = (*RESTClient)(nil)
|
||||
|
||||
func NewRESTClient(baseURL string, accountTag string, authToken string, logger logger.Service) *RESTClient {
|
||||
func NewRESTClient(baseURL string, accountTag, zoneTag string, authToken string, logger logger.Service) *RESTClient {
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
url := fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag)
|
||||
return &RESTClient{
|
||||
baseURL: url,
|
||||
baseEndpoints: &baseEndpoints{
|
||||
accountLevel: fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag),
|
||||
zoneLevel: fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag),
|
||||
},
|
||||
authToken: authToken,
|
||||
client: http.Client{
|
||||
Transport: &http.Transport{
|
||||
@@ -83,15 +163,12 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, er
|
||||
if name == "" {
|
||||
return nil, errors.New("tunnel name required")
|
||||
}
|
||||
body, err := json.Marshal(&newTunnel{
|
||||
body := &newTunnel{
|
||||
Name: name,
|
||||
TunnelSecret: tunnelSecret,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to serialize new tunnel request")
|
||||
}
|
||||
|
||||
resp, err := r.sendRequest("POST", "", bytes.NewBuffer(body))
|
||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
@@ -104,11 +181,12 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, er
|
||||
return nil, ErrTunnelNameConflict
|
||||
}
|
||||
|
||||
return nil, statusCodeToError("create", resp)
|
||||
return nil, r.statusCodeToError("create tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) GetTunnel(id string) (*Tunnel, error) {
|
||||
resp, err := r.sendRequest("GET", id, nil)
|
||||
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
|
||||
endpoint := fmt.Sprintf("%s/%v", r.baseEndpoints.accountLevel, tunnelID)
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
@@ -118,21 +196,22 @@ func (r *RESTClient) GetTunnel(id string) (*Tunnel, error) {
|
||||
return unmarshalTunnel(resp.Body)
|
||||
}
|
||||
|
||||
return nil, statusCodeToError("read", resp)
|
||||
return nil, r.statusCodeToError("get tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteTunnel(id string) error {
|
||||
resp, err := r.sendRequest("DELETE", id, nil)
|
||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
endpoint := fmt.Sprintf("%s/%v", r.baseEndpoints.accountLevel, tunnelID)
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return statusCodeToError("delete", resp)
|
||||
return r.statusCodeToError("delete tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListTunnels() ([]Tunnel, error) {
|
||||
resp, err := r.sendRequest("GET", "", nil)
|
||||
resp, err := r.sendRequest("GET", r.baseEndpoints.accountLevel, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
@@ -146,24 +225,48 @@ func (r *RESTClient) ListTunnels() ([]Tunnel, error) {
|
||||
return tunnels, nil
|
||||
}
|
||||
|
||||
return nil, statusCodeToError("list", resp)
|
||||
return nil, r.statusCodeToError("list tunnels", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) resolve(target string) string {
|
||||
if target != "" {
|
||||
return r.baseURL + "/" + target
|
||||
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID) error {
|
||||
endpoint := fmt.Sprintf("%s/%v/connections", r.baseEndpoints.accountLevel, tunnelID)
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
return r.baseURL
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("cleanup connections", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) sendRequest(method string, target string, body io.Reader) (*http.Response, error) {
|
||||
url := r.resolve(target)
|
||||
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route Route) error {
|
||||
endpoint := fmt.Sprintf("%s/%v/routes", r.baseEndpoints.zoneLevel, tunnelID)
|
||||
resp, err := r.sendRequest("PUT", endpoint, route)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("add route", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) sendRequest(method string, url string, body interface{}) (*http.Response, error) {
|
||||
r.logger.Debugf("%s %s", method, url)
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
if bodyBytes, err := json.Marshal(body); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to serialize json body")
|
||||
} else {
|
||||
bodyReader = bytes.NewBuffer(bodyBytes)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, bodyReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "can't create %s request", method)
|
||||
}
|
||||
if body != nil {
|
||||
if bodyReader != nil {
|
||||
req.Header.Set("Content-Type", jsonContentType)
|
||||
}
|
||||
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
|
||||
@@ -178,7 +281,7 @@ func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
|
||||
return &tunnel, nil
|
||||
}
|
||||
|
||||
func statusCodeToError(op string, resp *http.Response) error {
|
||||
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
@@ -189,6 +292,6 @@ func statusCodeToError(op string, resp *http.Response) error {
|
||||
case http.StatusNotFound:
|
||||
return ErrNotFound
|
||||
}
|
||||
return errors.Errorf("API call to %s tunnel failed with status %d: %s", op,
|
||||
return errors.Errorf("API call to %s failed with status %d: %s", op,
|
||||
resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user