Compare commits

..

68 Commits

Author SHA1 Message Date
Dalton f5b479dbbc Release 2020.8.2 2020-08-20 11:15:11 -05:00
Dalton b52728e829 AUTH-3021 fixed the git version call by using the older flag 2020-08-20 16:13:10 +00:00
cthuang 8eeb452cce TUN-3268: Each connection has its own event digest to reconnect 2020-08-20 16:44:22 +01:00
Dalton 9323844ea7 Release 2020.8.1 2020-08-19 21:59:21 -05:00
Dalton b698fe5ef3 AUTH-2864 - add macos build to github release 2020-08-19 21:31:50 -05:00
Dalton edb980d439 AUTH-2653 disabling signing until we can get the keys 2020-08-19 15:32:49 -05:00
Dalton 310bd0dbf1 AUTH-2712 fixed the mac build script 2020-08-19 14:57:43 +00:00
Adam Chalmers 1a96889141 TUN-3286: Use either ID or name in Named Tunnel subcommands. 2020-08-19 14:39:45 +00:00
Dalton Cherry 60de05bfc1 AUTH-2712 added MSI build for a windows agent 2020-08-17 14:44:28 -05:00
cthuang 3deef6197f TUN-3213: Create, route and run named tunnels in one command 2020-08-17 19:38:38 +00:00
Dalton 70114c2145 AUTH-2977 log file protection 2020-08-17 17:55:58 +00:00
Dalton 5499c77e62 AUTH-2975 don't check /etc on windows 2020-08-17 12:40:36 -05:00
cthuang 292a7f07a2 TUN-3243: Refactor tunnel subcommands to allow commands to compose better 2020-08-11 10:02:52 +01:00
Igor Postelnik 679f36303a TUN-3242: Build with go 1.14 2020-08-10 23:17:23 +00:00
Adam Chalmers 1b61d699c4 TUN-3237: By default, don't show connections that are pending reconnect 2020-08-07 10:16:13 -05:00
cthuang a7562dff68 TUN-3233: List tunnels support filtering by deleted, name, existed at and id 2020-08-07 10:09:26 +01:00
Dalton 8daf1e882f AUTH-2857 update homebrew script to use new url 2020-08-04 16:47:10 -05:00
Michael Borkenstein 20466dacb7 Release 2020.8.0 2020-08-03 12:18:44 -06:00
Dalton 5b3b592108 AUTH-2927 run message update after all github builds are done 2020-08-03 10:12:03 -05:00
Adam Chalmers 9952ce0069 TUN-3221: ConnectionOptions tracks numPreviousAttempts. 2020-07-31 20:22:33 +00:00
cthuang 1cbc8fb8ac TUN-3220: tunnel route reports created route 2020-07-31 17:48:58 +01:00
Michael Borkenstein edc69694cb AUTH-2714: Adds arm64 cloudflared build 2020-07-29 13:12:57 -06:00
Igor Postelnik bd15c6b8c3 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.
benchmark                                    old ns/op     new ns/op     delta
BenchmarkSingleStreamLargeResponseBody-8     17786594      12163494      -31.61%

benchmark                                    old allocs     new allocs     delta
BenchmarkSingleStreamLargeResponseBody-8     17086          15869          -7.12%

benchmark                                    old bytes     new bytes     delta
BenchmarkSingleStreamLargeResponseBody-8     58215169      21604391      -62.89%
2020-07-29 14:30:12 +00:00
Igor Postelnik 42fe2e7266 TUN-3208: Add benchmark for large response write 2020-07-29 14:30:12 +00:00
Igor Postelnik 44e3be2c88 TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2
benchmark                                    old ns/op     new ns/op     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     10360         5048          -51.27%

benchmark                                    old allocs     new allocs     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     135            26             -80.74%

