diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..26ca03f8 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Supported Versions + +The most recent version "release" version to appear on the +[releases][1] page is currently supported. + +## Reporting a Vulnerability + +To report a vulnerability, please use the +[Privately reporting a security vulnerability][2] +facility. + +[1]: https://github.com/cactus/go-camo/releases +[2]: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..f4ea2ada --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + labels: + - dependencies + schedule: + interval: "weekly" + day: "monday" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c81a3452..1771dd7a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,7 +2,7 @@ name: "CodeQL" on: push: - branches: ['**'] + branches: [master] pull_request: # The branches below must be a subset of the branches above branches: [main] @@ -13,23 +13,35 @@ jobs: analyse: name: Analyse runs-on: ubuntu-latest + permissions: + security-events: write + actions: read steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '>=1.22.0' + check-latest: true + id: go + + - name: Build + env: + GOPROXY: "https://proxy.golang.org" + run: make build + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: go - - name: build - run: make build - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/publish-docker-images.yml b/.github/workflows/publish-docker-images.yml new file mode 100644 index 00000000..55c3e059 --- /dev/null +++ b/.github/workflows/publish-docker-images.yml @@ -0,0 +1,78 @@ +name: publish-docker-images +on: + workflow_dispatch: + push: + tags: + - "v*" + +jobs: + build: + name: docker-publish + runs-on: ubuntu-latest + + steps: + - name: Src Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: write tags env vars + run: | + TAG=$(git describe --tags) + LATEST_TAG=$(git tag -l | grep -viE '(alpha|beta)' | sort -V | tail -n 1) + GITHASH="$(git rev-parse HEAD)" + echo "TAG=$TAG" + echo "TAG=${TAG}" >> "$GITHUB_ENV" + echo "LATEST_TAG=${LATEST_TAG}" + echo "LATEST_TAG=${LATEST_TAG}" >> "$GITHUB_ENV" + echo "GITHASH=${GITHASH}" + echo "GITHASH=${GITHASH}" >> "$GITHUB_ENV" + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + cactus4docker/go-camo + ghcr.io/cactus/go-camo + tags: | + # set latest tag for master branch + type=raw,value=${{ env.TAG }} + type=raw,value=latest,enable=${{ env.TAG == env.LATEST_TAG }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + password: ${{ secrets.DOCKER_PASSWORD }} + username: ${{ secrets.DOCKER_USERNAME }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + provenance: false + sbom: false + file: ./docker/Dockerfile + platforms: linux/amd64,linux/arm64 + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + build-args: | + GITHASH=${{env.GITHASH}} + APP_VER=${{env.TAG}} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml deleted file mode 100644 index f33d742c..00000000 --- a/.github/workflows/publish-docker.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: publish-docker -on: - push: - tags: - - 'v*' - -jobs: - build: - name: docker-publish - runs-on: ubuntu-latest - steps: - - name: Setup Go ${{ matrix.goVer }} - uses: actions/setup-go@v1 - with: - go-version: '1.19.x' - id: go - - - name: Src Checkout - uses: actions/checkout@v1 - with: - fetch-depth: 1 - - - name: Tests - if: success() - env: - GOPROXY: "https://proxy.golang.org" - run: make test - - - name: Build - if: success() - env: - GOPROXY: "https://proxy.golang.org" - run: make build - - - name: Build Container - if: success() - run: | - TAG=$(git describe --tags) - docker build -f examples/Dockerfile -t cactus4docker/go-camo:${TAG} . - # also tag as latest? - # docker tag cactus4docker/go-camo:${TAG} cactus4docker/go-camo:latest - - - name: Publish Container - if: success() - env: - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - run: | - TAG=$(git describe --tags) - LATEST_TAG=$(git tag -l | grep -viE '(alpha|beta)' | sort -V | tail -n 1) - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin - docker push cactus4docker/go-camo:${TAG} - if [[ "$TAG" = "$LATEST_TAG" ]]; then - docker tag cactus4docker/go-camo:${TAG} cactus4docker/go-camo:latest - docker push cactus4docker/go-camo:latest - fi - docker logout diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index d9235b1d..fe846f7c 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,30 +1,28 @@ name: unit-tests on: + workflow_dispatch: push: - branches: ['**'] + branches: ["**"] pull_request: branches: [main] jobs: - build: - name: Build - strategy: - matrix: - go: ['1.19.x'] - platform: [ubuntu-latest] - runs-on: ${{ matrix.platform }} - steps: - - name: Setup Go ${{ matrix.go }} - uses: actions/setup-go@v1 - with: - go-version: ${{ matrix.go }} - id: go + test: + runs-on: ubuntu-latest + steps: - name: Src Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v4 with: fetch-depth: 1 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ">=1.22.0" + check-latest: true + id: go + - name: Build env: GOPROXY: "https://proxy.golang.org" @@ -34,15 +32,46 @@ jobs: env: GOPROXY: "https://proxy.golang.org" run: | - go install honnef.co/go/tools/cmd/staticcheck@2023.1.1 - go install github.com/securego/gosec/v2/cmd/gosec@latest - hash -r make check - name: Tests env: GOPROXY: "https://proxy.golang.org" CI: true - run: - echo "skip" -# run: make test + run: make test + + test-qemu: + needs: test + runs-on: ubuntu-latest + strategy: + matrix: + arch: [arm64] + + steps: + - name: Src Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ">=1.22.0" + check-latest: true + id: go + + - name: Install QEMU + uses: docker/setup-qemu-action@v3 + + - name: Build + env: + GOPROXY: "https://proxy.golang.org" + GOARCH: ${{ matrix.arch }} + run: make build + + - name: Tests + env: + GOPROXY: "https://proxy.golang.org" + GOARCH: ${{ matrix.arch }} + CI: true + run: make test diff --git a/.gitignore b/.gitignore index c181b139..56c973f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.vscode /build /config.json /diagrams @@ -7,6 +8,7 @@ /server.crt /server.csr *.py[co] +/test.py /test-ruleset.conf /man/*.html /man/*.[1-9] diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 6b140add..aad9b747 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -19,6 +19,54 @@ toc::[] == HEAD +== v2.4.13 2024-04-22 +* Release tagged for arm64 docker building only. + +== v2.4.12 2024-04-20 +* Update library dependencies. +* Fix docker and github packages publishing issue. + +== v2.4.11 2024-04-03 +* Update library dependencies. +* Build with Go-1.22.2 + +== v2.4.10 2024-03-17 +* Update library dependencies. + +== v2.4.9 2024-02-16 +* Minimum Go version now 1.21 due to quic-go dependency, due to better + cryto/tls support for QUIC in Go-1.21. +* Update library dependencies. + +== v2.4.8 2023-12-19 +* Add `--automaxprocs` flag to set GOMAXPROCS automatically to match Linux + container CPU quota/limits. +* Update library dependencies. + +== v2.4.7 - 2023-11-13 +* Add http3/quic server support. New flag `--quic`. Requires `--ssl-listen`. + +== v2.4.6 - 2023-10-25 +* Add `--no-debug-vars` flag to disable /debug/vars when `--metrics` is + enabled. (#66, #67) + +== v2.4.5 - 2023-10-23 +* fix htrie matching of non punycode (eg. unicode) idna hostnames +* slightly faster logging (update to mlog dependency) +* address a logging issue with missing url path output in + `"built outgoing request"` debug log +* moderate improve performance of hostname rule processing + (approx 12-30% in microbenchmarks) +* slight improvement in request path url processing + (approx 2-4% in microbenchmarks) +* fix /debug/vars being enabled by default (#65) due to expvars import + side effect + +== v2.4.4 - 2023-07-25 +* update dependencies +* bump version in go.mod (and fix all internal module references) + + ref: discussion link:https://github.com/cactus/go-camo/discussions/62[#62] + == v2.4.3 - 2023-02-18 * update library dependency golang.org/x/net. + refs: diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index c0e6da31..00000000 --- a/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM golang:1.19 - -WORKDIR /app -ADD . /app - -RUN make build - -FROM alpine:latest -RUN apk add --no-cache ca-certificates -COPY --from=0 /app/build/bin/* /bin/ - -EXPOSE 8080/tcp -#USER nobody -ENTRYPOINT ["/bin/go-camo"] diff --git a/Dockerfile.cabotage b/Dockerfile.cabotage new file mode 120000 index 00000000..93c313e6 --- /dev/null +++ b/Dockerfile.cabotage @@ -0,0 +1 @@ +docker/Dockerfile \ No newline at end of file diff --git a/Makefile b/Makefile index 3a411719..7b9794e8 100644 --- a/Makefile +++ b/Makefile @@ -9,38 +9,48 @@ SIGN_KEY ?= ${HOME}/.minisign/go-camo.key # app specific info APP_NAME := go-camo APP_VER := $(shell git describe --always --tags|sed 's/^v//') +GOPATH := $(shell go env GOPATH) VERSION_VAR := main.ServerVersion # flags and build configuration GOBUILD_OPTIONS := -trimpath GOTEST_FLAGS := -cpu=1,2 +GOTEST_BENCHFLAGS := GOBUILD_DEPFLAGS := -tags netgo GOBUILD_LDFLAGS ?= -s -w GOBUILD_FLAGS := ${GOBUILD_DEPFLAGS} ${GOBUILD_OPTIONS} -ldflags "${GOBUILD_LDFLAGS} -X ${VERSION_VAR}=${APP_VER}" # cross compile defs -CC_BUILD_ARCHES = darwin/amd64 darwin/arm64 freebsd/amd64 linux/amd64 linux/arm64 windows/amd64 +CC_BUILD_TARGETS = go-camo url-tool +CC_BUILD_ARCHES = darwin/arm64 freebsd/amd64 linux/amd64 linux/arm64 windows/amd64 CC_OUTPUT_TPL := ${BUILDDIR}/bin/{{.Dir}}.{{.OS}}-{{.Arch}} # some exported vars (pre-configure go build behavior) export GO111MODULE=on export CGO_ENABLED=0 +## enable go 1.21 loopvar "experiment" +export GOEXPERIMENT=loopvar define HELP_OUTPUT Available targets: - help this help +* help this help (default target) clean clean up - all build binaries and man pages + check run checks and validators test run tests cover run tests with cover output + bench run benchmarks build build all binaries man build all man pages - tar build release tarball - cross-tar cross compile and build release tarballs + all build binaries and man pages + tar build release tarball for host platform only + cross-tar cross compile and build release tarballs for all platforms + release-sign sign release tarballs with minisign + release build and sign release + update-go-deps updates go.mod and go.sum files endef export HELP_OUTPUT -.PHONY: help clean build test cover man man-copy all tar cross-tar +.PHONY: help clean build test cover bench man man-copy all tar cross-tar setup-check help: @echo "$$HELP_OUTPUT" @@ -50,13 +60,16 @@ clean: setup: -setup-gox: - @if [ -z "$(shell which gox)" ]; then \ - echo "* 'gox' command not found."; \ - echo " install (or otherwise ensure presence in PATH)"; \ - echo " go install github.com/mitchellh/gox"; \ - exit 1;\ - fi +setup-check: ${GOPATH}/bin/staticcheck ${GOPATH}/bin/gosec ${GOPATH}/bin/govulncheck + +${GOPATH}/bin/staticcheck: + go install honnef.co/go/tools/cmd/staticcheck@latest + +${GOPATH}/bin/gosec: + go install github.com/securego/gosec/v2/cmd/gosec@latest + +${GOPATH}/bin/govulncheck: + go install golang.org/x/vuln/cmd/govulncheck@latest build: setup @[ -d "${BUILDDIR}/bin" ] || mkdir -p "${BUILDDIR}/bin" @@ -69,28 +82,32 @@ build: setup test: setup @echo "Running tests..." - @go test -count=1 -vet=off ${GOTEST_FLAGS} ./... + @go test -count=1 -cpu=4 -vet=off ${GOTEST_FLAGS} ./... + +bench: setup + @echo "Running benchmarks..." + @go test -bench="." -run="^$$" -test.benchmem=true ${GOTEST_BENCHFLAGS} ./... cover: setup @echo "Running tests with coverage..." @go test -vet=off -cover ${GOTEST_FLAGS} ./... -check: setup +check: setup setup-check @echo "Running checks and validators..." @echo "... staticcheck ..." - @$$(go env GOPATH)/bin/staticcheck ./... + @${GOPATH}/bin/staticcheck ./... @echo "... go-vet ..." @go vet ./... @echo "... gosec ..." - @$$(go env GOPATH)/bin/gosec -quiet ./... + @${GOPATH}/bin/gosec -quiet ./... + @echo "... govulncheck ..." + @${GOPATH}/bin/govulncheck ./... .PHONY: update-go-deps update-go-deps: @echo ">> updating Go dependencies" - @for m in $$(go list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ - go get $$m; \ - done - go mod tidy + @go get -u all + @go mod tidy ${BUILDDIR}/man/%: man/%.adoc @[ -d "${BUILDDIR}/man" ] || mkdir -p "${BUILDDIR}/man" @@ -108,30 +125,31 @@ tar: all @tar -C ${TARBUILDDIR} -czf ${TARBUILDDIR}/${APP_NAME}-${APP_VER}.${GOVER}.${OS}-${ARCH}.tar.gz ${APP_NAME}-${APP_VER} @rm -rf "${TARBUILDDIR}/${APP_NAME}-${APP_VER}" -cross-tar: man setup setup-gox +cross-tar: man setup @echo "Building (cross-compile: ${CC_BUILD_ARCHES})..." - @(for x in go-camo url-tool; do \ - echo "...$${x}..."; \ - env GOFLAGS="${GOBUILD_OPTIONS}" gox \ - -gocmd="go" \ - -output="${CC_OUTPUT_TPL}" \ - -osarch="${CC_BUILD_ARCHES}" \ - -ldflags "${GOBUILD_LDFLAGS} -X ${VERSION_VAR}=${APP_VER}" \ - ${GOBUILD_DEPFLAGS} ./cmd/$${x}; \ - echo; \ + @(for x in ${CC_BUILD_TARGETS}; do \ + for y in $(subst /,-,${CC_BUILD_ARCHES}); do \ + printf -- "--> %15s: %s\n" "$${y}" "$${x}"; \ + GOOS="$${y%%-*}"; \ + GOARCH="$${y##*-}"; \ + EXT=""; \ + if echo "$${y}" | grep -q 'windows-'; then EXT=".exe"; fi; \ + env GOOS=$${GOOS} GOARCH=$${GOARCH} go build ${GOBUILD_FLAGS} -o "${BUILDDIR}/bin/$${x}.$${GOOS}-$${GOARCH}$${EXT}" ./cmd/$${x}; \ + done; \ done) - @echo "...creating tar files..." + @echo "Creating tar archives..." @(for x in $(subst /,-,${CC_BUILD_ARCHES}); do \ - echo "making tar for ${APP_NAME}.$${x}"; \ + printf -- "--> %15s\n" "$${x}"; \ EXT=""; \ if echo "$${x}" | grep -q 'windows-'; then EXT=".exe"; fi; \ XDIR="${GOVER}.$${x}"; \ ODIR="${TARBUILDDIR}/$${XDIR}/${APP_NAME}-${APP_VER}"; \ mkdir -p "$${ODIR}/bin"; \ mkdir -p "$${ODIR}/man"; \ - cp ${BUILDDIR}/bin/${APP_NAME}.$${x}$${EXT} $${ODIR}/bin/${APP_NAME}$${EXT}; \ - cp ${BUILDDIR}/bin/url-tool.$${x}$${EXT} $${ODIR}/bin/url-tool$${EXT}; \ + for t in ${CC_BUILD_TARGETS}; do \ + cp ${BUILDDIR}/bin/$${t}.$${x}$${EXT} $${ODIR}/bin/$${t}$${EXT}; \ + done; \ cp ${BUILDDIR}/man/*.[1-9] $${ODIR}/man/; \ tar -C ${TARBUILDDIR}/$${XDIR} -czf ${TARBUILDDIR}/${APP_NAME}-${APP_VER}.$${XDIR}.tar.gz ${APP_NAME}-${APP_VER}; \ rm -rf "${TARBUILDDIR}/$${XDIR}/"; \ diff --git a/Procfile b/Procfile index 8a80fefa..79170671 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: /bin/go-camo --socket-listen=/var/run/cabotage/cabotage.sock --server-name=https://github.com/pypi/camo +web: /bin/go-camo --socket-listen=/var/run/cabotage/cabotage.sock --server-name=https://github.com/pypi/camo --max-size=$GOCAMO_MAXSIZE --max-size-redirect=$GOCAMO_MAXSIZEREDIRECT diff --git a/README.adoc b/README.adoc index a603102d..42d35adf 100644 --- a/README.adoc +++ b/README.adoc @@ -15,7 +15,6 @@ endif::[] :link-hmac: https://en.wikipedia.org/wiki/HMAC[HMAC] :link-hsts: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security[HSTS] :link-asciidoctor: https://asciidoctor.org[asciidoctor] -:link-gox: https://github.com/mitchellh/gox[gox] :link-damontools: https://cr.yp.to/daemontools.html[daemontools] :link-runit: https://en.wikipedia.org/wiki/Runit[runit] :link-upstart: https://en.wikipedia.org/wiki/Upstart_(software)[upstart] @@ -25,7 +24,8 @@ endif::[] :link-heroku-buildpack-go: https://github.com/kr/heroku-buildpack-go :link-dns-rebinding: https://en.wikipedia.org/wiki/DNS_rebinding[dns rebinding] :link-ip6-special-addresses: https://en.wikipedia.org/wiki/IPv6_address#Special_addresses -:link-containers: https://hub.docker.com/r/cactus4docker/go-camo[containers] +:link-docker-containers: https://hub.docker.com/r/cactus4docker/go-camo[docker hub] +:link-github-containers: https://github.com/cactus/go-camo/pkgs/container/go-camo[github packages] :link-releases: https://github.com/cactus/go-camo/releases[binary releases] :link-mit-license: https://www.opensource.org/licenses/mit-license.php[MIT license] :link-mrsaints: https://github.com/MrSaints[MrSaints] @@ -59,13 +59,13 @@ The general steps are as follows: * A client requests a page from the web app. * The original URL in the content is parsed. -* An HMAC signature of the url is generated. -* The url and hmac are encoded. -* The encoded url and hmac are placed into the expected format, - creating the signed url. -* The signed url replaces the original image URL. +* An HMAC signature of the URL is generated. +* The URL and hmac are encoded. +* The encoded URL and hmac are placed into the expected format, + creating the signed URL. +* The signed URL replaces the original image URL. * The web app returns the content to the client. -* The client requests the signed url from Go-Camo. +* The client requests the signed URL from Go-Camo. * Go-Camo validates the HMAC, decodes the URL, then requests the content from the origin server and streams it to the client. @@ -145,14 +145,11 @@ Extract, and copy files to desired locations. Building requires: * make +* posix compatible shell (sh) * git -* go (latest version recommended. At least version >= 1.13) +* go (latest version recommended. At least version >= 1.21) * {link-asciidoctor} (for building man pages only) -Additionally required, if cross compiling: - -* {link-gox} - Building: [source,text] @@ -167,9 +164,10 @@ Available targets: help this help clean clean up all build binaries and man pages + check run checks and validators test run tests cover run tests with cover output - build build all + build build all binaries man build all man pages tar build release tarball cross-tar cross compile and build release tarballs @@ -217,13 +215,14 @@ go-camo will generally do what you tell it to with regard to fetching signed url There is some limited support for trying to prevent {link-dns-rebinding} attacks. go-camo will attempt to reject any address matching an rfc1918 network block, -or a private scope ipv6 address, -be it in the url or via resulting hostname resolution. -Please note, however, -that this does not provide protecton for a network that uses public address space (ipv4 or ipv6), -or some of the {link-ip6-special-addresses}[more exotic] ipv6 addresses. +or a private scope ipv6 address, be it in the url or via resulting hostname +resolution. -The list of networks rejected include... +Please note, however, that this does not provide protection for a network that +uses public address space (ipv4 or ipv6), or some of the +{link-ip6-special-addresses}[more exotic] ipv6 addresses. + +The list of networks rejected includes... [%header%autowidth.stretch] |=== @@ -272,8 +271,6 @@ More generally, it is recommended to either: === Environment Vars * `GOCAMO_HMAC` - HMAC key to use. -* `GOCAMO_MAXSIZE` - Max allowed response size (KB) -* `GOCAMO_MAXSIZEREDIRECT` - URL to redirect to when max-size is exceeded. * `HTTPS_PROXY` - Configure an outbound proxy for HTTPS requests. + Either a complete URL or a `host[:port]`, in which case an HTTP scheme is assumed. See <> notes for more information. @@ -286,39 +283,46 @@ More generally, it is recommended to either: [source,text] ---- $ go-camo -h -Usage: - go-camo [OPTIONS] - -Application Options: - -k, --key= HMAC key - -H, --header= Add additional header to each response. This option can - be used multiple times to add multiple headers - --listen= Address:Port to bind to for HTTP (default: 0.0.0.0:8080) - --socket-listen= Path for unix domain socket to bind to for HTTP - --ssl-listen= Address:Port to bind to for HTTPS/SSL/TLS - --ssl-key= ssl private key (key.pem) path - --ssl-cert= ssl cert (cert.pem) path - --max-size= Max allowed response size (KB) - --max-size-redirect= URL to redirect to when max-size is exceeded - --timeout= Upstream request timeout (default: 4s) - --max-redirects= Maximum number of redirects to follow (default: 3) - --metrics Enable Prometheus compatible metrics endpoint - --no-log-ts Do not add a timestamp to logging - --log-json Log in JSON format - --no-fk Disable frontend http keep-alive support - --no-bk Disable backend http keep-alive support - --allow-content-video Additionally allow 'video/*' content - --allow-content-audio Additionally allow 'audio/*' content - --allow-credential-urls Allow urls to contain user/pass credentials - --filter-ruleset= Text file containing filtering rules (one per line) - --server-name= Value to use for the HTTP server field (default: go-camo) - --expose-server-version Include the server version in the HTTP server response header - --enable-xfwd4 Enable x-forwarded-for passthrough/generation - -v, --verbose Show verbose (debug) log level output - -V, --version Print version and exit; specify twice to show license information - -Help Options: - -h, --help Show this help message +Usage: go-camo + +An image proxy that proxies non-secure images over SSL/TLS + +Flags: + -h, --help Show context-sensitive help. + -k, --key=STRING HMAC key + -H, --header=HEADER,... Add additional header to each response. This option can + be used multiple times to add multiple headers. + --listen="0.0.0.0:8080" Address:Port to bind to for HTTP + --ssl-listen=STRING Address:Port to bind to for HTTPS/SSL/TLS + --socket-listen=STRING Path for unix domain socket to bind to for HTTP + --quic Enable http3/quic. Binds to the same port number as + ssl-listen but udp+quic. + --automaxprocs Set GOMAXPROCS automatically to match Linux container CPU + quota/limits. + --ssl-key=STRING ssl private key (key.pem) path + --ssl-cert=STRING ssl cert (cert.pem) path + --max-size=INT-64 Max allowed response size (KB) + --max-size-redirect URL to redirect to when max-size is exceeded + --timeout=4s Upstream request timeout + --max-redirects=3 Maximum number of redirects to follow + --metrics Enable Prometheus compatible metrics endpoint + --no-debug-vars Disable the /debug/vars/ metrics endpoint. This option has + no effects when the metrics are not enabled. + --no-log-ts Do not add a timestamp to logging + --log-json Log in JSON format + --no-fk Disable frontend http keep-alive support + --no-bk Disable backend http keep-alive support + --allow-content-video Additionally allow 'video/*' content + --allow-content-audio Additionally allow 'audio/*' content + --allow-credential-urls Allow urls to contain user/pass credentials + --filter-ruleset=STRING Text file containing filtering rules (one per line) + --server-name="go-camo" Value to use for the HTTP server field + --expose-server-version Include the server version in the HTTP server response + header + --enable-xfwd4 Enable x-forwarded-for passthrough/generation + -v, --verbose Show verbose (debug) log level output + -V, --version Print version and exit; specify twice to show + license information. ---- A few notes about specific flags: @@ -327,11 +331,11 @@ A few notes about specific flags: + -- If a `filter-ruleset` file is defined, -that file is read and each line converted into a filter rule. +that file is read and each line is converted into a filter rule. See link:man/go-camo-filtering.5.adoc[`go-camo-filtering(5)`] for more information regarding the format for the filter file itself. -Regarding evaluatation: The ruleset is NOT evaluated in-order. +Regarding evaluation: The ruleset is NOT evaluated in-order. The rules process in two phases: "allow rule phase" where the allow rules are evaluated, and the "deny rule phase" where the deny rules are evaluated. First match in each phase "wins" that phase. @@ -346,10 +350,10 @@ If there are no deny rules supplied, this phase is skipped. [NOTE] ==== -It is always preferable to do filtering at the point of url generation and signing. +It is always preferable to do filtering at the point of URL generation and signing. The `filter-ruleset` functionality (both allow and deny) is supplied predominantly as a fallback safety measure, -for cases where you have previously generated a url and you need a quick temporary fix, +for cases where you have previously generated a URL and you need a quick temporary fix, or where rolling keys takes a while and/or is difficult. ==== -- @@ -365,8 +369,15 @@ The default is `0`. * `--metrics` + -- -If the metrics flag is provided, -then the service will expose a Prometheus `/metrics` endpoint. +If the `metrics` flag is provided, then the service will expose a Prometheus +`/metrics` endpoint and a `/debug/vars` endpoint from the go `expvar` package. +-- + +* `--no-debug-vars` ++ +-- +If the `no-debug-vars` flag is provided along with the `metrics` flag, the +`/debug/vars` endpoint is removed. -- * `-k`, `--key` @@ -397,7 +408,7 @@ by default, you could add this to the command line: [source,text] ---- --H "Strict-Transport-Security: max-age=16070400" +-H "Strict-Transport-Security: max-age=16070400" ---- -- @@ -419,7 +430,7 @@ Some examples (list is not exhaustive): * The upstream http proxy itself may be responsible for following redirects (depending on configuration). As such, go-camo may not have visibility into the redirect chain. This could result in resource exhaustion (redirect - loops), or SSRF (redirects to internal urls). + loops), or SSRF (redirects to internal URLs). * The upstream http proxy itself will be responsible for connecting to external servers, and would need to be configured for any request size limits. While go-camo would still limit request sizes based on its own configuration, @@ -459,7 +470,7 @@ In addition, a number of custom metrics. | The number of responses that failed to send to the client. | camo_proxy_reponses_truncated_total | Counter -| The number of responess that were too large to send. +| The number of responses that were too large to send. | camo_responses_total | Counter | Total HTTP requests processed by the go-camo, excluding scrapes. @@ -473,6 +484,10 @@ In addition, you can expose some extra data to metrics via env vars, if desired: * BuildDate via `APP_INFO_BUILD_DATE` * You can also override the version by setting `APP_INFO_VERSION` +A `/debug/vars` endpoint is also included with `--metrics` by default. +This endpoint returns memstats and some additional data. This endpoint can be +disabled by additionally supplying the `--no-debug-vars` flag. + == Additional tools Go-Camo includes a couple of additional tools. @@ -490,14 +505,14 @@ Usage: Application Options: -k, --key= HMAC key - -p, --prefix= Optional url prefix used by encode output + -p, --prefix= Optional URL prefix used by encode output Help Options: -h, --help Show this help message Available commands: - decode Decode a url and print result - encode Encode a url and print result + decode Decode a URL and print result + encode Encode a URL and print result ---- Example usage: @@ -521,18 +536,13 @@ http://golang.org/doc/gopher/frontpage.png == Containers -There are {link-containers} built automatically from version tags. +There are containers built automatically from version tags, pushed to both {link-docker-containers} and {link-github-containers}. These containers are untested and provided only for those with specific containerization requirements. When in doubt, prefer the statically compiled {link-releases}, unless you specifically need a container. -== Alternative Implementations - -* {link-mrsaints}' go-camo {link-arachnys-fork} -- - supports proxying additional content types (fonts/css). - == Changelog See xref:CHANGELOG.adoc[CHANGELOG]. diff --git a/SECURITY.adoc b/SECURITY.adoc deleted file mode 100644 index 90ff8ed5..00000000 --- a/SECURITY.adoc +++ /dev/null @@ -1,40 +0,0 @@ -= Security Policy -:toc: macro -ifdef::env-github[] -:toc-title: -:tip-caption: :bulb: -:note-caption: :bulb: -:important-caption: :heavy_exclamation_mark: -:caution-caption: :fire: -:warning-caption: :warning: -endif::[] - -ifdef::env-github[] -[discrete] -== Contents -endif::[] -toc::[] - -== Supported Versions - -The most recent version "release" version to appear on the -https://github.com/cactus/go-camo/releases[releases] page is currently -supported. - -== Reporting a Vulnerability - -To report a vulnerability, please open a github Issue stating only that you -have found a security vulnerability or other problem that you would like to -report, and requesting a -https://help.github.com/en/articles/about-maintainer-security-advisories[Draft -Security Advisory] be created. - -A Draft Security Advisory will then be created, and the user that opened the -Issue will be invited to collaborate on the Draft Advisory. - -If the issue is accepted, a comment to that effect will be made in the original -issue. If the Security Advistory leaves draft state, it will eventually be -linked from the original issue. - -If the issue is declined, a comment to that effect will be made in the original -issue. diff --git a/cmd/go-camo/main.go b/cmd/go-camo/main.go index 067428be..1ad58584 100644 --- a/cmd/go-camo/main.go +++ b/cmd/go-camo/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -6,38 +6,40 @@ package main import ( - "bufio" "context" + "expvar" "fmt" "net" "net/http" - "net/url" + "net/http/pprof" "os" "os/signal" "runtime" - "strconv" "strings" "syscall" "time" - "github.com/cactus/go-camo/pkg/camo" - "github.com/cactus/go-camo/pkg/htrie" - "github.com/cactus/go-camo/pkg/router" - + "github.com/alecthomas/kong" + "github.com/cactus/go-camo/v2/pkg/camo" + "github.com/cactus/go-camo/v2/pkg/router" "github.com/cactus/mlog" - flags "github.com/jessevdk/go-flags" + "github.com/prometheus/client_golang/prometheus" + vcoll "github.com/prometheus/client_golang/prometheus/collectors/version" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/version" + "github.com/quic-go/quic-go/http3" + "go.uber.org/automaxprocs/maxprocs" ) const metricNamespace = "camo" -var ( - // ServerVersion holds the server version string - ServerVersion = "no-version" +// ServerVersion holds the server version string +var ServerVersion = "no-version" +var ( + // configure histograms and counters responseSize = promauto.NewHistogramVec( prometheus.HistogramOpts{ Namespace: metricNamespace, @@ -66,131 +68,54 @@ var ( ) ) -func loadFilterList(fname string) ([]camo.FilterFunc, error) { - // #nosec - file, err := os.Open(fname) - if err != nil { - return nil, fmt.Errorf("could not open filter-ruleset file: %s", err) - } - // #nosec - defer file.Close() - - allowFilter := htrie.NewURLMatcher() - denyFilter := htrie.NewURLMatcher() - hasAllow := false - hasDeny := false - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - - if strings.HasPrefix(line, "allow|") { - line = strings.TrimPrefix(line, "allow") - err = allowFilter.AddRule(line) - if err != nil { - break - } - hasAllow = true - } else if strings.HasPrefix(line, "deny|") { - line = strings.TrimPrefix(line, "deny") - err = denyFilter.AddRule(line) - if err != nil { - break - } - hasDeny = true - } else { - fmt.Println("ignoring line: ", line) - } - - err = scanner.Err() - if err != nil { - break - } - } - if err != nil { - return nil, fmt.Errorf("error building filter ruleset: %s", err) - } - - // append in order. allow first, then deny filters. - // first false value aborts the request. - filterFuncs := make([]camo.FilterFunc, 0) - - if hasAllow { - filterFuncs = append(filterFuncs, allowFilter.CheckURL) - } - - // denyFilter returns true on a match. we want a "false" value to abort processing. - // so just wrap and invert the bool. - if hasDeny { - denyF := func(u *url.URL) bool { - return !denyFilter.CheckURL(u) - } - filterFuncs = append(filterFuncs, denyF) - } - - if hasAllow && hasDeny { - mlog.Printf("Warning! Allow and Deny rules both supplied. Having Allow rules means anything not matching an allow rule is denied. THEN deny rules are evaluated. Be sure this is what you want!") - } - - return filterFuncs, nil +type CLI struct { + HMACKey string `name:"key" short:"k" help:"HMAC key"` + AddHeaders []string `name:"header" short:"H" help:"Add additional header to each response. This option can be used multiple times to add multiple headers."` + BindAddress string `name:"listen" default:"0.0.0.0:8080" help:"Address:Port to bind to for HTTP"` + BindAddressSSL string `name:"ssl-listen" help:"Address:Port to bind to for HTTPS/SSL/TLS"` + BindSocket string `name:"socket-listen" help:"Path for unix domain socket to bind to for HTTP"` + EnableQuic bool `name:"quic" help:"Enable http3/quic. Binds to the same port number as ssl-listen but udp+quic."` + AutoMaxProcs bool `name:"automaxprocs" help:"Set GOMAXPROCS automatically to match Linux container CPU quota/limits."` + SSLKey string `name:"ssl-key" help:"ssl private key (key.pem) path"` + SSLCert string `name:"ssl-cert" help:"ssl cert (cert.pem) path"` + MaxSize int64 `name:"max-size" help:"Max allowed response size (KB)"` + ReqTimeout time.Duration `name:"timeout" default:"4s" help:"Upstream request timeout"` + MaxRedirects int `name:"max-redirects" default:"3" help:"Maximum number of redirects to follow"` + MaxSizeRedirect string `long:"max-size-redirect" description:"URL to redirect when max-size is exceeded"` + Metrics bool `name:"metrics" help:"Enable Prometheus compatible metrics endpoint"` + NoDebugVars bool `name:"no-debug-vars" help:"Disable the /debug/vars/ metrics endpoint. This option has no effects when the metrics are not enabled."` + NoLogTS bool `name:"no-log-ts" help:"Do not add a timestamp to logging"` + Profile bool `name:"prof" help:"Enable go http profiler endpoint"` + LogJson bool `name:"log-json" help:"Log in JSON format"` + DisableKeepAlivesFE bool `name:"no-fk" help:"Disable frontend http keep-alive support"` + DisableKeepAlivesBE bool `name:"no-bk" help:"Disable backend http keep-alive support"` + AllowContentVideo bool `name:"allow-content-video" help:"Additionally allow 'video/*' content"` + AllowContentAudio bool `name:"allow-content-audio" help:"Additionally allow 'audio/*' content"` + AllowCredentialURLs bool `name:"allow-credential-urls" help:"Allow urls to contain user/pass credentials"` + FilterRuleset string `name:"filter-ruleset" help:"Text file containing filtering rules (one per line)"` + ServerName string `name:"server-name" default:"go-camo" help:"Value to use for the HTTP server field"` + ExposeServerVersion bool `name:"expose-server-version" help:"Include the server version in the HTTP server response header"` + EnableXFwdFor bool `name:"enable-xfwd4" help:"Enable x-forwarded-for passthrough/generation"` + Verbose bool `name:"verbose" short:"v" help:"Show verbose (debug) log level output"` + Version int `name:"version" short:"V" type:"counter" help:"Print version and exit; specify twice to show license information."` } -func main() { - // command line flags - var opts struct { - HMACKey string `short:"k" long:"key" description:"HMAC key"` - AddHeaders []string `short:"H" long:"header" description:"Add additional header to each response. This option can be used multiple times to add multiple headers"` - BindAddress string `long:"listen" default:"0.0.0.0:8080" description:"Address:Port to bind to for HTTP"` - BindAddressSSL string `long:"ssl-listen" description:"Address:Port to bind to for HTTPS/SSL/TLS"` - BindSocket string `long:"socket-listen" description:"Path for unix domain socket to bind to for HTTP"` - SSLKey string `long:"ssl-key" description:"ssl private key (key.pem) path"` - SSLCert string `long:"ssl-cert" description:"ssl cert (cert.pem) path"` - MaxSize int64 `long:"max-size" description:"Max allowed response size (KB)"` - MaxSizeRedirect string `long:"max-size-redirect" description:"URL to redirect when max-size is exceeded"` - ReqTimeout time.Duration `long:"timeout" default:"4s" description:"Upstream request timeout"` - MaxRedirects int `long:"max-redirects" default:"3" description:"Maximum number of redirects to follow"` - Metrics bool `long:"metrics" description:"Enable Prometheus compatible metrics endpoint"` - NoLogTS bool `long:"no-log-ts" description:"Do not add a timestamp to logging"` - LogJson bool `long:"log-json" description:"Log in JSON format"` - DisableKeepAlivesFE bool `long:"no-fk" description:"Disable frontend http keep-alive support"` - DisableKeepAlivesBE bool `long:"no-bk" description:"Disable backend http keep-alive support"` - AllowContentVideo bool `long:"allow-content-video" description:"Additionally allow 'video/*' content"` - AllowContentAudio bool `long:"allow-content-audio" description:"Additionally allow 'audio/*' content"` - AllowCredentialURLs bool `long:"allow-credential-urls" description:"Allow urls to contain user/pass credentials"` - FilterRuleset string `long:"filter-ruleset" description:"Text file containing filtering rules (one per line)"` - ServerName string `long:"server-name" default:"go-camo" description:"Value to use for the HTTP server field"` - ExposeServerVersion bool `long:"expose-server-version" description:"Include the server version in the HTTP server response header"` - EnableXFwdFor bool `long:"enable-xfwd4" description:"Enable x-forwarded-for passthrough/generation"` - Verbose bool `short:"v" long:"verbose" description:"Show verbose (debug) log level output"` - Version []bool `short:"V" long:"version" description:"Print version and exit; specify twice to show license information"` - } - - // parse said flags - _, err := flags.Parse(&opts) - if err != nil { - if e, ok := err.(*flags.Error); ok { - if e.Type == flags.ErrHelp { - os.Exit(0) - } - } - os.Exit(1) - } - +func (cli *CLI) Run() { // set the server name - ServerName := opts.ServerName + ServerName := cli.ServerName // setup the server response field - ServerResponse := opts.ServerName + ServerResponse := cli.ServerName // expand/override server response value if showing version is desired - if opts.ExposeServerVersion { + if cli.ExposeServerVersion { ServerResponse = fmt.Sprintf("%s %s", ServerName, ServerVersion) } // setup -V version output - if len(opts.Version) > 0 { + if cli.Version > 0 { fmt.Printf("%s %s (%s,%s-%s)\n", "go-camo", ServerVersion, runtime.Version(), runtime.Compiler, runtime.GOARCH) - if len(opts.Version) > 1 { + if cli.Version > 1 { fmt.Printf("\n%s\n", strings.TrimSpace(licenseText)) } os.Exit(0) @@ -206,40 +131,44 @@ func main() { } // flags override env var - if opts.HMACKey != "" { - config.HMACKey = []byte(opts.HMACKey) + if cli.HMACKey != "" { + config.HMACKey = []byte(cli.HMACKey) } if len(config.HMACKey) == 0 { mlog.Fatal("HMAC key required") } - if opts.BindAddress == "" && opts.BindAddressSSL == "" && opts.BindSocket == "" { + if cli.BindAddress == "" && cli.BindAddressSSL == "" && cli.BindSocket == "" { mlog.Fatal("One of listen or ssl-listen required") } - if opts.BindAddressSSL != "" && opts.SSLKey == "" { + if cli.BindAddressSSL != "" && cli.SSLKey == "" { mlog.Fatal("ssl-key is required when specifying ssl-listen") } - if opts.BindAddressSSL != "" && opts.SSLCert == "" { + if cli.BindAddressSSL != "" && cli.SSLCert == "" { mlog.Fatal("ssl-cert is required when specifying ssl-listen") } + if cli.EnableQuic && cli.BindAddressSSL == "" { + mlog.Fatal("ssl-listen is required when specifying quic") + } // set keepalive options - config.DisableKeepAlivesBE = opts.DisableKeepAlivesBE - config.DisableKeepAlivesFE = opts.DisableKeepAlivesFE + config.DisableKeepAlivesBE = cli.DisableKeepAlivesBE + config.DisableKeepAlivesFE = cli.DisableKeepAlivesFE // other options - config.EnableXFwdFor = opts.EnableXFwdFor - config.AllowCredentialURLs = opts.AllowCredentialURLs + config.EnableXFwdFor = cli.EnableXFwdFor + config.AllowCredentialURLs = cli.AllowCredentialURLs // additional content types to allow - config.AllowContentVideo = opts.AllowContentVideo - config.AllowContentAudio = opts.AllowContentAudio + config.AllowContentVideo = cli.AllowContentVideo + config.AllowContentAudio = cli.AllowContentAudio var filters []camo.FilterFunc - if opts.FilterRuleset != "" { - filters, err = loadFilterList(opts.FilterRuleset) + if cli.FilterRuleset != "" { + var err error + filters, err = loadFilterList(cli.FilterRuleset) if err != nil { mlog.Fatal("Could not read filter-ruleset", err) } @@ -252,7 +181,8 @@ func main() { "Content-Security-Policy": "default-src 'none'; img-src data:; style-src 'unsafe-inline'", } - for _, v := range opts.AddHeaders { + for _, v := range cli.AddHeaders { + fmt.Println(v) s := strings.SplitN(v, ":", 2) if len(s) != 2 { mlog.Printf("ignoring bad header: '%s'", v) @@ -271,49 +201,38 @@ func main() { // now configure a standard logger mlog.SetFlags(mlog.Lstd) - if opts.NoLogTS { + if cli.NoLogTS { mlog.SetFlags(mlog.Flags() ^ mlog.Ltimestamp) } - if opts.Verbose { + if cli.Verbose { mlog.SetFlags(mlog.Flags() | mlog.Ldebug) mlog.Debug("debug logging enabled") } - if opts.LogJson { - mlog.SetEmitter(&mlog.FormatWriterJSON{}) + if cli.AutoMaxProcs { + // #nosec G104 + maxprocs.Set( + maxprocs.Logger(mlog.Infof), + // uncomment once gomaxprocs has a new release, as this fixes + // https://github.com/uber-go/automaxprocs/issues/78 and similar. + // maxprocs.RoundQuotaFunc(func(v float64) int { return int(math.Ceil(v)) }), + ) } - if maxSize := os.Getenv("GOCAMO_MAXSIZE"); maxSize != "" { - // convert from string to int64 - maxSize, err := strconv.ParseInt(maxSize, 10, 64) - if err != nil { - mlog.Fatal("Invalid value for max-size", err) - } - // convert from KB to Bytes - config.MaxSize = maxSize * 1024 - } - - // flags override env var - if opts.MaxSize != 0 { - // convert from KB to Bytes - config.MaxSize = opts.MaxSize * 1024 - } - - if maxSizeRedirect := os.Getenv("GOCAMO_MAXSIZEREDIRECT"); maxSizeRedirect != ""{ - config.MaxSizeRedirect = maxSizeRedirect - } - // flags override env var - if opts.MaxSizeRedirect != "" { - config.MaxSizeRedirect = opts.MaxSizeRedirect + if cli.LogJson { + mlog.SetEmitter(&mlog.FormatWriterJSON{}) } - config.RequestTimeout = opts.ReqTimeout - config.MaxRedirects = opts.MaxRedirects + // convert from KB to Bytes + config.MaxSize = cli.MaxSize * 1024 + config.RequestTimeout = cli.ReqTimeout + config.MaxRedirects = cli.MaxRedirects + config.MaxSizeRedirect = cli.MaxSizeRedirect config.ServerName = ServerName // configure metrics collection in camo - if opts.Metrics { + if cli.Metrics { config.CollectMetrics = true } @@ -328,10 +247,11 @@ func main() { CamoHandler: proxy, } + mux := http.NewServeMux() + // configure router endpoint for rendering metrics - if opts.Metrics { + if cli.Metrics { mlog.Printf("Enabling metrics at /metrics") - http.Handle("/metrics", promhttp.Handler()) // Register a version info metric. verOverride := os.Getenv("APP_INFO_VERSION") if verOverride != "" { @@ -342,17 +262,64 @@ func main() { version.Revision = os.Getenv("APP_INFO_REVISION") version.Branch = os.Getenv("APP_INFO_BRANCH") version.BuildDate = os.Getenv("APP_INFO_BUILD_DATE") - prometheus.MustRegister(version.NewCollector(metricNamespace)) + prometheus.MustRegister(vcoll.NewCollector(metricNamespace)) + // Wrap the dumb router in instrumentation. router = promhttp.InstrumentHandlerDuration(responseDuration, router) router = promhttp.InstrumentHandlerCounter(responseCount, router) router = promhttp.InstrumentHandlerResponseSize(responseSize, router) + + // also configure expvars. this is usually a side effect of importing + // exvar, as it auto-adds it to the default servemux. Since we want + // to avoid it being available that when metrics is not enabled, we add + // it in manually only if metrics IS enabled. + if !cli.NoDebugVars { + mux.Handle("/debug/vars", expvar.Handler()) + } + mux.Handle("/metrics", promhttp.Handler()) + } + + if cli.Profile { + mlog.Printf("Enabling cpu profile at /debug/pprof") + mux.HandleFunc("/debug/pprof/", pprof.Index) + // mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + // mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + // mux.HandleFunc("/debug/pprof/trace", pprof.Trace) } - http.Handle("/", router) + mux.Handle("/", router) - srv := &http.Server{ - ReadTimeout: 30 * time.Second, + var httpSrv *http.Server + var tlsSrv *http.Server + var quicSrv *http3.Server + + if cli.BindAddress != "" { + httpSrv = &http.Server{ + Addr: cli.BindAddress, + ReadTimeout: 30 * time.Second, + Handler: mux, + } + } + + if cli.BindAddressSSL != "" { + tlsSrv = &http.Server{ + Addr: cli.BindAddressSSL, + ReadTimeout: 30 * time.Second, + Handler: mux, + } + + if cli.EnableQuic { + quicSrv = &http3.Server{ + Addr: cli.BindAddressSSL, + Handler: mux, + } + // wrap default mux to set some default quic reference headers on tls responses + tlsSrv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + quicSrv.SetQUICHeaders(w.Header()) // #nosec G104 - ignore error. should only happen if server.Port isn't discoverable + mux.ServeHTTP(w, r) + }) + } } idleConnsClosed := make(chan struct{}) @@ -362,64 +329,78 @@ func main() { signal.Notify(sigint, os.Interrupt, syscall.SIGTERM) s := <-sigint mlog.Info("Handling signal:", s) - mlog.Info("Starting graceful shutdown") - d := time.Now().Add(200 * time.Millisecond) - ctx, cancel := context.WithDeadline(context.Background(), d) + closeWait := 200 * time.Millisecond - if err := srv.Shutdown(ctx); err != nil { - mlog.Info("Error gracefully shutting down server:", err) - } - // Even though ctx may be expired, it is good practice to call its + ctx, cancel := context.WithTimeout(context.Background(), closeWait) + // Even though ctx may be expired by then, it is good practice to call its // cancellation function in any case. Failure to do so may keep the // context and its parent alive longer than necessary. - cancel() + defer cancel() + if httpSrv != nil { + if err := httpSrv.Shutdown(ctx); err != nil { + mlog.Info("Error gracefully shutting down HTTP server:", err) + } + } + + ctx, cancel = context.WithTimeout(context.Background(), closeWait) + defer cancel() + if tlsSrv != nil { + if err := tlsSrv.Shutdown(ctx); err != nil { + mlog.Info("Error gracefully shutting down HTTP/TLS server:", err) + } + } + + if quicSrv != nil { + if err := quicSrv.CloseGracefully(closeWait); err != nil { + mlog.Info("Error gracefully shutting down HTTP3/QUIC server:", err) + } + } close(idleConnsClosed) }() - if opts.BindSocket != "" { - if _, err := os.Stat(opts.BindSocket); err == nil { + if cli.BindSocket != "" { + if _, err := os.Stat(cli.BindSocket); err == nil { mlog.Fatal("Cannot bind to unix socket, file aready exists.") } - mlog.Printf("Starting HTTP server on: unix:%s", opts.BindSocket) + mlog.Printf("Starting HTTP server on: unix:%s", cli.BindSocket) go func() { - ln, err := net.Listen("unix", opts.BindSocket) + ln, err := net.Listen("unix", cli.BindSocket) if err != nil { mlog.Fatal("Error listening on unix socket", err) } - if err := srv.Serve(ln); err != http.ErrServerClosed { + if err := httpSrv.Serve(ln); err != http.ErrServerClosed { mlog.Fatal(err) } }() } - if opts.BindAddress != "" { - mlog.Printf("Starting HTTP server on: tcp:%s", opts.BindAddress) + if httpSrv != nil { + mlog.Printf("Starting HTTP server on: tcp:%s", cli.BindAddress) go func() { - ln, err := net.Listen("tcp", opts.BindAddress) - if err != nil { - mlog.Fatal("Error listening on tcp socket", err) - } - - if err := srv.Serve(ln); err != http.ErrServerClosed { + if err := httpSrv.ListenAndServe(); err != http.ErrServerClosed { mlog.Fatal(err) } }() } - if opts.BindAddressSSL != "" { - mlog.Printf("Starting TLS server on: tcp:%s", opts.BindAddressSSL) + if tlsSrv != nil { + mlog.Printf("Starting HTTP/TLS server on: tcp:%s", cli.BindAddressSSL) go func() { - ln, err := net.Listen("tcp", opts.BindAddressSSL) - if err != nil { - mlog.Fatal("Error listening on tcp socket", err) + if err := tlsSrv.ListenAndServeTLS(cli.SSLCert, cli.SSLKey); err != http.ErrServerClosed { + mlog.Fatal(err) } + }() + } - if err := srv.ServeTLS(ln, opts.SSLCert, opts.SSLKey); err != http.ErrServerClosed { + if quicSrv != nil { + mlog.Printf("Starting HTTP3/QUIC server on: udp:%s", cli.BindAddressSSL) + go func() { + if err := quicSrv.ListenAndServeTLS(cli.SSLCert, cli.SSLKey); err != http.ErrServerClosed { mlog.Fatal(err) } }() @@ -428,3 +409,14 @@ func main() { // just block waiting for closure <-idleConnsClosed } + +func main() { + cli := CLI{} + _ = kong.Parse(&cli, + kong.Name("go-camo"), + kong.Description("An image proxy that proxies non-secure images over SSL/TLS"), + kong.UsageOnError(), + kong.Vars{"version": ServerVersion}, + ) + cli.Run() +} diff --git a/cmd/go-camo/main_loadfilters.go b/cmd/go-camo/main_loadfilters.go new file mode 100644 index 00000000..343694d8 --- /dev/null +++ b/cmd/go-camo/main_loadfilters.go @@ -0,0 +1,97 @@ +// Copyright (c) 2012-2023 Eli Janssen +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "fmt" + "net/url" + "os" + "strings" + + "github.com/cactus/go-camo/v2/pkg/camo" + "github.com/cactus/go-camo/v2/pkg/htrie" + "github.com/cactus/mlog" +) + +func loadFilterList(fname string) ([]camo.FilterFunc, error) { + // #nosec + file, err := os.Open(fname) + if err != nil { + return nil, fmt.Errorf("could not open filter-ruleset file: %s", err) + } + // #nosec + defer file.Close() + + allowFilter := htrie.NewURLMatcher() + denyFilter := htrie.NewURLMatcher() + hasAllow := false + hasDeny := false + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if strings.HasPrefix(line, "allow|") { + line = strings.TrimPrefix(line, "allow") + err = allowFilter.AddRule(line) + if err != nil { + break + } + hasAllow = true + } else if strings.HasPrefix(line, "deny|") { + line = strings.TrimPrefix(line, "deny") + err = denyFilter.AddRule(line) + if err != nil { + break + } + hasDeny = true + } else { + fmt.Println("ignoring line: ", line) + } + + err = scanner.Err() + if err != nil { + break + } + } + if err != nil { + return nil, fmt.Errorf("error building filter ruleset: %s", err) + } + + // append in order. allow first, then deny filters. + // first false value aborts the request. + filterFuncs := make([]camo.FilterFunc, 0) + + if hasAllow { + filterFuncs = append(filterFuncs, allowFilter.CheckURL) + } + + // denyFilter returns true on a match. we want to invert this for a deny rule, so + // any deny rule match should return true, and anything _not_ matching should return false + // so just wrap and invert the bool. + if hasDeny { + denyF := func(u *url.URL) (bool, error) { + chk, err := denyFilter.CheckURL(u) + return !chk, err + } + filterFuncs = append(filterFuncs, denyF) + } + + if hasAllow && hasDeny { + mlog.Print( + strings.Join( + []string{ + "Warning! Allow and Deny rules both supplied.", + "Having Allow rules means anything not matching an allow rule is denied.", + "THEN deny rules are evaluated. Be sure this is what you want!", + }, + " ", + ), + ) + } + + return filterFuncs, nil +} diff --git a/cmd/go-camo/main_vers_gen.go b/cmd/go-camo/main_vers_gen.go index 7f207919..b4330aaa 100644 --- a/cmd/go-camo/main_vers_gen.go +++ b/cmd/go-camo/main_vers_gen.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -11,7 +11,7 @@ https://github.com/cactus/go-camo Portions of this software utilize third party libraries: * Runtime dependencies: ├── github.com/cactus/mlog (MIT license) - ├── github.com/jessevdk/go-flags (BSD license) + ├── github.com/alecthomas/kong (MIT license) ├── github.com/prometheus/client_golang (Apache 2.0) ├── github.com/prometheus/common (Apache 2.0) └── golang.org/x/net (BSD license) diff --git a/cmd/url-tool/main.go b/cmd/url-tool/main.go index 991469be..c06a2fde 100644 --- a/cmd/url-tool/main.go +++ b/cmd/url-tool/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -9,67 +9,65 @@ import ( "errors" "fmt" "net/url" - "os" "strings" - "github.com/cactus/go-camo/pkg/camo/encoding" + "github.com/cactus/go-camo/v2/pkg/camo/encoding" - flags "github.com/jessevdk/go-flags" + "github.com/alecthomas/kong" ) +// ServerVersion holds the server version string +var ServerVersion = "no-version" + // EncodeCommand holds command options for the encode command -type EncodeCommand struct { - Base string `short:"b" long:"base" default:"hex" description:"Encode/Decode base. Either hex or base64"` - Prefix string `short:"p" long:"prefix" default:"" description:"Optional url prefix used by encode output"` - Positional struct { - Url string `positional-arg-name:"URL"` - } `positional-args:"yes" required:"true"` +type EncodeCmd struct { + Base string `name:"base" short:"b" enum:"hex,base64" default:"hex" help:"Encode/Decode base. One of: ${enum}"` + Prefix string `name:"prefix" short:"p" default:"" help:"Optional url prefix used by encode output"` + Url string `arg:"" name:"URL" help:"URL to encode"` } // Execute runs the encode command -func (c *EncodeCommand) Execute(args []string) error { - if opts.HmacKey == "" { +func (cmd *EncodeCmd) Run(cli *CLI) error { + if cli.HmacKey == "" { return errors.New("empty HMAC") } - if len(c.Positional.Url) == 0 { + if len(cmd.Url) == 0 { return errors.New("no url argument provided") } - hmacKeyBytes := []byte(opts.HmacKey) + hmacKeyBytes := []byte(cli.HmacKey) var outURL string - switch c.Base { + switch cmd.Base { case "base64": - outURL = encoding.B64EncodeURL(hmacKeyBytes, c.Positional.Url) + outURL = encoding.B64EncodeURL(hmacKeyBytes, cmd.Url) case "hex": - outURL = encoding.HexEncodeURL(hmacKeyBytes, c.Positional.Url) + outURL = encoding.HexEncodeURL(hmacKeyBytes, cmd.Url) default: return errors.New("invalid base provided") } - fmt.Println(strings.TrimRight(c.Prefix, "/") + outURL) + fmt.Println(strings.TrimRight(cmd.Prefix, "/") + outURL) return nil } // DecodeCommand holds command options for the decode command -type DecodeCommand struct { - Positional struct { - Url string `positional-arg-name:"URL"` - } `positional-args:"yes" required:"true"` +type DecodeCmd struct { + Url string `arg:"" name:"URL" help:"URL to decode"` } // Execute runs the decode command -func (c *DecodeCommand) Execute(args []string) error { - if opts.HmacKey == "" { +func (cmd *DecodeCmd) Run(cli *CLI) error { + if cli.HmacKey == "" { return errors.New("empty HMAC") } - if len(c.Positional.Url) == 0 { + if len(cmd.Url) == 0 { return errors.New("no url argument provided") } - hmacKeyBytes := []byte(opts.HmacKey) + hmacKeyBytes := []byte(cli.HmacKey) - u, err := url.Parse(c.Positional.Url) + u, err := url.Parse(cmd.Url) if err != nil { return err } @@ -82,26 +80,25 @@ func (c *DecodeCommand) Execute(args []string) error { return nil } -var opts struct { - HmacKey string `short:"k" long:"key" description:"HMAC key"` +type CLI struct { + // global options + Version kong.VersionFlag `name:"version" short:"V" help:"Print version information and quit"` + HmacKey string `name:"key" short:"k" help:"HMAC key"` + + // subcommands + Encode EncodeCmd `cmd:"" aliases:"enc" help:"Encode a url and print result"` + Decode DecodeCmd `cmd:"" aliases:"dec" help:"Decode a url and print result"` } // #nosec G104 func main() { - parser := flags.NewParser(&opts, flags.Default) - parser.AddCommand("encode", "Encode a url and print result", - "Encode a url and print result", &EncodeCommand{}) - parser.AddCommand("decode", "Decode a url and print result", - "Decode a url and print result", &DecodeCommand{}) - - // parse said flags - _, err := parser.Parse() - if err != nil { - if e, ok := err.(*flags.Error); ok { - if e.Type == flags.ErrHelp { - os.Exit(0) - } - } - os.Exit(1) - } + cli := CLI{} + ctx := kong.Parse(&cli, + kong.Name("url-tool"), + kong.Description("A simple way to work with signed go-camo URLs from the command line"), + kong.UsageOnError(), + kong.Vars{"version": ServerVersion}, + ) + err := ctx.Run(&cli) + ctx.FatalIfErrorf(err) } diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..c20b5344 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:alpine as builder +RUN apk add --no-cache ca-certificates tzdata make git +WORKDIR /workdir +ENV GOEXPERIMENT=loopvar +COPY go.mod go.sum ./ +RUN go mod download +COPY . ./ +ARG APP_VER +ARG GITHASH +RUN make build APP_VER="${APP_VER}" GITHASH="${GITHASH}"; rm -rf /root/.cache/ + +FROM alpine:latest as run +RUN apk add --no-cache ca-certificates tzdata +WORKDIR /app +COPY --from=builder --link /workdir/build/bin/* /bin/ + +USER nobody +EXPOSE 8080/tcp +ENTRYPOINT ["/bin/go-camo"] diff --git a/examples/Dockerfile b/examples/Dockerfile deleted file mode 100644 index 168f8b37..00000000 --- a/examples/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM alpine:latest -RUN apk add --no-cache ca-certificates -COPY build/bin/* /bin/ -EXPOSE 8080/tcp -USER nobody -ENTRYPOINT ["/bin/go-camo"] diff --git a/examples/go-base64.go b/examples/go-base64.go index 71ae8276..d9d67a16 100644 --- a/examples/go-base64.go +++ b/examples/go-base64.go @@ -1,7 +1,8 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. +//go:build ignore // +build ignore package main diff --git a/examples/go-hex.go b/examples/go-hex.go deleted file mode 100644 index 3812b242..00000000 --- a/examples/go-hex.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2012-2019 Eli Janssen -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -// +build ignore - -package main - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/hex" - "fmt" - "strings" -) - -var CAMO_HOST = "https://img.example.com" - -func GenCamoUrl(hmacKey []byte, srcUrl string) string { - if strings.HasPrefix(srcUrl, "https:") { - return srcUrl - } - oBytes := []byte(srcUrl) - mac := hmac.New(sha1.New, hmacKey) - mac.Write(oBytes) - macSum := hex.EncodeToString(mac.Sum(nil)) - encodedUrl := hex.EncodeToString(oBytes) - hexurl := CAMO_HOST + "/" + macSum + "/" + encodedUrl - return hexurl -} - -func main() { - fmt.Println(GenCamoUrl([]byte("test"), "http://golang.org/doc/gopher/frontpage.png")) -} diff --git a/examples/python-base64.py b/examples/python-base64.py deleted file mode 100644 index 8739b045..00000000 --- a/examples/python-base64.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright (c) 2012-2019 Eli Janssen -# Use of this source code is governed by an MIT-style -# license that can be found in the LICENSE file. - -import hashlib -import hmac -import base64 - - -CAMO_HOST = 'https://img.example.com' - - -def camo_url(hmac_key, image_url): - if image_url.startswith("https:"): - return image_url - b64digest = base64.urlsafe_b64encode( - hmac.new(hmac_key, image_url, hashlib.sha1).digest()).strip('=') - b64url = base64.urlsafe_b64encode(image_url).strip('=') - requrl = '%s/%s/%s' % (CAMO_HOST, b64digest, b64url) - return requrl - - -print camo_url("test", "http://golang.org/doc/gopher/frontpage.png") -# 'https://img.example.org/D23vHLFHsOhPOcvdxeoQyAJTpvM/aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n' - diff --git a/examples/python-hex.py b/examples/python-hex.py deleted file mode 100644 index f5d84b5d..00000000 --- a/examples/python-hex.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2012-2019 Eli Janssen -# Use of this source code is governed by an MIT-style -# license that can be found in the LICENSE file. - -import hashlib -import hmac - - -CAMO_HOST = 'https://img.example.com' - - -def camo_url(hmac_key, image_url): - if image_url.startswith("https:"): - return image_url - hexdigest = hmac.new(hmac_key, image_url, hashlib.sha1).hexdigest() - hexurl = image_url.encode('hex') - requrl = '%s/%s/%s' % (CAMO_HOST, hexdigest, hexurl) - return requrl - - -print camo_url("test", "http://golang.org/doc/gopher/frontpage.png") -# 'https://img.example.org/0f6def1cb147b0e84f39cbddc5ea10c80253a6f3/687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67' diff --git a/examples/python3-base64-filtering.py b/examples/python3-base64-filtering.py index 28ded526..7fa49fc0 100644 --- a/examples/python3-base64-filtering.py +++ b/examples/python3-base64-filtering.py @@ -1,4 +1,4 @@ -# Copyright (c) 2012-2019 Eli Janssen +# Copyright (c) 2012-2023 Eli Janssen # Use of this source code is governed by an MIT-style # license that can be found in the LICENSE file. @@ -15,6 +15,11 @@ CAMO_HOST = 'https://img.example.com' +def wrap_encode(data): + """A little helper method to wrap b64encoding""" + return base64.urlsafe_b64encode(data).strip(b'=').decode('utf-8') + + def camo_url(hmac_key, image_url): url = urlsplit(image_url) @@ -32,10 +37,10 @@ def camo_url(hmac_key, image_url): hmac_key = hmac_key.encode() if isinstance(hmac_key, str) else hmac_key image_url = image_url.encode() if isinstance(image_url, str) else image_url - b64digest = base64.urlsafe_b64encode( + b64digest = wrap_encode( hmac.new(hmac_key, image_url, hashlib.sha1).digest() - ).strip(b'=').decode('utf-8') - b64url = base64.urlsafe_b64encode(image_url).strip(b'=').decode('utf-8') + ) + b64url = wrap_encode(image_url) requrl = '%s/%s/%s' % (CAMO_HOST, b64digest, b64url) return requrl diff --git a/examples/python3-base64.py b/examples/python3-base64.py new file mode 100644 index 00000000..c420d886 --- /dev/null +++ b/examples/python3-base64.py @@ -0,0 +1,43 @@ +# Copyright (c) 2012-2023 Eli Janssen +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file. + +import hashlib +import hmac +import base64 + + +CAMO_HOST = 'https://img.example.com' + + +def wrap_encode(data): + """A little helper method to wrap b64encoding""" + return base64.urlsafe_b64encode(data).strip(b'=').decode('utf-8') + + +def camo_url(hmac_key, image_url): + if image_url.startswith("https:"): + return image_url + + hmac_key = hmac_key.encode() if isinstance(hmac_key, str) else hmac_key + image_url = image_url.encode() if isinstance(image_url, str) else image_url + + # setup the hmac construction + mac = hmac.new(hmac_key, digestmod=hashlib.sha1) + # add image_url + mac.update(image_url) + + # generate digest + digest = mac.digest() + + ## now build url + b64digest = wrap_encode(digest) + b64url = wrap_encode(image_url) + requrl = '%s/%s/%s' % (CAMO_HOST, b64digest, b64url) + return requrl + + +print( + camo_url("test", "http://golang.org/doc/gopher/frontpage.png") +) +# https://img.example.org/D23vHLFHsOhPOcvdxeoQyAJTpvM/aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n \ No newline at end of file diff --git a/examples/ruby-base64.rb b/examples/ruby-base64.rb index 66bdebf7..0aea5839 100644 --- a/examples/ruby-base64.rb +++ b/examples/ruby-base64.rb @@ -1,4 +1,4 @@ -# Copyright (c) 2012-2019 Eli Janssen +# Copyright (c) 2012-2023 Eli Janssen # Use of this source code is governed by an MIT-style # license that can be found in the LICENSE file. diff --git a/examples/ruby-hex.rb b/examples/ruby-hex.rb deleted file mode 100644 index abc775d2..00000000 --- a/examples/ruby-hex.rb +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2012-2019 Eli Janssen -# Use of this source code is governed by an MIT-style -# license that can be found in the LICENSE file. - -require "openssl" - -CAMO_HOST = "https://img.example.com" - -def camo_url(hmac_key, image_url) - if image_url.start_with?("https:") - return image_url - end - hexdigest = OpenSSL::HMAC.hexdigest("sha1", hmac_key, image_url) - hexurl = image_url.unpack("U*").collect{|x| x.to_s(16)}.join - return "#{CAMO_HOST}/#{hexdigest}/#{hexurl}" -end - -puts camo_url("test", "http://golang.org/doc/gopher/frontpage.png") -# 'https://img.example.org/0f6def1cb147b0e84f39cbddc5ea10c80253a6f3/687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67' diff --git a/go.mod b/go.mod index 626c25fb..24bb95bf 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,36 @@ -module github.com/cactus/go-camo +module github.com/cactus/go-camo/v2 -go 1.19 +go 1.21 require ( - github.com/cactus/mlog v1.0.4 - github.com/jessevdk/go-flags v1.5.0 - github.com/prometheus/client_golang v1.14.0 - github.com/prometheus/common v0.39.0 - github.com/xlab/treeprint v1.1.0 - golang.org/x/net v0.7.0 - gotest.tools/v3 v3.4.0 + github.com/alecthomas/kong v0.9.0 + github.com/cactus/mlog v1.0.10 + github.com/prometheus/client_golang v1.19.1 + github.com/prometheus/common v0.54.0 + github.com/quic-go/quic-go v0.45.0 + github.com/xlab/treeprint v1.2.0 + go.uber.org/automaxprocs v1.5.3 + golang.org/x/net v0.26.0 + gotest.tools/v3 v3.5.1 ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/cactus/tai64 v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-cmp v0.5.8 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + github.com/cactus/tai64 v1.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect + github.com/onsi/ginkgo/v2 v2.19.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/quic-go/qpack v0.4.0 // indirect + go.uber.org/mock v0.4.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.22.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index c47ba834..96eb664f 100644 --- a/go.sum +++ b/go.sum @@ -1,83 +1,116 @@ +github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= +github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= +github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cactus/mlog v1.0.4 h1:EUSh0kpw+8p8yrzEF+oBzIXbcZ0cH7PqsbFv6lxbTZQ= -github.com/cactus/mlog v1.0.4/go.mod h1:QmoSa40W9mptIRIouyVJ/n/nsxux/Va7t2tegPO+BIA= -github.com/cactus/tai64 v1.0.1 h1:rXBf2Ab+1F8Fr9wAtB3xilSnDsHwfpeJUKgham4Bot0= -github.com/cactus/tai64 v1.0.1/go.mod h1:gu5LAXd6eWwrRD/HPw+aTrJF5WkieYswRVLSNslKGg4= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cactus/mlog v1.0.10 h1:xT6iHX2pT3EVrDtch5h5zWu3hWmfcV8gMfCUF4KXJwY= +github.com/cactus/mlog v1.0.10/go.mod h1:z0zgmK5rbywKn+lJPzhLEFZOJejzfRBBW1RqC/1i588= +github.com/cactus/tai64 v1.0.2/go.mod h1:gu5LAXd6eWwrRD/HPw+aTrJF5WkieYswRVLSNslKGg4= +github.com/cactus/tai64 v1.0.3 h1:GKdl8U1VprATgD16ob7Vjn7k+0G+rodh9roOcobjb3I= +github.com/cactus/tai64 v1.0.3/go.mod h1:Ciis5iTJ0/vRkbGsPqBvWTOtXbJdYobxVG/C4tSP9BY= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g= +github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= -github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= +github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= +github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= +github.com/quic-go/quic-go v0.45.0 h1:OHmkQGM37luZITyTSu6ff03HP/2IrwDX1ZFiNEhSFUE= +github.com/quic-go/quic-go v0.45.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= -github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/man/go-camo.1.adoc b/man/go-camo.1.adoc index d447ba65..6fe9897e 100644 --- a/man/go-camo.1.adoc +++ b/man/go-camo.1.adoc @@ -64,8 +64,9 @@ For exmaple, if the HMAC key is provided on the command line, it will override *-H*, *--header*=<__HEADER__>:: + -- -Extra header to return for each response. This option can be used -multiple times to add multiple headers. +Add additional header to each response. +This option can be used multiple times to add multiple headers. +When specified, these headers are set unconditionally on all responses. See __<>__ for more info. -- @@ -88,6 +89,12 @@ See __<>__ for more info. Path to ssl certificate. + Default: `cert.pem` +*--quic*:: + Enable http3/quic. Binds to the same port number as _--ssl-listen_ but udp+quic. + +*--automaxprocs*:: + Set GOMAXPROCS automatically to match Linux container CPU quota/limits. + *--max-size*=<__SIZE__>:: Max response size allowed in KB. Set to `0` to disable size restriction. + Default: `0` @@ -105,8 +112,17 @@ See __<>__ for more info. -- Enable Prometheus compatible metrics endpoint. -If metrics flag is provided, then the service will make a Prometheus -compatible endpoint available at `/metrics` via HTTP GET. +If the `metrics` flag is provided, then the service will expose a Prometheus +`/metrics` endpoint and a `/debug/vars` endpoint from the go `expvar` package. + +See __<>__ for more info. +-- + +*--no-debug-vars*:: ++ +-- +If the `no-debug-vars` flag is provided along with the `metrics` flag, the +`/debug/vars` endpoint is removed. See __<>__ for more info. -- @@ -197,6 +213,10 @@ systems to gather data. The endpoint includes all of the default `go_` and `process_`. In addition, a number of custom metrics. +A `/debug/vars` endpoint is also included with *--metrics* by default. +This endpoint returns memstats and some additional data. This endpoint can be +disabled by additionally supplying the *--no-debug-vars* flag. + .Exposed Camo Metrics [%header,cols=" MaxSize", mlog.Map{"req": req}) + mlog.Debugx("response to client truncated: size > MaxSize", mlog.A("req", req)) } return } if mlog.HasDebug() { - mlog.Debugm("response to client", mlog.Map{"resp": w}) + mlog.Debugx("response to client", mlog.A("headers", h), mlog.A("status", resp.StatusCode)) } } @@ -401,11 +402,24 @@ func (p *Proxy) checkURL(reqURL *url.URL) error { return errors.New("Bad url scheme") } + uHostname := reqURL.Hostname() + // reject empy hostnames + if uHostname == "" { + return errors.New("Bad url host") + } + + // ensure valid idna lookup mapping for hostname + // this is also used by htrie filtering (localsFilter and p.filters) + cleanHostname, err := htrie.CleanHostname(uHostname) + if err != nil { + mlog.Infof("Filter lookup rejected: malformed hostname: %s", uHostname) + return errors.New("Bad url host") + } + // reject localhost urls - // lower case for matching is done by CheckHostname, so no need to + // lower case for matching is done by IdnaLookupMap above, so no need to // ToLower here also - uHostname := reqURL.Hostname() - if uHostname == "" || localsFilter.CheckHostname(uHostname) { + if localsFilter.CheckCleanHostname(cleanHostname) { return errors.New("Bad url host") } @@ -414,9 +428,9 @@ func (p *Proxy) checkURL(reqURL *url.URL) error { return errors.New("Userinfo URL rejected") } - // evaluate filters. first false value "fails" + // evaluate filters. first false (or filter error) value "fails" for i := 0; i < p.filtersLen; i++ { - if !p.filters[i](reqURL) { + if chk, err := p.filters[i](reqURL); err != nil || !chk { return errors.New("Rejected due to filter-ruleset") } } @@ -584,14 +598,14 @@ func New(pc Config) (*Proxy, error) { client.CheckRedirect = func(req *http.Request, via []*http.Request) error { if len(via) >= pc.MaxRedirects { if mlog.HasDebug() { - mlog.Debug("Got bad redirect: Too many redirects", mlog.Map{"url": req}) + mlog.Debugx("Got bad redirect: Too many redirects", mlog.A("url", req)) } return fmt.Errorf("Too many redirects: %w", ErrRedirect) } err := p.checkURL(req.URL) if err != nil { if mlog.HasDebug() { - mlog.Debugm("Got bad redirect", mlog.Map{"url": req}) + mlog.Debugx("Got bad redirect", mlog.A("url", req)) } return fmt.Errorf("Bad redirect: %w", ErrRedirect) } diff --git a/pkg/camo/proxy_filter_test.go b/pkg/camo/proxy_filter_test.go index cdcf48fd..e8962a1d 100644 --- a/pkg/camo/proxy_filter_test.go +++ b/pkg/camo/proxy_filter_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -18,9 +18,9 @@ func TestFilterListAcceptSimple(t *testing.T) { called := false filters := []FilterFunc{ - func(*url.URL) bool { + func(*url.URL) (bool, error) { called = true - return true + return true, nil }, } testURL := "http://www.google.com/images/srpr/logo11w.png" @@ -31,43 +31,89 @@ func TestFilterListAcceptSimple(t *testing.T) { assert.Check(t, called, "filter func wasn't called") } +func TestFilterListAcceptSimpleWithFilterError(t *testing.T) { + t.Parallel() + + called := false + filters := []FilterFunc{ + func(*url.URL) (bool, error) { + called = true + return true, fmt.Errorf("bad hostname") + }, + } + testURL := "http://www.google.com/images/srpr/logo11w.png" + req, err := makeReq(camoConfig, testURL) + assert.Check(t, err) + _, err = processRequest(req, 404, camoConfig, filters) + assert.Check(t, err) + assert.Check(t, called, "filter func wasn't called") +} + func TestFilterListMatrixMultiples(t *testing.T) { t.Parallel() testURL := "http://www.google.com/images/srpr/logo11w.png" req, err := makeReq(camoConfig, testURL) assert.Check(t, err) + type chkResponse struct { + chk bool + err error + } - var mixtests = []struct { - filterRuleAnswers []bool + mixtests := []struct { + filterRuleAnswers []chkResponse expectedCallMatrix []bool respcode int }{ // all rules return true, so all rules should have been called // so pass: http200 { - []bool{true, true, true}, + []chkResponse{{true, nil}, {true, nil}, {true, nil}}, []bool{true, true, true}, 200, }, + // 3rd rule should not be called, because 2nd returned false // so no pass: http404 { - []bool{true, false, true}, + []chkResponse{{true, nil}, {false, nil}, {true, nil}}, + []bool{true, true, false}, + 404, + }, + // 3rd rule should not be called, because 2nd returned an error + // so no pass: http404 + { + []chkResponse{{true, nil}, {true, fmt.Errorf("some error")}, {true, nil}}, []bool{true, true, false}, 404, }, + // 2nd, 3rd rules should not be called, because 1st returned false // so no pass: http404 { - []bool{false, false, true}, + []chkResponse{{false, nil}, {false, nil}, {true, nil}}, + []bool{true, false, false}, + 404, + }, + // 2nd, 3rd rules should not be called, because 1st returned an error + // so no pass: http404 + { + []chkResponse{{true, fmt.Errorf("some error")}, {false, nil}, {true, nil}}, []bool{true, false, false}, 404, }, + // last rule returns false, but all rules should be called. // so no pass: http404 { - []bool{true, true, false}, + []chkResponse{{true, nil}, {true, nil}, {false, nil}}, + []bool{true, true, true}, + 404, + }, + // last rule returns an error, but all rules should be called. + // so no pass: http404 + { + []chkResponse{{true, nil}, {true, nil}, {true, fmt.Errorf("some error")}}, []bool{true, true, true}, 404, }, @@ -78,10 +124,10 @@ func TestFilterListMatrixMultiples(t *testing.T) { filters := make([]FilterFunc, 0) for i := 0; i < 3; i++ { filters = append( - filters, func(x int) func(*url.URL) bool { - return func(*url.URL) bool { + filters, func(x int) FilterFunc { + return func(*url.URL) (bool, error) { callMatrix[x] = true - return tt.filterRuleAnswers[x] + return tt.filterRuleAnswers[x].chk, tt.filterRuleAnswers[x].err } }(i), ) @@ -95,7 +141,6 @@ func TestFilterListMatrixMultiples(t *testing.T) { "filter func called='%t' wanted '%t'", callMatrix[i], tt.expectedCallMatrix[i], )) - } } } diff --git a/pkg/camo/proxy_test.go b/pkg/camo/proxy_test.go index 5cd9a73e..e1a7b6c4 100644 --- a/pkg/camo/proxy_test.go +++ b/pkg/camo/proxy_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -58,13 +58,6 @@ func TestSimpleValidImageURL(t *testing.T) { } } -func TestGoogleChartURL(t *testing.T) { - t.Parallel() - testURL := "http://chart.apis.google.com/chart?chs=920x200&chxl=0:%7C2010-08-13%7C2010-09-12%7C2010-10-12%7C2010-11-11%7C1:%7C0%7C0%7C0%7C0%7C0%7C0&chm=B,EBF5FB,0,0,0&chco=008Cd6&chls=3,1,0&chg=8.3,20,1,4&chd=s:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&chxt=x,y&cht=lc" - _, err := makeTestReq(testURL, 200, camoConfig) - assert.Check(t, err) -} - func TestChunkedImageFile(t *testing.T) { t.Parallel() testURL := "https://www.igvita.com/posts/12/spdyproxy-diagram.png" @@ -88,21 +81,21 @@ func TestStrangeFormatRedirects(t *testing.T) { func TestRedirectsWithPathOnly(t *testing.T) { t.Parallel() - testURL := "http://mockbin.org/redirect/302?to=%2Fredirect%2F302%3Fto%3Dhttp%3A%2F%2Fwww.google.com%2Fimages%2Fsrpr%2Flogo11w.png" + testURL := "http://httpbin.org/redirect-to?status_code=302&url=%2Fredirect-to%3Furl%3Dhttp%3A%2F%2Fwww.google.com%2Fimages%2Fsrpr%2Flogo11w.png%26status_code%3D302" _, err := makeTestReq(testURL, 200, camoConfig) assert.Check(t, err) } func TestFollowPermRedirects(t *testing.T) { t.Parallel() - testURL := "http://mockbin.org/redirect/301?to=http://www.google.com/images/srpr/logo11w.png" + testURL := "http://httpbin.org/redirect-to?status_code=301&url=http://www.google.com/images/srpr/logo11w.png" _, err := makeTestReq(testURL, 200, camoConfig) assert.Check(t, err) } func TestFollowTempRedirects(t *testing.T) { t.Parallel() - testURL := "http://mockbin.org/redirect/302?to=http://www.google.com/images/srpr/logo11w.png" + testURL := "http://httpbin.org/redirect-to?status_code=302&url=http://www.google.com/images/srpr/logo11w.png" _, err := makeTestReq(testURL, 200, camoConfig) assert.Check(t, err) } @@ -221,7 +214,7 @@ func TestVideoContentTypeAllowed(t *testing.T) { // bump limit, try again (should succeed) camoConfigWithVideo.MaxSize = 5000 * 1024 _, err = makeTestReq(testURL, 200, camoConfigWithVideo) - fmt.Println(err) + // fmt.Println(err) assert.Check(t, err) } @@ -293,7 +286,7 @@ func Test404OnCredentialURL(t *testing.T) { func Test404InfiniRedirect(t *testing.T) { t.Parallel() - testURL := "http://mockbin.org/redirect/302/4" + testURL := "http://httpbin.org/redirect/4" _, err := makeTestReq(testURL, 404, camoConfig) assert.Check(t, err) } @@ -383,7 +376,7 @@ func Test404OnLocalhostWithPort(t *testing.T) { func Test404OnRedirectWithLocalhostTarget(t *testing.T) { t.Parallel() - testURL := "http://mockbin.org/redirect/302?to=http://localhost/some.png" + testURL := "http://httpbin.org/redirect-to?status_code=302&url=http://localhost/some.png" resp, err := makeTestReq(testURL, 404, camoConfig) if assert.Check(t, err) { bodyAssert(t, "Error Fetching Resource\n", resp) @@ -392,7 +385,7 @@ func Test404OnRedirectWithLocalhostTarget(t *testing.T) { func Test404OnRedirectWithLoopbackIP(t *testing.T) { t.Parallel() - testURL := "http://mockbin.org/redirect/302?to=http://127.0.0.100/some.png" + testURL := "http://httpbin.org/redirect-to?status_code=302&url=http://127.0.0.100/some.png" resp, err := makeTestReq(testURL, 404, camoConfig) if assert.Check(t, err) { bodyAssert(t, "Error Fetching Resource\n", resp) @@ -401,7 +394,7 @@ func Test404OnRedirectWithLoopbackIP(t *testing.T) { func Test404OnRedirectWithLoopbackIPwCreds(t *testing.T) { t.Parallel() - testURL := "http://mockbin.org/redirect/302?to=http://user:pass@127.0.0.100/some.png" + testURL := "http://httpbin.org/redirect-to?status_code=302&url=http://user:pass@127.0.0.100/some.png" resp, err := makeTestReq(testURL, 404, camoConfig) if assert.Check(t, err) { bodyAssert(t, "Error Fetching Resource\n", resp) @@ -422,7 +415,7 @@ func Test404OnLoopback(t *testing.T) { skipIfCI(t) t.Parallel() - testURL := "http://mockbin.org/redirect/302?to=http://test.vcap.me" + testURL := "http://httpbin.org/redirect-to?status_code=302&url=http://test.vcap.me" req, err := makeReq(camoConfig, testURL) assert.Check(t, err) diff --git a/pkg/camo/proxy_timeout_test.go b/pkg/camo/proxy_timeout_test.go index c9514ad4..37d1c679 100644 --- a/pkg/camo/proxy_timeout_test.go +++ b/pkg/camo/proxy_timeout_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -15,8 +15,8 @@ import ( "testing" "time" - "github.com/cactus/go-camo/pkg/camo/encoding" - "github.com/cactus/go-camo/pkg/router" + "github.com/cactus/go-camo/v2/pkg/camo/encoding" + "github.com/cactus/go-camo/v2/pkg/router" "github.com/cactus/mlog" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -40,7 +40,6 @@ func TestTimeout(t *testing.T) { r.Close = true _, err := w.Write([]byte("ok")) assert.Check(t, err) - })) defer ts.Close() @@ -101,7 +100,7 @@ func TestClientCancelEarly(t *testing.T) { _, err := fmt.Fprintf(w, "Chunk #%d\n", i) // conn closed/broken pipe if err != nil { - mlog.Debugm("write error", mlog.Map{"err": err, "i": i}) + mlog.Debugx("write error", mlog.A("err", err), mlog.A("i", i)) break } flusher.Flush() // Trigger "chunked" encoding and send a chunk... @@ -132,7 +131,7 @@ func TestClientCancelEarly(t *testing.T) { assert.Check(t, err) conn.Close() time.Sleep(100 * time.Millisecond) - fmt.Printf("done\n") + // fmt.Printf("done\n") } func TestClientCancelLate(t *testing.T) { @@ -156,7 +155,7 @@ func TestClientCancelLate(t *testing.T) { _, err := fmt.Fprintf(w, "Chunk #%d\n", i) // conn closed/broken pipe if err != nil { - mlog.Debugm("write error", mlog.Map{"err": err, "i": i}) + mlog.Debugx("write error", mlog.A("err", err), mlog.A("i", i)) break } flusher.Flush() // Trigger "chunked" encoding and send a chunk... @@ -210,7 +209,7 @@ func TestClientCancelLate(t *testing.T) { } } conn.Close() - fmt.Printf("done\n") + // fmt.Printf("done\n") } func TestServerEarlyEOF(t *testing.T) { diff --git a/pkg/camo/upstream_proxy.go b/pkg/camo/upstream_proxy.go index 557d7e37..7c2bdc30 100644 --- a/pkg/camo/upstream_proxy.go +++ b/pkg/camo/upstream_proxy.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -15,10 +15,10 @@ import ( ) type innerUpstreamProxyConfig struct { - addresses []net.IP scheme string host string port string + addresses []net.IP } func (ic *innerUpstreamProxyConfig) matchesIP(ip net.IP, port string) bool { diff --git a/pkg/camo/upstream_proxy_test.go b/pkg/camo/upstream_proxy_test.go index f92accd3..95ecff81 100644 --- a/pkg/camo/upstream_proxy_test.go +++ b/pkg/camo/upstream_proxy_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -7,12 +7,21 @@ package camo import ( "fmt" "net" + "sort" "testing" "golang.org/x/net/http/httpproxy" "gotest.tools/v3/assert" ) +func IPListToStringList(sliceList []net.IP) []string { + list := make([]string, 0, len(sliceList)) + for _, item := range sliceList { + list = append(list, item.String()) + } + return list +} + func TestUpstreamProxyParsing(t *testing.T) { t.Parallel() @@ -38,12 +47,15 @@ func TestUpstreamProxyParsing(t *testing.T) { assert.Equal(t, uspc.httpsProxy.scheme, "socks5") assert.Equal(t, uspc.httpsProxy.host, "localhost") assert.Equal(t, uspc.httpsProxy.port, "9999") + + addresses := IPListToStringList(uspc.httpsProxy.addresses) + sort.Strings(addresses) assert.DeepEqual( t, - uspc.httpsProxy.addresses, - []net.IP{ - net.ParseIP("::1"), - net.ParseIP("127.0.0.1"), + addresses, + []string{ + "127.0.0.1", + "::1", }, ) } @@ -128,5 +140,4 @@ func TestUpstreamProxyMatching(t *testing.T) { errMsg(elem.matchtype, elem.address, elem.port, elem.result), ) } - } diff --git a/pkg/camo/vars.go b/pkg/camo/vars.go index 6cea3d53..3c7d99c1 100644 --- a/pkg/camo/vars.go +++ b/pkg/camo/vars.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -7,7 +7,7 @@ package camo import ( "errors" - "github.com/cactus/go-camo/pkg/htrie" + "github.com/cactus/go-camo/v2/pkg/htrie" ) var ( diff --git a/pkg/htrie/glob_path_chk.go b/pkg/htrie/glob_path_chk.go index 6293b49c..b3dc20eb 100644 --- a/pkg/htrie/glob_path_chk.go +++ b/pkg/htrie/glob_path_chk.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -133,8 +133,9 @@ func (gpc *GlobPathChecker) AddRule(rule string) error { // CheckPath checks the supplied path (as a string). // Note: CheckPathString requires that the url path component is already escaped, -// in a similar way to `(*url.URL).EscapePath()`, as well as TrimSpace'd. +// in a similar way to `(*url.URL).EscapePath()`. func (gpc *GlobPathChecker) CheckPath(url string) bool { + url = strings.TrimSpace(url) ulen := len(url) // if we have a case sensitive checker, check that one first diff --git a/pkg/htrie/glob_path_chk_printer_test.go b/pkg/htrie/glob_path_chk_printer_test.go index aa251753..628bef92 100644 --- a/pkg/htrie/glob_path_chk_printer_test.go +++ b/pkg/htrie/glob_path_chk_printer_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. diff --git a/pkg/htrie/glob_path_chk_test.go b/pkg/htrie/glob_path_chk_test.go index 0ac8f658..a25fa919 100644 --- a/pkg/htrie/glob_path_chk_test.go +++ b/pkg/htrie/glob_path_chk_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -53,7 +53,7 @@ func TestGlobPathChecker(t *testing.T) { assert.Check(t, err) } - //fmt.Println(gpc.RenderTree()) + // fmt.Println(gpc.RenderTree()) for _, u := range testMatch { u, _ := url.Parse(u) @@ -105,7 +105,7 @@ func TestGlobPathCheckerPathsMisc(t *testing.T) { assert.Check(t, err) } - //fmt.Println(gpc.RenderTree()) + // fmt.Println(gpc.RenderTree()) for _, u := range testMatch { assert.Check(t, gpc.CheckPath(u), fmt.Sprintf("should have matched: %s", u)) @@ -148,9 +148,7 @@ func BenchmarkGlobPathChecker(b *testing.B) { assert.Check(b, err) } - var ( - testIters = 10000 - ) + testIters := 10000 // avoid inlining optimization var x bool diff --git a/pkg/htrie/glob_path_node.go b/pkg/htrie/glob_path_node.go index 1a4b6e6b..9fcfd207 100644 --- a/pkg/htrie/glob_path_node.go +++ b/pkg/htrie/glob_path_node.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -27,18 +27,37 @@ type globPathNode struct { // // Since we would /want/ to use uint8 here, use uint32 instead // Ugly and wasteful, but quite a bit faster for now... + // + // Further, using a map of uint32 is also slightly faster than using + // a [][]uint list to hold indexes into another []uint8 list + // eg. SOA (Struct of Array) vs this AOS (Array of Struct) method. + // Go maps are pretty efficient, and these structs are pointers, + // which reduces some overhead. + // Note that as far as memory usage goes, SOA does come out ahead, + // but tree setup is a one-time startup cost, and we are willing to + // trade off some additional memory and/or slowness there + // for faster rules processing speed. + + // subtree of nodes subtrees map[uint32]*globPathNode // used to avoid map lookup when there is only one subtree candidate oneShot *globPathNode // char for this node nodeChar uint32 - // is this path component a glob + + // note: Using a bitmask instead of separate boolean slots, uses + // less memory, but we again make the tradeoff of slight increase + // in memory for slightly faster rules processing speed. + // A boolean check is faster than a bitcheck+equality check + + // whether this node is a glob node isGlob bool // determines whether a node can be a match even if it isn't a leaf node; // this becomes necessary due to the possibility of longer and shorter // paths overlapping canMatch bool - // optimization to avoid an extra map lookup on every char + // whether the node has a wildcard/glob descendent + // this is an optimization to avoid an extra map lookup on every char hasGlobChild bool // is this a case insensitive comparison tree? icase bool @@ -52,7 +71,6 @@ func (gpn *globPathNode) addPath(s string) error { curnode := gpn prevnode := curnode mlen := len(s) - //for _, part := range s { for i := 0; i < mlen; i++ { part := uint32(s[i]) @@ -248,7 +266,7 @@ func newGlobPathNode(icase bool) *globPathNode { // so a total possible of 85 chars, but spread out over 94 slots // since there are quite a few possible slots, let's use a map for now... // web searches say a map is faster in go above a certain size. benchmark later... - + // // for now, since realloc cost is paid at creation, and we want to RSS size // and since we only /really/ care about lookup costs, just start with 0 initial // map size and let it grow as needed diff --git a/pkg/htrie/glob_path_node_printer_test.go b/pkg/htrie/glob_path_node_printer_test.go index 30a4fb73..ee17a496 100644 --- a/pkg/htrie/glob_path_node_printer_test.go +++ b/pkg/htrie/glob_path_node_printer_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. diff --git a/pkg/htrie/htrie.go b/pkg/htrie/htrie.go index 5a629368..52f12803 100644 --- a/pkg/htrie/htrie.go +++ b/pkg/htrie/htrie.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -68,6 +68,10 @@ func uniformLower(s, cutset string) string { return s } +func CleanHostname(s string) (string, error) { + return idna.Lookup.ToASCII(strings.ToLower(strings.TrimSpace(s))) +} + func (dt *URLMatcher) getOrNewSubTree(s string) *URLMatcher { subdt, ok := dt.subtrees[s] if !ok { @@ -224,12 +228,30 @@ func (dt *URLMatcher) AddRule(rule string) error { func (dt *URLMatcher) walkFind(s string) []*URLMatcher { // hostname should already be lowercase. avoid work by not doing it. matches := *getURLMatcherSlice() - labels := reverse(strings.Split(s, ".")) - plen := len(labels) curnode := dt + slen := len(s) // kind of weird ordering, because the root node isn't part of the search - // space. - for i, label := range labels { + // space, but walk backwards slicing as we go because we want hostname + // components in reverse order. eg. foo.example.com -> [com example foo] + // note: doing this manually because it saves allocations vs strings.Split + // (and is a bit faster as well) + for i := slen - 1; i >= 0; i-- { + label := "" + atLabel := false + if s[i] == '.' { + label = s[i+1 : slen] + slen = i + atLabel = true + } + if i == 0 { + label = s[i:slen] + slen = i + atLabel = true + } + if !atLabel { + continue + } + if curnode.subtrees == nil || len(curnode.subtrees) == 0 { break } @@ -250,13 +272,13 @@ func (dt *URLMatcher) walkFind(s string) []*URLMatcher { // not at a domain terminus, and there is a wildcard label, // so add child to match (if exists) - if i < plen-1 && curnode.hasWildChild { + if i > 0 && curnode.hasWildChild { if x, ok := curnode.subtrees["*"]; ok { matches = append(matches, x) } } // hit the end, and we can match at this level - if i == plen-1 && curnode.canMatch { + if i == 0 && curnode.canMatch { matches = append(matches, curnode) } } @@ -266,41 +288,58 @@ func (dt *URLMatcher) walkFind(s string) []*URLMatcher { // CheckURL checks a *url.URL against the URLMatcher. // If the url matches (a "hit"), it returns true. // If the url does not match (a "miss"), it return false. -func (dt *URLMatcher) CheckURL(u *url.URL) bool { +func (dt *URLMatcher) CheckURL(u *url.URL) (bool, error) { // alas, (*url.URL).Hostname() does not ToLower - hostname := strings.ToLower(u.Hostname()) + // so lower and idna map + hostname, err := CleanHostname(u.Hostname()) + if err != nil { + // invalid idna is a fail/false + return false, fmt.Errorf("bad hostname: %w", err) + } + matches := dt.walkFind(hostname) defer putURLMatcherSlice(&matches) // check for base domain matches first, to avoid path checking if possible for _, match := range matches { + // we can shortcut lookups only if the match has no associated url rules if !match.hasRules { - return true + return true, nil } } // no luck, so try path rules this time for _, match := range matches { // anything match.hasRules _shouldn't_ be nil, so this check is - // likely superfluous... + // likely superfluous... but retained for extra safety in case + // the api changes at some point if match.pathChecker == nil { continue } if match.pathChecker.CheckPath(u.EscapedPath()) { - return true + return true, nil } } - return false + return false, nil } // CheckHostname checks the supplied hostname (as a string). -// Note: CheckHostname requires that the hostname is already escaped, -// sanitized, space trimmed, and lowercased... -// Basically sanitized in a way similar to: -// -// strings.ToLower((*url.URL).Hostname()) -func (dt *URLMatcher) CheckHostname(hostname string) bool { - hostname = strings.ToLower(hostname) +// Returns an error if the hostname is not idna lookup compliant. +func (dt *URLMatcher) CheckHostname(hostname string) (bool, error) { + // do idna lookup mapping. if mapping fails, return false + hostname, err := CleanHostname(hostname) + if err != nil { + // invalid idna is a fail/false + return false, fmt.Errorf("bad hostname: %w", err) + } + + return dt.CheckCleanHostname(hostname), nil +} + +// CheckHostnameClean checks the supplied hostname (as a string). +// The supplied hostname must already be safe/cleaned, in a way +// similar to IdnaLookupMap. +func (dt *URLMatcher) CheckCleanHostname(hostname string) bool { matches := dt.walkFind(hostname) defer putURLMatcherSlice(&matches) return len(matches) > 0 diff --git a/pkg/htrie/htrie_printer_test.go b/pkg/htrie/htrie_printer_test.go index da109dd8..f98e567a 100644 --- a/pkg/htrie/htrie_printer_test.go +++ b/pkg/htrie/htrie_printer_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -29,7 +29,6 @@ func (dt *URLMatcher) printTree(stree treeprint.Tree) { subTree := stree.AddBranch(k) v.printTree(subTree) } - } func (dt *URLMatcher) RenderTree() string { diff --git a/pkg/htrie/htrie_test.go b/pkg/htrie/htrie_test.go index 01ae6a2c..db1c2a25 100644 --- a/pkg/htrie/htrie_test.go +++ b/pkg/htrie/htrie_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -8,6 +8,7 @@ import ( "fmt" "net/url" "regexp" + "strings" "testing" "gotest.tools/v3/assert" @@ -23,6 +24,7 @@ func TestHTrieCheckURL(t *testing.T) { "||foo.example.net||/", "||bar.example.net|i|*/test.png", "||bar.example.net|i|*/test.png.extra", + "||bücher.example.net||", } testMatch := []string{ @@ -33,6 +35,8 @@ func TestHTrieCheckURL(t *testing.T) { "http://foo.example.net/", "http://bar.example.net/foo/test.png", "http://bar.example.net/foo/test.png.extra", + "http://bücher.example.net/", + "http://xn--bcher-kva.example.net/", } testNoMatch := []string{ @@ -41,6 +45,7 @@ func TestHTrieCheckURL(t *testing.T) { "http://foo.example.net/nope", "http://bar.example.org/foo/testx.png", "http://bar.example.net/foo/test.png.ex", + "http://bücher.example.com/", } dt := NewURLMatcher() @@ -49,24 +54,35 @@ func TestHTrieCheckURL(t *testing.T) { assert.Check(t, err) } - //fmt.Println(dt.RenderTree()) + // fmt.Println(dt.RenderTree()) for _, u := range testMatch { u, _ := url.Parse(u) - assert.Check(t, dt.CheckURL(u), fmt.Sprintf("should have matched: %s", u)) + chk, err := dt.CheckURL(u) + assert.NilError(t, err) + assert.Check(t, chk, fmt.Sprintf("should have matched: %s", urlPathUnescape(u))) } for _, u := range testNoMatch { u, _ := url.Parse(u) - assert.Check(t, !dt.CheckURL(u), fmt.Sprintf("should not have matched: %s", u)) + chk, err := dt.CheckURL(u) + assert.NilError(t, err) + assert.Check(t, !chk, fmt.Sprintf("should not have matched: %s", urlPathUnescape(u))) } } +func urlPathUnescape(u *url.URL) string { + s := u.String() + p, _ := url.PathUnescape(s) + return p +} + func TestHTrieCheckHostname(t *testing.T) { t.Parallel() rules := []string{ "|s|localhost||", "|s|localdomain||", + "||bücher.example.net||", } testMatch := []string{ @@ -76,6 +92,8 @@ func TestHTrieCheckHostname(t *testing.T) { "http://localdomain/foo/TEST.png", "http://foo.localdomain/foo/test.png", "http://bar.foo.localdomain/foo/test.png", + "http://bücher.example.net/", + "http://xn--bcher-kva.example.net/", } testNoMatch := []string{ @@ -84,6 +102,7 @@ func TestHTrieCheckHostname(t *testing.T) { "http://foo.example.net/nope", "http://bar.example.org/foo/testx.png", "http://bar.example.net/foo/test.png.ex", + "http://bücher.example.com/", } dt := NewURLMatcher() @@ -94,15 +113,19 @@ func TestHTrieCheckHostname(t *testing.T) { } } - //fmt.Println(dt.RenderTree()) + // fmt.Println(dt.RenderTree()) for _, u := range testMatch { u, _ := url.Parse(u) - assert.Check(t, dt.CheckHostname(u.Hostname()), fmt.Sprintf("should have matched: %s", u)) + result, err := dt.CheckHostname(u.Hostname()) + assert.NilError(t, err) + assert.Check(t, result, fmt.Sprintf("should have matched: %s", urlPathUnescape(u))) } for _, u := range testNoMatch { u, _ := url.Parse(u) - assert.Check(t, !dt.CheckHostname(u.Hostname()), fmt.Sprintf("should not have matched: %s", u)) + result, err := dt.CheckHostname(u.Hostname()) + assert.NilError(t, err) + assert.Check(t, !result, fmt.Sprintf("should not have matched: %s", urlPathUnescape(u))) } } @@ -111,6 +134,10 @@ func BenchmarkHTrieCreate(b *testing.B) { urls := []string{ "||*.example.com||*/test.png", "|s|example.org|i|*/test.png", + "||foo.example.net||/test.png", + "||bar.example.net||/test.png", + "||*.bar.example.net||/test.png", + "||*.hodor.example.net||/*/test.png", } var err error b.ResetTimer() @@ -146,6 +173,16 @@ func BenchmarkRegexCreate(b *testing.B) { _ = err } +var urlMatchTestURLs = []string{ + "http://example.com/foo/test.png", + "http://bar.example.com/foo/test.png", + "http://bar.example.com/foo/testx.png", + "http://bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/test.png", + "http://bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/testx.png", + // this one kills the regex pretty bad. :( + "http://bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/testx.png", +} + func BenchmarkHTrieMatch(b *testing.B) { rules := []string{ "||foo.example.net||/test.png", @@ -156,16 +193,6 @@ func BenchmarkHTrieMatch(b *testing.B) { "|s|example.org|i|*/test.png", } - testURLs := []string{ - "http://example.com/foo/test.png", - "http://bar.example.com/foo/test.png", - "http://bar.example.com/foo/testx.png", - "http://bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/test.png", - "http://bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/testx.png", - // this one kills the regex pretty bad. - "bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/testx.png", - } - testIters := 10000 dt := NewURLMatcher() @@ -175,7 +202,7 @@ func BenchmarkHTrieMatch(b *testing.B) { } parsed := make([]*url.URL, 0) - for _, u := range testURLs { + for _, u := range urlMatchTestURLs { u, _ := url.Parse(u) parsed = append(parsed, u) } @@ -186,7 +213,7 @@ func BenchmarkHTrieMatch(b *testing.B) { for _, u := range parsed { for i := 0; i < testIters; i++ { - x = dt.CheckURL(u) + x, _ = dt.CheckURL(u) } } _ = x @@ -203,16 +230,6 @@ func BenchmarkRegexMatch(b *testing.B) { `^(.*\.)?example.org/(?:i.*/test.png)`, } - testURLs := []string{ - "example.com/foo/test.png", - "bar.example.com/foo/test.png", - "bar.example.com/foo/testx.png", - "bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/test.png", - "bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/testx.png", - // this one kills the regex pretty bad. :( - //"bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/testx.png", - } - testIters := 10000 rexes := make([]*regexp.Regexp, 0) @@ -221,11 +238,17 @@ func BenchmarkRegexMatch(b *testing.B) { rexes = append(rexes, rx) } + // strip protocol prefix to make regex matches easier + testUrls := make([]string, len(urlMatchTestURLs)) + for _, u := range urlMatchTestURLs { + testUrls = append(testUrls, strings.TrimPrefix(u, "http://")) + } + // avoid inlining optimization var x bool b.ResetTimer() - for _, u := range testURLs { + for _, u := range testUrls { for i := 0; i < testIters; i++ { // walk regexes in order. first match wins for _, rx := range rexes { @@ -249,14 +272,6 @@ func BenchmarkHTrieMatchHostname(b *testing.B) { "|s|example.org|i|*/test.png", } - testURLs := []string{ - "http://example.com/foo/test.png", - "http://bar.example.com/foo/test.png", - "http://bar.example.com/foo/testx.png", - "http://bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/test.png", - "http://bar.example.com/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/foo/testx.png", - } - testIters := 10000 dt := NewURLMatcher() @@ -266,7 +281,7 @@ func BenchmarkHTrieMatchHostname(b *testing.B) { } parsed := make([]string, 0) - for _, u := range testURLs { + for _, u := range urlMatchTestURLs { u, _ := url.Parse(u) parsed = append(parsed, u.Hostname()) } @@ -275,10 +290,21 @@ func BenchmarkHTrieMatchHostname(b *testing.B) { var x bool b.ResetTimer() - for _, u := range parsed { - for i := 0; i < testIters; i++ { - x = dt.CheckHostname(u) + b.Run("CheckHostname", func(b *testing.B) { + for _, u := range parsed { + for i := 0; i < testIters; i++ { + x, _ = dt.CheckHostname(u) + } } - } + }) + + b.Run("CheckCleanHostname", func(b *testing.B) { + for _, u := range parsed { + for i := 0; i < testIters; i++ { + x = dt.CheckCleanHostname(u) + } + } + }) + _ = x } diff --git a/pkg/router/httpdate.go b/pkg/router/httpdate.go index 74798012..68567074 100644 --- a/pkg/router/httpdate.go +++ b/pkg/router/httpdate.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. diff --git a/pkg/router/httpdate_test.go b/pkg/router/httpdate_test.go index b41df652..d9111338 100644 --- a/pkg/router/httpdate_test.go +++ b/pkg/router/httpdate_test.go @@ -1,16 +1,22 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. package router import ( + "bytes" + "flag" + "os" "testing" "time" + "github.com/cactus/mlog" "gotest.tools/v3/assert" ) +var logBuffer = &bytes.Buffer{} + func TestHTTPDateGoroutineUpdate(t *testing.T) { t.Parallel() d := newiHTTPDate() @@ -51,3 +57,20 @@ func BenchmarkDataString(b *testing.B) { } }) } + +func TestMain(m *testing.M) { + flag.Parse() + + debug := os.Getenv("DEBUG") + // now configure a standard logger + mlog.SetFlags(mlog.Lstd) + + if debug != "" { + mlog.SetFlags(mlog.Flags() | mlog.Ldebug) + mlog.Debug("debug logging enabled") + } + + mlog.DefaultLogger = mlog.New(logBuffer, mlog.Lstd) + + os.Exit(m.Run()) +} diff --git a/pkg/router/router.go b/pkg/router/router.go index 2b15dddf..2679df9b 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -1,4 +1,4 @@ -// Copyright (c) 2012-2019 Eli Janssen +// Copyright (c) 2012-2023 Eli Janssen // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. @@ -11,9 +11,9 @@ import ( // DumbRouter is a basic, special purpose, http router type DumbRouter struct { - ServerName string CamoHandler http.Handler AddHeaders map[string]string + ServerName string } // SetHeaders sets the headers on the response @@ -46,10 +46,6 @@ func (dr *DumbRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { dr.HealthCheckHandler(w, r) return } - if r.URL.Path == "/_health/" { - dr.HealthCheckHandler(w, r) - return - } components := strings.Split(r.URL.Path, "/") if len(components) == 3 {