benchmark                                    old bytes     new bytes     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     8543          3667          -57.08%
2020-07-29 14:29:07 +00:00
Igor Postelnik 61d5461138 TUN-3209: Add benchmark for header serialization 2020-07-29 14:29:07 +00:00
Michael Borkenstein b696ca8b1c Removes centos 6 build 2020-07-29 14:01:34 +00:00
Dalton 6a4d020c27 AUTH-2016 adds sha256 hashes to releases 2020-07-27 10:36:31 -05:00
Igor Postelnik 4791ba3b87 TUN-3194: Don't render log output when level is not enabled 2020-07-23 18:36:31 -05:00
Igor Postelnik cf1c9a3083 TUN-3192: Use zone ID in tunnelstore request path; improve debug logging 2020-07-21 18:07:50 -05:00
Igor Postelnik d61e3fb130 TUN-3190: Initialize logger using command line flags in tunnels subcommands 2020-07-21 17:25:48 +00:00
Dalton ca7d6797e1 AUTH-2902 redirect with just the root host on curl commands 2020-07-21 11:08:31 -05:00
Areg Harutyunyan af0d04d0f3 Release 2020.7.4 2020-07-21 11:02:29 +01:00
Ivan Babrou 6274567e16 Build cloudflared for arm64 on native agents 2020-07-20 17:42:50 -07:00
cthuang 8836ee1dda TUN-3156: Add route subcommand under tunnel 2020-07-17 05:51:24 +08:00
Michael Borkenstein 7afde79600 AUTH-2890: adds error handler to cli actions 2020-07-15 14:33:36 +00:00
Rachel Williams 3d782f7162 TUN-3048: Handle error when user tries to delete active tunnel 2020-07-14 15:32:46 +00:00
Michael Borkenstein 058598ea58 Release 2020.7.3 2020-07-13 13:32:38 -05:00
Michael Borkenstein 8e617df914 Change scp command to use file glob that matches both cloudflared rpms and debs 2020-07-13 12:15:01 -05:00
Michael Borkenstein 7260f3e487 Release 2020.7.2 2020-07-13 09:57:36 -05:00
Michael Borkenstein a42b66e8bd AUTH-2872: Adds centos-6 build 2020-07-10 20:39:50 +00:00
Michael Borkenstein 28d556b8d4 AUTH-2858: Set file to disable autoupdate 2020-07-10 18:03:07 +00:00
Areg Harutyunyan 33701f50ec Merge branch 'master' of github.com:cloudflare/cloudflared 2020-07-08 16:35:04 +01:00
Michael Borkenstein abfeebf67d AUTH-2871: fix rpm builds 2020-07-08 14:39:28 +00:00
Sven Höxter 3b293048f4 beautify package meta information generated by fpm (#218)
Current Debian and RPM meta information are very generic, e.g.
$ apt-cache show cloudflared
Package: cloudflared
Version: 2020.7.0
License: unknown
Vendor: @k8s-managed-krwtk.teamcity-agents.svc.cluster.local
Architecture: amd64
Maintainer: <@k8s-managed-krwtk.teamcity-agents.svc.cluster.local>
Installed-Size: 42883
Section: default
Priority: extra
Homepage: http://example.com/no-uri-given
Description: no description given
2020-07-08 15:36:42 +01:00
Igor Postelnik ac3638f6b1 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-07 16:35:44 -05:00
Rachel Williams f7ff41f1dc TUN-3150: cloudflared tunnel list's table should use intelligent column width 2020-07-07 15:48:56 +00:00
cthuang f5c8ff77e9 TUN-3008: Implement cloudflared tunnel cleanup command 2020-07-07 21:56:46 +08:00
cthuang 87e06100df TUN-3131: Allow user to specify tunnel credentials path, and remove it in tunnel delete command 2020-07-07 14:22:08 +08:00
Michael Borkenstein 1ed9e0fceb AUTH-2644: Change install location and add man page 2020-07-06 19:27:25 +00:00
Michael Borkenstein b9d6aaebbe Release 2020.7.1 2020-07-06 11:47:37 -05:00
Adam Chalmers a99780ed9d TUN-3161: Upgrade golang.org/x/ deps 2020-07-06 16:42:48 +00:00
Dalton 92765b4261 AUTH-2850 log config file path 2020-07-06 16:32:57 +00:00
Robert McNeil 8b88b4a403 DEVTOOLS-7321: Push GitHub homebrew updates to master 2020-07-02 16:51:04 +00:00
Michael Borkenstein 2ce6720a6e AUTH-2854: Create cloudflared RPMs 2020-07-01 14:50:38 -05:00
Michael Borkenstein 772ccc9607 Release 2020.7.0 2020-07-01 10:52:35 -05:00
Michael Borkenstein 7724ff8176 AUTH-2860: Fix builds 2020-07-01 15:51:02 +00:00
Igor Postelnik 2a3d486126 TUN-3007: Implement named tunnel connection registration and unregistration.
Removed flag for using quick reconnect, this logic is now always enabled.
2020-07-01 04:19:30 +00:00
Rachel Williams 932e383051 TUN-3140: Add timestamps to terminal log entries 2020-06-30 22:28:22 +00:00
Adam Chalmers dbe3516204 Release 2020.6.6 2020-06-30 12:55:54 -05:00
Dalton 6fc9f1b405 AUTH-2849 all log output to stderr 2020-06-30 16:54:31 +00:00
Michael Borkenstein 370c17e48c AUTH-2718: Add target for publishing deb to pkg.cloudflare repo 2020-06-30 14:46:24 +00:00
Michael Borkenstein 92da73aa9d AUTH-2652: Update cloudflare repo 2020-06-26 14:24:20 +00:00
Dalton 0c65daaa7d AUTH-2712 mac package build script and better config file handling when started as a service 2020-06-25 16:44:57 -05:00
Michael Borkenstein b46acd7f63 AUTH-2685: Adds script to create release 2020-06-25 18:39:37 +00:00
Michael Borkenstein e3a9aa4296 AUTH-2652: Adds .docker-images to push images to docker hub 2020-06-23 11:35:48 -05:00
cthuang 3886021ba5 TUN-3107: UnregisterConnection shouldn't wrap nil error as RPC error 2020-06-18 21:17:43 +08:00
Adam Chalmers 4d3ebaf984 TUN-3106: Pass NamedTunnel config to StartServer 2020-06-17 23:20:37 +00:00
380 changed files with 34944 additions and 94075 deletions
+8
View File
@@ -0,0 +1,8 @@
images:
- name: cloudflared
dockerfile: Dockerfile
context: .
registries:
- name: docker.io/cloudflare
user: env:DOCKER_USER
password: env:DOCKER_PASSWORD
+2
View File
@@ -9,7 +9,9 @@ guide/public
\#*\#
cscope.*
cloudflared
cloudflared.pkg
cloudflared.exe
cloudflared.msi
!cmd/cloudflared/
.DS_Store
*-session.log
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# uninstall first in case this is an upgrade
/usr/local/bin/cloudflared service uninstall
# install the new service using launchctl
/usr/local/bin/cloudflared service install
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
/usr/local/bin/cloudflared service uninstall
rm /usr/local/bin/cloudflared
pkgutil --forget com.cloudflare.cloudflared
+110 -1
View File
@@ -13,9 +13,118 @@ export GO111MODULE=on
# build 'cloudflared-darwin-amd64.tgz'
mkdir -p artifacts
FILENAME="$(pwd)/artifacts/cloudflared-darwin-amd64.tgz"
PKGNAME="$(pwd)/artifacts/cloudflared-amd64.pkg"
TARGET_DIRECTORY=".build"
BINARY_NAME="cloudflared"
VERSION=$(git describe --tags --always --dirty="-dev")
PRODUCT="cloudflared"
CODE_SIGN_PRIV="code_sign.pk12"
CODE_SIGN_CERT="code_sign.cer"
INSTALLER_PRIV="installer.pk12"
INSTALLER_CERT="installer.cer"
export PATH="$PATH:/usr/local/bin"
mkdir -p ../src/github.com/cloudflare/
cp -r . ../src/github.com/cloudflare/cloudflared
cd ../src/github.com/cloudflare/cloudflared
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
tar czf "$FILENAME" cloudflared
# TODO: AUTH-2653 - The CFD_CODE_SIGN_KEY and CFD_INSTALLER_KEY are "doubly" gpg encrypted.
# this needs to be fixed, but I don't have access to the keys to do it.
# The private keys are on from Dane's laptop
# # Add code signing private key to the key chain
# if [[ -n "${CFD_CODE_SIGN_KEY:-}" ]]; then
# if [[ -n "${CFD_CODE_SIGN_PASS:-}" ]]; then
# # write private key to disk and then import it keychain
# echo -n -e ${CFD_CODE_SIGN_KEY} | base64 -D > ${CODE_SIGN_PRIV}
# security import ${CODE_SIGN_PRIV} -A -P "${CFD_CODE_SIGN_PASS}"
# rm ${CODE_SIGN_PRIV}
# fi
# fi
# # Add code signing certificate to the key chain
# if [[ -n "${CFD_CODE_SIGN_CERT:-}" ]]; then
# # write certificate to disk and then import it keychain
# echo -n -e ${CFD_CODE_SIGN_CERT} | base64 -D > ${CODE_SIGN_CERT}
# security import ${CODE_SIGN_CERT}
# rm ${CODE_SIGN_CERT}
# fi
# # Add package signing private key to the key chain
# if [[ -n "${CFD_INSTALLER_KEY:-}" ]]; then
# if [[ -n "${CFD_INSTALLER_PASS:-}" ]]; then
# # write private key to disk and then import it into the keychain
# echo -n -e ${CFD_INSTALLER_KEY} | base64 -D > ${INSTALLER_PRIV}
# security import ${INSTALLER_PRIV} -A -P "${CFD_INSTALLER_PASS}"
# rm ${INSTALLER_PRIV}
# fi
# fi
# # Add package signing certificate to the key chain
# if [[ -n "${CFD_INSTALLER_CERT:-}" ]]; then
# # write certificate to disk and then import it keychain
# echo -n -e ${CFD_INSTALLER_CERT} | base64 -D > ${INSTALLER_CERT}
# security import ${INSTALLER_CERT}
# rm ${INSTALLER_CERT}
# fi
# # get the code signing certificate name
# if [[ -n "${CFD_CODE_SIGN_NAME:-}" ]]; then
# CODE_SIGN_NAME="${CFD_CODE_SIGN_NAME}"
# else
# if [[ -n "$(security find-identity -v | cut -d'"' -f 2 -s | grep "Developer ID Application:")" ]]; then
# CODE_SIGN_NAME=$(security find-identity -v | cut -d'"' -f 2 -s | grep "Developer ID Application:")
# else
# CODE_SIGN_NAME=""
# fi
# fi
# # get the package signing certificate name
# if [[ -n "${CFD_INSTALLER_NAME:-}" ]]; then
# PKG_SIGN_NAME="${CFD_INSTALLER_NAME}"
# else
# if [[ -n "$(security find-identity -v | cut -d'"' -f 2 -s | grep "Developer ID Installer:")" ]]; then
# PKG_SIGN_NAME=$(security find-identity -v | cut -d'"' -f 2 -s | grep "Developer ID Installer:")
# else
# PKG_SIGN_NAME=""
# fi
# fi
# # sign the cloudflared binary
# if [[ -n "${CODE_SIGN_NAME:-}" ]]; then
# codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
# fi
# creating build directory
mkdir "${TARGET_DIRECTORY}"
mkdir "${TARGET_DIRECTORY}/contents"
cp -r ".mac_resources/scripts" "${TARGET_DIRECTORY}/scripts"
# copy cloudflared into the build directory
cp ${BINARY_NAME} "${TARGET_DIRECTORY}/contents/${PRODUCT}"
# compress cloudflared into a tar and gzipped file
tar czf "$FILENAME" "${BINARY_NAME}"
# build the installer package
if [[ -n "${PKG_SIGN_NAME:-}" ]]; then
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
--sign "${PKG_SIGN_NAME}" \
${PKGNAME}
else
pkgbuild --identifier com.cloudflare.${PRODUCT} \
--version ${VERSION} \
--scripts ${TARGET_DIRECTORY}/scripts \
--root ${TARGET_DIRECTORY}/contents \
--install-location /usr/local/bin \
${PKGNAME}
fi
# cleaning up the build directory
rm -rf $TARGET_DIRECTORY
+2 -5
View File
@@ -43,7 +43,7 @@ git checkout -f master
git reset --hard origin/master
# modify cloudflared.rb
URL="https://developers.cloudflare.com/argo-tunnel/dl/cloudflared-$VERSION-darwin-amd64.tgz"
URL="https://packages.argotunnel.com/dl/cloudflared-$VERSION-darwin-amd64.tgz"
tee cloudflared.rb <<EOF
class Cloudflared < Formula
desc 'Argo Tunnel'
@@ -64,7 +64,4 @@ git config user.name "cloudflare-warp-bot"
git config user.email "warp-bot@cloudflare.com"
git commit -m "Release Argo Tunnel $VERSION"
#git push -v origin master
# don't push to master, push to some testing branch
git checkout -b deploy-test
git push -v origin deploy-test
git push -v origin master
+74 -7
View File
@@ -1,10 +1,14 @@
VERSION := $(shell git describe --tags --always --dirty="-dev")
VERSION := $(shell git describe --tags --always --dirty="-dev" --match "[0-9][0-9][0-9][0-9].*.*")
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'
MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut -c2-)
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
#e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI.
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)" \
@@ -17,7 +21,11 @@ ifeq ($(EQUINOX_IS_DRAFT), true)
endif
LOCAL_ARCH ?= $(shell uname -m)
ifeq ($(LOCAL_ARCH),x86_64)
ifneq ($(GOARCH),)
TARGET_ARCH ?= $(GOARCH)
else ifeq ($(LOCAL_ARCH),x86_64)
TARGET_ARCH ?= amd64
else ifeq ($(LOCAL_ARCH),i686)
TARGET_ARCH ?= amd64
else ifeq ($(shell echo $(LOCAL_ARCH) | head -c 5),armv8)
TARGET_ARCH ?= arm64
@@ -40,6 +48,18 @@ else
$(error This system's OS $(LOCAL_OS) isn't supported)
endif
ifeq ($(TARGET_OS), windows)
EXECUTABLE_PATH=./cloudflared.exe
else
EXECUTABLE_PATH=./cloudflared
endif
ifeq ($(FLAVOR), centos-7)
TARGET_PUBLIC_REPO ?= el7
else
TARGET_PUBLIC_REPO ?= $(FLAVOR)
endif
.PHONY: all
all: cloudflared test
@@ -63,12 +83,42 @@ test: vet
test-ssh-server:
docker-compose -f ssh_server_tests/docker-compose.yml up
.PHONY: cloudflared-deb
cloudflared-deb: cloudflared
define publish_package
for HOST in $(CF_PKG_HOSTS); do \
ssh-keyscan -t rsa $$HOST >> ~/.ssh/known_hosts; \
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,deb,apt)
.PHONY: publish-rpm
publish-rpm: cloudflared-rpm
$(call publish_package,rpm,yum)
define build_package
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/
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
$(call build_package,deb)
.PHONY: cloudflared-rpm
cloudflared-rpm: cloudflared
$(call build_package,rpm)
.PHONY: cloudflared-darwin-amd64.tgz
cloudflared-darwin-amd64.tgz: cloudflared
@@ -88,6 +138,19 @@ homebrew-release: homebrew-upload
release: bin/equinox
bin/equinox release $(EQUINOX_FLAGS) -- $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/cloudflared
.PHONY: github-release
github-release: cloudflared
python3 github_release.py --path $(EXECUTABLE_PATH) --release-version $(VERSION)
.PHONY: github-message
github-message:
python3 github_message.py --release-version $(VERSION)
.PHONY: github-mac-upload
github-mac-upload:
python3 github_release.py --path .artifacts/cloudflared-darwin-amd64.tgz --release-version $(VERSION) --name cloudflared-darwin-amd64.tgz
python3 github_release.py --path .artifacts/cloudflared-amd64.pkg --release-version $(VERSION) --name cloudflared-amd64.pkg
bin/equinox:
mkdir -p bin
curl -s https://bin.equinox.io/c/75JtLRTsJ3n/release-tool-beta-$(EQUINOX_PLATFORM).tgz | tar xz -C bin/
@@ -105,3 +168,7 @@ vet:
go vet -mod=vendor ./...
which go-sumtype # go get github.com/BurntSushi/go-sumtype
go-sumtype $$(go list -mod=vendor ./...)
.PHONY: msi
msi: cloudflared
go-msi make --msi cloudflared.msi --version $(MSI_VERSION)
+76
View File
@@ -1,3 +1,79 @@
2020.8.2
- 2020-08-20 AUTH-3021 fixed the git version call by using the older flag
- 2020-08-18 TUN-3268: Each connection has its own event digest to reconnect
2020.8.1
- 2020-08-14 AUTH-2975 don't check /etc on windows
- 2020-08-14 AUTH-2977 log file protection
- 2020-08-18 TUN-3286: Use either ID or name in Named Tunnel subcommands.
- 2020-08-18 AUTH-2712 fixed the mac build script
- 2020-08-19 AUTH-2653 disabling signing until we can get the keys
- 2020-08-05 TUN-3233: List tunnels support filtering by deleted, name, existed at and id
- 2020-08-05 TUN-3237: By default, don't show connections that are pending reconnect
- 2020-08-06 TUN-3242: Build with go 1.14
- 2020-08-07 TUN-3243: Refactor tunnel subcommands to allow commands to compose better
- 2020-08-07 AUTH-2864 - add macos build to github release
- 2020-07-31 AUTH-2857 update homebrew script to use new url
- 2020-07-30 TUN-3213: Create, route and run named tunnels in one command
- 2020-07-07 AUTH-2712 added MSI build for a windows agent
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
- 2020-06-30 AUTH-2854: Create cloudflared RPMs
- 2020-06-26 AUTH-2850 log config file path
2020.7.0
- 2020-06-30 TUN-3140: Add timestamps to terminal log entries
- 2020-06-30 AUTH-2860: Fix builds
- 2020-06-25 TUN-3007: Implement named tunnel connection registration and unregistration.
2020.6.6
- 2020-06-23 AUTH-2685: Adds script to create release
- 2020-06-25 AUTH-2652: Update cloudflare repo
- 2020-06-26 AUTH-2718: Add target for publishing deb to pkg.cloudflare repo
- 2020-06-26 AUTH-2849 all log output to stderr
- 2020-06-17 TUN-3106: Pass NamedTunnel config to StartServer
- 2020-06-18 TUN-3107: UnregisterConnection shouldn't wrap nil error as RPC error
- 2020-06-17 AUTH-2652: Adds .docker-images to push images to docker hub
- 2020-06-18 AUTH-2712 mac package build script and better config file handling when started as a service
2020.6.5
- 2020-06-16 DEVTOOLS-7321: Don't skip macOS builds based on tag
- 2020-06-16 fix for a flaky test
+145 -1
View File
@@ -1,4 +1,4 @@
pinned_go: &pinned_go go=1.12.9-1
pinned_go: &pinned_go go=1.14.7-1
build_dir: &build_dir /cfsetup_build
default-flavor: stretch
stretch: &stretch
@@ -22,6 +22,29 @@ 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:
- *pinned_go
- build-essential
- fakeroot
- rubygem-fpm
- openssh-client
post-cache:
- export GOOS=linux
- export GOARCH=amd64
- make publish-deb
release-linux-amd64:
build_dir: *build_dir
builddeps:
@@ -31,6 +54,18 @@ stretch: &stretch
- export GOOS=linux
- export GOARCH=amd64
- make release
github-release-linux-amd64:
build_dir: *build_dir
builddeps:
- *pinned_go
- build-essential
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- export GOOS=linux
- export GOARCH=amd64
- make github-release
release-linux-armv6:
build_dir: *build_dir
builddeps:
@@ -42,6 +77,20 @@ stretch: &stretch
- export GOARCH=arm
- export CC=arm-linux-gnueabihf-gcc
- make release
github-release-linux-armv6:
build_dir: *build_dir
builddeps:
- *pinned_go
- crossbuild-essential-armhf
- gcc-arm-linux-gnueabihf
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- export GOOS=linux
- export GOARCH=arm
- export CC=arm-linux-gnueabihf-gcc
- make github-release
release-linux-386:
build_dir: *build_dir
builddeps:
@@ -51,6 +100,18 @@ stretch: &stretch
- export GOOS=linux
- export GOARCH=386
- make release
github-release-linux-386:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-multilib
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- export GOOS=linux
- export GOARCH=386
- make github-release
release-windows-amd64:
build_dir: *build_dir
builddeps:
@@ -61,6 +122,19 @@ stretch: &stretch
- export GOARCH=amd64
- export CC=x86_64-w64-mingw32-gcc
- make release
github-release-windows-amd64:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-mingw-w64
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- export GOOS=windows
- export GOARCH=amd64
- export CC=x86_64-w64-mingw32-gcc
- make github-release
release-windows-386:
build_dir: *build_dir
builddeps:
@@ -71,6 +145,42 @@ stretch: &stretch
- export GOARCH=386
- export CC=i686-w64-mingw32-gcc-win32
- make release
github-release-windows-386:
build_dir: *build_dir
builddeps:
- *pinned_go
- gcc-mingw-w64
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- export GOOS=windows
- export GOARCH=386
- export CC=i686-w64-mingw32-gcc-win32
- make github-release
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
github-release-macos-amd64:
build_dir: *build_dir
builddeps:
- *pinned_go
- python3-setuptools
- python3-pip
post-cache:
- pip3 install pygithub
- make github-mac-upload
test:
build_dir: *build_dir
builddeps:
@@ -89,11 +199,45 @@ 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
buster: *stretch
centos-7:
publish-rpm:
build_dir: *build_dir
builddeps: &el7_builddeps
- https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
post-cache:
- sudo yum install -y fakeroot
- wget https://golang.org/dl/go1.14.7.linux-amd64.tar.gz -P /tmp/
- sudo tar -C /usr/local -xzf /tmp/go1.14.7.linux-amd64.tar.gz
- export PATH=$PATH:/usr/local/go/bin
- 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.14.7.linux-amd64.tar.gz -P /tmp/
- sudo tar -C /usr/local -xzf /tmp/go1.14.7.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
test_dbconnect:
+6
View File
@@ -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.
+2 -2
View File
@@ -12,7 +12,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/validation"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
)
// StartForwarder starts a client side websocket forward
@@ -106,7 +106,7 @@ func ssh(c *cli.Context) error {
wsConn := carrier.NewWSConnection(logger, false)
if c.NArg() > 0 || c.IsSet(sshURLFlag) {
localForwarder, err := config.ValidateUrl(c)
localForwarder, err := config.ValidateUrl(c, true)
if err != nil {
logger.Errorf("Error validating origin URL: %s", err)
return errors.Wrap(err, "error validating origin URL")
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"golang.org/x/net/idna"
"github.com/getsentry/raven-go"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
const (
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
type usageError string
+96 -12
View File
@@ -4,25 +4,76 @@ import (
"errors"
"os"
"path/filepath"
"runtime"
homedir "github.com/mitchellh/go-homedir"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/validation"
homedir "github.com/mitchellh/go-homedir"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"gopkg.in/yaml.v2"
)
var (
// File names from which we attempt to read configuration.
// DefaultConfigFiles is the file names from which we attempt to read configuration.
DefaultConfigFiles = []string{"config.yml", "config.yaml"}
// DefaultUnixConfigLocation is the primary location to find a config file
DefaultUnixConfigLocation = "/usr/local/etc/cloudflared"
// DefaultUnixLogLocation is the primary location to find log files
DefaultUnixLogLocation = "/var/log/cloudflared"
// Launchd doesn't set root env variables, so there is default
// Windows default config dir was ~/cloudflare-warp in documentation; let's keep it compatible
DefaultConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp", "/usr/local/etc/cloudflared", "/etc/cloudflared"}
defaultUserConfigDirs = []string{"~/.cloudflared", "~/.cloudflare-warp", "~/cloudflare-warp"}
defaultNixConfigDirs = []string{"/etc/cloudflared", DefaultUnixConfigLocation}
)
const DefaultCredentialFile = "cert.pem"
// DefaultConfigDirectory returns the default directory of the config file
func DefaultConfigDirectory() string {
if runtime.GOOS == "windows" {
path := os.Getenv("CFDPATH")
if path == "" {
path = filepath.Join(os.Getenv("ProgramFiles(x86)"), "cloudflared")
if _, err := os.Stat(path); os.IsNotExist(err) { //doesn't exist, so return an empty failure string
return ""
}
}
return path
}
return DefaultUnixConfigLocation
}
// DefaultLogDirectory returns the default directory for log files
func DefaultLogDirectory() string {
if runtime.GOOS == "windows" {
return DefaultConfigDirectory()
}
return DefaultUnixLogLocation
}
// DefaultConfigPath returns the default location of a config file
func DefaultConfigPath() string {
dir := DefaultConfigDirectory()
if dir == "" {
return DefaultConfigFiles[0]
}
return filepath.Join(dir, DefaultConfigFiles[0])
}
// DefaultConfigSearchDirectories returns the default folder locations of the config
func DefaultConfigSearchDirectories() []string {
dirs := make([]string, len(defaultUserConfigDirs))
copy(dirs, defaultUserConfigDirs)
if runtime.GOOS != "windows" {
dirs = append(dirs, defaultNixConfigDirs...)
}
return dirs
}
// FileExists checks to see if a file exist at the provided path.
func FileExists(path string) (bool, error) {
f, err := os.Open(path)
@@ -46,10 +97,10 @@ func FindInputSourceContext(context *cli.Context) (altsrc.InputSourceContext, er
}
// FindDefaultConfigPath returns the first path that contains a config file.
// If none of the combination of DefaultConfigDirs and DefaultConfigFiles
// If none of the combination of DefaultConfigSearchDirectories() and DefaultConfigFiles
// contains a config file, return empty string.
func FindDefaultConfigPath() string {
for _, configDir := range DefaultConfigDirs {
for _, configDir := range DefaultConfigSearchDirectories() {
for _, configFile := range DefaultConfigFiles {
dirPath, err := homedir.Expand(configDir)
if err != nil {
@@ -64,10 +115,43 @@ func FindDefaultConfigPath() string {
return ""
}
// FindOrCreateConfigPath returns the first path that contains a config file
// or creates one in the primary default path if it doesn't exist
func FindOrCreateConfigPath() string {
path := FindDefaultConfigPath()
if path == "" {
// create the default directory if it doesn't exist
path = DefaultConfigPath()
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return ""
}
// write a new config file out
file, err := os.Create(path)
if err != nil {
return ""
}
defer file.Close()
logDir := DefaultLogDirectory()
os.MkdirAll(logDir, os.ModePerm) //try and create it. Doesn't matter if it succeed or not, only byproduct will be no logs
c := Root{
LogDirectory: logDir,
}
if err := yaml.NewEncoder(file).Encode(&c); err != nil {
return ""
}
}
return path
}
// FindLogSettings gets the log directory and level from the config file
func FindLogSettings() (string, string) {
configPath := FindDefaultConfigPath()
defaultDirectory := filepath.Dir(configPath)
configPath := FindOrCreateConfigPath()
defaultDirectory := DefaultLogDirectory()
defaultLevel := "info"
file, err := os.Open(configPath)
@@ -104,9 +188,9 @@ func ValidateUnixSocket(c *cli.Context) (string, error) {
// ValidateUrl will validate url flag correctness. It can be either from --url or argument
// Notice ValidateUnixSocket, it will enforce --unix-socket is not used with --url or argument
func ValidateUrl(c *cli.Context) (string, error) {
func ValidateUrl(c *cli.Context, allowFromArgs bool) (string, error) {
var url = c.String("url")
if c.NArg() > 0 {
if allowFromArgs && c.NArg() > 0 {
if c.IsSet("url") {
return "", errors.New("Specified origin urls using both --url and argument. Decide which one you want, I can only support one.")
}
-3
View File
@@ -49,9 +49,6 @@ func TestConfigChanged(t *testing.T) {
os.Remove(filePath)
}()
c := &Root{
OrgKey: "abcd",
ConfigType: "mytype",
CheckinInterval: 1,
Forwarders: []Forwarder{
{
URL: "test.daltoniam.com",
+5 -8
View File
@@ -34,14 +34,11 @@ type DNSResolver struct {
// Root is the base options to configure the service
type Root struct {
OrgKey string `json:"org_key" yaml:"orgKey"`
ConfigType string `json:"type"`
LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"`
LogLevel string `json:"log_level" yaml:"logLevel"`
CheckinInterval int `json:"checkin_interval" yaml:"checkinInterval"`
Forwarders []Forwarder `json:"forwarders,omitempty"`
Tunnels []Tunnel `json:"tunnels,omitempty"`
Resolver DNSResolver `json:"resolver,omitempty"`
LogDirectory string `json:"log_directory" yaml:"logDirectory,omitempty"`
LogLevel string `json:"log_level" yaml:"logLevel,omitempty"`
Forwarders []Forwarder `json:"forwarders,omitempty" yaml:"forwarders,omitempty"`
Tunnels []Tunnel `json:"tunnels,omitempty" yaml:"tunnels,omitempty"`
Resolver DNSResolver `json:"resolver,omitempty" yaml:"resolver,omitempty"`
}
// Hash returns the computed values to see if the forwarder values change
+1 -1
View File
@@ -5,7 +5,7 @@ package main
import (
"os"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
)
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
+4 -3
View File
@@ -7,10 +7,11 @@ 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"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
)
func runApp(app *cli.App, shutdownC, graceShutdownC chan struct{}) {
@@ -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),
},
},
})
+4 -3
View File
@@ -6,8 +6,9 @@ import (
"fmt"
"os"
"gopkg.in/urfave/cli.v2"
"github.com/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),
},
},
})
+5 -4
View File
@@ -17,7 +17,7 @@ import (
raven "github.com/getsentry/raven-go"
homedir "github.com/mitchellh/go-homedir"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
"github.com/pkg/errors"
)
@@ -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.
@@ -129,7 +129,7 @@ func action(version string, shutdownC, graceShutdownC chan struct{}) cli.ActionF
tags := make(map[string]string)
tags["hostname"] = c.String("hostname")
raven.SetTagsContext(tags)
raven.CapturePanic(func() { err = tunnel.StartServer(c, version, shutdownC, graceShutdownC) }, nil)
raven.CapturePanic(func() { err = tunnel.TunnelCommand(c) }, nil)
exitCode := 0
if err != nil {
handleError(err)
@@ -182,12 +182,13 @@ func handleServiceMode(shutdownC chan struct{}) error {
return err
}
configPath := config.FindDefaultConfigPath()
configPath := config.FindOrCreateConfigPath()
configManager, err := config.NewFileManager(f, configPath, logger)
if err != nil {
logger.Errorf("Cannot setup config file for monitoring: %s", err)
return err
}
logger.Infof("monitoring config file at: %s", configPath)
serviceCallback := func(t string, name string, err error) {
if err != nil {
+1 -1
View File
@@ -13,7 +13,7 @@ import (
// GenerateFilePathFromURL will return a filepath for given access application url
func GenerateFilePathFromURL(url *url.URL, suffix string) (string, error) {
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
if err != nil {
return "", err
}
+9 -9
View File
@@ -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
}
+8 -8
View File
@@ -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
}
+82 -25
View File
@@ -33,6 +33,7 @@ import (
"github.com/cloudflare/cloudflared/sshserver"
"github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/cloudflare/cloudflared/websocket"
"github.com/coreos/go-systemd/daemon"
@@ -42,8 +43,8 @@ import (
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
const (
@@ -169,14 +170,17 @@ func Commands() []*cli.Command {
c.Hidden = false
subcommands = append(subcommands, &c)
}
subcommands = append(subcommands, buildCreateCommand())
subcommands = append(subcommands, buildListCommand())
subcommands = append(subcommands, buildDeleteCommand())
subcommands = append(subcommands, buildRunCommand())
subcommands = append(subcommands, buildCleanupCommand())
subcommands = append(subcommands, buildRouteCommand())
cmds = append(cmds, &cli.Command{
Name: "tunnel",
Action: cliutil.ErrorHandler(tunnel),
Action: cliutil.ErrorHandler(TunnelCommand),
Before: Before,
Category: "Tunnel",
Usage: "Make a locally-running web service accessible over the internet using Argo Tunnel.",
@@ -206,14 +210,63 @@ func Commands() []*cli.Command {
return cmds
}
func tunnel(c *cli.Context) error {
return StartServer(c, version, shutdownC, graceShutdownC)
func TunnelCommand(c *cli.Context) error {
if name := c.String("name"); name != "" {
return adhocNamedTunnel(c, name)
}
logger, err := createLogger(c, false)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
return StartServer(c, version, shutdownC, graceShutdownC, nil, logger)
}
func Init(v string, s, g chan struct{}) {
version, shutdownC, graceShutdownC = v, s, g
}
// adhocNamedTunnel create, route and run a named tunnel in one command
func adhocNamedTunnel(c *cli.Context, name string) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
tunnel, ok, err := sc.tunnelActive(name)
if err != nil || !ok {
tunnel, err = sc.create(name)
if err != nil {
return errors.Wrap(err, "failed to create tunnel")
}
} else {
sc.logger.Infof("Tunnel already created with ID %s", tunnel.ID)
}
if r, ok := routeFromFlag(c, tunnel.ID); ok {
if err := sc.route(tunnel.ID, r); err != nil {
sc.logger.Errorf("failed to create route, please create it manually. err: %v.", err)
} else {
sc.logger.Infof(r.SuccessSummary())
}
}
if err := sc.run(tunnel.ID); err != nil {
return errors.Wrap(err, "error running tunnel")
}
return nil
}
func routeFromFlag(c *cli.Context, tunnelID uuid.UUID) (tunnelstore.Route, bool) {
if hostname := c.String("hostname"); hostname != "" {
if lbPool := c.String("lb-pool"); lbPool != "" {
return tunnelstore.NewLBRoute(hostname, lbPool), true
}
return tunnelstore.NewDNSRoute(hostname), true
}
return nil, false
}
func createLogger(c *cli.Context, isTransport bool) (logger.Service, error) {
loggerOpts := []logger.Option{}
@@ -238,12 +291,7 @@ func createLogger(c *cli.Context, isTransport bool) (logger.Service, error) {
return logger.New(loggerOpts...)
}
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}) error {
logger, err := createLogger(c, false)
if err != nil {
return cliutil.PrintLoggerSetupError("error setting up logger", err)
}
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}, namedTunnel *origin.NamedTunnelConfig, logger logger.Service) error {
_ = raven.SetDSN(sentryDSN)
var wg sync.WaitGroup
listeners := gracenet.Net{}
@@ -252,7 +300,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
dnsReadySignal := make(chan struct{})
if c.String("config") == "" {
logger.Infof("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
logger.Infof("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigSearchDirectories())
}
if c.IsSet("trace-output") {
@@ -359,7 +407,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
defer wg.Done()
hello.StartHelloWorldServer(logger, helloListener, shutdownC)
}()
c.Set("url", "https://"+helloListener.Addr().String())
forceSetFlag(c, "url", "https://"+helloListener.Addr().String())
}
if c.IsSet(sshServerFlag) {
@@ -409,7 +457,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
close(shutdownC)
}
}()
c.Set("url", "ssh://"+localServerAddress)
forceSetFlag(c, "url", "ssh://"+localServerAddress)
}
url := c.String("url")
@@ -453,7 +501,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
}
errC <- websocket.StartProxyServer(logger, listener, staticHost, shutdownC, streamHandler)
}()
c.Set("url", "http://"+listener.Addr().String())
forceSetFlag(c, "url", "http://"+listener.Addr().String())
}
transportLogger, err := createLogger(c, true)
@@ -461,7 +509,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
return errors.Wrap(err, "error setting up transport logger")
}
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, logger, transportLogger)
tunnelConfig, err := prepareTunnelConfig(c, buildInfo, version, logger, transportLogger, namedTunnel)
if err != nil {
return err
}
@@ -481,6 +529,15 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"), logger)
}
// forceSetFlag attempts to set the given flag value in the closest context that has it defined
func forceSetFlag(c *cli.Context, name, value string) {
for _, ctx := range c.Lineage() {
if err := ctx.Set(name, value); err == nil {
break
}
}
}
func Before(c *cli.Context) error {
logger, err := createLogger(c, false)
if err != nil {
@@ -488,7 +545,7 @@ func Before(c *cli.Context) error {
}
if c.String("config") == "" {
logger.Debugf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigDirs)
logger.Debugf("Cannot determine default configuration path. No file %v in %v", config.DefaultConfigFiles, config.DefaultConfigSearchDirectories())
}
inputSource, err := config.FindInputSourceContext(c)
if err != nil {
@@ -603,7 +660,7 @@ func dbConnectCmd() *cli.Command {
cmd.Action = cliutil.ErrorHandler(func(c *cli.Context) error {
err := dbconnect.CmdAction(c)
if err == nil {
err = tunnel(c)
err = TunnelCommand(c)
}
return err
})
@@ -969,13 +1026,6 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_USE_RECONNECT_TOKEN"},
Hidden: true,
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "use-quick-reconnects",
Usage: "Test reestablishing connections with the new 'connection digest' flow.",
Value: true,
EnvVars: []string{"TUNNEL_USE_QUICK_RECONNECTS"},
Hidden: true,
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: "dial-edge-timeout",
Usage: "Maximum wait time to set up a connection with the edge",
@@ -1065,6 +1115,13 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
EnvVars: []string{"TUNNEL_BASTION"},
Hidden: shouldHide,
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
EnvVars: []string{"TUNNEL_NAME"},
Usage: "Stable name to identify the tunnel. Using this flag will create, route and run a tunnel. For production usage, execute each command separately",
Hidden: true,
}),
}
}
+53 -36
View File
@@ -23,8 +23,8 @@ import (
"github.com/google/uuid"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/ssh/terminal"
"gopkg.in/urfave/cli.v2"
)
var (
@@ -34,10 +34,10 @@ var (
argumentsUrl = developerPortal + "/reference/arguments/"
)
// returns the first path that contains a cert.pem file. If none of the DefaultConfigDirs
// returns the first path that contains a cert.pem file. If none of the DefaultConfigSearchDirectories
// contains a cert.pem file, return empty string
func findDefaultOriginCertPath() string {
for _, defaultConfigDir := range config.DefaultConfigDirs {
for _, defaultConfigDir := range config.DefaultConfigSearchDirectories() {
originCertPath, _ := homedir.Expand(filepath.Join(defaultConfigDir, config.DefaultCredentialFile))
if ok, _ := config.FileExists(originCertPath); ok {
return originCertPath
@@ -95,7 +95,7 @@ func dnsProxyStandAlone(c *cli.Context) bool {
func findOriginCert(c *cli.Context, logger logger.Service) (string, error) {
originCertPath := c.String("origincert")
if originCertPath == "" {
logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigDirs)
logger.Infof("Cannot determine default origin certificate path. No file %s in %v", config.DefaultCredentialFile, config.DefaultConfigSearchDirectories())
if isRunningFromTerminal() {
logger.Errorf("You need to specify the origin certificate path with --origincert option, or set TUNNEL_ORIGIN_CERT environment variable. See %s for more information.", argumentsUrl)
return "", fmt.Errorf("Client didn't specify origincert path when running from terminal")
@@ -158,7 +158,10 @@ func prepareTunnelConfig(
version string,
logger logger.Service,
transportLogger logger.Service,
namedTunnel *origin.NamedTunnelConfig,
) (*origin.TunnelConfig, error) {
compatibilityMode := namedTunnel == nil
hostname, err := validation.ValidateHostname(c.String("hostname"))
if err != nil {
logger.Errorf("Invalid hostname: %s", err)
@@ -181,7 +184,7 @@ func prepareTunnelConfig(
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
originURL, err := config.ValidateUrl(c)
originURL, err := config.ValidateUrl(c, compatibilityMode)
if err != nil {
logger.Errorf("Error validating origin URL: %s", err)
return nil, errors.Wrap(err, "Error validating origin URL")
@@ -254,38 +257,52 @@ func prepareTunnelConfig(
return nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
}
if namedTunnel != nil {
clientUUID, err := uuid.NewRandom()
if err != nil {
return nil, errors.Wrap(err, "can't generate clientUUID")
}
namedTunnel.Client = tunnelpogs.ClientInfo{
ClientID: clientUUID[:],
Features: []string{origin.FeatureSerializedHeaders},
Version: version,
Arch: fmt.Sprintf("%s_%s", buildInfo.GoOS, buildInfo.GoArch),
}
}
return &origin.TunnelConfig{
BuildInfo: buildInfo,
ClientID: clientID,
ClientTlsConfig: httpTransport.TLSClientConfig,
CompressionQuality: c.Uint64("compression-quality"),
EdgeAddrs: c.StringSlice("edge"),
GracePeriod: c.Duration("grace-period"),
HAConnections: c.Int("ha-connections"),
HTTPTransport: httpTransport,
HeartbeatInterval: c.Duration("heartbeat-interval"),
Hostname: hostname,
HTTPHostHeader: c.String("http-host-header"),
IncidentLookup: origin.NewIncidentLookup(),
IsAutoupdated: c.Bool("is-autoupdated"),
IsFreeTunnel: isFreeTunnel,
LBPool: c.String("lb-pool"),
Logger: logger,
TransportLogger: transportLogger,
MaxHeartbeats: c.Uint64("heartbeat-count"),
Metrics: tunnelMetrics,
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
OriginCert: originCert,
OriginUrl: originURL,
ReportedVersion: version,
Retries: c.Uint("retries"),
RunFromTerminal: isRunningFromTerminal(),
Tags: tags,
TlsConfig: toEdgeTLSConfig,
UseDeclarativeTunnel: c.Bool("use-declarative-tunnels"),
UseReconnectToken: c.Bool("use-reconnect-token"),
UseQuickReconnects: c.Bool("use-quick-reconnects"),
BuildInfo: buildInfo,
ClientID: clientID,
ClientTlsConfig: httpTransport.TLSClientConfig,
CompressionQuality: c.Uint64("compression-quality"),
EdgeAddrs: c.StringSlice("edge"),
GracePeriod: c.Duration("grace-period"),
HAConnections: c.Int("ha-connections"),
HTTPTransport: httpTransport,
HeartbeatInterval: c.Duration("heartbeat-interval"),
Hostname: hostname,
HTTPHostHeader: c.String("http-host-header"),
IncidentLookup: origin.NewIncidentLookup(),
IsAutoupdated: c.Bool("is-autoupdated"),
IsFreeTunnel: isFreeTunnel,
LBPool: c.String("lb-pool"),
Logger: logger,
TransportLogger: transportLogger,
MaxHeartbeats: c.Uint64("heartbeat-count"),
Metrics: tunnelMetrics,
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
NoChunkedEncoding: c.Bool("no-chunked-encoding"),
OriginCert: originCert,
OriginUrl: originURL,
ReportedVersion: version,
Retries: c.Uint("retries"),
RunFromTerminal: isRunningFromTerminal(),
Tags: tags,
TlsConfig: toEdgeTLSConfig,
NamedTunnel: namedTunnel,
ReplaceExisting: c.Bool("force"),
// turn off use of reconnect token and auth refresh when using named tunnels
UseReconnectToken: compatibilityMode && c.Bool("use-reconnect-token"),
}, nil
}
+3 -3
View File
@@ -12,7 +12,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
)
const (
@@ -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
@@ -51,7 +51,7 @@ func login(c *cli.Context) error {
}
func checkForExistingCert() (string, bool, error) {
configPath, err := homedir.Expand(config.DefaultConfigDirs[0])
configPath, err := homedir.Expand(config.DefaultConfigSearchDirectories()[0])
if err != nil {
return "", false, err
}
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunneldns"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
"github.com/pkg/errors"
)
@@ -0,0 +1,340 @@
package tunnel
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/cloudflare/cloudflared/certutil"
"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"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to pass between subcommands,
// and make sure they are only initialized once
type subcommandContext struct {
c *cli.Context
logger logger.Service
// These fields should be accessed using their respective Getter
tunnelstoreClient tunnelstore.Client
userCredential *userCredential
}
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
logger, err := createLogger(c, false)
if err != nil {
return nil, errors.Wrap(err, "error setting up logger")
}
return &subcommandContext{
c: c,
logger: logger,
}, nil
}
type userCredential struct {
cert *certutil.OriginCert
certPath string
}
func (sc *subcommandContext) client() (tunnelstore.Client, error) {
if sc.tunnelstoreClient != nil {
return sc.tunnelstoreClient, nil
}
credential, err := sc.credential()
if err != nil {
return nil, err
}
client, err := tunnelstore.NewRESTClient(sc.c.String("api-url"), credential.cert.AccountID, credential.cert.ZoneID, credential.cert.ServiceKey, sc.logger)
if err != nil {
return nil, err
}
sc.tunnelstoreClient = client
return client, nil
}
func (sc *subcommandContext) credential() (*userCredential, error) {
if sc.userCredential == nil {
originCertPath, err := findOriginCert(sc.c, sc.logger)
if err != nil {
return nil, errors.Wrap(err, "Error locating origin cert")
}
blocks, err := readOriginCert(originCertPath, sc.logger)
if err != nil {
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
}
cert, err := certutil.DecodeOriginCert(blocks)
if err != nil {
return nil, errors.Wrap(err, "Error decoding origin cert")
}
if cert.AccountID == "" {
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
}
sc.userCredential = &userCredential{
cert: cert,
certPath: originCertPath,
}
}
return sc.userCredential, nil
}
func (sc *subcommandContext) readTunnelCredentials(tunnelID uuid.UUID) (*pogs.TunnelAuth, error) {
filePath, err := sc.tunnelCredentialsPath(tunnelID)
if err != nil {
return nil, err
}
body, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
}
var auth pogs.TunnelAuth
if err = json.Unmarshal(body, &auth); err != nil {
return nil, err
}
return &auth, nil
}
func (sc *subcommandContext) tunnelCredentialsPath(tunnelID uuid.UUID) (string, error) {
if filePath := sc.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(sc.c, sc.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.DefaultConfigSearchDirectories() {
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
if validFilePath(filePath) {
return filePath, nil
}
}
}
return "", fmt.Errorf("Tunnel credentials file not found")
}
func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
client, err := sc.client()
if err != nil {
return nil, errors.Wrap(err, "couldn't create client to talk to Argo Tunnel backend")
}
tunnelSecret, err := generateTunnelSecret()
if err != nil {
return nil, errors.Wrap(err, "couldn't generate the secret for your new tunnel")
}
tunnel, err := client.CreateTunnel(name, tunnelSecret)
if err != nil {
return nil, errors.Wrap(err, "Create Tunnel API call failed")
}
credential, err := sc.credential()
if err != nil {
return nil, err
}
if writeFileErr := writeTunnelCredentials(tunnel.ID, credential.cert.AccountID, credential.certPath, tunnelSecret, sc.logger); err != nil {
var errorLines []string
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil {
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
} else {
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the tunnelfile"))
}
errorMsg := strings.Join(errorLines, "\n")
return nil, errors.New(errorMsg)
}
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
return nil, renderOutput(outputFormat, &tunnel)
}
sc.logger.Infof("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
return tunnel, nil
}
func (sc *subcommandContext) list(filter *tunnelstore.Filter) ([]*tunnelstore.Tunnel, error) {
client, err := sc.client()
if err != nil {
return nil, err
}
return client.ListTunnels(filter)
}
func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
forceFlagSet := sc.c.Bool("force")
client, err := sc.client()
if err != nil {
return err
}
for _, id := range tunnelIDs {
tunnel, err := client.GetTunnel(id)
if err != nil {
return errors.Wrapf(err, "Can't get tunnel information. Please check tunnel id: %s", tunnel.ID)
}
// Check if tunnel DeletedAt field has already been set
if !tunnel.DeletedAt.IsZero() {
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
}
// Check if tunnel has existing connections and if force flag is set, cleanup connections
if len(tunnel.Connections) > 0 {
if !forceFlagSet {
return fmt.Errorf("You can not delete tunnel %s 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.", id)
}
if err := client.CleanupConnections(tunnel.ID); err != nil {
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
}
}
if err := client.DeleteTunnel(tunnel.ID); err != nil {
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
}
tunnelCredentialsPath, err := sc.tunnelCredentialsPath(tunnel.ID)
if err != nil {
sc.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 {
sc.logger.Infof("Cannot delete tunnel credentials, error: %v. Please delete the file manually", err)
}
}
return nil
}
func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
credentials, err := sc.readTunnelCredentials(tunnelID)
if err != nil {
return err
}
return StartServer(sc.c, version, shutdownC, graceShutdownC, &origin.NamedTunnelConfig{Auth: *credentials, ID: tunnelID}, sc.logger)
}
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
client, err := sc.client()
if err != nil {
return err
}
for _, tunnelID := range tunnelIDs {
sc.logger.Infof("Cleanup connection for tunnel %s", tunnelID)
if err := client.CleanupConnections(tunnelID); err != nil {
sc.logger.Errorf("Error cleaning up connections for tunnel %v, error :%v", tunnelID, err)
}
}
return nil
}
func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) error {
client, err := sc.client()
if err != nil {
return err
}
if err := client.RouteTunnel(tunnelID, r); err != nil {
return err
}
return nil
}
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
filter := tunnelstore.NewFilter()
filter.NoDeleted()
filter.ByName(name)
tunnels, err := sc.list(filter)
if err != nil {
return nil, false, err
}
if len(tunnels) == 0 {
return nil, false, nil
}
// There should only be 1 active tunnel for a given name
return tunnels[0], true, nil
}
// findID parses the input. If it's a UUID, return the UUID.
// Otherwise, assume it's a name, and look up the ID of that tunnel.
func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
if u, err := uuid.Parse(input); err == nil {
return u, nil
}
if tunnel, found, err := sc.tunnelActive(input); err != nil {
return uuid.Nil, err
} else if found {
return tunnel.ID, nil
}
return uuid.Nil, fmt.Errorf("%s is neither the ID nor the name of any of your tunnels", input)
}
// findIDs is just like mapping `findID` over a slice, but it only uses
// one Tunnelstore API call.
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
// First, look up all tunnels the user has
filter := tunnelstore.NewFilter()
filter.NoDeleted()
tunnels, err := sc.list(filter)
if err != nil {
return nil, err
}
// Do the pure list-processing in its own function, so that it can be
// unit tested easily.
return findIDs(tunnels, inputs)
}
func findIDs(tunnels []*tunnelstore.Tunnel, inputs []string) ([]uuid.UUID, error) {
// Put them into a dictionary for faster lookups
nameToID := make(map[string]uuid.UUID, len(tunnels))
for _, tunnel := range tunnels {
nameToID[tunnel.Name] = tunnel.ID
}
// For each input, try to find the tunnel ID.
tunnelIDs := make([]uuid.UUID, len(inputs))
var badInputs []string
for i, input := range inputs {
if id, err := uuid.Parse(input); err == nil {
tunnelIDs[i] = id
} else if id, ok := nameToID[input]; ok {
tunnelIDs[i] = id
} else {
badInputs = append(badInputs, input)
}
}
if len(badInputs) > 0 {
msg := "Please specify either the ID or name of a tunnel. The following inputs were neither: %s"
return nil, fmt.Errorf(msg, strings.Join(badInputs, ", "))
}
return tunnelIDs, nil
}
@@ -0,0 +1,82 @@
package tunnel
import (
"reflect"
"testing"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/google/uuid"
)
func Test_findIDs(t *testing.T) {
type args struct {
tunnels []*tunnelstore.Tunnel
inputs []string
}
tests := []struct {
name string
args args
want []uuid.UUID
wantErr bool
}{
{
name: "input not found",
args: args{
inputs: []string{"asdf"},
},
wantErr: true,
},
{
name: "only UUID",
args: args{
inputs: []string{"a8398a0b-876d-48ed-b609-3fcfd67a4950"},
},
want: []uuid.UUID{uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950")},
},
{
name: "only name",
args: args{
tunnels: []*tunnelstore.Tunnel{
{
ID: uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
Name: "tunnel1",
},
},
inputs: []string{"tunnel1"},
},
want: []uuid.UUID{uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950")},
},
{
name: "both UUID and name",
args: args{
tunnels: []*tunnelstore.Tunnel{
{
ID: uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
Name: "tunnel1",
},
{
ID: uuid.MustParse("bf028b68-744f-466e-97f8-c46161d80aa5"),
Name: "tunnel2",
},
},
inputs: []string{"tunnel1", "bf028b68-744f-466e-97f8-c46161d80aa5"},
},
want: []uuid.UUID{
uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
uuid.MustParse("bf028b68-744f-466e-97f8-c46161d80aa5"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := findIDs(tt.args.tunnels, tt.args.inputs)
if (err != nil) != tt.wantErr {
t.Errorf("findIDs() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("findIDs() = %v, want %v", got, tt.want)
}
})
}
}
+266 -163
View File
@@ -9,31 +9,58 @@ 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"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelstore"
)
const (
credFileFlagAlias = "cred-file"
)
var (
showDeletedFlag = &cli.BoolFlag{
Name: "show-deleted",
Aliases: []string{"d"},
Usage: "Include deleted tunnels in the list",
}
listNameFlag = &cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "List tunnels with the given name",
}
listExistedAtFlag = &cli.TimestampFlag{
Name: "when",
Aliases: []string{"w"},
Usage: fmt.Sprintf("List tunnels that are active at the given time, expect format in RFC3339 (%s)", time.Now().Format(tunnelstore.TimeLayout)),
Layout: tunnelstore.TimeLayout,
}
listIDFlag = &cli.StringFlag{
Name: "id",
Aliases: []string{"i"},
Usage: "List tunnel by ID",
}
showRecentlyDisconnected = &cli.BoolFlag{
Name: "show-recently-disconnected",
Aliases: []string{"rd"},
Usage: "Include connections that have recently disconnected in the list",
}
outputFormatFlag = &cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Render output using given `FORMAT`. Valid options are 'json' or 'yaml'",
}
forceFlag = &cli.StringFlag{
forceFlag = &cli.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "By default, if a tunnel is currently being run from a cloudflared, you can't " +
@@ -41,6 +68,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
@@ -48,7 +85,7 @@ const hideSubcommands = true
func buildCreateCommand() *cli.Command {
return &cli.Command{
Name: "create",
Action: cliutil.ErrorHandler(createTunnel),
Action: cliutil.ErrorHandler(createCommand),
Usage: "Create a new tunnel with given name",
ArgsUsage: "TUNNEL-NAME",
Hidden: hideSubcommands,
@@ -63,66 +100,30 @@ func generateTunnelSecret() ([]byte, error) {
return randomBytes, err
}
func createTunnel(c *cli.Context) error {
func createCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel create" requires exactly 1 argument, the name of tunnel to create.`)
}
name := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
tunnelSecret, err := generateTunnelSecret()
if err != nil {
return err
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
cert, err := getOriginCertFromContext(originCertPath, logger)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
tunnel, err := client.CreateTunnel(name, tunnelSecret)
if err != nil {
return errors.Wrap(err, "Error creating a new tunnel")
}
if writeFileErr := writeTunnelCredentials(tunnel.ID, cert.AccountID, originCertPath, tunnelSecret, logger); err != nil {
var errorLines []string
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil {
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
} else {
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the tunnelfile"))
}
errorMsg := strings.Join(errorLines, "\n")
return errors.New(errorMsg)
}
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
return renderOutput(outputFormat, &tunnel)
}
logger.Infof("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
return nil
_, err = sc.create(name)
return errors.Wrap(err, "failed to create tunnel")
}
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
}
@@ -138,85 +139,104 @@ 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 validFilePath(path string) bool {
fileStat, err := os.Stat(path)
if err != nil {
return nil, err
return false
}
body, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
}
auth := pogs.TunnelAuth{}
err = json.Unmarshal(body, &auth)
return &auth, errors.Wrap(err, "couldn't parse tunnel credentials from JSON")
return !fileStat.IsDir()
}
func buildListCommand() *cli.Command {
return &cli.Command{
Name: "list",
Action: cliutil.ErrorHandler(listTunnels),
Action: cliutil.ErrorHandler(listCommand),
Usage: "List existing tunnels",
ArgsUsage: " ",
Hidden: hideSubcommands,
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag},
Flags: []cli.Flag{outputFormatFlag, showDeletedFlag, listNameFlag, listExistedAtFlag, listIDFlag, showRecentlyDisconnected},
}
}
func listTunnels(c *cli.Context) error {
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
cert, err := getOriginCertFromContext(originCertPath, logger)
func listCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
client := newTunnelstoreClient(c, cert, logger)
allTunnels, err := client.ListTunnels()
if err != nil {
return errors.Wrap(err, "Error listing tunnels")
filter := tunnelstore.NewFilter()
if !c.Bool("show-deleted") {
filter.NoDeleted()
}
if name := c.String("name"); name != "" {
filter.ByName(name)
}
if existedAt := c.Timestamp("time"); existedAt != nil {
filter.ByExistedAt(*existedAt)
}
if id := c.String("id"); id != "" {
tunnelID, err := uuid.Parse(id)
if err != nil {
return errors.Wrapf(err, "%s is not a valid tunnel ID", id)
}
filter.ByTunnelID(tunnelID)
}
var tunnels []tunnelstore.Tunnel
if c.Bool("show-deleted") {
tunnels = allTunnels
} else {
for _, tunnel := range allTunnels {
if tunnel.DeletedAt.IsZero() {
tunnels = append(tunnels, tunnel)
}
}
tunnels, err := sc.list(filter)
if err != nil {
return err
}
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, c.Bool("show-recently-disconnected"))
} else {
fmt.Println("You have no tunnels, use 'cloudflared tunnel create' to define a new tunnel")
}
return nil
}
func fmtConnections(connections []tunnelstore.Connection) string {
func fmtAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
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, showRecentlyDisconnected),
)
fmt.Fprintln(writer, formattedStr)
}
// Write data buffered in tabwriter to output
writer.Flush()
}
func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnected bool) string {
// Count connections per colo
numConnsPerColo := make(map[string]uint, len(connections))
for _, connection := range connections {
numConnsPerColo[connection.ColoName]++
if !connection.IsPendingReconnect || showRecentlyDisconnected {
numConnsPerColo[connection.ColoName]++
}
}
// Get sorted list of colos
@@ -237,39 +257,30 @@ func fmtConnections(connections []tunnelstore.Connection) string {
func buildDeleteCommand() *cli.Command {
return &cli.Command{
Name: "delete",
Action: cliutil.ErrorHandler(deleteTunnel),
Usage: "Delete existing tunnel with given ID",
Action: cliutil.ErrorHandler(deleteCommand),
Usage: "Delete existing tunnel with given IDs",
ArgsUsage: "TUNNEL-ID",
Hidden: hideSubcommands,
Flags: []cli.Flag{credentialsFileFlag, forceDeleteFlag},
}
}
func deleteTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel delete" requires exactly 1 argument, the ID of the tunnel to delete.`)
}
id := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
cert, err := getOriginCertFromContext(originCertPath, logger)
func deleteCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
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)
if c.NArg() < 1 {
return cliutil.UsageError(`"cloudflared tunnel delete" requires at least 1 argument, the ID or name of the tunnel to delete.`)
}
return nil
tunnelIDs, err := sc.findIDs(c.Args().Slice())
if err != nil {
return err
}
return sc.delete(tunnelIDs)
}
func renderOutput(format string, v interface{}) error {
@@ -285,59 +296,151 @@ 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)
return client
}
func getOriginCertFromContext(originCertPath string, logger logger.Service) (*certutil.OriginCert, error) {
blocks, err := readOriginCert(originCertPath, logger)
if err != nil {
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
}
cert, err := certutil.DecodeOriginCert(blocks)
if err != nil {
return nil, errors.Wrap(err, "Error decoding origin cert")
}
if cert.AccountID == "" {
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
}
return cert, nil
}
func buildRunCommand() *cli.Command {
return &cli.Command{
Name: "run",
Action: cliutil.ErrorHandler(runTunnel),
Action: cliutil.ErrorHandler(runCommand),
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},
}
}
func runTunnel(c *cli.Context) error {
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID of the tunnel to run.`)
}
id := c.Args().First()
logger, err := logger.New()
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
originCertPath, err := findOriginCert(c, logger)
if err != nil {
return errors.Wrap(err, "Error locating origin cert")
}
credentials, err := readTunnelCredentials(id, originCertPath)
func runCommand(c *cli.Context) error {
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
logger.Debugf("Read credentials for %v", credentials.AccountTag)
panic("TODO: start tunnel supervisor")
if c.NArg() != 1 {
return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID or name of the tunnel to run.`)
}
tunnelID, err := uuid.Parse(c.Args().First())
if err != nil {
return errors.Wrap(err, "error parsing tunnel ID")
}
return sc.run(tunnelID)
}
func buildCleanupCommand() *cli.Command {
return &cli.Command{
Name: "cleanup",
Action: cliutil.ErrorHandler(cleanupCommand),
Usage: "Cleanup connections for the tunnel with given IDs",
ArgsUsage: "TUNNEL-IDS",
Hidden: hideSubcommands,
}
}
func cleanupCommand(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.`)
}
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
tunnelIDs, err := sc.findIDs(c.Args().Slice())
if err != nil {
return err
}
return sc.cleanupConnections(tunnelIDs)
}
func buildRouteCommand() *cli.Command {
return &cli.Command{
Name: "route",
Action: cliutil.ErrorHandler(routeCommand),
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 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) (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 = defaultPoolName(tunnelID)
}
return tunnelstore.NewLBRoute(lbName, lbPool), nil
}
func routeCommand(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 or name of the tunnel`)
}
sc, err := newSubcommandContext(c)
if err != nil {
return err
}
const tunnelIDIndex = 1
tunnelID, err := sc.findID(c.Args().Get(tunnelIDIndex))
if err != nil {
return err
}
routeType := c.Args().First()
var r tunnelstore.Route
switch routeType {
case "dns":
r, err = dnsRouteFromArg(c, tunnelID)
if err != nil {
return err
}
case "lb":
r, err = lbRouteFromArg(c, tunnelID)
if err != nil {
return err
}
default:
return cliutil.UsageError("%s is not a recognized route type. Supported route types are dns and lb", routeType)
}
if err := sc.route(tunnelID, r); err != nil {
return err
}
sc.logger.Infof(r.SuccessSummary())
return nil
}
func defaultPoolName(tunnelID uuid.UUID) string {
return fmt.Sprintf("tunnel:%v", tunnelID)
}
+24 -3
View File
@@ -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"
@@ -37,6 +40,19 @@ func Test_fmtConnections(t *testing.T) {
},
want: "1xDFW",
},
{
name: "with a pending reconnect",
args: args{
connections: []tunnelstore.Connection{
{
ColoName: "DFW",
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
IsPendingReconnect: true,
},
},
},
want: "",
},
{
name: "many colos",
args: args{
@@ -64,7 +80,7 @@ func Test_fmtConnections(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := fmtConnections(tt.args.connections); got != tt.want {
if got := fmtConnections(tt.args.connections, false); got != tt.want {
t.Errorf("fmtConnections() = %v, want %v", got, tt.want)
}
})
@@ -72,8 +88,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)
}
+25 -6
View File
@@ -4,12 +4,14 @@ import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"time"
"github.com/urfave/cli/v2"
"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 (
@@ -34,7 +38,7 @@ EKx0BZogHSor9Wy5VztdFaAaVbsJiCbO
)
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
// https://pkg.go.dev/gopkg.in/urfave/cli.v2?tab=doc#ExitCoder
// https://pkg.go.dev/github.com/urfave/cli/v2?tab=doc#ExitCoder
type statusSuccess struct {
newVersion string
}
@@ -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()))
}
+4 -5
View File
@@ -14,7 +14,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/pkg/errors"
cli "gopkg.in/urfave/cli.v2"
cli "github.com/urfave/cli/v2"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
@@ -150,12 +150,11 @@ func (s *windowsService) Execute(serviceArgs []string, r <-chan svc.ChangeReques
switch c.Cmd {
case svc.Interrogate:
statusChan <- c.CurrentStatus
case svc.Stop:
case svc.Stop, svc.Shutdown:
close(s.graceShutdownC)
statusChan <- svc.Status{State: svc.Stopped, Accepts: cmdsAccepted}
statusChan <- svc.Status{State: svc.StopPending}
case svc.Shutdown:
close(s.shutdownC)
statusChan <- svc.Status{State: svc.StopPending}
return
default:
elog.Error(1, fmt.Sprintf("unexpected control request #%d", c))
}
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"net"
"strconv"
"gopkg.in/urfave/cli.v2"
"gopkg.in/urfave/cli.v2/altsrc"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
// Cmd is the entrypoint command for dbconnect.
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
func TestCmd(t *testing.T) {
+117
View File
@@ -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()
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/python3
"""
Creates Github Releases and uploads assets
"""
import argparse
import logging
import os
import shutil
from github import Github, GithubException, UnknownObjectException
FORMAT = "%(levelname)s - %(asctime)s: %(message)s"
logging.basicConfig(format=FORMAT)
CLOUDFLARED_REPO = os.environ.get("GITHUB_REPO", "cloudflare/cloudflared")
GITHUB_CONFLICT_CODE = "already_exists"
def assert_tag_exists(repo, version):
""" Raise exception if repo does not contain a tag matching version """
tags = repo.get_tags()
if not tags or tags[0].name != version:
raise Exception("Tag {} not found".format(version))
def get_or_create_release(repo, version, dry_run=False):
"""
Get a Github Release matching the version tag or create a new one.
If a conflict occurs on creation, attempt to fetch the Release on last time
"""
try:
release = repo.get_release(version)
logging.info("Release %s found", version)
return release
except UnknownObjectException:
logging.info("Release %s not found", version)
# We dont want to create a new release tag if one doesnt already exist
assert_tag_exists(repo, version)
if dry_run:
logging.info("Skipping Release creation because of dry-run")
return
try:
logging.info("Creating release %s", version)
return repo.create_git_release(version, version, "")
except GithubException as e:
errors = e.data.get("errors", [])
if e.status == 422 and any(
[err.get("code") == GITHUB_CONFLICT_CODE for err in errors]
):
logging.warning(
"Conflict: Release was likely just made by a different build: %s",
e.data,
)
return repo.get_release(version)
raise e
def parse_args():
""" Parse and validate args """
parser = argparse.ArgumentParser(
description="Creates Github Releases and uploads assets."
)
parser.add_argument(
"--api-key", default=os.environ.get("API_KEY"), help="Github API key"
)
parser.add_argument(
"--release-version",
metavar="version",
default=os.environ.get("VERSION"),
help="Release version",
)
parser.add_argument(
"--path", default=os.environ.get("ASSET_PATH"), help="Asset path"
)
parser.add_argument(
"--name", default=os.environ.get("ASSET_NAME"), help="Asset Name"
)
parser.add_argument(
"--dry-run", action="store_true", help="Do not create release or upload asset"
)
args = parser.parse_args()
is_valid = True
if not args.release_version:
logging.error("Missing release version")
is_valid = False
if not args.path:
logging.error("Missing asset path")
is_valid = False
if not args.name:
logging.error("Missing asset name")
is_valid = False
if not args.api_key:
logging.error("Missing API key")
is_valid = False
if is_valid:
return args
parser.print_usage()
exit(1)
def main():
""" Attempts to upload Asset to Github Release. Creates Release if it doesnt exist """
try:
args = parse_args()
client = Github(args.api_key)
repo = client.get_repo(CLOUDFLARED_REPO)
release = get_or_create_release(repo, args.release_version, args.dry_run)
if args.dry_run:
logging.info("Skipping asset upload because of dry-run")
return
release.upload_asset(args.path, name=args.name)
# 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)
main()
+5 -4
View File
@@ -56,18 +56,19 @@ require (
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
github.com/stretchr/testify v1.3.0
github.com/tinylib/msgp v1.1.0 // indirect
github.com/urfave/cli/v2 v2.2.0
github.com/xo/dburl v0.0.0-20191005012637-293c3298d6c0
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.0.0-20200625001655-4c5254603344
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae
google.golang.org/appengine v1.5.0 // indirect
google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd // indirect
google.golang.org/grpc v1.24.0 // indirect
gopkg.in/coreos/go-oidc.v2 v2.1.0
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8
gopkg.in/urfave/cli.v2 v2.0.0-20180128181224-d604b6ffeee8 // indirect
gopkg.in/yaml.v2 v2.2.4
zombiezen.com/go/capnproto2 v2.18.0+incompatible
)
+16
View File
@@ -45,6 +45,8 @@ github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73 h1:7CNPV0LWRCa1FNmq
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a h1:W8b4lQ4tFF21aspRGoBuCNV6V2fFJBF+pm1J6OY8Lys=
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -177,6 +179,10 @@ github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQl
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -187,6 +193,9 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
@@ -206,6 +215,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -224,6 +235,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 h1:p9xBe/w/OzkeYVKm234g55gMdD1nSIooTir5kV11kfA=
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@ -245,6 +258,9 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9Y
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2 h1:nq114VpM8lsSlP+lyUbANecYHYiFcSNFtqcBlxRV+gA=
golang.org/x/sys v0.0.0-20191020212454-3e7259c5e7c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+6 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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"`
}
+45 -23
View File
@@ -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)
@@ -365,18 +372,18 @@ func randomASCIIPrintableChar(rand *rand.Rand) int {
// between 1 and `maxLength`.
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
length := minLength + rand.Intn(maxLength)
result := ""
var result strings.Builder
for i := 0; i < length; i++ {
c := randomASCIIPrintableChar(rand)
// 1/4 chance of using percent encoding when not necessary
if c == '%' || rand.Intn(4) == 0 {
result += fmt.Sprintf("%%%02X", c)
result.WriteString(fmt.Sprintf("%%%02X", c))
} else {
result += string(c)
result.WriteByte(byte(c))
}
}
return result
return result.String()
}
// Calls `randomASCIIText` and ensures the result is a valid URL path,
@@ -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,26 @@ 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
View File
@@ -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 {
+16 -14
View File
@@ -3,6 +3,7 @@ package logger
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
@@ -106,7 +107,7 @@ func New(opts ...Option) (Service, error) {
l := NewOutputWriter(SharedWriteManager)
if config.logFileDirectory != "" {
l.Add(NewFileRollingWriter(config.logFileDirectory,
l.Add(NewFileRollingWriter(SanitizeLogPath(config.logFileDirectory),
"cloudflared",
int64(config.maxFileSize),
config.maxFileCount),
@@ -114,21 +115,12 @@ func New(opts ...Option) (Service, error) {
}
if !config.terminalOutputDisabled {
terminalFormatter := NewTerminalFormatter(time.RFC3339)
if len(config.supportedTerminalLevels) == 0 {
l.Add(os.Stdout, NewTerminalFormatter(""), InfoLevel)
l.Add(os.Stderr, NewTerminalFormatter(""), ErrorLevel, FatalLevel)
l.Add(os.Stderr, terminalFormatter, InfoLevel, ErrorLevel, FatalLevel)
} else {
errLevels := []Level{}
outLevels := []Level{}
for _, level := range config.supportedTerminalLevels {
if level == ErrorLevel || level == FatalLevel {
errLevels = append(errLevels, level)
} else {
outLevels = append(outLevels, level)
}
}
l.Add(os.Stdout, NewTerminalFormatter(""), outLevels...)
l.Add(os.Stderr, NewTerminalFormatter(""), errLevels...)
l.Add(os.Stderr, terminalFormatter, config.supportedTerminalLevels...)
}
}
return l, nil
@@ -148,3 +140,13 @@ func ParseLevelString(lvl string) ([]Level, error) {
}
return []Level{}, fmt.Errorf("not a valid log level: %q", lvl)
}
// SanitizeLogPath checks that the logger log path
func SanitizeLogPath(path string) string {
newPath := strings.TrimSpace(path)
// make sure it has a log file extension and is not a directory
if filepath.Ext(newPath) != ".log" && !(isDirectory(newPath) || strings.HasSuffix(newPath, "/")) {
newPath = newPath + ".log"
}
return newPath
}
+46
View File
@@ -0,0 +1,46 @@
package logger
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLogLevelParse(t *testing.T) {
lvls, err := ParseLevelString("fatal")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel}, lvls)
lvls, err = ParseLevelString("error")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel}, lvls)
lvls, err = ParseLevelString("info")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel}, lvls)
lvls, err = ParseLevelString("info")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel}, lvls)
lvls, err = ParseLevelString("warn")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel}, lvls)
lvls, err = ParseLevelString("debug")
assert.NoError(t, err)
assert.Equal(t, []Level{FatalLevel, ErrorLevel, InfoLevel, DebugLevel}, lvls)
_, err = ParseLevelString("blah")
assert.Error(t, err)
_, err = ParseLevelString("")
assert.Error(t, err)
}
func TestPathSanitizer(t *testing.T) {
assert.Equal(t, "somebad/path/log.bat.log", SanitizeLogPath("\t somebad/path/log.bat\n\n"))
assert.Equal(t, "proper/path/cloudflared.log", SanitizeLogPath("proper/path/cloudflared.log"))
assert.Equal(t, "proper/path/", SanitizeLogPath("proper/path/"))
assert.Equal(t, "proper/path/cloudflared.log", SanitizeLogPath("\tproper/path/cloudflared\n\n"))
}
+12 -11
View File
@@ -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)
@@ -76,17 +76,18 @@ func NewTerminalFormatter(format string) Formatter {
// Timestamp returns the log level with a matching color to the log type
func (f *TerminalFormatter) Timestamp(l Level, d time.Time) string {
t := ""
dateStr := "[" + d.Format(f.format) + "] "
switch l {
case InfoLevel:
t = f.output("[INFO] ", skittles.Cyan)
t = f.output("INFO", skittles.Cyan)
case ErrorLevel:
t = f.output("[ERROR] ", skittles.Red)
t = f.output("ERROR", skittles.Red)
case DebugLevel:
t = f.output("[DEBUG] ", skittles.Yellow)
t = f.output("DEBUG", skittles.Yellow)
case FatalLevel:
t = f.output("[FATAL] ", skittles.Red)
t = f.output("FATAL", skittles.Red)
}
return t
return t + dateStr
}
// Content just writes the log line straight to the sources
+5 -5
View File
@@ -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:
+8 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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
View File
@@ -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))
+2
View File
@@ -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())
+3 -28
View File
@@ -58,11 +58,9 @@ type TunnelMetrics struct {
// oldServerLocations stores the last server the tunnel was connected to
oldServerLocations map[string]string
regSuccess *prometheus.CounterVec
regFail *prometheus.CounterVec
authSuccess prometheus.Counter
authFail *prometheus.CounterVec
rpcFail *prometheus.CounterVec
regSuccess *prometheus.CounterVec
regFail *prometheus.CounterVec
rpcFail *prometheus.CounterVec
muxerMetrics *muxerMetrics
tunnelsHA tunnelsForHA
@@ -456,27 +454,6 @@ func NewTunnelMetrics() *TunnelMetrics {
)
prometheus.MustRegister(registerSuccess)
authSuccess := prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_authenticate_success",
Help: "Count of successful tunnel authenticate",
},
)
prometheus.MustRegister(authSuccess)
authFail := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_authenticate_fail",
Help: "Count of tunnel authenticate errors by type",
},
[]string{"error"},
)
prometheus.MustRegister(authFail)
return &TunnelMetrics{
haConnections: haConnections,
activeStreams: activeStreams,
@@ -497,8 +474,6 @@ func NewTunnelMetrics() *TunnelMetrics {
regFail: registerFail,
rpcFail: rpcFail,
userHostnamesCounts: userHostnamesCounts,
authSuccess: authSuccess,
authFail: authFail,
}
}
+191
View File
@@ -0,0 +1,191 @@
package origin
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/tunnelrpc"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
)
var (
errJWTUnset = errors.New("JWT unset")
)
// reconnectTunnelCredentialManager is invoked by functions in tunnel.go to
// get/set parameters for ReconnectTunnel RPC calls.
type reconnectCredentialManager struct {
mu sync.RWMutex
jwt []byte
eventDigest map[uint8][]byte
connDigest map[uint8][]byte
authSuccess prometheus.Counter
authFail *prometheus.CounterVec
}
func newReconnectCredentialManager(namespace, subsystem string, haConnections int) *reconnectCredentialManager {
authSuccess := prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "tunnel_authenticate_success",
Help: "Count of successful tunnel authenticate",
},
)
authFail := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "tunnel_authenticate_fail",
Help: "Count of tunnel authenticate errors by type",
},
[]string{"error"},
)
prometheus.MustRegister(authSuccess, authFail)
return &reconnectCredentialManager{
eventDigest: make(map[uint8][]byte, haConnections),
connDigest: make(map[uint8][]byte, haConnections),
authSuccess: authSuccess,
authFail: authFail,
}
}
func (cm *reconnectCredentialManager) ReconnectToken() ([]byte, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
if cm.jwt == nil {
return nil, errJWTUnset
}
return cm.jwt, nil
}
func (cm *reconnectCredentialManager) SetReconnectToken(jwt []byte) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.jwt = jwt
}
func (cm *reconnectCredentialManager) EventDigest(connID uint8) ([]byte, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
digest, ok := cm.eventDigest[connID]
if !ok {
return nil, fmt.Errorf("no event digest for connection %v", connID)
}
return digest, nil
}
func (cm *reconnectCredentialManager) SetEventDigest(connID uint8, digest []byte) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.eventDigest[connID] = digest
}
func (cm *reconnectCredentialManager) ConnDigest(connID uint8) ([]byte, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
digest, ok := cm.connDigest[connID]
if !ok {
return nil, fmt.Errorf("no conneciton digest for connection %v", connID)
}
return digest, nil
}
func (cm *reconnectCredentialManager) SetConnDigest(connID uint8, digest []byte) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.connDigest[connID] = digest
}
func (cm *reconnectCredentialManager) RefreshAuth(
ctx context.Context,
backoff *BackoffHandler,
authenticate func(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error),
) (retryTimer <-chan time.Time, err error) {
authOutcome, err := authenticate(ctx, backoff.Retries())
if err != nil {
cm.authFail.WithLabelValues(err.Error()).Inc()
if _, ok := backoff.GetBackoffDuration(ctx); ok {
return backoff.BackoffTimer(), nil
}
return nil, err
}
// clear backoff timer
backoff.SetGracePeriod()
switch outcome := authOutcome.(type) {
case tunnelpogs.AuthSuccess:
cm.SetReconnectToken(outcome.JWT())
cm.authSuccess.Inc()
return timeAfter(outcome.RefreshAfter()), nil
case tunnelpogs.AuthUnknown:
duration := outcome.RefreshAfter()
cm.authFail.WithLabelValues(outcome.Error()).Inc()
return timeAfter(duration), nil
case tunnelpogs.AuthFail:
cm.authFail.WithLabelValues(outcome.Error()).Inc()
return nil, outcome
default:
err := fmt.Errorf("refresh_auth: Unexpected outcome type %T", authOutcome)
cm.authFail.WithLabelValues(err.Error()).Inc()
return nil, err
}
}
func ReconnectTunnel(
ctx context.Context,
muxer *h2mux.Muxer,
config *TunnelConfig,
logger logger.Service,
connectionID uint8,
originLocalAddr string,
uuid uuid.UUID,
credentialManager *reconnectCredentialManager,
) error {
token, err := credentialManager.ReconnectToken()
if err != nil {
return err
}
eventDigest, err := credentialManager.EventDigest(connectionID)
if err != nil {
return err
}
connDigest, err := credentialManager.ConnDigest(connectionID)
if err != nil {
return err
}
config.TransportLogger.Debug("initiating RPC stream to reconnect")
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, reconnect)
}
defer tunnelServer.Close()
// Request server info without blocking tunnel registration; must use capnp library directly.
serverInfoPromise := tunnelrpc.TunnelServer{Client: tunnelServer.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
return nil
})
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger)
registration := tunnelServer.ReconnectTunnel(
ctx,
token,
eventDigest,
connDigest,
config.Hostname,
config.RegistrationOptions(connectionID, originLocalAddr, uuid),
)
if registrationErr := registration.DeserializeError(); registrationErr != nil {
// ReconnectTunnel RPC failure
return processRegisterTunnelError(registrationErr, config.Metrics, reconnect)
}
return processRegistrationSuccess(config, logger, connectionID, registration, reconnect, credentialManager)
}
@@ -7,51 +7,19 @@ import (
"testing"
"time"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/logger"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
func testConfig(logger logger.Service) *TunnelConfig {
metrics := TunnelMetrics{}
metrics.authSuccess = prometheus.NewCounter(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_authenticate_success",
Help: "Count of successful tunnel authenticate",
},
)
metrics.authFail = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricsNamespace,
Subsystem: tunnelSubsystem,
Name: "tunnel_authenticate_fail",
Help: "Count of tunnel authenticate errors by type",
},
[]string{"error"},
)
return &TunnelConfig{Logger: logger, Metrics: &metrics}
}
func TestRefreshAuthBackoff(t *testing.T) {
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
rcm := newReconnectCredentialManager(t.Name(), t.Name(), 4)
var wait time.Duration
timeAfter = func(d time.Duration) <-chan time.Time {
wait = d
return time.After(d)
}
s, err := NewSupervisor(testConfig(logger), uuid.New())
if !assert.NoError(t, err) {
t.FailNow()
}
backoff := &BackoffHandler{MaxRetries: 3}
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
return nil, fmt.Errorf("authentication failure")
@@ -59,17 +27,17 @@ func TestRefreshAuthBackoff(t *testing.T) {
// authentication failures should consume the backoff
for i := uint(0); i < backoff.MaxRetries; i++ {
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
retryChan, err := rcm.RefreshAuth(context.Background(), backoff, auth)
assert.NoError(t, err)
assert.NotNil(t, retryChan)
assert.Equal(t, (1<<i)*time.Second, wait)
}
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
retryChan, err := rcm.RefreshAuth(context.Background(), backoff, auth)
assert.Error(t, err)
assert.Nil(t, retryChan)
// now we actually make contact with the remote server
_, _ = s.refreshAuth(context.Background(), backoff, func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
_, _ = rcm.RefreshAuth(context.Background(), backoff, func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
return tunnelpogs.NewAuthUnknown(errors.New("auth unknown"), 19), nil
})
@@ -84,7 +52,7 @@ func TestRefreshAuthBackoff(t *testing.T) {
}
func TestRefreshAuthSuccess(t *testing.T) {
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
rcm := newReconnectCredentialManager(t.Name(), t.Name(), 4)
var wait time.Duration
timeAfter = func(d time.Duration) <-chan time.Time {
@@ -92,27 +60,23 @@ func TestRefreshAuthSuccess(t *testing.T) {
return time.After(d)
}
s, err := NewSupervisor(testConfig(logger), uuid.New())
if !assert.NoError(t, err) {
t.FailNow()
}
backoff := &BackoffHandler{MaxRetries: 3}
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
return tunnelpogs.NewAuthSuccess([]byte("jwt"), 19), nil
}
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
retryChan, err := rcm.RefreshAuth(context.Background(), backoff, auth)
assert.NoError(t, err)
assert.NotNil(t, retryChan)
assert.Equal(t, 19*time.Hour, wait)
token, err := s.ReconnectToken()
token, err := rcm.ReconnectToken()
assert.NoError(t, err)
assert.Equal(t, []byte("jwt"), token)
}
func TestRefreshAuthUnknown(t *testing.T) {
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
rcm := newReconnectCredentialManager(t.Name(), t.Name(), 4)
var wait time.Duration
timeAfter = func(d time.Duration) <-chan time.Time {
@@ -120,42 +84,34 @@ func TestRefreshAuthUnknown(t *testing.T) {
return time.After(d)
}
s, err := NewSupervisor(testConfig(logger), uuid.New())
if !assert.NoError(t, err) {
t.FailNow()
}
backoff := &BackoffHandler{MaxRetries: 3}
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
return tunnelpogs.NewAuthUnknown(errors.New("auth unknown"), 19), nil
}
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
retryChan, err := rcm.RefreshAuth(context.Background(), backoff, auth)
assert.NoError(t, err)
assert.NotNil(t, retryChan)
assert.Equal(t, 19*time.Hour, wait)
token, err := s.ReconnectToken()
token, err := rcm.ReconnectToken()
assert.Equal(t, errJWTUnset, err)
assert.Nil(t, token)
}
func TestRefreshAuthFail(t *testing.T) {
logger := logger.NewOutputWriter(logger.NewMockWriteManager())
rcm := newReconnectCredentialManager(t.Name(), t.Name(), 4)
s, err := NewSupervisor(testConfig(logger), uuid.New())
if !assert.NoError(t, err) {
t.FailNow()
}
backoff := &BackoffHandler{MaxRetries: 3}
auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) {
return tunnelpogs.NewAuthFail(errors.New("auth fail")), nil
}
retryChan, err := s.refreshAuth(context.Background(), backoff, auth)
retryChan, err := rcm.RefreshAuth(context.Background(), backoff, auth)
assert.Error(t, err)
assert.Nil(t, retryChan)
token, err := s.ReconnectToken()
token, err := rcm.ReconnectToken()
assert.Equal(t, errJWTUnset, err)
assert.Nil(t, token)
}
+20 -113
View File
@@ -3,9 +3,7 @@ package origin
import (
"context"
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/google/uuid"
@@ -37,7 +35,6 @@ const (
)
var (
errJWTUnset = errors.New("JWT unset")
errEventDigestUnset = errors.New("event digest unset")
)
@@ -58,14 +55,7 @@ type Supervisor struct {
logger logger.Service
jwtLock sync.RWMutex
jwt []byte
eventDigestLock sync.RWMutex
eventDigest []byte
connDigestLock sync.RWMutex
connDigest map[uint8][]byte
reconnectCredentialManager *reconnectCredentialManager
bufferPool *buffer.Pool
}
@@ -80,7 +70,7 @@ type tunnelError struct {
err error
}
func NewSupervisor(config *TunnelConfig, u uuid.UUID) (*Supervisor, error) {
func NewSupervisor(config *TunnelConfig, cloudflaredUUID uuid.UUID) (*Supervisor, error) {
var (
edgeIPs *edgediscovery.Edge
err error
@@ -93,15 +83,16 @@ func NewSupervisor(config *TunnelConfig, u uuid.UUID) (*Supervisor, error) {
if err != nil {
return nil, err
}
return &Supervisor{
cloudflaredUUID: u,
config: config,
edgeIPs: edgeIPs,
tunnelErrors: make(chan tunnelError),
tunnelsConnecting: map[int]chan struct{}{},
logger: config.Logger,
connDigest: make(map[uint8][]byte),
bufferPool: buffer.NewPool(512 * 1024),
cloudflaredUUID: cloudflaredUUID,
config: config,
edgeIPs: edgeIPs,
tunnelErrors: make(chan tunnelError),
tunnelsConnecting: map[int]chan struct{}{},
logger: config.Logger,
reconnectCredentialManager: newReconnectCredentialManager(metricsNamespace, tunnelSubsystem, config.HAConnections),
bufferPool: buffer.NewPool(512 * 1024),
}, nil
}
@@ -120,7 +111,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, re
var refreshAuthBackoffTimer <-chan time.Time
if s.config.UseReconnectToken {
if timer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate); err == nil {
if timer, err := s.reconnectCredentialManager.RefreshAuth(ctx, refreshAuthBackoff, s.authenticate); err == nil {
refreshAuthBackoffTimer = timer
} else {
logger.Errorf("supervisor: initial refreshAuth failed, retrying in %v: %s", refreshAuthRetryDuration, err)
@@ -163,7 +154,7 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, re
tunnelsWaiting = nil
// Time to call Authenticate
case <-refreshAuthBackoffTimer:
newTimer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate)
newTimer, err := s.reconnectCredentialManager.RefreshAuth(ctx, refreshAuthBackoff, s.authenticate)
if err != nil {
logger.Errorf("supervisor: Authentication failed: %s", err)
// Permanent failure. Leave the `select` without setting the
@@ -226,17 +217,17 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign
addr *net.TCPAddr
err error
)
const thisConnID = 0
const firstConnIndex = 0
defer func() {
s.tunnelErrors <- tunnelError{index: thisConnID, addr: addr, err: err}
s.tunnelErrors <- tunnelError{index: firstConnIndex, addr: addr, err: err}
}()
addr, err = s.edgeIPs.GetAddr(thisConnID)
addr, err = s.edgeIPs.GetAddr(firstConnIndex)
if err != nil {
return
}
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
err = ServeTunnelLoop(ctx, s.reconnectCredentialManager, s.config, addr, firstConnIndex, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
// If the first tunnel disconnects, keep restarting it.
edgeErrors := 0
for s.unusedIPs() {
@@ -254,12 +245,12 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign
return
}
if edgeErrors >= 2 {
addr, err = s.edgeIPs.GetDifferentAddr(thisConnID)
addr, err = s.edgeIPs.GetDifferentAddr(firstConnIndex)
if err != nil {
return
}
}
err = ServeTunnelLoop(ctx, s, s.config, addr, thisConnID, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
err = ServeTunnelLoop(ctx, s.reconnectCredentialManager, s.config, addr, firstConnIndex, connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
}
}
@@ -278,7 +269,7 @@ func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal
if err != nil {
return
}
err = ServeTunnelLoop(ctx, s, s.config, addr, uint8(index), connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
err = ServeTunnelLoop(ctx, s.reconnectCredentialManager, s.config, addr, uint8(index), connectedSignal, s.cloudflaredUUID, s.bufferPool, reconnectCh)
}
func (s *Supervisor) newConnectedTunnelSignal(index int) *signal.Signal {
@@ -304,90 +295,6 @@ func (s *Supervisor) unusedIPs() bool {
return s.edgeIPs.AvailableAddrs() > s.config.HAConnections
}
func (s *Supervisor) ReconnectToken() ([]byte, error) {
s.jwtLock.RLock()
defer s.jwtLock.RUnlock()
if s.jwt == nil {
return nil, errJWTUnset
}
return s.jwt, nil
}
func (s *Supervisor) SetReconnectToken(jwt []byte) {
s.jwtLock.Lock()
defer s.jwtLock.Unlock()
s.jwt = jwt
}
func (s *Supervisor) EventDigest() ([]byte, error) {
s.eventDigestLock.RLock()
defer s.eventDigestLock.RUnlock()
if s.eventDigest == nil {
return nil, errEventDigestUnset
}
return s.eventDigest, nil
}
func (s *Supervisor) SetEventDigest(eventDigest []byte) {
s.eventDigestLock.Lock()
defer s.eventDigestLock.Unlock()
s.eventDigest = eventDigest
}
func (s *Supervisor) ConnDigest(connID uint8) ([]byte, error) {
s.connDigestLock.RLock()
defer s.connDigestLock.RUnlock()
digest, ok := s.connDigest[connID]
if !ok {
return nil, fmt.Errorf("no connection digest for connection %v", connID)
}
return digest, nil
}
func (s *Supervisor) SetConnDigest(connID uint8, connDigest []byte) {
s.connDigestLock.Lock()
defer s.connDigestLock.Unlock()
s.connDigest[connID] = connDigest
}
func (s *Supervisor) refreshAuth(
ctx context.Context,
backoff *BackoffHandler,
authenticate func(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error),
) (retryTimer <-chan time.Time, err error) {
logger := s.config.Logger
authOutcome, err := authenticate(ctx, backoff.Retries())
if err != nil {
s.config.Metrics.authFail.WithLabelValues(err.Error()).Inc()
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
logger.Debugf("refresh_auth: Retrying in %v: %s", duration, err)
return backoff.BackoffTimer(), nil
}
return nil, err
}
// clear backoff timer
backoff.SetGracePeriod()
switch outcome := authOutcome.(type) {
case tunnelpogs.AuthSuccess:
s.SetReconnectToken(outcome.JWT())
s.config.Metrics.authSuccess.Inc()
return timeAfter(outcome.RefreshAfter()), nil
case tunnelpogs.AuthUnknown:
duration := outcome.RefreshAfter()
s.config.Metrics.authFail.WithLabelValues(outcome.Error()).Inc()
logger.Debugf("refresh_auth: Retrying in %v: %s", duration, outcome)
return timeAfter(duration), nil
case tunnelpogs.AuthFail:
s.config.Metrics.authFail.WithLabelValues(outcome.Error()).Inc()
return nil, outcome
default:
err := fmt.Errorf("refresh_auth: Unexpected outcome type %T", authOutcome)
s.config.Metrics.authFail.WithLabelValues(err.Error()).Inc()
return nil, err
}
}
func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error) {
arbitraryEdgeIP, err := s.edgeIPs.GetAddrForRPC()
if err != nil {
+179 -147
View File
@@ -26,6 +26,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/tunnelrpc"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/validation"
"github.com/cloudflare/cloudflared/websocket"
@@ -47,63 +48,54 @@ type registerRPCName string
const (
register registerRPCName = "register"
reconnect registerRPCName = "reconnect"
unknown registerRPCName = "unknown"
)
type TunnelConfig struct {
BuildInfo *buildinfo.BuildInfo
ClientID string
ClientTlsConfig *tls.Config
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
CompressionQuality uint64
EdgeAddrs []string
GracePeriod time.Duration
HAConnections int
HTTPTransport http.RoundTripper
HeartbeatInterval time.Duration
Hostname string
HTTPHostHeader string
IncidentLookup IncidentLookup
IsAutoupdated bool
IsFreeTunnel bool
LBPool string
Logger logger.Service
TransportLogger logger.Service
MaxHeartbeats uint64
Metrics *TunnelMetrics
MetricsUpdateFreq time.Duration
NoChunkedEncoding bool
OriginCert []byte
ReportedVersion string
Retries uint
RunFromTerminal bool
Tags []tunnelpogs.Tag
TlsConfig *tls.Config
UseDeclarativeTunnel bool
WSGI bool
BuildInfo *buildinfo.BuildInfo
ClientID string
ClientTlsConfig *tls.Config
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
CompressionQuality uint64
EdgeAddrs []string
GracePeriod time.Duration
HAConnections int
HTTPTransport http.RoundTripper
HeartbeatInterval time.Duration
Hostname string
HTTPHostHeader string
IncidentLookup IncidentLookup
IsAutoupdated bool
IsFreeTunnel bool
LBPool string
Logger logger.Service
TransportLogger logger.Service
MaxHeartbeats uint64
Metrics *TunnelMetrics
MetricsUpdateFreq time.Duration
NoChunkedEncoding bool
OriginCert []byte
ReportedVersion string
Retries uint
RunFromTerminal bool
Tags []tunnelpogs.Tag
TlsConfig *tls.Config
WSGI bool
// OriginUrl may not be used if a user specifies a unix socket.
OriginUrl string
// feature-flag to use new edge reconnect tokens
UseReconnectToken bool
// feature-flag for using ConnectionDigest
UseQuickReconnects bool
}
// ReconnectTunnelCredentialManager is invoked by functions in this file to
// get/set parameters for ReconnectTunnel RPC calls.
type ReconnectTunnelCredentialManager interface {
ReconnectToken() ([]byte, error)
EventDigest() ([]byte, error)
SetEventDigest(eventDigest []byte)
ConnDigest(connID uint8) ([]byte, error)
SetConnDigest(connID uint8, connDigest []byte)
NamedTunnel *NamedTunnelConfig
ReplaceExisting bool
}
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{}
@@ -170,12 +162,32 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
}
}
func (c *TunnelConfig) SupportedFeatures() []string {
basic := []string{FeatureSerializedHeaders}
if c.UseQuickReconnects {
basic = append(basic, FeatureQuickReconnects)
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),
NumPreviousAttempts: numPreviousAttempts,
}
return basic
}
func (c *TunnelConfig) SupportedFeatures() []string {
features := []string{FeatureSerializedHeaders}
if c.NamedTunnel == nil {
features = append(features, FeatureQuickReconnects)
}
return features
}
type NamedTunnelConfig struct {
Auth pogs.TunnelAuth
ID uuid.UUID
Client pogs.ClientInfo
}
func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID, reconnectCh chan ReconnectSignal) error {
@@ -187,12 +199,12 @@ func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSigna
}
func ServeTunnelLoop(ctx context.Context,
credentialManager ReconnectTunnelCredentialManager,
credentialManager *reconnectCredentialManager,
config *TunnelConfig,
addr *net.TCPAddr,
connectionID uint8,
connectionIndex uint8,
connectedSignal *signal.Signal,
u uuid.UUID,
cloudflaredUUID uuid.UUID,
bufferPool *buffer.Pool,
reconnectCh chan ReconnectSignal,
) error {
@@ -213,16 +225,16 @@ func ServeTunnelLoop(ctx context.Context,
credentialManager,
config,
config.Logger,
addr, connectionID,
addr, connectionIndex,
connectedFuse,
&backoff,
u,
cloudflaredUUID,
bufferPool,
reconnectCh,
)
if recoverable {
if duration, ok := backoff.GetBackoffDuration(ctx); ok {
config.Logger.Infof("Retrying in %s seconds: connectionID: %d", duration, connectionID)
config.Logger.Infof("Retrying connection %d in %s seconds", connectionIndex, duration)
backoff.Backoff(ctx)
continue
}
@@ -233,14 +245,14 @@ func ServeTunnelLoop(ctx context.Context,
func ServeTunnel(
ctx context.Context,
credentialManager ReconnectTunnelCredentialManager,
credentialManager *reconnectCredentialManager,
config *TunnelConfig,
logger logger.Service,
addr *net.TCPAddr,
connectionID uint8,
connectionIndex uint8,
connectedFuse *h2mux.BooleanFuse,
backoff *BackoffHandler,
u uuid.UUID,
cloudflaredUUID uuid.UUID,
bufferPool *buffer.Pool,
reconnectCh chan ReconnectSignal,
) (err error, recoverable bool) {
@@ -256,22 +268,18 @@ func ServeTunnel(
}
}()
connectionTag := uint8ToString(connectionID)
// additional tags to send other than hostname which is set in cloudflared main package
tags := make(map[string]string)
tags["ha"] = connectionTag
connectionTag := uint8ToString(connectionIndex)
// Returns error from parsing the origin URL or handshake errors
handler, originLocalIP, err := NewTunnelHandler(ctx, config, addr, connectionID, bufferPool)
handler, originLocalAddr, err := NewTunnelHandler(ctx, config, addr, connectionIndex, bufferPool)
if err != nil {
switch err.(type) {
case connection.DialError:
logger.Errorf("Unable to dial edge: %s connectionID: %d", err, connectionID)
logger.Errorf("Connection %d unable to dial edge: %s", connectionIndex, err)
case h2mux.MuxerHandshakeError:
logger.Errorf("Handshake failed with edge server: %s connectionID: %d", err, connectionID)
logger.Errorf("Connection %d handshake with edge server failed: %s", connectionIndex, err)
default:
logger.Errorf("Tunnel creation failure: %s connectionID: %d", err, connectionID)
logger.Errorf("Connection %d failed: %s", connectionIndex, err)
return err, false
}
return err, true
@@ -287,30 +295,19 @@ func ServeTunnel(
}
}()
if config.UseReconnectToken && connectedFuse.Value() {
token, tokenErr := credentialManager.ReconnectToken()
eventDigest, eventDigestErr := credentialManager.EventDigest()
// if we have both credentials, we can reconnect
if tokenErr == nil && eventDigestErr == nil {
var connDigest []byte
if config.NamedTunnel != nil {
return RegisterConnection(ctx, handler.muxer, config, connectionIndex, originLocalAddr, uint8(backoff.retries))
}
// check if we can use Quick Reconnects
if config.UseQuickReconnects {
if digest, connDigestErr := credentialManager.ConnDigest(connectionID); connDigestErr == nil {
connDigest = digest
}
}
return ReconnectTunnel(serveCtx, token, eventDigest, connDigest, handler.muxer, config, logger, connectionID, originLocalIP, u, credentialManager)
if config.UseReconnectToken && connectedFuse.Value() {
err := ReconnectTunnel(serveCtx, handler.muxer, config, logger, connectionIndex, originLocalAddr, cloudflaredUUID, credentialManager)
if err == nil {
return nil
}
// log errors and proceed to RegisterTunnel
if tokenErr != nil {
logger.Errorf("Couldn't get reconnect token: %s", tokenErr)
}
if eventDigestErr != nil {
logger.Errorf("Couldn't get event digest: %s", eventDigestErr)
}
logger.Errorf("Couldn't reconnect connection %d. Reregistering it instead. Error was: %v", connectionIndex, err)
}
return RegisterTunnel(serveCtx, credentialManager, handler.muxer, config, logger, connectionID, originLocalIP, u)
return RegisterTunnel(serveCtx, credentialManager, handler.muxer, config, logger, connectionIndex, originLocalAddr, cloudflaredUUID)
})
errGroup.Go(func() error {
@@ -319,12 +316,15 @@ func ServeTunnel(
select {
case <-serveCtx.Done():
// UnregisterTunnel blocks until the RPC call returns
var err error
if connectedFuse.Value() {
err = UnregisterTunnel(handler.muxer, config.GracePeriod, config.TransportLogger)
if config.NamedTunnel != nil {
_ = UnregisterConnection(ctx, handler.muxer, config)
} else {
_ = UnregisterTunnel(handler.muxer, config.GracePeriod, config.TransportLogger)
}
}
handler.muxer.Shutdown()
return err
return nil
case <-updateMetricsTickC:
handler.UpdateMetrics(connectionTag)
}
@@ -355,31 +355,33 @@ func ServeTunnel(
err = errGroup.Wait()
if err != nil {
_ = newClientRegisterTunnelError(err, config.Metrics.regFail, unknown)
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 due to reconnect signal in %d seconds", 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
}
@@ -387,9 +389,78 @@ func ServeTunnel(
return nil, true
}
func RegisterConnection(
ctx context.Context,
muxer *h2mux.Muxer,
config *TunnelConfig,
connectionIndex uint8,
originLocalAddr string,
numPreviousAttempts uint8,
) error {
const registerConnection = "registerConnection"
config.TransportLogger.Debug("initiating RPC stream for RegisterConnection")
rpc, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, registerConnection)
}
defer rpc.Close()
conn, err := rpc.RegisterConnection(
ctx,
config.NamedTunnel.Auth,
config.NamedTunnel.ID,
connectionIndex,
config.ConnectionOptions(originLocalAddr, numPreviousAttempts),
)
if err != nil {
if err.Error() == DuplicateConnectionError {
config.Metrics.regFail.WithLabelValues("dup_edge_conn", registerConnection).Inc()
return errDuplicationConnection
}
config.Metrics.regFail.WithLabelValues("server_error", registerConnection).Inc()
return serverRegistrationErrorFromRPC(err)
}
config.Metrics.regSuccess.WithLabelValues(registerConnection).Inc()
config.Logger.Infof("Connection %d registered with %s using ID %s", connectionIndex, conn.Location, conn.UUID)
return nil
}
func serverRegistrationErrorFromRPC(err error) *serverRegisterTunnelError {
if retryable, ok := err.(*tunnelpogs.RetryableError); ok {
return &serverRegisterTunnelError{
cause: retryable.Unwrap(),
permanent: false,
}
}
return &serverRegisterTunnelError{
cause: err,
permanent: true,
}
}
func UnregisterConnection(
ctx context.Context,
muxer *h2mux.Muxer,
config *TunnelConfig,
) error {
config.TransportLogger.Debug("initiating RPC stream for UnregisterConnection")
rpc, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, register)
}
defer rpc.Close()
return rpc.UnregisterConnection(ctx)
}
func RegisterTunnel(
ctx context.Context,
credentialManager ReconnectTunnelCredentialManager,
credentialManager *reconnectCredentialManager,
muxer *h2mux.Muxer,
config *TunnelConfig,
logger logger.Service,
@@ -419,56 +490,17 @@ func RegisterTunnel(
// RegisterTunnel RPC failure
return processRegisterTunnelError(registrationErr, config.Metrics, register)
}
credentialManager.SetEventDigest(registration.EventDigest)
credentialManager.SetEventDigest(connectionID, registration.EventDigest)
return processRegistrationSuccess(config, logger, connectionID, registration, register, credentialManager)
}
func ReconnectTunnel(
ctx context.Context,
token []byte,
eventDigest, connDigest []byte,
muxer *h2mux.Muxer,
config *TunnelConfig,
logger logger.Service,
connectionID uint8,
originLocalIP string,
uuid uuid.UUID,
credentialManager ReconnectTunnelCredentialManager,
) error {
config.TransportLogger.Debug("initiating RPC stream to reconnect")
tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger, openStreamTimeout)
if err != nil {
// RPC stream open error
return newClientRegisterTunnelError(err, config.Metrics.rpcFail, reconnect)
}
defer tunnelServer.Close()
// Request server info without blocking tunnel registration; must use capnp library directly.
serverInfoPromise := tunnelrpc.TunnelServer{Client: tunnelServer.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error {
return nil
})
LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger)
registration := tunnelServer.ReconnectTunnel(
ctx,
token,
eventDigest,
connDigest,
config.Hostname,
config.RegistrationOptions(connectionID, originLocalIP, uuid),
)
if registrationErr := registration.DeserializeError(); registrationErr != nil {
// ReconnectTunnel RPC failure
return processRegisterTunnelError(registrationErr, config.Metrics, reconnect)
}
return processRegistrationSuccess(config, logger, connectionID, registration, reconnect, credentialManager)
}
func processRegistrationSuccess(
config *TunnelConfig,
logger logger.Service,
connectionID uint8,
registration *tunnelpogs.TunnelRegistration,
name registerRPCName,
credentialManager ReconnectTunnelCredentialManager,
credentialManager *reconnectCredentialManager,
) error {
for _, logLine := range registration.LogLines {
logger.Info(logLine)
@@ -502,11 +534,11 @@ func processRegistrationSuccess(
func processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, metrics *TunnelMetrics, name registerRPCName) error {
if err.Error() == DuplicateConnectionError {
metrics.regFail.WithLabelValues("dup_edge_conn", string(name)).Inc()
return dupConnRegisterTunnelError{}
return errDuplicationConnection
}
metrics.regFail.WithLabelValues("server_error", string(name)).Inc()
return serverRegisterTunnelError{
cause: fmt.Errorf("Server error: %s", err.Error()),
cause: err,
permanent: err.IsPermanent(),
}
}
+5
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
set -eu
rm /usr/local/bin/cloudflared
rm /usr/local/etc/cloudflared/.installedFromPackageManager || true
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/cloudflare/cloudflared/logger"
"github.com/getsentry/raven-go"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
const (
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/cache"
"github.com/pkg/errors"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
// Listener is an adapter between CoreDNS server and Warp runnable
+10 -6
View File
@@ -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 {
@@ -223,11 +224,14 @@ func (c TunnelServer_PogsClient) RegisterConnection(ctx context.Context, auth Tu
return nil, newRPCError("unknown result which %d", result.Which())
}
func (c TunnelServer_PogsClient) Unregister(ctx context.Context) error {
func (c TunnelServer_PogsClient) UnregisterConnection(ctx context.Context) error {
client := tunnelrpc.TunnelServer{Client: c.Client}
promise := client.UnregisterConnection(ctx, func(p tunnelrpc.RegistrationServer_unregisterConnection_Params) error {
return nil
})
_, err := promise.Struct()
return wrapRPCError(err)
if err != nil {
return wrapRPCError(err)
}
return nil
}
+6 -3
View File
@@ -37,9 +37,12 @@ func (re *RPCError) Error() string {
}
func wrapRPCError(err error) *RPCError {
return &RPCError{
err: err,
if err != nil {
return &RPCError{
err: err,
}
}
return nil
}
func newRPCError(format string, args ...interface{}) *RPCError {
@@ -50,4 +53,4 @@ func newRPCError(format string, args ...interface{}) *RPCError {
func (re *RPCError) Unwrap() error {
return re.err
}
}
+2
View File
@@ -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
View File
@@ -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,
+163 -42
View File
@@ -6,12 +6,15 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/cloudflare/cloudflared/logger"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/cloudflare/cloudflared/logger"
)
const (
@@ -27,7 +30,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"`
@@ -35,33 +38,121 @@ type Tunnel struct {
}
type Connection struct {
ColoName string `json:"colo_name"`
ID uuid.UUID `json:"uuid"`
ColoName string `json:"colo_name"`
ID uuid.UUID `json:"uuid"`
IsPendingReconnect bool `json:"is_pending_reconnect"`
}
// 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
ListTunnels() ([]Tunnel, error)
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
DeleteTunnel(tunnelID uuid.UUID) error
ListTunnels(filter *Filter) ([]*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 url.URL
zoneLevel url.URL
}
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, error) {
if strings.HasSuffix(baseURL, "/") {
baseURL = baseURL[:len(baseURL)-1]
}
url := fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag)
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
}
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint")
}
return &RESTClient{
baseURL: url,
baseEndpoints: &baseEndpoints{
accountLevel: *accountLevelEndpoint,
zoneLevel: *zoneLevelEndpoint,
},
authToken: authToken,
client: http.Client{
Transport: &http.Transport{
@@ -71,7 +162,7 @@ func NewRESTClient(baseURL string, accountTag string, authToken string, logger l
Timeout: defaultTimeout,
},
logger: logger,
}
}, nil
}
type newTunnel struct {
@@ -83,15 +174,15 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, er
if name == "" {
return nil, errors.New("tunnel name required")
}
body, err := json.Marshal(&newTunnel{
if _, err := uuid.Parse(name); err == nil {
return nil, errors.New("you cannot use UUIDs as tunnel names")
}
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 +195,13 @@ 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 := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
@@ -118,52 +211,80 @@ 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 := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", 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)
func (r *RESTClient) ListTunnels(filter *Filter) ([]*Tunnel, error) {
endpoint := r.baseEndpoints.accountLevel
endpoint.RawQuery = filter.encode()
resp, err := r.sendRequest("GET", endpoint, nil)
if err != nil {
return nil, errors.Wrap(err, "REST request failed")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
var tunnels []Tunnel
var tunnels []*Tunnel
if err := json.NewDecoder(resp.Body).Decode(&tunnels); err != nil {
return nil, errors.Wrap(err, "failed to decode response")
}
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 := r.baseEndpoints.accountLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", 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)
r.logger.Debugf("%s %s", method, url)
req, err := http.NewRequest(method, url, body)
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route Route) error {
endpoint := r.baseEndpoints.zoneLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", 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 url.URL, body interface{}) (*http.Response, error) {
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.String(), 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 +299,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 +310,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))
}
+42
View File
@@ -0,0 +1,42 @@
package tunnelstore
import (
"net/url"
"time"
"github.com/google/uuid"
)
const (
TimeLayout = time.RFC3339
)
type Filter struct {
queryParams url.Values
}
func NewFilter() *Filter {
return &Filter{
queryParams: url.Values{},
}
}
func (f *Filter) ByName(name string) {
f.queryParams.Set("name", name)
}
func (f *Filter) NoDeleted() {
f.queryParams.Set("is_deleted", "false")
}
func (f *Filter) ByExistedAt(existedAt time.Time) {
f.queryParams.Set("existed_at", existedAt.Format(TimeLayout))
}
func (f *Filter) ByTunnelID(tunnelID uuid.UUID) {
f.queryParams.Set("uuid", tunnelID.String())
}
func (f Filter) encode() string {
return f.queryParams.Encode()
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Brian Goff
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+14
View File
@@ -0,0 +1,14 @@
package md2man
import (
"github.com/russross/blackfriday/v2"
)
// Render converts a markdown document into a roff formatted document.
func Render(doc []byte) []byte {
renderer := NewRoffRenderer()
return blackfriday.Run(doc,
[]blackfriday.Option{blackfriday.WithRenderer(renderer),
blackfriday.WithExtensions(renderer.GetExtensions())}...)
}
+345
View File
@@ -0,0 +1,345 @@
package md2man
import (
"fmt"
"io"
"os"
"strings"
"github.com/russross/blackfriday/v2"
)
// roffRenderer implements the blackfriday.Renderer interface for creating
// roff format (manpages) from markdown text
type roffRenderer struct {
extensions blackfriday.Extensions
listCounters []int
firstHeader bool
defineTerm bool
listDepth int
}
const (
titleHeader = ".TH "
topLevelHeader = "\n\n.SH "
secondLevelHdr = "\n.SH "
otherHeader = "\n.SS "
crTag = "\n"
emphTag = "\\fI"
emphCloseTag = "\\fP"
strongTag = "\\fB"
strongCloseTag = "\\fP"
breakTag = "\n.br\n"
paraTag = "\n.PP\n"
hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n"
linkTag = "\n\\[la]"
linkCloseTag = "\\[ra]"
codespanTag = "\\fB\\fC"
codespanCloseTag = "\\fR"
codeTag = "\n.PP\n.RS\n\n.nf\n"
codeCloseTag = "\n.fi\n.RE\n"
quoteTag = "\n.PP\n.RS\n"
quoteCloseTag = "\n.RE\n"
listTag = "\n.RS\n"
listCloseTag = "\n.RE\n"
arglistTag = "\n.TP\n"
tableStart = "\n.TS\nallbox;\n"
tableEnd = ".TE\n"
tableCellStart = "T{\n"
tableCellEnd = "\nT}\n"
)
// NewRoffRenderer creates a new blackfriday Renderer for generating roff documents
// from markdown
func NewRoffRenderer() *roffRenderer { // nolint: golint
var extensions blackfriday.Extensions
extensions |= blackfriday.NoIntraEmphasis
extensions |= blackfriday.Tables
extensions |= blackfriday.FencedCode
extensions |= blackfriday.SpaceHeadings
extensions |= blackfriday.Footnotes
extensions |= blackfriday.Titleblock
extensions |= blackfriday.DefinitionLists
return &roffRenderer{
extensions: extensions,
}
}
// GetExtensions returns the list of extensions used by this renderer implementation
func (r *roffRenderer) GetExtensions() blackfriday.Extensions {
return r.extensions
}
// RenderHeader handles outputting the header at document start
func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) {
// disable hyphenation
out(w, ".nh\n")
}
// RenderFooter handles outputting the footer at the document end; the roff
// renderer has no footer information
func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) {
}
// RenderNode is called for each node in a markdown document; based on the node
// type the equivalent roff output is sent to the writer
func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
var walkAction = blackfriday.GoToNext
switch node.Type {
case blackfriday.Text:
r.handleText(w, node, entering)
case blackfriday.Softbreak:
out(w, crTag)
case blackfriday.Hardbreak:
out(w, breakTag)
case blackfriday.Emph:
if entering {
out(w, emphTag)
} else {
out(w, emphCloseTag)
}
case blackfriday.Strong:
if entering {
out(w, strongTag)
} else {
out(w, strongCloseTag)
}
case blackfriday.Link:
if !entering {
out(w, linkTag+string(node.LinkData.Destination)+linkCloseTag)
}
case blackfriday.Image:
// ignore images
walkAction = blackfriday.SkipChildren
case blackfriday.Code:
out(w, codespanTag)
escapeSpecialChars(w, node.Literal)
out(w, codespanCloseTag)
case blackfriday.Document:
break
case blackfriday.Paragraph:
// roff .PP markers break lists
if r.listDepth > 0 {
return blackfriday.GoToNext
}
if entering {
out(w, paraTag)
} else {
out(w, crTag)
}
case blackfriday.BlockQuote:
if entering {
out(w, quoteTag)
} else {
out(w, quoteCloseTag)
}
case blackfriday.Heading:
r.handleHeading(w, node, entering)
case blackfriday.HorizontalRule:
out(w, hruleTag)
case blackfriday.List:
r.handleList(w, node, entering)
case blackfriday.Item:
r.handleItem(w, node, entering)
case blackfriday.CodeBlock:
out(w, codeTag)
escapeSpecialChars(w, node.Literal)
out(w, codeCloseTag)
case blackfriday.Table:
r.handleTable(w, node, entering)
case blackfriday.TableCell:
r.handleTableCell(w, node, entering)
case blackfriday.TableHead:
case blackfriday.TableBody:
case blackfriday.TableRow:
// no action as cell entries do all the nroff formatting
return blackfriday.GoToNext
default:
fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String())
}
return walkAction
}
func (r *roffRenderer) handleText(w io.Writer, node *blackfriday.Node, entering bool) {
var (
start, end string
)
// handle special roff table cell text encapsulation
if node.Parent.Type == blackfriday.TableCell {
if len(node.Literal) > 30 {
start = tableCellStart
end = tableCellEnd
} else {
// end rows that aren't terminated by "tableCellEnd" with a cr if end of row
if node.Parent.Next == nil && !node.Parent.IsHeader {
end = crTag
}
}
}
out(w, start)
escapeSpecialChars(w, node.Literal)
out(w, end)
}
func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) {
if entering {
switch node.Level {
case 1:
if !r.firstHeader {
out(w, titleHeader)
r.firstHeader = true
break
}
out(w, topLevelHeader)
case 2:
out(w, secondLevelHdr)
default:
out(w, otherHeader)
}
}
}
func (r *roffRenderer) handleList(w io.Writer, node *blackfriday.Node, entering bool) {
openTag := listTag
closeTag := listCloseTag
if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
// tags for definition lists handled within Item node
openTag = ""
closeTag = ""
}
if entering {
r.listDepth++
if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
r.listCounters = append(r.listCounters, 1)
}
out(w, openTag)
} else {
if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
r.listCounters = r.listCounters[:len(r.listCounters)-1]
}
out(w, closeTag)
r.listDepth--
}
}
func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) {
if entering {
if node.ListFlags&blackfriday.ListTypeOrdered != 0 {
out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1]))
r.listCounters[len(r.listCounters)-1]++
} else if node.ListFlags&blackfriday.ListTypeDefinition != 0 {
// state machine for handling terms and following definitions
// since blackfriday does not distinguish them properly, nor
// does it seperate them into separate lists as it should
if !r.defineTerm {
out(w, arglistTag)
r.defineTerm = true
} else {
r.defineTerm = false
}
} else {
out(w, ".IP \\(bu 2\n")
}
} else {
out(w, "\n")
}
}
func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) {
if entering {
out(w, tableStart)
//call walker to count cells (and rows?) so format section can be produced
columns := countColumns(node)
out(w, strings.Repeat("l ", columns)+"\n")
out(w, strings.Repeat("l ", columns)+".\n")
} else {
out(w, tableEnd)
}
}
func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) {
var (
start, end string
)
if node.IsHeader {
start = codespanTag
end = codespanCloseTag
}
if entering {
if node.Prev != nil && node.Prev.Type == blackfriday.TableCell {
out(w, "\t"+start)
} else {
out(w, start)
}
} else {
// need to carriage return if we are at the end of the header row
if node.IsHeader && node.Next == nil {
end = end + crTag
}
out(w, end)
}
}
// because roff format requires knowing the column count before outputting any table
// data we need to walk a table tree and count the columns
func countColumns(node *blackfriday.Node) int {
var columns int
node.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
switch node.Type {
case blackfriday.TableRow:
if !entering {
return blackfriday.Terminate
}
case blackfriday.TableCell:
if entering {
columns++
}
default:
}
return blackfriday.GoToNext
})
return columns
}
func out(w io.Writer, output string) {
io.WriteString(w, output) // nolint: errcheck
}
func needsBackslash(c byte) bool {
for _, r := range []byte("-_&\\~") {
if c == r {
return true
}
}
return false
}
func escapeSpecialChars(w io.Writer, text []byte) {
for i := 0; i < len(text); i++ {
// escape initial apostrophe or period
if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') {
out(w, "\\&")
}
// directly copy normal characters
org := i
for i < len(text) && !needsBackslash(text[i]) {
i++
}
if i > org {
w.Write(text[org:i]) // nolint: errcheck
}
// escape a character
if i >= len(text) {
break
}
w.Write([]byte{'\\', text[i]}) // nolint: errcheck
}
}
+8
View File
@@ -0,0 +1,8 @@
*.out
*.swp
*.8
*.6
_obj
_test*
markdown
tags
+17
View File
@@ -0,0 +1,17 @@
sudo: false
language: go
go:
- "1.10.x"
- "1.11.x"
- tip
matrix:
fast_finish: true
allow_failures:
- go: tip
install:
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d -s .)
- go tool vet .
- go test -v ./...
+29
View File
@@ -0,0 +1,29 @@
Blackfriday is distributed under the Simplified BSD License:
> Copyright © 2011 Russ Ross
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions
> are met:
>
> 1. Redistributions of source code must retain the above copyright
> notice, this list of conditions and the following disclaimer.
>
> 2. Redistributions in binary form must reproduce the above
> copyright notice, this list of conditions and the following
> disclaimer in the documentation and/or other materials provided with
> the distribution.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
> POSSIBILITY OF SUCH DAMAGE.
+291
View File
@@ -0,0 +1,291 @@
Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday)
===========
Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It
is paranoid about its input (so you can safely feed it user-supplied
data), it is fast, it supports common extensions (tables, smart
punctuation substitutions, etc.), and it is safe for all utf-8
(unicode) input.
HTML output is currently supported, along with Smartypants
extensions.
It started as a translation from C of [Sundown][3].
Installation
------------
Blackfriday is compatible with any modern Go release. With Go 1.7 and git
installed:
go get gopkg.in/russross/blackfriday.v2
will download, compile, and install the package into your `$GOPATH`
directory hierarchy. Alternatively, you can achieve the same if you
import it into a project:
import "gopkg.in/russross/blackfriday.v2"
and `go get` without parameters.
Versions
--------
Currently maintained and recommended version of Blackfriday is `v2`. It's being
developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the
documentation is available at
https://godoc.org/gopkg.in/russross/blackfriday.v2.
It is `go get`-able via via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`,
but we highly recommend using package management tool like [dep][7] or
[Glide][8] and make use of semantic versioning. With package management you
should import `github.com/russross/blackfriday` and specify that you're using
version 2.0.0.
Version 2 offers a number of improvements over v1:
* Cleaned up API
* A separate call to [`Parse`][4], which produces an abstract syntax tree for
the document
* Latest bug fixes
* Flexibility to easily add your own rendering extensions
Potential drawbacks:
* Our benchmarks show v2 to be slightly slower than v1. Currently in the
ballpark of around 15%.
* API breakage. If you can't afford modifying your code to adhere to the new API
and don't care too much about the new features, v2 is probably not for you.
* Several bug fixes are trailing behind and still need to be forward-ported to
v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for
tracking.
Usage
-----
For the most sensible markdown processing, it is as simple as getting your input
into a byte slice and calling:
```go
output := blackfriday.Run(input)
```
Your input will be parsed and the output rendered with a set of most popular
extensions enabled. If you want the most basic feature set, corresponding with
the bare Markdown specification, use:
```go
output := blackfriday.Run(input, blackfriday.WithNoExtensions())
```
### Sanitize untrusted content
Blackfriday itself does nothing to protect against malicious content. If you are
dealing with user-supplied markdown, we recommend running Blackfriday's output
through HTML sanitizer such as [Bluemonday][5].
Here's an example of simple usage of Blackfriday together with Bluemonday:
```go
import (
"github.com/microcosm-cc/bluemonday"
"github.com/russross/blackfriday"
)
// ...
unsafe := blackfriday.Run(input)
html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
```
### Custom options
If you want to customize the set of options, use `blackfriday.WithExtensions`,
`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`.
You can also check out `blackfriday-tool` for a more complete example
of how to use it. Download and install it using:
go get github.com/russross/blackfriday-tool
This is a simple command-line tool that allows you to process a
markdown file using a standalone program. You can also browse the
source directly on github if you are just looking for some example
code:
* <http://github.com/russross/blackfriday-tool>
Note that if you have not already done so, installing
`blackfriday-tool` will be sufficient to download and install
blackfriday in addition to the tool itself. The tool binary will be
installed in `$GOPATH/bin`. This is a statically-linked binary that
can be copied to wherever you need it without worrying about
dependencies and library versions.
Features
--------
All features of Sundown are supported, including:
* **Compatibility**. The Markdown v1.0.3 test suite passes with
the `--tidy` option. Without `--tidy`, the differences are
mostly in whitespace and entity escaping, where blackfriday is
more consistent and cleaner.
* **Common extensions**, including table support, fenced code
blocks, autolinks, strikethroughs, non-strict emphasis, etc.
* **Safety**. Blackfriday is paranoid when parsing, making it safe
to feed untrusted user input without fear of bad things
happening. The test suite stress tests this and there are no
known inputs that make it crash. If you find one, please let me
know and send me the input that does it.
NOTE: "safety" in this context means *runtime safety only*. In order to
protect yourself against JavaScript injection in untrusted content, see
[this example](https://github.com/russross/blackfriday#sanitize-untrusted-content).
* **Fast processing**. It is fast enough to render on-demand in
most web applications without having to cache the output.
* **Thread safety**. You can run multiple parsers in different
goroutines without ill effect. There is no dependence on global
shared state.
* **Minimal dependencies**. Blackfriday only depends on standard
library packages in Go. The source code is pretty
self-contained, so it is easy to add to any project, including
Google App Engine projects.
* **Standards compliant**. Output successfully validates using the
W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional.
Extensions
----------
In addition to the standard markdown syntax, this package
implements the following extensions:
* **Intra-word emphasis supression**. The `_` character is
commonly used inside words when discussing code, so having
markdown interpret it as an emphasis command is usually the
wrong thing. Blackfriday lets you treat all emphasis markers as
normal characters when they occur inside a word.
* **Tables**. Tables can be created by drawing them in the input
using a simple syntax:
```
Name | Age
--------|------
Bob | 27
Alice | 23
```
* **Fenced code blocks**. In addition to the normal 4-space
indentation to mark code blocks, you can explicitly mark them
and supply a language (to make syntax highlighting simple). Just
mark it like this:
```go
func getTrue() bool {
return true
}
```
You can use 3 or more backticks to mark the beginning of the
block, and the same number to mark the end of the block.
* **Definition lists**. A simple definition list is made of a single-line
term followed by a colon and the definition for that term.
Cat
: Fluffy animal everyone likes
Internet
: Vector of transmission for pictures of cats
Terms must be separated from the previous definition by a blank line.
* **Footnotes**. A marker in the text that will become a superscript number;
a footnote definition that will be placed in a list of footnotes at the
end of the document. A footnote looks like this:
This is a footnote.[^1]
[^1]: the footnote text.
* **Autolinking**. Blackfriday can find URLs that have not been
explicitly marked as links and turn them into links.
* **Strikethrough**. Use two tildes (`~~`) to mark text that
should be crossed out.
* **Hard line breaks**. With this extension enabled newlines in the input
translate into line breaks in the output. This extension is off by default.
* **Smart quotes**. Smartypants-style punctuation substitution is
supported, turning normal double- and single-quote marks into
curly quotes, etc.
* **LaTeX-style dash parsing** is an additional option, where `--`
is translated into `&ndash;`, and `---` is translated into
`&mdash;`. This differs from most smartypants processors, which
turn a single hyphen into an ndash and a double hyphen into an
mdash.
* **Smart fractions**, where anything that looks like a fraction
is translated into suitable HTML (instead of just a few special
cases like most smartypant processors). For example, `4/5`
becomes `<sup>4</sup>&frasl;<sub>5</sub>`, which renders as
<sup>4</sup>&frasl;<sub>5</sub>.
Other renderers
---------------
Blackfriday is structured to allow alternative rendering engines. Here
are a few of note:
* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown):
provides a GitHub Flavored Markdown renderer with fenced code block
highlighting, clickable heading anchor links.
It's not customizable, and its goal is to produce HTML output
equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode),
except the rendering is performed locally.
* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt,
but for markdown.
* [LaTeX output](https://github.com/Ambrevar/Blackfriday-LaTeX):
renders output as LaTeX.
* [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer.
Todo
----
* More unit testing
* Improve unicode support. It does not understand all unicode
rules (about what constitutes a letter, a punctuation symbol,
etc.), so it may fail to detect word boundaries correctly in
some instances. It is safe on all utf-8 input.
License
-------
[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt)
[1]: https://daringfireball.net/projects/markdown/ "Markdown"
[2]: https://golang.org/ "Go Language"
[3]: https://github.com/vmg/sundown "Sundown"
[4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func"
[5]: https://github.com/microcosm-cc/bluemonday "Bluemonday"
[6]: https://labix.org/gopkg.in "gopkg.in"
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
// Package blackfriday is a markdown processor.
//
// It translates plain text with simple formatting rules into an AST, which can
// then be further processed to HTML (provided by Blackfriday itself) or other
// formats (provided by the community).
//
// The simplest way to invoke Blackfriday is to call the Run function. It will
// take a text input and produce a text output in HTML (or other format).
//
// A slightly more sophisticated way to use Blackfriday is to create a Markdown
// processor and to call Parse, which returns a syntax tree for the input
// document. You can leverage Blackfriday's parsing for content extraction from
// markdown documents. You can assign a custom renderer and set various options
// to the Markdown processor.
//
// If you're interested in calling Blackfriday from command line, see
// https://github.com/russross/blackfriday-tool.
package blackfriday
+34
View File
@@ -0,0 +1,34 @@
package blackfriday
import (
"html"
"io"
)
var htmlEscaper = [256][]byte{
'&': []byte("&amp;"),
'<': []byte("&lt;"),
'>': []byte("&gt;"),
'"': []byte("&quot;"),
}
func escapeHTML(w io.Writer, s []byte) {
var start, end int
for end < len(s) {
escSeq := htmlEscaper[s[end]]
if escSeq != nil {
w.Write(s[start:end])
w.Write(escSeq)
start = end + 1
}
end++
}
if start < len(s) && end <= len(s) {
w.Write(s[start:end])
}
}
func escLink(w io.Writer, text []byte) {
unesc := html.UnescapeString(string(text))
escapeHTML(w, []byte(unesc))
}
+1
View File
@@ -0,0 +1 @@
module github.com/russross/blackfriday/v2
+949
View File
@@ -0,0 +1,949 @@
//
// Blackfriday Markdown Processor
// Available at http://github.com/russross/blackfriday
//
// Copyright © 2011 Russ Ross <russ@russross.com>.
// Distributed under the Simplified BSD License.
// See README.md for details.
//
//
//
// HTML rendering backend
//
//
package blackfriday
import (
"bytes"
"fmt"
"io"
"regexp"
"strings"
)
// HTMLFlags control optional behavior of HTML renderer.
type HTMLFlags int
// HTML renderer configuration options.
const (
HTMLFlagsNone HTMLFlags = 0
SkipHTML HTMLFlags = 1 << iota // Skip preformatted HTML blocks
SkipImages // Skip embedded images
SkipLinks // Skip all links
Safelink // Only link to trusted protocols
NofollowLinks // Only link with rel="nofollow"
NoreferrerLinks // Only link with rel="noreferrer"
NoopenerLinks // Only link with rel="noopener"
HrefTargetBlank // Add a blank target
CompletePage // Generate a complete HTML page
UseXHTML // Generate XHTML output instead of HTML
FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source
Smartypants // Enable smart punctuation substitutions
SmartypantsFractions // Enable smart fractions (with Smartypants)
SmartypantsDashes // Enable smart dashes (with Smartypants)
SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants)
SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering
SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants)
TOC // Generate a table of contents
)
var (
htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag)
)
const (
htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" +
processingInstruction + "|" + declaration + "|" + cdata + ")"
closeTag = "</" + tagName + "\\s*[>]"
openTag = "<" + tagName + attribute + "*" + "\\s*/?>"
attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)"
attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")"
attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")"
attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*"
cdata = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>"
declaration = "<![A-Z]+" + "\\s+[^>]*>"
doubleQuotedValue = "\"[^\"]*\""
htmlComment = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->"
processingInstruction = "[<][?].*?[?][>]"
singleQuotedValue = "'[^']*'"
tagName = "[A-Za-z][A-Za-z0-9-]*"
unquotedValue = "[^\"'=<>`\\x00-\\x20]+"
)
// HTMLRendererParameters is a collection of supplementary parameters tweaking
// the behavior of various parts of HTML renderer.
type HTMLRendererParameters struct {
// Prepend this text to each relative URL.
AbsolutePrefix string
// Add this text to each footnote anchor, to ensure uniqueness.
FootnoteAnchorPrefix string
// Show this text inside the <a> tag for a footnote return link, if the
// HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string
// <sup>[return]</sup> is used.
FootnoteReturnLinkContents string
// If set, add this text to the front of each Heading ID, to ensure
// uniqueness.
HeadingIDPrefix string
// If set, add this text to the back of each Heading ID, to ensure uniqueness.
HeadingIDSuffix string
// Increase heading levels: if the offset is 1, <h1> becomes <h2> etc.
// Negative offset is also valid.
// Resulting levels are clipped between 1 and 6.
HeadingLevelOffset int
Title string // Document title (used if CompletePage is set)
CSS string // Optional CSS file URL (used if CompletePage is set)
Icon string // Optional icon file URL (used if CompletePage is set)
Flags HTMLFlags // Flags allow customizing this renderer's behavior
}
// HTMLRenderer is a type that implements the Renderer interface for HTML output.
//
// Do not create this directly, instead use the NewHTMLRenderer function.
type HTMLRenderer struct {
HTMLRendererParameters
closeTag string // how to end singleton tags: either " />" or ">"
// Track heading IDs to prevent ID collision in a single generation.
headingIDs map[string]int
lastOutputLen int
disableTags int
sr *SPRenderer
}
const (
xhtmlClose = " />"
htmlClose = ">"
)
// NewHTMLRenderer creates and configures an HTMLRenderer object, which
// satisfies the Renderer interface.
func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer {
// configure the rendering engine
closeTag := htmlClose
if params.Flags&UseXHTML != 0 {
closeTag = xhtmlClose
}
if params.FootnoteReturnLinkContents == "" {
params.FootnoteReturnLinkContents = `<sup>[return]</sup>`
}
return &HTMLRenderer{
HTMLRendererParameters: params,
closeTag: closeTag,
headingIDs: make(map[string]int),
sr: NewSmartypantsRenderer(params.Flags),
}
}
func isHTMLTag(tag []byte, tagname string) bool {
found, _ := findHTMLTagPos(tag, tagname)
return found
}
// Look for a character, but ignore it when it's in any kind of quotes, it
// might be JavaScript
func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int {
inSingleQuote := false
inDoubleQuote := false
inGraveQuote := false
i := start
for i < len(html) {
switch {
case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote:
return i
case html[i] == '\'':
inSingleQuote = !inSingleQuote
case html[i] == '"':
inDoubleQuote = !inDoubleQuote
case html[i] == '`':
inGraveQuote = !inGraveQuote
}
i++
}
return start
}
func findHTMLTagPos(tag []byte, tagname string) (bool, int) {
i := 0
if i < len(tag) && tag[0] != '<' {
return false, -1
}
i++
i = skipSpace(tag, i)
if i < len(tag) && tag[i] == '/' {
i++
}
i = skipSpace(tag, i)
j := 0
for ; i < len(tag); i, j = i+1, j+1 {
if j >= len(tagname) {
break
}
if strings.ToLower(string(tag[i]))[0] != tagname[j] {
return false, -1
}
}
if i == len(tag) {
return false, -1
}
rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>')
if rightAngle >= i {
return true, rightAngle
}
return false, -1
}
func skipSpace(tag []byte, i int) int {
for i < len(tag) && isspace(tag[i]) {
i++
}
return i
}
func isRelativeLink(link []byte) (yes bool) {
// a tag begin with '#'
if link[0] == '#' {
return true
}
// link begin with '/' but not '//', the second maybe a protocol relative link
if len(link) >= 2 && link[0] == '/' && link[1] != '/' {
return true
}
// only the root '/'
if len(link) == 1 && link[0] == '/' {
return true
}
// current directory : begin with "./"
if bytes.HasPrefix(link, []byte("./")) {
return true
}
// parent directory : begin with "../"
if bytes.HasPrefix(link, []byte("../")) {
return true
}
return false
}
func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string {
for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] {
tmp := fmt.Sprintf("%s-%d", id, count+1)
if _, tmpFound := r.headingIDs[tmp]; !tmpFound {
r.headingIDs[id] = count + 1
id = tmp
} else {
id = id + "-1"
}
}
if _, found := r.headingIDs[id]; !found {
r.headingIDs[id] = 0
}
return id
}
func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte {
if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' {
newDest := r.AbsolutePrefix
if link[0] != '/' {
newDest += "/"
}
newDest += string(link)
return []byte(newDest)
}
return link
}
func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string {
if isRelativeLink(link) {
return attrs
}
val := []string{}
if flags&NofollowLinks != 0 {
val = append(val, "nofollow")
}
if flags&NoreferrerLinks != 0 {
val = append(val, "noreferrer")
}
if flags&NoopenerLinks != 0 {
val = append(val, "noopener")
}
if flags&HrefTargetBlank != 0 {
attrs = append(attrs, "target=\"_blank\"")
}
if len(val) == 0 {
return attrs
}
attr := fmt.Sprintf("rel=%q", strings.Join(val, " "))
return append(attrs, attr)
}
func isMailto(link []byte) bool {
return bytes.HasPrefix(link, []byte("mailto:"))
}
func needSkipLink(flags HTMLFlags, dest []byte) bool {
if flags&SkipLinks != 0 {
return true
}
return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest)
}
func isSmartypantable(node *Node) bool {
pt := node.Parent.Type
return pt != Link && pt != CodeBlock && pt != Code
}
func appendLanguageAttr(attrs []string, info []byte) []string {
if len(info) == 0 {
return attrs
}
endOfLang := bytes.IndexAny(info, "\t ")
if endOfLang < 0 {
endOfLang = len(info)
}
return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang]))
}
func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) {
w.Write(name)
if len(attrs) > 0 {
w.Write(spaceBytes)
w.Write([]byte(strings.Join(attrs, " ")))
}
w.Write(gtBytes)
r.lastOutputLen = 1
}
func footnoteRef(prefix string, node *Node) []byte {
urlFrag := prefix + string(slugify(node.Destination))
anchor := fmt.Sprintf(`<a href="#fn:%s">%d</a>`, urlFrag, node.NoteID)
return []byte(fmt.Sprintf(`<sup class="footnote-ref" id="fnref:%s">%s</sup>`, urlFrag, anchor))
}
func footnoteItem(prefix string, slug []byte) []byte {
return []byte(fmt.Sprintf(`<li id="fn:%s%s">`, prefix, slug))
}
func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte {
const format = ` <a class="footnote-return" href="#fnref:%s%s">%s</a>`
return []byte(fmt.Sprintf(format, prefix, slug, returnLink))
}
func itemOpenCR(node *Node) bool {
if node.Prev == nil {
return false
}
ld := node.Parent.ListData
return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0
}
func skipParagraphTags(node *Node) bool {
grandparent := node.Parent.Parent
if grandparent == nil || grandparent.Type != List {
return false
}
tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0
return grandparent.Type == List && tightOrTerm
}
func cellAlignment(align CellAlignFlags) string {
switch align {
case TableAlignmentLeft:
return "left"
case TableAlignmentRight:
return "right"
case TableAlignmentCenter:
return "center"
default:
return ""
}
}
func (r *HTMLRenderer) out(w io.Writer, text []byte) {
if r.disableTags > 0 {
w.Write(htmlTagRe.ReplaceAll(text, []byte{}))
} else {
w.Write(text)
}
r.lastOutputLen = len(text)
}
func (r *HTMLRenderer) cr(w io.Writer) {
if r.lastOutputLen > 0 {
r.out(w, nlBytes)
}
}
var (
nlBytes = []byte{'\n'}
gtBytes = []byte{'>'}
spaceBytes = []byte{' '}
)
var (
brTag = []byte("<br>")
brXHTMLTag = []byte("<br />")
emTag = []byte("<em>")
emCloseTag = []byte("</em>")
strongTag = []byte("<strong>")
strongCloseTag = []byte("</strong>")
delTag = []byte("<del>")
delCloseTag = []byte("</del>")
ttTag = []byte("<tt>")
ttCloseTag = []byte("</tt>")
aTag = []byte("<a")
aCloseTag = []byte("</a>")
preTag = []byte("<pre>")
preCloseTag = []byte("</pre>")
codeTag = []byte("<code>")
codeCloseTag = []byte("</code>")
pTag = []byte("<p>")
pCloseTag = []byte("</p>")
blockquoteTag = []byte("<blockquote>")
blockquoteCloseTag = []byte("</blockquote>")
hrTag = []byte("<hr>")
hrXHTMLTag = []byte("<hr />")
ulTag = []byte("<ul>")
ulCloseTag = []byte("</ul>")
olTag = []byte("<ol>")
olCloseTag = []byte("</ol>")
dlTag = []byte("<dl>")
dlCloseTag = []byte("</dl>")
liTag = []byte("<li>")
liCloseTag = []byte("</li>")
ddTag = []byte("<dd>")
ddCloseTag = []byte("</dd>")
dtTag = []byte("<dt>")
dtCloseTag = []byte("</dt>")
tableTag = []byte("<table>")
tableCloseTag = []byte("</table>")
tdTag = []byte("<td")
tdCloseTag = []byte("</td>")
thTag = []byte("<th")
thCloseTag = []byte("</th>")
theadTag = []byte("<thead>")
theadCloseTag = []byte("</thead>")
tbodyTag = []byte("<tbody>")
tbodyCloseTag = []byte("</tbody>")
trTag = []byte("<tr>")
trCloseTag = []byte("</tr>")
h1Tag = []byte("<h1")
h1CloseTag = []byte("</h1>")
h2Tag = []byte("<h2")
h2CloseTag = []byte("</h2>")
h3Tag = []byte("<h3")
h3CloseTag = []byte("</h3>")
h4Tag = []byte("<h4")
h4CloseTag = []byte("</h4>")
h5Tag = []byte("<h5")
h5CloseTag = []byte("</h5>")
h6Tag = []byte("<h6")
h6CloseTag = []byte("</h6>")
footnotesDivBytes = []byte("\n<div class=\"footnotes\">\n\n")
footnotesCloseDivBytes = []byte("\n</div>\n")
)
func headingTagsFromLevel(level int) ([]byte, []byte) {
if level <= 1 {
return h1Tag, h1CloseTag
}
switch level {
case 2:
return h2Tag, h2CloseTag
case 3:
return h3Tag, h3CloseTag
case 4:
return h4Tag, h4CloseTag
case 5:
return h5Tag, h5CloseTag
}
return h6Tag, h6CloseTag
}
func (r *HTMLRenderer) outHRTag(w io.Writer) {
if r.Flags&UseXHTML == 0 {
r.out(w, hrTag)
} else {
r.out(w, hrXHTMLTag)
}
}
// RenderNode is a default renderer of a single node of a syntax tree. For
// block nodes it will be called twice: first time with entering=true, second
// time with entering=false, so that it could know when it's working on an open
// tag and when on close. It writes the result to w.
//
// The return value is a way to tell the calling walker to adjust its walk
// pattern: e.g. it can terminate the traversal by returning Terminate. Or it
// can ask the walker to skip a subtree of this node by returning SkipChildren.
// The typical behavior is to return GoToNext, which asks for the usual
// traversal to the next node.
func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus {
attrs := []string{}
switch node.Type {
case Text:
if r.Flags&Smartypants != 0 {
var tmp bytes.Buffer
escapeHTML(&tmp, node.Literal)
r.sr.Process(w, tmp.Bytes())
} else {
if node.Parent.Type == Link {
escLink(w, node.Literal)
} else {
escapeHTML(w, node.Literal)
}
}
case Softbreak:
r.cr(w)
// TODO: make it configurable via out(renderer.softbreak)
case Hardbreak:
if r.Flags&UseXHTML == 0 {
r.out(w, brTag)
} else {
r.out(w, brXHTMLTag)
}
r.cr(w)
case Emph:
if entering {
r.out(w, emTag)
} else {
r.out(w, emCloseTag)
}
case Strong:
if entering {
r.out(w, strongTag)
} else {
r.out(w, strongCloseTag)
}
case Del:
if entering {
r.out(w, delTag)
} else {
r.out(w, delCloseTag)
}
case HTMLSpan:
if r.Flags&SkipHTML != 0 {
break
}
r.out(w, node.Literal)
case Link:
// mark it but don't link it if it is not a safe link: no smartypants
dest := node.LinkData.Destination
if needSkipLink(r.Flags, dest) {
if entering {
r.out(w, ttTag)
} else {
r.out(w, ttCloseTag)
}
} else {
if entering {
dest = r.addAbsPrefix(dest)
var hrefBuf bytes.Buffer
hrefBuf.WriteString("href=\"")
escLink(&hrefBuf, dest)
hrefBuf.WriteByte('"')
attrs = append(attrs, hrefBuf.String())
if node.NoteID != 0 {
r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node))
break
}
attrs = appendLinkAttrs(attrs, r.Flags, dest)
if len(node.LinkData.Title) > 0 {
var titleBuff bytes.Buffer
titleBuff.WriteString("title=\"")
escapeHTML(&titleBuff, node.LinkData.Title)
titleBuff.WriteByte('"')
attrs = append(attrs, titleBuff.String())
}
r.tag(w, aTag, attrs)
} else {
if node.NoteID != 0 {
break
}
r.out(w, aCloseTag)
}
}
case Image:
if r.Flags&SkipImages != 0 {
return SkipChildren
}
if entering {
dest := node.LinkData.Destination
dest = r.addAbsPrefix(dest)
if r.disableTags == 0 {
//if options.safe && potentiallyUnsafe(dest) {
//out(w, `<img src="" alt="`)
//} else {
r.out(w, []byte(`<img src="`))
escLink(w, dest)
r.out(w, []byte(`" alt="`))
//}
}
r.disableTags++
} else {
r.disableTags--
if r.disableTags == 0 {
if node.LinkData.Title != nil {
r.out(w, []byte(`" title="`))
escapeHTML(w, node.LinkData.Title)
}
r.out(w, []byte(`" />`))
}
}
case Code:
r.out(w, codeTag)
escapeHTML(w, node.Literal)
r.out(w, codeCloseTag)
case Document:
break
case Paragraph:
if skipParagraphTags(node) {
break
}
if entering {
// TODO: untangle this clusterfuck about when the newlines need
// to be added and when not.
if node.Prev != nil {
switch node.Prev.Type {
case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule:
r.cr(w)
}
}
if node.Parent.Type == BlockQuote && node.Prev == nil {
r.cr(w)
}
r.out(w, pTag)
} else {
r.out(w, pCloseTag)
if !(node.Parent.Type == Item && node.Next == nil) {
r.cr(w)
}
}
case BlockQuote:
if entering {
r.cr(w)
r.out(w, blockquoteTag)
} else {
r.out(w, blockquoteCloseTag)
r.cr(w)
}
case HTMLBlock:
if r.Flags&SkipHTML != 0 {
break
}
r.cr(w)
r.out(w, node.Literal)
r.cr(w)
case Heading:
headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level
openTag, closeTag := headingTagsFromLevel(headingLevel)
if entering {
if node.IsTitleblock {
attrs = append(attrs, `class="title"`)
}
if node.HeadingID != "" {
id := r.ensureUniqueHeadingID(node.HeadingID)
if r.HeadingIDPrefix != "" {
id = r.HeadingIDPrefix + id
}
if r.HeadingIDSuffix != "" {
id = id + r.HeadingIDSuffix
}
attrs = append(attrs, fmt.Sprintf(`id="%s"`, id))
}
r.cr(w)
r.tag(w, openTag, attrs)
} else {
r.out(w, closeTag)
if !(node.Parent.Type == Item && node.Next == nil) {
r.cr(w)
}
}
case HorizontalRule:
r.cr(w)
r.outHRTag(w)
r.cr(w)
case List:
openTag := ulTag
closeTag := ulCloseTag
if node.ListFlags&ListTypeOrdered != 0 {
openTag = olTag
closeTag = olCloseTag
}
if node.ListFlags&ListTypeDefinition != 0 {
openTag = dlTag
closeTag = dlCloseTag
}
if entering {
if node.IsFootnotesList {
r.out(w, footnotesDivBytes)
r.outHRTag(w)
r.cr(w)
}
r.cr(w)
if node.Parent.Type == Item && node.Parent.Parent.Tight {
r.cr(w)
}
r.tag(w, openTag[:len(openTag)-1], attrs)
r.cr(w)
} else {
r.out(w, closeTag)
//cr(w)
//if node.parent.Type != Item {
// cr(w)
//}
if node.Parent.Type == Item && node.Next != nil {
r.cr(w)
}
if node.Parent.Type == Document || node.Parent.Type == BlockQuote {
r.cr(w)
}
if node.IsFootnotesList {
r.out(w, footnotesCloseDivBytes)
}
}
case Item:
openTag := liTag
closeTag := liCloseTag
if node.ListFlags&ListTypeDefinition != 0 {
openTag = ddTag
closeTag = ddCloseTag
}
if node.ListFlags&ListTypeTerm != 0 {
openTag = dtTag
closeTag = dtCloseTag
}
if entering {
if itemOpenCR(node) {
r.cr(w)
}
if node.ListData.RefLink != nil {
slug := slugify(node.ListData.RefLink)
r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug))
break
}
r.out(w, openTag)
} else {
if node.ListData.RefLink != nil {
slug := slugify(node.ListData.RefLink)
if r.Flags&FootnoteReturnLinks != 0 {
r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug))
}
}
r.out(w, closeTag)
r.cr(w)
}
case CodeBlock:
attrs = appendLanguageAttr(attrs, node.Info)
r.cr(w)
r.out(w, preTag)
r.tag(w, codeTag[:len(codeTag)-1], attrs)
escapeHTML(w, node.Literal)
r.out(w, codeCloseTag)
r.out(w, preCloseTag)
if node.Parent.Type != Item {
r.cr(w)
}
case Table:
if entering {
r.cr(w)
r.out(w, tableTag)
} else {
r.out(w, tableCloseTag)
r.cr(w)
}
case TableCell:
openTag := tdTag
closeTag := tdCloseTag
if node.IsHeader {
openTag = thTag
closeTag = thCloseTag
}
if entering {
align := cellAlignment(node.Align)
if align != "" {
attrs = append(attrs, fmt.Sprintf(`align="%s"`, align))
}
if node.Prev == nil {
r.cr(w)
}
r.tag(w, openTag, attrs)
} else {
r.out(w, closeTag)
r.cr(w)
}
case TableHead:
if entering {
r.cr(w)
r.out(w, theadTag)
} else {
r.out(w, theadCloseTag)
r.cr(w)
}
case TableBody:
if entering {
r.cr(w)
r.out(w, tbodyTag)
// XXX: this is to adhere to a rather silly test. Should fix test.
if node.FirstChild == nil {
r.cr(w)
}
} else {
r.out(w, tbodyCloseTag)
r.cr(w)
}
case TableRow:
if entering {
r.cr(w)
r.out(w, trTag)
} else {
r.out(w, trCloseTag)
r.cr(w)
}
default:
panic("Unknown node type " + node.Type.String())
}
return GoToNext
}
// RenderHeader writes HTML document preamble and TOC if requested.
func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) {
r.writeDocumentHeader(w)
if r.Flags&TOC != 0 {
r.writeTOC(w, ast)
}
}
// RenderFooter writes HTML document footer.
func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) {
if r.Flags&CompletePage == 0 {
return
}
io.WriteString(w, "\n</body>\n</html>\n")
}
func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) {
if r.Flags&CompletePage == 0 {
return
}
ending := ""
if r.Flags&UseXHTML != 0 {
io.WriteString(w, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ")
io.WriteString(w, "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n")
io.WriteString(w, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n")
ending = " /"
} else {
io.WriteString(w, "<!DOCTYPE html>\n")
io.WriteString(w, "<html>\n")
}
io.WriteString(w, "<head>\n")
io.WriteString(w, " <title>")
if r.Flags&Smartypants != 0 {
r.sr.Process(w, []byte(r.Title))
} else {
escapeHTML(w, []byte(r.Title))
}
io.WriteString(w, "</title>\n")
io.WriteString(w, " <meta name=\"GENERATOR\" content=\"Blackfriday Markdown Processor v")
io.WriteString(w, Version)
io.WriteString(w, "\"")
io.WriteString(w, ending)
io.WriteString(w, ">\n")
io.WriteString(w, " <meta charset=\"utf-8\"")
io.WriteString(w, ending)
io.WriteString(w, ">\n")
if r.CSS != "" {
io.WriteString(w, " <link rel=\"stylesheet\" type=\"text/css\" href=\"")
escapeHTML(w, []byte(r.CSS))
io.WriteString(w, "\"")
io.WriteString(w, ending)
io.WriteString(w, ">\n")
}
if r.Icon != "" {
io.WriteString(w, " <link rel=\"icon\" type=\"image/x-icon\" href=\"")
escapeHTML(w, []byte(r.Icon))
io.WriteString(w, "\"")
io.WriteString(w, ending)
io.WriteString(w, ">\n")
}
io.WriteString(w, "</head>\n")
io.WriteString(w, "<body>\n\n")
}
func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) {
buf := bytes.Buffer{}
inHeading := false
tocLevel := 0
headingCount := 0
ast.Walk(func(node *Node, entering bool) WalkStatus {
if node.Type == Heading && !node.HeadingData.IsTitleblock {
inHeading = entering
if entering {
node.HeadingID = fmt.Sprintf("toc_%d", headingCount)
if node.Level == tocLevel {
buf.WriteString("</li>\n\n<li>")
} else if node.Level < tocLevel {
for node.Level < tocLevel {
tocLevel--
buf.WriteString("</li>\n</ul>")
}
buf.WriteString("</li>\n\n<li>")
} else {
for node.Level > tocLevel {
tocLevel++
buf.WriteString("\n<ul>\n<li>")
}
}
fmt.Fprintf(&buf, `<a href="#toc_%d">`, headingCount)
headingCount++
} else {
buf.WriteString("</a>")
}
return GoToNext
}
if inHeading {
return r.RenderNode(&buf, node, entering)
}
return GoToNext
})
for ; tocLevel > 0; tocLevel-- {
buf.WriteString("</li>\n</ul>")
}
if buf.Len() > 0 {
io.WriteString(w, "<nav>\n")
w.Write(buf.Bytes())
io.WriteString(w, "\n\n</nav>\n")
}
r.lastOutputLen = buf.Len()
}
File diff suppressed because it is too large Load Diff
+950
View File
@@ -0,0 +1,950 @@
// Blackfriday Markdown Processor
// Available at http://github.com/russross/blackfriday
//
// Copyright © 2011 Russ Ross <russ@russross.com>.
// Distributed under the Simplified BSD License.
// See README.md for details.
package blackfriday
import (
"bytes"
"fmt"
"io"
"strings"
"unicode/utf8"
)
//
// Markdown parsing and processing
//
// Version string of the package. Appears in the rendered document when
// CompletePage flag is on.
const Version = "2.0"
// Extensions is a bitwise or'ed collection of enabled Blackfriday's
// extensions.
type Extensions int
// These are the supported markdown parsing extensions.
// OR these values together to select multiple extensions.
const (
NoExtensions Extensions = 0
NoIntraEmphasis Extensions = 1 << iota // Ignore emphasis markers inside words
Tables // Render tables
FencedCode // Render fenced code blocks
Autolink // Detect embedded URLs that are not explicitly marked
Strikethrough // Strikethrough text using ~~test~~
LaxHTMLBlocks // Loosen up HTML block parsing rules
SpaceHeadings // Be strict about prefix heading rules
HardLineBreak // Translate newlines into line breaks
TabSizeEight // Expand tabs to eight spaces instead of four
Footnotes // Pandoc-style footnotes
NoEmptyLineBeforeBlock // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block
HeadingIDs // specify heading IDs with {#id}
Titleblock // Titleblock ala pandoc
AutoHeadingIDs // Create the heading ID from the text
BackslashLineBreak // Translate trailing backslashes into line breaks
DefinitionLists // Render definition lists
CommonHTMLFlags HTMLFlags = UseXHTML | Smartypants |
SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes
CommonExtensions Extensions = NoIntraEmphasis | Tables | FencedCode |
Autolink | Strikethrough | SpaceHeadings | HeadingIDs |
BackslashLineBreak | DefinitionLists
)
// ListType contains bitwise or'ed flags for list and list item objects.
type ListType int
// These are the possible flag values for the ListItem renderer.
// Multiple flag values may be ORed together.
// These are mostly of interest if you are writing a new output format.
const (
ListTypeOrdered ListType = 1 << iota
ListTypeDefinition
ListTypeTerm
ListItemContainsBlock
ListItemBeginningOfList // TODO: figure out if this is of any use now
ListItemEndOfList
)
// CellAlignFlags holds a type of alignment in a table cell.
type CellAlignFlags int
// These are the possible flag values for the table cell renderer.
// Only a single one of these values will be used; they are not ORed together.
// These are mostly of interest if you are writing a new output format.
const (
TableAlignmentLeft CellAlignFlags = 1 << iota
TableAlignmentRight
TableAlignmentCenter = (TableAlignmentLeft | TableAlignmentRight)
)
// The size of a tab stop.
const (
TabSizeDefault = 4
TabSizeDouble = 8
)
// blockTags is a set of tags that are recognized as HTML block tags.
// Any of these can be included in markdown text without special escaping.
var blockTags = map[string]struct{}{
"blockquote": {},
"del": {},
"div": {},
"dl": {},
"fieldset": {},
"form": {},
"h1": {},
"h2": {},
"h3": {},
"h4": {},
"h5": {},
"h6": {},
"iframe": {},
"ins": {},
"math": {},
"noscript": {},
"ol": {},
"pre": {},
"p": {},
"script": {},
"style": {},
"table": {},
"ul": {},
// HTML5
"address": {},
"article": {},
"aside": {},
"canvas": {},
"figcaption": {},
"figure": {},
"footer": {},
"header": {},
"hgroup": {},
"main": {},
"nav": {},
"output": {},
"progress": {},
"section": {},
"video": {},
}
// Renderer is the rendering interface. This is mostly of interest if you are
// implementing a new rendering format.
//
// Only an HTML implementation is provided in this repository, see the README
// for external implementations.
type Renderer interface {
// RenderNode is the main rendering method. It will be called once for
// every leaf node and twice for every non-leaf node (first with
// entering=true, then with entering=false). The method should write its
// rendition of the node to the supplied writer w.
RenderNode(w io.Writer, node *Node, entering bool) WalkStatus
// RenderHeader is a method that allows the renderer to produce some
// content preceding the main body of the output document. The header is
// understood in the broad sense here. For example, the default HTML
// renderer will write not only the HTML document preamble, but also the
// table of contents if it was requested.
//
// The method will be passed an entire document tree, in case a particular
// implementation needs to inspect it to produce output.
//
// The output should be written to the supplied writer w. If your
// implementation has no header to write, supply an empty implementation.
RenderHeader(w io.Writer, ast *Node)
// RenderFooter is a symmetric counterpart of RenderHeader.
RenderFooter(w io.Writer, ast *Node)
}
// Callback functions for inline parsing. One such function is defined
// for each character that triggers a response when parsing inline data.
type inlineParser func(p *Markdown, data []byte, offset int) (int, *Node)
// Markdown is a type that holds extensions and the runtime state used by
// Parse, and the renderer. You can not use it directly, construct it with New.
type Markdown struct {
renderer Renderer
referenceOverride ReferenceOverrideFunc
refs map[string]*reference
inlineCallback [256]inlineParser
extensions Extensions
nesting int
maxNesting int
insideLink bool
// Footnotes need to be ordered as well as available to quickly check for
// presence. If a ref is also a footnote, it's stored both in refs and here
// in notes. Slice is nil if footnotes not enabled.
notes []*reference
doc *Node
tip *Node // = doc
oldTip *Node
lastMatchedContainer *Node // = doc
allClosed bool
}
func (p *Markdown) getRef(refid string) (ref *reference, found bool) {
if p.referenceOverride != nil {
r, overridden := p.referenceOverride(refid)
if overridden {
if r == nil {
return nil, false
}
return &reference{
link: []byte(r.Link),
title: []byte(r.Title),
noteID: 0,
hasBlock: false,
text: []byte(r.Text)}, true
}
}
// refs are case insensitive
ref, found = p.refs[strings.ToLower(refid)]
return ref, found
}
func (p *Markdown) finalize(block *Node) {
above := block.Parent
block.open = false
p.tip = above
}
func (p *Markdown) addChild(node NodeType, offset uint32) *Node {
return p.addExistingChild(NewNode(node), offset)
}
func (p *Markdown) addExistingChild(node *Node, offset uint32) *Node {
for !p.tip.canContain(node.Type) {
p.finalize(p.tip)
}
p.tip.AppendChild(node)
p.tip = node
return node
}
func (p *Markdown) closeUnmatchedBlocks() {
if !p.allClosed {
for p.oldTip != p.lastMatchedContainer {
parent := p.oldTip.Parent
p.finalize(p.oldTip)
p.oldTip = parent
}
p.allClosed = true
}
}
//
//
// Public interface
//
//
// Reference represents the details of a link.
// See the documentation in Options for more details on use-case.
type Reference struct {
// Link is usually the URL the reference points to.
Link string
// Title is the alternate text describing the link in more detail.
Title string
// Text is the optional text to override the ref with if the syntax used was
// [refid][]
Text string
}
// ReferenceOverrideFunc is expected to be called with a reference string and
// return either a valid Reference type that the reference string maps to or
// nil. If overridden is false, the default reference logic will be executed.
// See the documentation in Options for more details on use-case.
type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool)
// New constructs a Markdown processor. You can use the same With* functions as
// for Run() to customize parser's behavior and the renderer.
func New(opts ...Option) *Markdown {
var p Markdown
for _, opt := range opts {
opt(&p)
}
p.refs = make(map[string]*reference)
p.maxNesting = 16
p.insideLink = false
docNode := NewNode(Document)
p.doc = docNode
p.tip = docNode
p.oldTip = docNode
p.lastMatchedContainer = docNode
p.allClosed = true
// register inline parsers
p.inlineCallback[' '] = maybeLineBreak
p.inlineCallback['*'] = emphasis
p.inlineCallback['_'] = emphasis
if p.extensions&Strikethrough != 0 {
p.inlineCallback['~'] = emphasis
}
p.inlineCallback['`'] = codeSpan
p.inlineCallback['\n'] = lineBreak
p.inlineCallback['['] = link
p.inlineCallback['<'] = leftAngle
p.inlineCallback['\\'] = escape
p.inlineCallback['&'] = entity
p.inlineCallback['!'] = maybeImage
p.inlineCallback['^'] = maybeInlineFootnote
if p.extensions&Autolink != 0 {
p.inlineCallback['h'] = maybeAutoLink
p.inlineCallback['m'] = maybeAutoLink
p.inlineCallback['f'] = maybeAutoLink
p.inlineCallback['H'] = maybeAutoLink
p.inlineCallback['M'] = maybeAutoLink
p.inlineCallback['F'] = maybeAutoLink
}
if p.extensions&Footnotes != 0 {
p.notes = make([]*reference, 0)
}
return &p
}
// Option customizes the Markdown processor's default behavior.
type Option func(*Markdown)
// WithRenderer allows you to override the default renderer.
func WithRenderer(r Renderer) Option {
return func(p *Markdown) {
p.renderer = r
}
}
// WithExtensions allows you to pick some of the many extensions provided by
// Blackfriday. You can bitwise OR them.
func WithExtensions(e Extensions) Option {
return func(p *Markdown) {
p.extensions = e
}
}
// WithNoExtensions turns off all extensions and custom behavior.
func WithNoExtensions() Option {
return func(p *Markdown) {
p.extensions = NoExtensions
p.renderer = NewHTMLRenderer(HTMLRendererParameters{
Flags: HTMLFlagsNone,
})
}
}
// WithRefOverride sets an optional function callback that is called every
// time a reference is resolved.
//
// In Markdown, the link reference syntax can be made to resolve a link to
// a reference instead of an inline URL, in one of the following ways:
//
// * [link text][refid]
// * [refid][]
//
// Usually, the refid is defined at the bottom of the Markdown document. If
// this override function is provided, the refid is passed to the override
// function first, before consulting the defined refids at the bottom. If
// the override function indicates an override did not occur, the refids at
// the bottom will be used to fill in the link details.
func WithRefOverride(o ReferenceOverrideFunc) Option {
return func(p *Markdown) {
p.referenceOverride = o
}
}
// Run is the main entry point to Blackfriday. It parses and renders a
// block of markdown-encoded text.
//
// The simplest invocation of Run takes one argument, input:
// output := Run(input)
// This will parse the input with CommonExtensions enabled and render it with
// the default HTMLRenderer (with CommonHTMLFlags).
//
// Variadic arguments opts can customize the default behavior. Since Markdown
// type does not contain exported fields, you can not use it directly. Instead,
// use the With* functions. For example, this will call the most basic
// functionality, with no extensions:
// output := Run(input, WithNoExtensions())
//
// You can use any number of With* arguments, even contradicting ones. They
// will be applied in order of appearance and the latter will override the
// former:
// output := Run(input, WithNoExtensions(), WithExtensions(exts),
// WithRenderer(yourRenderer))
func Run(input []byte, opts ...Option) []byte {
r := NewHTMLRenderer(HTMLRendererParameters{
Flags: CommonHTMLFlags,
})
optList := []Option{WithRenderer(r), WithExtensions(CommonExtensions)}
optList = append(optList, opts...)
parser := New(optList...)
ast := parser.Parse(input)
var buf bytes.Buffer
parser.renderer.RenderHeader(&buf, ast)
ast.Walk(func(node *Node, entering bool) WalkStatus {
return parser.renderer.RenderNode(&buf, node, entering)
})
parser.renderer.RenderFooter(&buf, ast)
return buf.Bytes()
}
// Parse is an entry point to the parsing part of Blackfriday. It takes an
// input markdown document and produces a syntax tree for its contents. This
// tree can then be rendered with a default or custom renderer, or
// analyzed/transformed by the caller to whatever non-standard needs they have.
// The return value is the root node of the syntax tree.
func (p *Markdown) Parse(input []byte) *Node {
p.block(input)
// Walk the tree and finish up some of unfinished blocks
for p.tip != nil {
p.finalize(p.tip)
}
// Walk the tree again and process inline markdown in each block
p.doc.Walk(func(node *Node, entering bool) WalkStatus {
if node.Type == Paragraph || node.Type == Heading || node.Type == TableCell {
p.inline(node, node.content)
node.content = nil
}
return GoToNext
})
p.parseRefsToAST()
return p.doc
}
func (p *Markdown) parseRefsToAST() {
if p.extensions&Footnotes == 0 || len(p.notes) == 0 {
return
}
p.tip = p.doc
block := p.addBlock(List, nil)
block.IsFootnotesList = true
block.ListFlags = ListTypeOrdered
flags := ListItemBeginningOfList
// Note: this loop is intentionally explicit, not range-form. This is
// because the body of the loop will append nested footnotes to p.notes and
// we need to process those late additions. Range form would only walk over
// the fixed initial set.
for i := 0; i < len(p.notes); i++ {
ref := p.notes[i]
p.addExistingChild(ref.footnote, 0)
block := ref.footnote
block.ListFlags = flags | ListTypeOrdered
block.RefLink = ref.link
if ref.hasBlock {
flags |= ListItemContainsBlock
p.block(ref.title)
} else {
p.inline(block, ref.title)
}
flags &^= ListItemBeginningOfList | ListItemContainsBlock
}
above := block.Parent
finalizeList(block)
p.tip = above
block.Walk(func(node *Node, entering bool) WalkStatus {
if node.Type == Paragraph || node.Type == Heading {
p.inline(node, node.content)
node.content = nil
}
return GoToNext
})
}
//
// Link references
//
// This section implements support for references that (usually) appear
// as footnotes in a document, and can be referenced anywhere in the document.
// The basic format is:
//
// [1]: http://www.google.com/ "Google"
// [2]: http://www.github.com/ "Github"
//
// Anywhere in the document, the reference can be linked by referring to its
// label, i.e., 1 and 2 in this example, as in:
//
// This library is hosted on [Github][2], a git hosting site.
//
// Actual footnotes as specified in Pandoc and supported by some other Markdown
// libraries such as php-markdown are also taken care of. They look like this:
//
// This sentence needs a bit of further explanation.[^note]
//
// [^note]: This is the explanation.
//
// Footnotes should be placed at the end of the document in an ordered list.
// Finally, there are inline footnotes such as:
//
// Inline footnotes^[Also supported.] provide a quick inline explanation,
// but are rendered at the bottom of the document.
//
// reference holds all information necessary for a reference-style links or
// footnotes.
//
// Consider this markdown with reference-style links:
//
// [link][ref]
//
// [ref]: /url/ "tooltip title"
//
// It will be ultimately converted to this HTML:
//
// <p><a href=\"/url/\" title=\"title\">link</a></p>
//
// And a reference structure will be populated as follows:
//
// p.refs["ref"] = &reference{
// link: "/url/",
// title: "tooltip title",
// }
//
// Alternatively, reference can contain information about a footnote. Consider
// this markdown:
//
// Text needing a footnote.[^a]
//
// [^a]: This is the note
//
// A reference structure will be populated as follows:
//
// p.refs["a"] = &reference{
// link: "a",
// title: "This is the note",
// noteID: <some positive int>,
// }
//
// TODO: As you can see, it begs for splitting into two dedicated structures
// for refs and for footnotes.
type reference struct {
link []byte
title []byte
noteID int // 0 if not a footnote ref
hasBlock bool
footnote *Node // a link to the Item node within a list of footnotes
text []byte // only gets populated by refOverride feature with Reference.Text
}
func (r *reference) String() string {
return fmt.Sprintf("{link: %q, title: %q, text: %q, noteID: %d, hasBlock: %v}",
r.link, r.title, r.text, r.noteID, r.hasBlock)
}
// Check whether or not data starts with a reference link.
// If so, it is parsed and stored in the list of references
// (in the render struct).
// Returns the number of bytes to skip to move past it,
// or zero if the first line is not a reference.
func isReference(p *Markdown, data []byte, tabSize int) int {
// up to 3 optional leading spaces
if len(data) < 4 {
return 0
}
i := 0
for i < 3 && data[i] == ' ' {
i++
}
noteID := 0
// id part: anything but a newline between brackets
if data[i] != '[' {
return 0
}
i++
if p.extensions&Footnotes != 0 {
if i < len(data) && data[i] == '^' {
// we can set it to anything here because the proper noteIds will
// be assigned later during the second pass. It just has to be != 0
noteID = 1
i++
}
}
idOffset := i
for i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != ']' {
i++
}
if i >= len(data) || data[i] != ']' {
return 0
}
idEnd := i
// footnotes can have empty ID, like this: [^], but a reference can not be
// empty like this: []. Break early if it's not a footnote and there's no ID
if noteID == 0 && idOffset == idEnd {
return 0
}
// spacer: colon (space | tab)* newline? (space | tab)*
i++
if i >= len(data) || data[i] != ':' {
return 0
}
i++
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
i++
}
if i < len(data) && (data[i] == '\n' || data[i] == '\r') {
i++
if i < len(data) && data[i] == '\n' && data[i-1] == '\r' {
i++
}
}
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
i++
}
if i >= len(data) {
return 0
}
var (
linkOffset, linkEnd int
titleOffset, titleEnd int
lineEnd int
raw []byte
hasBlock bool
)
if p.extensions&Footnotes != 0 && noteID != 0 {
linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize)
lineEnd = linkEnd
} else {
linkOffset, linkEnd, titleOffset, titleEnd, lineEnd = scanLinkRef(p, data, i)
}
if lineEnd == 0 {
return 0
}
// a valid ref has been found
ref := &reference{
noteID: noteID,
hasBlock: hasBlock,
}
if noteID > 0 {
// reusing the link field for the id since footnotes don't have links
ref.link = data[idOffset:idEnd]
// if footnote, it's not really a title, it's the contained text
ref.title = raw
} else {
ref.link = data[linkOffset:linkEnd]
ref.title = data[titleOffset:titleEnd]
}
// id matches are case-insensitive
id := string(bytes.ToLower(data[idOffset:idEnd]))
p.refs[id] = ref
return lineEnd
}
func scanLinkRef(p *Markdown, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) {
// link: whitespace-free sequence, optionally between angle brackets
if data[i] == '<' {
i++
}
linkOffset = i
for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' {
i++
}
linkEnd = i
if data[linkOffset] == '<' && data[linkEnd-1] == '>' {
linkOffset++
linkEnd--
}
// optional spacer: (space | tab)* (newline | '\'' | '"' | '(' )
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
i++
}
if i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' && data[i] != '"' && data[i] != '(' {
return
}
// compute end-of-line
if i >= len(data) || data[i] == '\r' || data[i] == '\n' {
lineEnd = i
}
if i+1 < len(data) && data[i] == '\r' && data[i+1] == '\n' {
lineEnd++
}
// optional (space|tab)* spacer after a newline
if lineEnd > 0 {
i = lineEnd + 1
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
i++
}
}
// optional title: any non-newline sequence enclosed in '"() alone on its line
if i+1 < len(data) && (data[i] == '\'' || data[i] == '"' || data[i] == '(') {
i++
titleOffset = i
// look for EOL
for i < len(data) && data[i] != '\n' && data[i] != '\r' {
i++
}
if i+1 < len(data) && data[i] == '\n' && data[i+1] == '\r' {
titleEnd = i + 1
} else {
titleEnd = i
}
// step back
i--
for i > titleOffset && (data[i] == ' ' || data[i] == '\t') {
i--
}
if i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')') {
lineEnd = titleEnd
titleEnd = i
}
}
return
}
// The first bit of this logic is the same as Parser.listItem, but the rest
// is much simpler. This function simply finds the entire block and shifts it
// over by one tab if it is indeed a block (just returns the line if it's not).
// blockEnd is the end of the section in the input buffer, and contents is the
// extracted text that was shifted over one tab. It will need to be rendered at
// the end of the document.
func scanFootnote(p *Markdown, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) {
if i == 0 || len(data) == 0 {
return
}
// skip leading whitespace on first line
for i < len(data) && data[i] == ' ' {
i++
}
blockStart = i
// find the end of the line
blockEnd = i
for i < len(data) && data[i-1] != '\n' {
i++
}
// get working buffer
var raw bytes.Buffer
// put the first line into the working buffer
raw.Write(data[blockEnd:i])
blockEnd = i
// process the following lines
containsBlankLine := false
gatherLines:
for blockEnd < len(data) {
i++
// find the end of this line
for i < len(data) && data[i-1] != '\n' {
i++
}
// if it is an empty line, guess that it is part of this item
// and move on to the next line
if p.isEmpty(data[blockEnd:i]) > 0 {
containsBlankLine = true
blockEnd = i
continue
}
n := 0
if n = isIndented(data[blockEnd:i], indentSize); n == 0 {
// this is the end of the block.
// we don't want to include this last line in the index.
break gatherLines
}
// if there were blank lines before this one, insert a new one now
if containsBlankLine {
raw.WriteByte('\n')
containsBlankLine = false
}
// get rid of that first tab, write to buffer
raw.Write(data[blockEnd+n : i])
hasBlock = true
blockEnd = i
}
if data[blockEnd-1] != '\n' {
raw.WriteByte('\n')
}
contents = raw.Bytes()
return
}
//
//
// Miscellaneous helper functions
//
//
// Test if a character is a punctuation symbol.
// Taken from a private function in regexp in the stdlib.
func ispunct(c byte) bool {
for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") {
if c == r {
return true
}
}
return false
}
// Test if a character is a whitespace character.
func isspace(c byte) bool {
return ishorizontalspace(c) || isverticalspace(c)
}
// Test if a character is a horizontal whitespace character.
func ishorizontalspace(c byte) bool {
return c == ' ' || c == '\t'
}
// Test if a character is a vertical character.
func isverticalspace(c byte) bool {
return c == '\n' || c == '\r' || c == '\f' || c == '\v'
}
// Test if a character is letter.
func isletter(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
// Test if a character is a letter or a digit.
// TODO: check when this is looking for ASCII alnum and when it should use unicode
func isalnum(c byte) bool {
return (c >= '0' && c <= '9') || isletter(c)
}
// Replace tab characters with spaces, aligning to the next TAB_SIZE column.
// always ends output with a newline
func expandTabs(out *bytes.Buffer, line []byte, tabSize int) {
// first, check for common cases: no tabs, or only tabs at beginning of line
i, prefix := 0, 0
slowcase := false
for i = 0; i < len(line); i++ {
if line[i] == '\t' {
if prefix == i {
prefix++
} else {
slowcase = true
break
}
}
}
// no need to decode runes if all tabs are at the beginning of the line
if !slowcase {
for i = 0; i < prefix*tabSize; i++ {
out.WriteByte(' ')
}
out.Write(line[prefix:])
return
}
// the slow case: we need to count runes to figure out how
// many spaces to insert for each tab
column := 0
i = 0
for i < len(line) {
start := i
for i < len(line) && line[i] != '\t' {
_, size := utf8.DecodeRune(line[i:])
i += size
column++
}
if i > start {
out.Write(line[start:i])
}
if i >= len(line) {
break
}
for {
out.WriteByte(' ')
column++
if column%tabSize == 0 {
break
}
}
i++
}
}
// Find if a line counts as indented or not.
// Returns number of characters the indent is (0 = not indented).
func isIndented(data []byte, indentSize int) int {
if len(data) == 0 {
return 0
}
if data[0] == '\t' {
return 1
}
if len(data) < indentSize {
return 0
}
for i := 0; i < indentSize; i++ {
if data[i] != ' ' {
return 0
}
}
return indentSize
}
// Create a url-safe slug for fragments
func slugify(in []byte) []byte {
if len(in) == 0 {
return in
}
out := make([]byte, 0, len(in))
sym := false
for _, ch := range in {
if isalnum(ch) {
sym = false
out = append(out, ch)
} else if sym {
continue
} else {
out = append(out, '-')
sym = true
}
}
var a, b int
var ch byte
for a, ch = range out {
if ch != '-' {
break
}
}
for b = len(out) - 1; b > 0; b-- {
if out[b] != '-' {
break
}
}
return out[a : b+1]
}
+354
View File
@@ -0,0 +1,354 @@
package blackfriday
import (
"bytes"
"fmt"
)
// NodeType specifies a type of a single node of a syntax tree. Usually one
// node (and its type) corresponds to a single markdown feature, e.g. emphasis
// or code block.
type NodeType int
// Constants for identifying different types of nodes. See NodeType.
const (
Document NodeType = iota
BlockQuote
List
Item
Paragraph
Heading
HorizontalRule
Emph
Strong
Del
Link
Image
Text
HTMLBlock
CodeBlock
Softbreak
Hardbreak
Code
HTMLSpan
Table
TableCell
TableHead
TableBody
TableRow
)
var nodeTypeNames = []string{
Document: "Document",
BlockQuote: "BlockQuote",
List: "List",
Item: "Item",
Paragraph: "Paragraph",
Heading: "Heading",
HorizontalRule: "HorizontalRule",
Emph: "Emph",
Strong: "Strong",
Del: "Del",
Link: "Link",
Image: "Image",
Text: "Text",
HTMLBlock: "HTMLBlock",
CodeBlock: "CodeBlock",
Softbreak: "Softbreak",
Hardbreak: "Hardbreak",
Code: "Code",
HTMLSpan: "HTMLSpan",
Table: "Table",
TableCell: "TableCell",
TableHead: "TableHead",
TableBody: "TableBody",
TableRow: "TableRow",
}
func (t NodeType) String() string {
return nodeTypeNames[t]
}
// ListData contains fields relevant to a List and Item node type.
type ListData struct {
ListFlags ListType
Tight bool // Skip <p>s around list item data if true
BulletChar byte // '*', '+' or '-' in bullet lists
Delimiter byte // '.' or ')' after the number in ordered lists
RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering
IsFootnotesList bool // This is a list of footnotes
}
// LinkData contains fields relevant to a Link node type.
type LinkData struct {
Destination []byte // Destination is what goes into a href
Title []byte // Title is the tooltip thing that goes in a title attribute
NoteID int // NoteID contains a serial number of a footnote, zero if it's not a footnote
Footnote *Node // If it's a footnote, this is a direct link to the footnote Node. Otherwise nil.
}
// CodeBlockData contains fields relevant to a CodeBlock node type.
type CodeBlockData struct {
IsFenced bool // Specifies whether it's a fenced code block or an indented one
Info []byte // This holds the info string
FenceChar byte
FenceLength int
FenceOffset int
}
// TableCellData contains fields relevant to a TableCell node type.
type TableCellData struct {
IsHeader bool // This tells if it's under the header row
Align CellAlignFlags // This holds the value for align attribute
}
// HeadingData contains fields relevant to a Heading node type.
type HeadingData struct {
Level int // This holds the heading level number
HeadingID string // This might hold heading ID, if present
IsTitleblock bool // Specifies whether it's a title block
}
// Node is a single element in the abstract syntax tree of the parsed document.
// It holds connections to the structurally neighboring nodes and, for certain
// types of nodes, additional information that might be needed when rendering.
type Node struct {
Type NodeType // Determines the type of the node
Parent *Node // Points to the parent
FirstChild *Node // Points to the first child, if any
LastChild *Node // Points to the last child, if any
Prev *Node // Previous sibling; nil if it's the first child
Next *Node // Next sibling; nil if it's the last child
Literal []byte // Text contents of the leaf nodes
HeadingData // Populated if Type is Heading
ListData // Populated if Type is List
CodeBlockData // Populated if Type is CodeBlock
LinkData // Populated if Type is Link
TableCellData // Populated if Type is TableCell
content []byte // Markdown content of the block nodes
open bool // Specifies an open block node that has not been finished to process yet
}
// NewNode allocates a node of a specified type.
func NewNode(typ NodeType) *Node {
return &Node{
Type: typ,
open: true,
}
}
func (n *Node) String() string {
ellipsis := ""
snippet := n.Literal
if len(snippet) > 16 {
snippet = snippet[:16]
ellipsis = "..."
}
return fmt.Sprintf("%s: '%s%s'", n.Type, snippet, ellipsis)
}
// Unlink removes node 'n' from the tree.
// It panics if the node is nil.
func (n *Node) Unlink() {
if n.Prev != nil {
n.Prev.Next = n.Next
} else if n.Parent != nil {
n.Parent.FirstChild = n.Next
}
if n.Next != nil {
n.Next.Prev = n.Prev
} else if n.Parent != nil {
n.Parent.LastChild = n.Prev
}
n.Parent = nil
n.Next = nil
n.Prev = nil
}
// AppendChild adds a node 'child' as a child of 'n'.
// It panics if either node is nil.
func (n *Node) AppendChild(child *Node) {
child.Unlink()
child.Parent = n
if n.LastChild != nil {
n.LastChild.Next = child
child.Prev = n.LastChild
n.LastChild = child
} else {
n.FirstChild = child
n.LastChild = child
}
}
// InsertBefore inserts 'sibling' immediately before 'n'.
// It panics if either node is nil.
func (n *Node) InsertBefore(sibling *Node) {
sibling.Unlink()
sibling.Prev = n.Prev
if sibling.Prev != nil {
sibling.Prev.Next = sibling
}
sibling.Next = n
n.Prev = sibling
sibling.Parent = n.Parent
if sibling.Prev == nil {
sibling.Parent.FirstChild = sibling
}
}
func (n *Node) isContainer() bool {
switch n.Type {
case Document:
fallthrough
case BlockQuote:
fallthrough
case List:
fallthrough
case Item:
fallthrough
case Paragraph:
fallthrough
case Heading:
fallthrough
case Emph:
fallthrough
case Strong:
fallthrough
case Del:
fallthrough
case Link:
fallthrough
case Image:
fallthrough
case Table:
fallthrough
case TableHead:
fallthrough
case TableBody:
fallthrough
case TableRow:
fallthrough
case TableCell:
return true
default:
return false
}
}
func (n *Node) canContain(t NodeType) bool {
if n.Type == List {
return t == Item
}
if n.Type == Document || n.Type == BlockQuote || n.Type == Item {
return t != Item
}
if n.Type == Table {
return t == TableHead || t == TableBody
}
if n.Type == TableHead || n.Type == TableBody {
return t == TableRow
}
if n.Type == TableRow {
return t == TableCell
}
return false
}
// WalkStatus allows NodeVisitor to have some control over the tree traversal.
// It is returned from NodeVisitor and different values allow Node.Walk to
// decide which node to go to next.
type WalkStatus int
const (
// GoToNext is the default traversal of every node.
GoToNext WalkStatus = iota
// SkipChildren tells walker to skip all children of current node.
SkipChildren
// Terminate tells walker to terminate the traversal.
Terminate
)
// NodeVisitor is a callback to be called when traversing the syntax tree.
// Called twice for every node: once with entering=true when the branch is
// first visited, then with entering=false after all the children are done.
type NodeVisitor func(node *Node, entering bool) WalkStatus
// Walk is a convenience method that instantiates a walker and starts a
// traversal of subtree rooted at n.
func (n *Node) Walk(visitor NodeVisitor) {
w := newNodeWalker(n)
for w.current != nil {
status := visitor(w.current, w.entering)
switch status {
case GoToNext:
w.next()
case SkipChildren:
w.entering = false
w.next()
case Terminate:
return
}
}
}
type nodeWalker struct {
current *Node
root *Node
entering bool
}
func newNodeWalker(root *Node) *nodeWalker {
return &nodeWalker{
current: root,
root: root,
entering: true,
}
}
func (nw *nodeWalker) next() {
if (!nw.current.isContainer() || !nw.entering) && nw.current == nw.root {
nw.current = nil
return
}
if nw.entering && nw.current.isContainer() {
if nw.current.FirstChild != nil {
nw.current = nw.current.FirstChild
nw.entering = true
} else {
nw.entering = false
}
} else if nw.current.Next == nil {
nw.current = nw.current.Parent
nw.entering = false
} else {
nw.current = nw.current.Next
nw.entering = true
}
}
func dump(ast *Node) {
fmt.Println(dumpString(ast))
}
func dumpR(ast *Node, depth int) string {
if ast == nil {
return ""
}
indent := bytes.Repeat([]byte("\t"), depth)
content := ast.Literal
if content == nil {
content = ast.content
}
result := fmt.Sprintf("%s%s(%q)\n", indent, ast.Type, content)
for n := ast.FirstChild; n != nil; n = n.Next {
result += dumpR(n, depth+1)
}
return result
}
func dumpString(ast *Node) string {
return dumpR(ast, 0)
}
+457
View File
@@ -0,0 +1,457 @@
//
// Blackfriday Markdown Processor
// Available at http://github.com/russross/blackfriday
//
// Copyright © 2011 Russ Ross <russ@russross.com>.
// Distributed under the Simplified BSD License.
// See README.md for details.
//
//
//
// SmartyPants rendering
//
//
package blackfriday
import (
"bytes"
"io"
)
// SPRenderer is a struct containing state of a Smartypants renderer.
type SPRenderer struct {
inSingleQuote bool
inDoubleQuote bool
callbacks [256]smartCallback
}
func wordBoundary(c byte) bool {
return c == 0 || isspace(c) || ispunct(c)
}
func tolower(c byte) byte {
if c >= 'A' && c <= 'Z' {
return c - 'A' + 'a'
}
return c
}
func isdigit(c byte) bool {
return c >= '0' && c <= '9'
}
func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool {
// edge of the buffer is likely to be a tag that we don't get to see,
// so we treat it like text sometimes
// enumerate all sixteen possibilities for (previousChar, nextChar)
// each can be one of {0, space, punct, other}
switch {
case previousChar == 0 && nextChar == 0:
// context is not any help here, so toggle
*isOpen = !*isOpen
case isspace(previousChar) && nextChar == 0:
// [ "] might be [ "<code>foo...]
*isOpen = true
case ispunct(previousChar) && nextChar == 0:
// [!"] hmm... could be [Run!"] or [("<code>...]
*isOpen = false
case /* isnormal(previousChar) && */ nextChar == 0:
// [a"] is probably a close
*isOpen = false
case previousChar == 0 && isspace(nextChar):
// [" ] might be [...foo</code>" ]
*isOpen = false
case isspace(previousChar) && isspace(nextChar):
// [ " ] context is not any help here, so toggle
*isOpen = !*isOpen
case ispunct(previousChar) && isspace(nextChar):
// [!" ] is probably a close
*isOpen = false
case /* isnormal(previousChar) && */ isspace(nextChar):
// [a" ] this is one of the easy cases
*isOpen = false
case previousChar == 0 && ispunct(nextChar):
// ["!] hmm... could be ["$1.95] or [</code>"!...]
*isOpen = false
case isspace(previousChar) && ispunct(nextChar):
// [ "!] looks more like [ "$1.95]
*isOpen = true
case ispunct(previousChar) && ispunct(nextChar):
// [!"!] context is not any help here, so toggle
*isOpen = !*isOpen
case /* isnormal(previousChar) && */ ispunct(nextChar):
// [a"!] is probably a close
*isOpen = false
case previousChar == 0 /* && isnormal(nextChar) */ :
// ["a] is probably an open
*isOpen = true
case isspace(previousChar) /* && isnormal(nextChar) */ :
// [ "a] this is one of the easy cases
*isOpen = true
case ispunct(previousChar) /* && isnormal(nextChar) */ :
// [!"a] is probably an open
*isOpen = true
default:
// [a'b] maybe a contraction?
*isOpen = false
}
// Note that with the limited lookahead, this non-breaking
// space will also be appended to single double quotes.
if addNBSP && !*isOpen {
out.WriteString("&nbsp;")
}
out.WriteByte('&')
if *isOpen {
out.WriteByte('l')
} else {
out.WriteByte('r')
}
out.WriteByte(quote)
out.WriteString("quo;")
if addNBSP && *isOpen {
out.WriteString("&nbsp;")
}
return true
}
func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
if len(text) >= 2 {
t1 := tolower(text[1])
if t1 == '\'' {
nextChar := byte(0)
if len(text) >= 3 {
nextChar = text[2]
}
if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) {
return 1
}
}
if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) {
out.WriteString("&rsquo;")
return 0
}
if len(text) >= 3 {
t2 := tolower(text[2])
if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) &&
(len(text) < 4 || wordBoundary(text[3])) {
out.WriteString("&rsquo;")
return 0
}
}
}
nextChar := byte(0)
if len(text) > 1 {
nextChar = text[1]
}
if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) {
return 0
}
out.WriteByte(text[0])
return 0
}
func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []byte) int {
if len(text) >= 3 {
t1 := tolower(text[1])
t2 := tolower(text[2])
if t1 == 'c' && t2 == ')' {
out.WriteString("&copy;")
return 2
}
if t1 == 'r' && t2 == ')' {
out.WriteString("&reg;")
return 2
}
if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' {
out.WriteString("&trade;")
return 3
}
}
out.WriteByte(text[0])
return 0
}
func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte) int {
if len(text) >= 2 {
if text[1] == '-' {
out.WriteString("&mdash;")
return 1
}
if wordBoundary(previousChar) && wordBoundary(text[1]) {
out.WriteString("&ndash;")
return 0
}
}
out.WriteByte(text[0])
return 0
}
func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text []byte) int {
if len(text) >= 3 && text[1] == '-' && text[2] == '-' {
out.WriteString("&mdash;")
return 2
}
if len(text) >= 2 && text[1] == '-' {
out.WriteString("&ndash;")
return 1
}
out.WriteByte(text[0])
return 0
}
func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte, addNBSP bool) int {
if bytes.HasPrefix(text, []byte("&quot;")) {
nextChar := byte(0)
if len(text) >= 7 {
nextChar = text[6]
}
if smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, addNBSP) {
return 5
}
}
if bytes.HasPrefix(text, []byte("&#0;")) {
return 3
}
out.WriteByte('&')
return 0
}
func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int {
var quote byte = 'd'
if angledQuotes {
quote = 'a'
}
return func(out *bytes.Buffer, previousChar byte, text []byte) int {
return r.smartAmpVariant(out, previousChar, text, quote, addNBSP)
}
}
func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int {
if len(text) >= 3 && text[1] == '.' && text[2] == '.' {
out.WriteString("&hellip;")
return 2
}
if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' {
out.WriteString("&hellip;")
return 4
}
out.WriteByte(text[0])
return 0
}
func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text []byte) int {
if len(text) >= 2 && text[1] == '`' {
nextChar := byte(0)
if len(text) >= 3 {
nextChar = text[2]
}
if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) {
return 1
}
}
out.WriteByte(text[0])
return 0
}
func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, text []byte) int {
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
// is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b
// note: check for regular slash (/) or fraction slash (, 0x2044, or 0xe2 81 84 in utf-8)
// and avoid changing dates like 1/23/2005 into fractions.
numEnd := 0
for len(text) > numEnd && isdigit(text[numEnd]) {
numEnd++
}
if numEnd == 0 {
out.WriteByte(text[0])
return 0
}
denStart := numEnd + 1
if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 {
denStart = numEnd + 3
} else if len(text) < numEnd+2 || text[numEnd] != '/' {
out.WriteByte(text[0])
return 0
}
denEnd := denStart
for len(text) > denEnd && isdigit(text[denEnd]) {
denEnd++
}
if denEnd == denStart {
out.WriteByte(text[0])
return 0
}
if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' {
out.WriteString("<sup>")
out.Write(text[:numEnd])
out.WriteString("</sup>&frasl;<sub>")
out.Write(text[denStart:denEnd])
out.WriteString("</sub>")
return denEnd - 1
}
}
out.WriteByte(text[0])
return 0
}
func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []byte) int {
if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 {
if text[0] == '1' && text[1] == '/' && text[2] == '2' {
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' {
out.WriteString("&frac12;")
return 2
}
}
if text[0] == '1' && text[1] == '/' && text[2] == '4' {
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') {
out.WriteString("&frac14;")
return 2
}
}
if text[0] == '3' && text[1] == '/' && text[2] == '4' {
if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') {
out.WriteString("&frac34;")
return 2
}
}
}
out.WriteByte(text[0])
return 0
}
func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int {
nextChar := byte(0)
if len(text) > 1 {
nextChar = text[1]
}
if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) {
out.WriteString("&quot;")
}
return 0
}
func (r *SPRenderer) smartDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
return r.smartDoubleQuoteVariant(out, previousChar, text, 'd')
}
func (r *SPRenderer) smartAngledDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int {
return r.smartDoubleQuoteVariant(out, previousChar, text, 'a')
}
func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text []byte) int {
i := 0
for i < len(text) && text[i] != '>' {
i++
}
out.Write(text[:i+1])
return i
}
type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int
// NewSmartypantsRenderer constructs a Smartypants renderer object.
func NewSmartypantsRenderer(flags HTMLFlags) *SPRenderer {
var (
r SPRenderer
smartAmpAngled = r.smartAmp(true, false)
smartAmpAngledNBSP = r.smartAmp(true, true)
smartAmpRegular = r.smartAmp(false, false)
smartAmpRegularNBSP = r.smartAmp(false, true)
addNBSP = flags&SmartypantsQuotesNBSP != 0
)
if flags&SmartypantsAngledQuotes == 0 {
r.callbacks['"'] = r.smartDoubleQuote
if !addNBSP {
r.callbacks['&'] = smartAmpRegular
} else {
r.callbacks['&'] = smartAmpRegularNBSP
}
} else {
r.callbacks['"'] = r.smartAngledDoubleQuote
if !addNBSP {
r.callbacks['&'] = smartAmpAngled
} else {
r.callbacks['&'] = smartAmpAngledNBSP
}
}
r.callbacks['\''] = r.smartSingleQuote
r.callbacks['('] = r.smartParens
if flags&SmartypantsDashes != 0 {
if flags&SmartypantsLatexDashes == 0 {
r.callbacks['-'] = r.smartDash
} else {
r.callbacks['-'] = r.smartDashLatex
}
}
r.callbacks['.'] = r.smartPeriod
if flags&SmartypantsFractions == 0 {
r.callbacks['1'] = r.smartNumber
r.callbacks['3'] = r.smartNumber
} else {
for ch := '1'; ch <= '9'; ch++ {
r.callbacks[ch] = r.smartNumberGeneric
}
}
r.callbacks['<'] = r.smartLeftAngle
r.callbacks['`'] = r.smartBacktick
return &r
}
// Process is the entry point of the Smartypants renderer.
func (r *SPRenderer) Process(w io.Writer, text []byte) {
mark := 0
for i := 0; i < len(text); i++ {
if action := r.callbacks[text[i]]; action != nil {
if i > mark {
w.Write(text[mark:i])
}
previousChar := byte(0)
if i > 0 {
previousChar = text[i-1]
}
var tmp bytes.Buffer
i += action(&tmp, previousChar, text[i:])
w.Write(tmp.Bytes())
mark = i + 1
}
}
if mark < len(text) {
w.Write(text[mark:])
}
}
+16
View File
@@ -0,0 +1,16 @@
sudo: false
language: go
go:
- 1.x
- master
matrix:
allow_failures:
- go: master
fast_finish: true
install:
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
script:
- go get -t -v ./...
- diff -u <(echo -n) <(gofmt -d -s .)
- go tool vet .
- go test -v -race ./...
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2015 Dmitri Shuralyov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+36
View File
@@ -0,0 +1,36 @@
sanitized_anchor_name
=====================
[![Build Status](https://travis-ci.org/shurcooL/sanitized_anchor_name.svg?branch=master)](https://travis-ci.org/shurcooL/sanitized_anchor_name) [![GoDoc](https://godoc.org/github.com/shurcooL/sanitized_anchor_name?status.svg)](https://godoc.org/github.com/shurcooL/sanitized_anchor_name)
Package sanitized_anchor_name provides a func to create sanitized anchor names.
Its logic can be reused by multiple packages to create interoperable anchor names
and links to those anchors.
At this time, it does not try to ensure that generated anchor names
are unique, that responsibility falls on the caller.
Installation
------------
```bash
go get -u github.com/shurcooL/sanitized_anchor_name
```
Example
-------
```Go
anchorName := sanitized_anchor_name.Create("This is a header")
fmt.Println(anchorName)
// Output:
// this-is-a-header
```
License
-------
- [MIT License](LICENSE)
+1
View File
@@ -0,0 +1 @@
module github.com/shurcooL/sanitized_anchor_name
+29
View File
@@ -0,0 +1,29 @@
// Package sanitized_anchor_name provides a func to create sanitized anchor names.
//
// Its logic can be reused by multiple packages to create interoperable anchor names
// and links to those anchors.
//
// At this time, it does not try to ensure that generated anchor names
// are unique, that responsibility falls on the caller.
package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name"
import "unicode"
// Create returns a sanitized anchor name for the given text.
func Create(text string) string {
var anchorName []rune
var futureDash = false
for _, r := range text {
switch {
case unicode.IsLetter(r) || unicode.IsNumber(r):
if futureDash && len(anchorName) > 0 {
anchorName = append(anchorName, '-')
}
futureDash = false
anchorName = append(anchorName, unicode.ToLower(r))
default:
futureDash = true
}
}
return string(anchorName)
}
+7
View File
@@ -0,0 +1,7 @@
*.coverprofile
*.orig
node_modules/
vendor
.idea
internal/*/built-example
coverage.txt
+74
View File
@@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting Dan Buch at dan@meatballhat.com. All complaints will be
reviewed and investigated and will result in a response that is deemed necessary
and appropriate to the circumstances. The project team is obligated to maintain
confidentiality with regard to the reporter of an incident. Further details of
specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
+66
View File
@@ -0,0 +1,66 @@
cli
===
[![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://godoc.org/github.com/urfave/cli)
[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli)
[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli)
[![codecov](https://codecov.io/gh/urfave/cli/branch/master/graph/badge.svg)](https://codecov.io/gh/urfave/cli)
cli is a simple, fast, and fun package for building command line apps in Go. The
goal is to enable developers to write fast and distributable command line
applications in an expressive way.
## Usage Documentation
Usage documentation exists for each major version. Don't know what version you're on? You're probably using the version from the `master` branch, which is currently `v2`.
- `v2` - [./docs/v2/manual.md](./docs/v2/manual.md)
- `v1` - [./docs/v1/manual.md](./docs/v1/manual.md)
## Installation
Make sure you have a working Go environment. Go version 1.11+ is supported. [See the install instructions for Go](http://golang.org/doc/install.html).
Go Modules are strongly recommended when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules).
### Using `v2` releases
```
$ GO111MODULE=on go get github.com/urfave/cli/v2
```
```go
...
import (
"github.com/urfave/cli/v2" // imports as package "cli"
)
...
```
### Using `v1` releases
```
$ GO111MODULE=on go get github.com/urfave/cli
```
```go
...
import (
"github.com/urfave/cli"
)
...
```
### GOPATH
Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can
be easily used:
```
export PATH=$PATH:$GOPATH/bin
```
### Supported platforms
cli is tested against multiple versions of Go on Linux, and against the latest
released version of Go on OS X and Windows. This project uses Github Actions for
builds. For more build info, please look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/master/.github/workflows/cli.yml).
@@ -6,7 +6,7 @@ import (
"strconv"
"syscall"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
// FlagInputSourceExtension is an extension interface of cli.Flag that
@@ -72,7 +72,7 @@ func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourc
}
if value != nil {
for _, name := range f.Names() {
f.set.Set(name, value.String())
_ = f.set.Set(name, value.String())
}
}
}
@@ -135,7 +135,7 @@ func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo
}
if value {
for _, name := range f.Names() {
f.set.Set(name, strconv.FormatBool(value))
_ = f.set.Set(name, strconv.FormatBool(value))
}
}
}
@@ -153,7 +153,7 @@ func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSource
}
if value != "" {
for _, name := range f.Names() {
f.set.Set(name, value)
_ = f.set.Set(name, value)
}
}
}
@@ -181,7 +181,7 @@ func (f *PathFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo
value = filepath.Join(filepath.Dir(basePathAbs), value)
}
f.set.Set(name, value)
_ = f.set.Set(name, value)
}
}
}
@@ -199,7 +199,7 @@ func (f *IntFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCon
}
if value > 0 {
for _, name := range f.Names() {
f.set.Set(name, strconv.FormatInt(int64(value), 10))
_ = f.set.Set(name, strconv.FormatInt(int64(value), 10))
}
}
}
@@ -217,7 +217,7 @@ func (f *DurationFlag) ApplyInputSourceValue(context *cli.Context, isc InputSour
}
if value > 0 {
for _, name := range f.Names() {
f.set.Set(name, value.String())
_ = f.set.Set(name, value.String())
}
}
}
@@ -236,7 +236,7 @@ func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourc
if value > 0 {
floatStr := float64ToString(value)
for _, name := range f.Names() {
f.set.Set(name, floatStr)
_ = f.set.Set(name, floatStr)
}
}
}
+275
View File
@@ -0,0 +1,275 @@
// Code generated by fg; DO NOT EDIT.
package altsrc
import (
"flag"
"github.com/urfave/cli/v2"
)
// BoolFlag is the flag type that wraps cli.BoolFlag to allow
// for other values to be specified
type BoolFlag struct {
*cli.BoolFlag
set *flag.FlagSet
}
// NewBoolFlag creates a new BoolFlag
func NewBoolFlag(fl *cli.BoolFlag) *BoolFlag {
return &BoolFlag{BoolFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped BoolFlag.Apply
func (f *BoolFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.BoolFlag.Apply(set)
}
// DurationFlag is the flag type that wraps cli.DurationFlag to allow
// for other values to be specified
type DurationFlag struct {
*cli.DurationFlag
set *flag.FlagSet
}
// NewDurationFlag creates a new DurationFlag
func NewDurationFlag(fl *cli.DurationFlag) *DurationFlag {
return &DurationFlag{DurationFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped DurationFlag.Apply
func (f *DurationFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.DurationFlag.Apply(set)
}
// Float64Flag is the flag type that wraps cli.Float64Flag to allow
// for other values to be specified
type Float64Flag struct {
*cli.Float64Flag
set *flag.FlagSet
}
// NewFloat64Flag creates a new Float64Flag
func NewFloat64Flag(fl *cli.Float64Flag) *Float64Flag {
return &Float64Flag{Float64Flag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped Float64Flag.Apply
func (f *Float64Flag) Apply(set *flag.FlagSet) error {
f.set = set
return f.Float64Flag.Apply(set)
}
// GenericFlag is the flag type that wraps cli.GenericFlag to allow
// for other values to be specified
type GenericFlag struct {
*cli.GenericFlag
set *flag.FlagSet
}
// NewGenericFlag creates a new GenericFlag
func NewGenericFlag(fl *cli.GenericFlag) *GenericFlag {
return &GenericFlag{GenericFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped GenericFlag.Apply
func (f *GenericFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.GenericFlag.Apply(set)
}
// Int64Flag is the flag type that wraps cli.Int64Flag to allow
// for other values to be specified
type Int64Flag struct {
*cli.Int64Flag
set *flag.FlagSet
}
// NewInt64Flag creates a new Int64Flag
func NewInt64Flag(fl *cli.Int64Flag) *Int64Flag {
return &Int64Flag{Int64Flag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped Int64Flag.Apply
func (f *Int64Flag) Apply(set *flag.FlagSet) error {
f.set = set
return f.Int64Flag.Apply(set)
}
// IntFlag is the flag type that wraps cli.IntFlag to allow
// for other values to be specified
type IntFlag struct {
*cli.IntFlag
set *flag.FlagSet
}
// NewIntFlag creates a new IntFlag
func NewIntFlag(fl *cli.IntFlag) *IntFlag {
return &IntFlag{IntFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped IntFlag.Apply
func (f *IntFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.IntFlag.Apply(set)
}
// IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow
// for other values to be specified
type IntSliceFlag struct {
*cli.IntSliceFlag
set *flag.FlagSet
}
// NewIntSliceFlag creates a new IntSliceFlag
func NewIntSliceFlag(fl *cli.IntSliceFlag) *IntSliceFlag {
return &IntSliceFlag{IntSliceFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped IntSliceFlag.Apply
func (f *IntSliceFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.IntSliceFlag.Apply(set)
}
// Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow
// for other values to be specified
type Int64SliceFlag struct {
*cli.Int64SliceFlag
set *flag.FlagSet
}
// NewInt64SliceFlag creates a new Int64SliceFlag
func NewInt64SliceFlag(fl *cli.Int64SliceFlag) *Int64SliceFlag {
return &Int64SliceFlag{Int64SliceFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped Int64SliceFlag.Apply
func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.Int64SliceFlag.Apply(set)
}
// Float64SliceFlag is the flag type that wraps cli.Float64SliceFlag to allow
// for other values to be specified
type Float64SliceFlag struct {
*cli.Float64SliceFlag
set *flag.FlagSet
}
// NewFloat64SliceFlag creates a new Float64SliceFlag
func NewFloat64SliceFlag(fl *cli.Float64SliceFlag) *Float64SliceFlag {
return &Float64SliceFlag{Float64SliceFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped Float64SliceFlag.Apply
func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.Float64SliceFlag.Apply(set)
}
// StringFlag is the flag type that wraps cli.StringFlag to allow
// for other values to be specified
type StringFlag struct {
*cli.StringFlag
set *flag.FlagSet
}
// NewStringFlag creates a new StringFlag
func NewStringFlag(fl *cli.StringFlag) *StringFlag {
return &StringFlag{StringFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped StringFlag.Apply
func (f *StringFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.StringFlag.Apply(set)
}
// PathFlag is the flag type that wraps cli.PathFlag to allow
// for other values to be specified
type PathFlag struct {
*cli.PathFlag
set *flag.FlagSet
}
// NewPathFlag creates a new PathFlag
func NewPathFlag(fl *cli.PathFlag) *PathFlag {
return &PathFlag{PathFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls the
// wrapped PathFlag.Apply
func (f *PathFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.PathFlag.Apply(set)
}
// StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow
// for other values to be specified
type StringSliceFlag struct {
*cli.StringSliceFlag
set *flag.FlagSet
}
// NewStringSliceFlag creates a new StringSliceFlag
func NewStringSliceFlag(fl *cli.StringSliceFlag) *StringSliceFlag {
return &StringSliceFlag{StringSliceFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped StringSliceFlag.Apply
func (f *StringSliceFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.StringSliceFlag.Apply(set)
}
// Uint64Flag is the flag type that wraps cli.Uint64Flag to allow
// for other values to be specified
type Uint64Flag struct {
*cli.Uint64Flag
set *flag.FlagSet
}
// NewUint64Flag creates a new Uint64Flag
func NewUint64Flag(fl *cli.Uint64Flag) *Uint64Flag {
return &Uint64Flag{Uint64Flag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped Uint64Flag.Apply
func (f *Uint64Flag) Apply(set *flag.FlagSet) error {
f.set = set
return f.Uint64Flag.Apply(set)
}
// UintFlag is the flag type that wraps cli.UintFlag to allow
// for other values to be specified
type UintFlag struct {
*cli.UintFlag
set *flag.FlagSet
}
// NewUintFlag creates a new UintFlag
func NewUintFlag(fl *cli.UintFlag) *UintFlag {
return &UintFlag{UintFlag: fl, set: nil}
}
// Apply saves the flagSet for later usage calls, then calls
// the wrapped UintFlag.Apply
func (f *UintFlag) Apply(set *flag.FlagSet) error {
f.set = set
return f.UintFlag.Apply(set)
}
@@ -3,7 +3,7 @@ package altsrc
import (
"time"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
// InputSourceContext is an interface used to allow
@@ -8,7 +8,7 @@ import (
"strings"
"time"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
// NewJSONSourceFromFlagFunc returns a func that takes a cli.Context
@@ -29,13 +29,8 @@ func NewJSONSourceFromFile(f string) (InputSourceContext, error) {
if err != nil {
return nil, err
}
s, err := newJSONSource(data)
if err != nil {
return nil, err
}
s.file = f
return s, nil
return NewJSONSource(data)
}
// NewJSONSourceFromReader returns an InputSourceContext suitable for
@@ -51,10 +46,6 @@ func NewJSONSourceFromReader(r io.Reader) (InputSourceContext, error) {
// NewJSONSource returns an InputSourceContext suitable for retrieving
// config variables from raw JSON data.
func NewJSONSource(data []byte) (InputSourceContext, error) {
return newJSONSource(data)
}
func newJSONSource(data []byte) (*jsonSource, error) {
var deserialized map[string]interface{}
if err := json.Unmarshal(data, &deserialized); err != nil {
return nil, err
@@ -76,10 +67,10 @@ func (x *jsonSource) Int(name string) (int, error) {
return 0, fmt.Errorf("unexpected type %T for %q", i, name)
case int:
return v, nil
case float64:
return int(float64(v)), nil
case float32:
return int(float32(v)), nil
return int(v), nil
case float64:
return int(v), nil
}
}
@@ -88,9 +79,9 @@ func (x *jsonSource) Duration(name string) (time.Duration, error) {
if err != nil {
return 0, err
}
v, ok := (time.Duration)(0), false
if v, ok = i.(time.Duration); !ok {
return v, fmt.Errorf("unexpected type %T for %q", i, name)
v, ok := i.(time.Duration)
if !ok {
return 0, fmt.Errorf("unexpected type %T for %q", i, name)
}
return v, nil
}
@@ -100,9 +91,9 @@ func (x *jsonSource) Float64(name string) (float64, error) {
if err != nil {
return 0, err
}
v, ok := (float64)(0), false
if v, ok = i.(float64); !ok {
return v, fmt.Errorf("unexpected type %T for %q", i, name)
v, ok := i.(float64)
if !ok {
return 0, fmt.Errorf("unexpected type %T for %q", i, name)
}
return v, nil
}
@@ -112,9 +103,9 @@ func (x *jsonSource) String(name string) (string, error) {
if err != nil {
return "", err
}
v, ok := "", false
if v, ok = i.(string); !ok {
return v, fmt.Errorf("unexpected type %T for %q", i, name)
v, ok := i.(string)
if !ok {
return "", fmt.Errorf("unexpected type %T for %q", i, name)
}
return v, nil
}
@@ -170,9 +161,9 @@ func (x *jsonSource) Generic(name string) (cli.Generic, error) {
if err != nil {
return nil, err
}
v, ok := (cli.Generic)(nil), false
if v, ok = i.(cli.Generic); !ok {
return v, fmt.Errorf("unexpected type %T for %q", i, name)
v, ok := i.(cli.Generic)
if !ok {
return nil, fmt.Errorf("unexpected type %T for %q", i, name)
}
return v, nil
}
@@ -182,9 +173,9 @@ func (x *jsonSource) Bool(name string) (bool, error) {
if err != nil {
return false, err
}
v, ok := false, false
if v, ok = i.(bool); !ok {
return v, fmt.Errorf("unexpected type %T for %q", i, name)
v, ok := i.(bool)
if !ok {
return false, fmt.Errorf("unexpected type %T for %q", i, name)
}
return v, nil
}
@@ -6,7 +6,7 @@ import (
"strings"
"time"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
// MapInputSource implements InputSourceContext to return
@@ -23,15 +23,15 @@ func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool
if sections := strings.Split(name, "."); len(sections) > 1 {
node := tree
for _, section := range sections[:len(sections)-1] {
if child, ok := node[section]; !ok {
child, ok := node[section]
if !ok {
return nil, false
} else {
if ctype, ok := child.(map[interface{}]interface{}); !ok {
return nil, false
} else {
node = ctype
}
}
ctype, ok := child.(map[interface{}]interface{})
if !ok {
return nil, false
}
node = ctype
}
if val, ok := node[sections[len(sections)-1]]; ok {
return val, true
@@ -1,8 +1,3 @@
// Disabling building of toml support in cases where golang is 1.0 or 1.1
// as the encoding library is not implemented or supported.
// +build go1.2
package altsrc
import (
@@ -10,7 +5,7 @@ import (
"reflect"
"github.com/BurntSushi/toml"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
)
type tomlMap struct {
@@ -28,7 +23,7 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) {
case reflect.String:
ret[key] = val.(string)
case reflect.Int:
ret[key] = int(val.(int))
ret[key] = val.(int)
case reflect.Int8:
ret[key] = int(val.(int8))
case reflect.Int16:
@@ -50,7 +45,7 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) {
case reflect.Float32:
ret[key] = float64(val.(float32))
case reflect.Float64:
ret[key] = float64(val.(float64))
ret[key] = val.(float64)
case reflect.Map:
if tmp, err := unmarshalMap(val); err == nil {
ret[key] = tmp
@@ -66,9 +61,9 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) {
return ret, nil
}
func (self *tomlMap) UnmarshalTOML(i interface{}) error {
func (tm *tomlMap) UnmarshalTOML(i interface{}) error {
if tmp, err := unmarshalMap(i); err == nil {
self.Map = tmp
tm.Map = tmp
} else {
return err
}
@@ -1,8 +1,3 @@
// Disabling building of yaml support in cases where golang is 1.0 or 1.1
// as the encoding library is not implemented or supported.
// +build go1.2
package altsrc
import (
@@ -14,7 +9,7 @@ import (
"runtime"
"strings"
"gopkg.in/urfave/cli.v2"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v2"
)
@@ -86,7 +81,7 @@ func loadDataFrom(filePath string) ([]byte, error) {
return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath)
}
return ioutil.ReadFile(filePath)
} else {
return nil, fmt.Errorf("unable to determine how to load from path %s", filePath)
}
return nil, fmt.Errorf("unable to determine how to load from path %s", filePath)
}

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