diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 73ab02a..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: 2 -updates: -- package-ecosystem: gomod - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 - assignees: - - willnorris diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 51d8f03..207ba07 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,65 +2,37 @@ name: "CodeQL" on: push: - branches: [main, master] + branches: [main] pull_request: - # The branches below must be a subset of the branches above branches: [main] schedule: - - cron: '0 1 * * 6' + - cron: "0 1 * * 6" # run weekly on Saturdays jobs: analyze: name: Analyze runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read strategy: fail-fast: false matrix: - # Override automatic language detection by changing the below list - # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] - language: ['go'] - # Learn more... - # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + language: ["go"] steps: - - name: Checkout repository - uses: actions/checkout@v2 - 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: Checkout repository + uses: actions/checkout@v6 - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main + - name: Autobuild + uses: github/codeql-action/autobuild@v4 - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0d45d10..9b636d2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,11 +1,13 @@ +name: Docker + on: push: - branches: - - 'main' - tags: - - '*' - -name: Publish Docker image + branches: ["main"] + tags: ["v*"] + pull_request: + # Run the workflow on pull_request events to ensure we can still build the image. + # We only publish the image on push events (see if statements in steps below). + branches: ["main"] env: REGISTRY: ghcr.io @@ -17,24 +19,49 @@ jobs: permissions: contents: read packages: write + id-token: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 # v1.10.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Docker buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - id: meta - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + if: github.event_name == 'push' + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0 + with: + context: . + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + + # Sign the Docker image + - name: Install cosign + if: github.event_name == 'push' + uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb #v3.8.2 + - name: Sign the published Docker image + if: github.event_name == 'push' + env: + COSIGN_YES: "true" + run: cosign sign ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 0056757..ce6d1fd 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -4,21 +4,20 @@ on: - main pull_request: branches: - - '**' + - "**" name: linter jobs: lint: - strategy: - matrix: - go-version: [1.x] - platform: [ubuntu-latest] - runs-on: ${{ matrix.platform }} + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version: stable - - name: golangci-lint - uses: golangci/golangci-lint-action@v2 - with: - version: v1.31 + - name: golangci-lint + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 #v9.2.0 + with: + version: v2.11.4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7c36f56..0d6e835 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,59 +1,99 @@ +name: tests on: push: branches: - main pull_request: branches: - - '**' -name: tests -env: - GO111MODULE: on - + - "**" + schedule: # daily at 07:30 UTC + - cron: "30 7 * * *" + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }} + cancel-in-progress: true jobs: test: strategy: + fail-fast: false matrix: - go-version: - # support the two most recent major go versions - - 1.x - - 1.16.x + go: + # test with the two most recent major go versions, + # as well as the minimum supported from go.mod. + - { go-version: stable } + - { go-version: oldstable } + - { go-version-file: go.mod } platform: [ubuntu-latest] include: - # minimum go version that works. This is not necessarily supported in - # any way, and will be bumped up without notice as needed. But it at - # least lets us know what go version should work. - - go-version: 1.13 - platform: ubuntu-latest - # include windows, but only with the latest Go version, since there # is very little in the library that is platform specific - - go-version: 1.x + - go: { go-version: stable } platform: windows-latest # only update test coverage stats with most recent go version on linux - - go-version: 1.x + - go: { go-version: stable } platform: ubuntu-latest update-coverage: true runs-on: ${{ matrix.platform }} - steps: - - uses: actions/checkout@v2 - - - uses: actions/setup-go@v2 + - uses: actions/checkout@v6 with: - go-version: ${{ matrix.go-version }} - - - name: Cache go modules - uses: actions/cache@v2 + persist-credentials: false + - uses: actions/setup-go@v6 with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: ${{ runner.os }}-go- - + go-version: ${{ matrix.go.go-version }} + go-version-file: ${{ matrix.go.go-version-file }} - name: Run go test run: go test -v -race -coverprofile coverage.txt -covermode atomic ./... - - name: Upload coverage to Codecov if: ${{ matrix.update-coverage }} - uses: codecov/codecov-action@v1 - timeout-minutes: 2 + uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 + test-latest: + strategy: + fail-fast: false + matrix: + go: + - { go-version: stable } + - { go-version-file: go.mod } + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-go@v6 + with: + go-version: ${{ matrix.go.go-version }} + go-version-file: ${{ matrix.go.go-version-file }} + - uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 + with: + run: | + go get -u -t ./... + go test -race ./... + staticcheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-go@v6 + with: + go-version: stable + - uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 + with: + run: go run honnef.co/go/tools/cmd/staticcheck@latest ./... + govulncheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-go@v6 + with: + go-version: stable + - uses: geomys/sandboxed-step@7d75eb49d17fdeeb3656b3a57d35932d205bcfb9 # v1.2.1 + with: + run: | + go run golang.org/x/vuln/cmd/govulncheck@latest ./... + pushd caddy; go run golang.org/x/vuln/cmd/govulncheck@latest ./...; popd diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85ca5df --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.cache +./imageproxy diff --git a/.golangci.yml b/.golangci.yml index d8c4f69..8252e1b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,24 +1,31 @@ +version: "2" linters: enable: - dogsled - dupl - - goimports + - errorlint - gosec - misspell - nakedret - - stylecheck - unconvert - unparam - whitespace -issues: - exclude-rules: - # Some cache implementations use md5 hashes for cached filenames. There is - # a slight risk of cache poisoning if an attacker could construct a URL - # with the same hash, but it would also need to be allowed by the proxies - # security settings. Changing these to a more secure hash algorithm would - # result in 100% cache misses when users upgrade. For now, just leave these - # alone. - - path: internal/.*cache - linters: gosec - text: G(401|501) + # TODO: fix issues and reenable these checks + disable: + - errcheck + - gosec + - staticcheck + + exclusions: + rules: + # Some cache implementations use md5 hashes for cached filenames. There is + # a slight risk of cache poisoning if an attacker could construct a URL + # with the same hash, but the URL would also need to be allowed by the + # proxy's security settings (host allowlist, URL signature, etc). Changing + # these to a more secure hash algorithm would result in 100% cache misses + # when users upgrade. For now, just leave these alone. + - path: internal/.*cache + linters: + - gosec + text: G(401|501) diff --git a/Dockerfile b/Dockerfile index b40ae84..1898e92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,26 +1,23 @@ -FROM golang:1.17 as build +# syntax=docker/dockerfile:1.4 +FROM --platform=$BUILDPLATFORM cgr.dev/chainguard/wolfi-base as build LABEL maintainer="Will Norris " -RUN useradd -u 1001 go +RUN apk update && apk add build-base git openssh go-1.24 WORKDIR /app - COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -v ./cmd/imageproxy +ARG TARGETOS +ARG TARGETARCH +RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -v ./cmd/imageproxy -FROM scratch +FROM cgr.dev/chainguard/static:latest -COPY --from=build /etc/passwd /etc/passwd -COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo -COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=build /app/imageproxy /app/imageproxy -USER go - CMD ["-addr", "0.0.0.0:8080"] ENTRYPOINT ["/app/imageproxy"] diff --git a/README.md b/README.md index 998a4f0..6fd083b 100644 --- a/README.md +++ b/README.md @@ -5,212 +5,256 @@ [![Test Coverage](https://codecov.io/gh/willnorris/imageproxy/branch/main/graph/badge.svg)](https://codecov.io/gh/willnorris/imageproxy) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2611/badge)](https://bestpractices.coreinfrastructure.org/projects/2611) -imageproxy is a caching image proxy server written in go. It features: +imageproxy is a caching image proxy server written in go. It features: - - basic image adjustments like resizing, cropping, and rotation - - access control using allowed hosts list or request signing (HMAC-SHA256) - - support for jpeg, png, webp (decode only), tiff, and gif image formats - (including animated gifs) - - caching in-memory, on disk, or with Amazon S3, Google Cloud Storage, Azure - Storage, or Redis - - easy deployment, since it's pure go +- basic image adjustments like resizing, cropping, and rotation +- access control using allowed hosts list or request signing (HMAC-SHA256) +- support for jpeg, png, webp (decode only), tiff, and gif image formats + (including animated gifs) +- caching in-memory, on disk, or with Amazon S3, Google Cloud Storage, Azure + Storage, or Redis +- easy deployment, since it's pure go Personally, I use it primarily to dynamically resize images hosted on my own -site (read more in [this post][]). But you can also enable request signing and +site (read more in [this post][]). But you can also enable request signing and use it as an SSL proxy for remote images, similar to [atmos/camo][] but with additional image adjustment options. -I aim to keep imageproxy compatible with the two [most recent major go -releases][]. I also keep track of the minimum go version that still works -(currently go1.13 with modules enabled), but that might change at any time. You -can see the go versions that are tested against in -[.github/workflows/tests.yml][]. +I aim to keep imageproxy compatible with the two [most recent major go releases][]. +I also keep track of the minimum go version that still works (currently go1.18), but that might change at any time. +You can see the go versions that are tested against in [.github/workflows/tests.yml][]. [this post]: https://willnorris.com/2014/01/a-self-hosted-alternative-to-jetpacks-photon-service [atmos/camo]: https://github.com/atmos/camo [most recent major go releases]: https://golang.org/doc/devel/release.html [.github/workflows/tests.yml]: ./.github/workflows/tests.yml -## URL Structure ## +## URL Structure imageproxy URLs are of the form `http://localhost/{options}/{remote_url}`. -### Options ### +### Options Options are available for cropping, resizing, rotation, flipping, and digital -signatures among a few others. Options for are specified as a comma delimited -list of parameters, which can be supplied in any order. Duplicate parameters +signatures among a few others. Options for are specified as a comma delimited +list of parameters, which can be supplied in any order. Duplicate parameters overwrite previous values. See the full list of available options at -. +. -### Remote URL ### +### Remote URL The URL of the original image to load is specified as the remainder of the -path, without any encoding. For example, -`http://localhost/200/https://willnorris.com/logo.jpg`. +path. It may be included in plain text without any encoding, +percent-encoded (aka URL encoded), or base64 encoded (URL safe, no padding). -In order to [optimize caching][], it is recommended that URLs not contain query -strings. +When no encoding is used, any URL query string is treated as part of the remote URL. +For example, given the proxy URL of `http://localhost/x/http://example.com/?id=1`, +the remote URL is `http://example.com/?id=1`. -[optimize caching]: http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/ +When percent-encoding is used, the full URL must be encoded. +Any query string on the proxy URL is NOT included as part of the remote URL. +Percent-encoded URLs must be absolute URLs; +they cannot be relative URLs used with a default base URL. +For example, `http://localhost/x/http%3A%2F%2Fexample.com%2F%3Fid%3D1`. -### Examples ### +When base64 encoding is used, the full URL must be encoded. +Any query string on the proxy URL is NOT included as part of the remote URL. +Base64 encoded URLs may be relative URLs used with a default base URL. +For example, `http://localhost/x/aHR0cDovL2V4YW1wbGUuY29tLz9pZD0x`. + +### Examples The following live examples demonstrate setting different options on [this source image][small-things], which measures 1024 by 678 pixels. -[small-things]: https://willnorris.com/2013/12/small-things.jpg +[small-things]: https://willnorris.com/images/imageproxy/small-things.jpg -Options | Meaning | Image ---------|------------------------------------------|------ -200x | 200px wide, proportional height | 200x -x0.15 | 15% original height, proportional width | x0.15 -100x150 | 100 by 150 pixels, cropping as needed | 100x150 -100 | 100px square, cropping as needed | 100 -150,fit | scale to fit 150px square, no cropping | 150,fit -100,r90 | 100px square, rotated 90 degrees | 100,r90 -100,fv,fh | 100px square, flipped horizontal and vertical | 100,fv,fh -200x,q60 | 200px wide, proportional height, 60% quality | 200x,q60 -200x,png | 200px wide, converted to PNG format | 200x,png -cx175,cw400,ch300,100x | crop to 400x300px starting at (175,0), scale to 100px wide | cx175,cw400,ch300,100x +| Options | Meaning | Image | +| ---------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 200x | 200px wide, proportional height | 200x | +| x0.15 | 15% original height, proportional width | x0.15 | +| 100x150 | 100 by 150 pixels, cropping as needed | 100x150 | +| 100 | 100px square, cropping as needed | 100 | +| 150,fit | scale to fit 150px square, no cropping | 150,fit | +| 100,r90 | 100px square, rotated 90 degrees | 100,r90 | +| 100,fv,fh | 100px square, flipped horizontal and vertical | 100,fv,fh | +| 200x,q60 | 200px wide, proportional height, 60% quality | 200x,q60 | +| 200x,png | 200px wide, converted to PNG format | 200x,png | +| cx175,cw400,ch300,100x | crop to 400x300px starting at (175,0), scale to 100px wide | cx175,cw400,ch300,100x | -The [smart crop feature](https://godoc.org/willnorris.com/go/imageproxy#hdr-Smart_Crop) +The [smart crop feature](https://pkg.go.dev/willnorris.com/go/imageproxy#hdr-Smart_Crop-ParseOptions) can best be seen by comparing crops of [this source image][judah-sheets], with and without smart crop enabled. -Options | Meaning | Image ---------|------------------------------------------|------ -150x300 | 150x300px, standard crop | 200x400,sc -150x300,sc | 150x300px, smart crop | 200x400 +| Options | Meaning | Image | +| ---------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 150x300 | 150x300px, standard crop | 200x400,sc | +| 150x300,sc | 150x300px, smart crop | 200x400 | [judah-sheets]: https://judahnorris.com/images/judah-sheets.jpg -Transformation also works on animated gifs. Here is [this source +Transformation also works on animated gifs. Here is [this source image][material-animation] resized to 200px square and rotated 270 degrees: -[material-animation]: https://willnorris.com/2015/05/material-animations.gif +[material-animation]: https://willnorris.com/images/imageproxy/material-animations.gif -200,r270 +200,r270 -## Getting Started ## +## Getting Started Install the package using: - go install willnorris.com/go/imageproxy/cmd/imageproxy@latest +```sh +go install willnorris.com/go/imageproxy/cmd/imageproxy@latest +``` Once installed, ensure `$GOPATH/bin` is in your `$PATH`, then run the proxy using: - imageproxy +```sh +imageproxy +``` This will start the proxy on port 8080, without any caching and with no allowed -host list (meaning any remote URL can be proxied). Test this by navigating to +host list (meaning any remote URL can be proxied). Test this by navigating to and you should see a 500px square coder octocat. -### Cache ### +### Cache By default, the imageproxy command does not cache responses, but caching can be -enabled using the `-cache` flag. It supports the following values: +enabled using the `-cache` flag. It supports the following values: - - `memory` - uses an in-memory LRU cache. By default, this is limited to - 100mb. To customize the size of the cache or the max age for cached items, - use the format `memory:size:age` where size is measured in mb and age is a - duration. For example, `memory:200:4h` will create a 200mb cache that will - cache items no longer than 4 hours. - - directory on local disk (e.g. `/tmp/imageproxy`) - will cache images - on disk +- `memory` - uses an in-memory LRU cache. By default, this is limited to + 100mb. To customize the size of the cache or the max age for cached items, + use the format `memory:size:age` where size is measured in mb and age is a + duration. For example, `memory:200:4h` will create a 200mb cache that will + cache items no longer than 4 hours. +- directory on local disk (e.g. `/tmp/imageproxy`) - will cache images + on disk - - s3 URL (e.g. `s3://region/bucket-name/optional-path-prefix`) - will cache - images on Amazon S3. This requires either an IAM role and instance profile - with access to your your bucket or `AWS_ACCESS_KEY_ID` and `AWS_SECRET_KEY` - environmental variables be set. (Additional methods of loading credentials - are documented in the [aws-sdk-go session - package](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/)). +- s3 URL (e.g. `s3://region/bucket-name/optional-path-prefix`) - will cache + images on Amazon S3. This requires either an IAM role and instance profile + with access to your your bucket or `AWS_ACCESS_KEY_ID` and `AWS_SECRET_KEY` + environmental variables be set. (Additional methods of loading credentials + are documented in the [aws-sdk-go session + package](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/)). - Additional configuration options ([further documented here][aws-options]) - may be specified as URL query string parameters, which are mostly useful - when working with s3-compatible services: - - "endpoint" - specify an alternate API endpoint - - "disableSSL" - set to "1" to disable SSL when calling the API - - "s3ForcePathStyle" - set to "1" to force the request to use path-style addressing + Additional configuration options ([further documented here][aws-options]) + may be specified as URL query string parameters, which are mostly useful + when working with s3-compatible services: - For example, when working with [minio](https://minio.io), which doesn't use - regions, provide a dummy region value and custom endpoint value: + - "endpoint" - specify an alternate API endpoint + - "disableSSL" - set to "1" to disable SSL when calling the API + - "s3ForcePathStyle" - set to "1" to force the request to use path-style addressing - s3://fake-region/bucket/folder?endpoint=minio:9000&disableSSL=1&s3ForcePathStyle=1 + For example, when working with [minio](https://minio.io), which doesn't use + regions, provide a dummy region value and custom endpoint value: - Similarly, for [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/), - provide a dummy region value and the appropriate endpoint for your space: + ``` + s3://fake-region/bucket/folder?endpoint=minio:9000&disableSSL=1&s3ForcePathStyle=1 + ``` - s3://fake-region/bucket/folder?endpoint=sfo2.digitaloceanspaces.com + Similarly, for [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/), + provide a dummy region value and the appropriate endpoint for your space: - [aws-options]: https://docs.aws.amazon.com/sdk-for-go/api/aws/#Config + ``` + s3://fake-region/bucket/folder?endpoint=sfo2.digitaloceanspaces.com + ``` - - gcs URL (e.g. `gcs://bucket-name/optional-path-prefix`) - will cache images - on Google Cloud Storage. Authentication is documented in Google's - [Application Default Credentials - docs](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application). - - azure URL (e.g. `azure://container-name/`) - will cache images on - Azure Storage. This requires `AZURESTORAGE_ACCOUNT_NAME` and - `AZURESTORAGE_ACCESS_KEY` environment variables to bet set. - - redis URL (e.g. `redis://hostname/`) - will cache images on - the specified redis host. The full URL syntax is defined by the [redis URI - registration](https://www.iana.org/assignments/uri-schemes/prov/redis). - Rather than specify password in the URI, use the `REDIS_PASSWORD` - environment variable. + [aws-options]: https://docs.aws.amazon.com/sdk-for-go/api/aws/#Config + +- gcs URL (e.g. `gcs://bucket-name/optional-path-prefix`) - will cache images + on Google Cloud Storage. Authentication is documented in Google's + [Application Default Credentials + docs](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application). +- azure URL (e.g. `azure://container-name/`) - will cache images on + Azure Storage. This requires `AZURESTORAGE_ACCOUNT_NAME` and + `AZURESTORAGE_ACCESS_KEY` environment variables to bet set. +- redis URL (e.g. `redis://hostname/`) - will cache images on + the specified redis host. The full URL syntax is defined by the [redis URI + registration](https://www.iana.org/assignments/uri-schemes/prov/redis). + Rather than specify password in the URI, use the `REDIS_PASSWORD` + environment variable. For example, to cache files on disk in the `/tmp/imageproxy` directory: - imageproxy -cache /tmp/imageproxy +```sh +imageproxy -cache /tmp/imageproxy +``` Reload the [codercat URL][], and then inspect the contents of -`/tmp/imageproxy`. Within the subdirectories, there should be two files, one +`/tmp/imageproxy`. Within the subdirectories, there should be two files, one for the original full-size codercat image, and one for the resized 500px version. [codercat URL]: http://localhost:8080/500/https://octodex.github.com/images/codercat.jpg Multiple caches can be specified by separating them by spaces or by repeating -the `-cache` flag multiple times. The caches will be created in a [tiered +the `-cache` flag multiple times. The caches will be created in a [tiered fashion][]. Typically this is used to put a smaller and faster in-memory cache -in front of a larger but slower on-disk cache. For example, the following will +in front of a larger but slower on-disk cache. For example, the following will first check an in-memory cache for an image, followed by a gcs bucket: - imageproxy -cache memory -cache gcs://my-bucket/ +```sh +imageproxy -cache memory -cache gcs://my-bucket/ +``` -[tiered fashion]: https://godoc.org/github.com/die-net/lrucache/twotier +[tiered fashion]: https://pkg.go.dev/github.com/die-net/lrucache/twotier -### Allowed Referrer List ### +#### Override Cache Directives + +By default, imageproxy will respect the caching directives in response headers, +including the cache duration and explicit instructions **not** to cache the response, +such as `no-store` and `private` cache-control directives. + +You can force imageproxy to cache responses, even if they explicitly say not to, +by passing the `-forceCache` flag. Note that this is generally not recommended. + +A minimum cache duration can be set using the `-minCacheDuration` flag. This +will extend the cache duration if the response header indicates a shorter value. +If called without the `-forceCache` flag, this will have no effect on responses +with the `no-store` or `private` directives. + +```sh +imageproxy -cache /tmp/imageproxy -minCacheDuration 5m +``` + +### Allowed Referrer List You can limit images to only be accessible for certain hosts in the HTTP referrer header, which can help prevent others from hotlinking to images. It can be enabled by running: - imageproxy -referrers example.com +```sh +imageproxy -referrers example.com +``` - -Reload the [codercat URL][], and you should now get an error message. You can +Reload the [codercat URL][], and you should now get an error message. You can specify multiple hosts as a comma separated list, or prefix a host value with `*.` to allow all sub-domains as well. -### Allowed and Denied Hosts List ### +### Allowed and Denied Hosts List You can limit the remote hosts that the proxy will fetch images from using the -`allowHosts` and `denyHosts` flags. This is useful, for example, for locking -the proxy down to your own hosts to prevent others from abusing it. Of course +`allowHosts` and `denyHosts` flags. This is useful, for example, for locking +the proxy down to your own hosts to prevent others from abusing it. Of course if you want to support fetching from any host, leave off these flags. Try it out by running: - imageproxy -allowHosts example.com +```sh +imageproxy -allowHosts example.com +``` Reload the [codercat URL][], and you should now get an error message. Alternately, try running: - imageproxy -denyHosts octodex.github.com +```sh +imageproxy -denyHosts octodex.github.com +``` Reloading the [codercat URL][] will still return an error message. @@ -221,7 +265,7 @@ blocking reserved ranges like `127.0.0.0/8`, `192.168.0.0/16`, etc. If a host matches both an allowed and denied host, the request will be denied. -### Allowed Content-Type List ### +### Allowed Content-Type List You can limit what content types can be proxied by using the `contentTypes` flag. By default, this is set to `image/*`, meaning that imageproxy will @@ -229,34 +273,38 @@ process any image types. You can specify multiple content types as a comma separated list, and suffix values with `*` to perform a wildcard match. Set the flag to an empty string to proxy all requests, regardless of content type. -### Signed Requests ### +### Signed Requests -Instead of an allowed host list, you can require that requests be signed. This +Instead of an allowed host list, you can require that requests be signed. This is useful in preventing abuse when you don't have just a static list of hosts -you want to allow. Signatures are generated using HMAC-SHA256 against the +you want to allow. Signatures are generated using HMAC-SHA256 against the remote URL, and url-safe base64 encoding the result: - base64urlencode(hmac.New(sha256, ).digest()) +``` +base64urlencode(hmac.New(sha256, ).digest()) +``` -The HMAC key is specified using the `signatureKey` flag. If this flag +The HMAC key is specified using the `signatureKey` flag. If this flag begins with an "@", the remainder of the value is interpreted as a file on disk which contains the HMAC key. Try it out by running: - imageproxy -signatureKey "secretkey" +```sh +imageproxy -signatureKey "secretkey" +``` -Reload the [codercat URL][], and you should see an error message. Now load a +Reload the [codercat URL][], and you should see an error message. Now load a [signed codercat URL][] (which contains the [signature option][]) and verify that it loads properly. [signed codercat URL]: http://localhost:8080/500,sXyMwWKIC5JPCtlYOQ2f4yMBTqpjtUsfI67Sp7huXIYY=/https://octodex.github.com/images/codercat.jpg -[signature option]: https://godoc.org/willnorris.com/go/imageproxy#hdr-Signature +[signature option]: https://pkg.go.dev/willnorris.com/go/imageproxy#hdr-Signature-ParseOptions Some simple code samples for generating signatures in various languages can be -found in [docs/url-signing.md](/docs/url-signing.md). Multiple valid signature +found in [docs/url-signing.md](/docs/url-signing.md). Multiple valid signature keys may be provided to support key rotation by repeating the `signatureKey` -flag multiple times, or by providing a space-separated list of keys. To use a +flag multiple times, or by providing a space-separated list of keys. To use a key with a literal space character, load the key from a file using the "@" prefix documented above. @@ -264,34 +312,46 @@ If both a whiltelist and signatureKey are specified, requests can match either. In other words, requests that match one of the allowed hosts don't necessarily need to be signed, though they can be. -### Default Base URL ### +To limit how long a URL is valid (particularly useful for signed URLs), +you can specify a "valid until" time using the `vu` option with a Unix timestamp. +For example, the following signed URL would only be valid until 2020-01-01: + +``` +http://localhost:8080/vu1577836800,sjNcVf6LxzKEvR6Owgg3zhEMN7xbWxlpf-eyYbRfFK4A=/https://example.com/image +``` + +### Default Base URL Typically, remote images to be proxied are specified as absolute URLs. However, if you commonly proxy images from a single source, you can provide a -base URL and then specify remote images relative to that base. Try it out by +base URL and then specify remote images relative to that base. Try it out by running: - imageproxy -baseURL https://octodex.github.com/ +```sh +imageproxy -baseURL https://octodex.github.com/ +``` Then load the codercat image, specified as a URL relative to that base: -. Note that this is not an +. Note that this is not an effective method to mask the true source of the images being proxied; it is -trivial to discover the base URL being used. Even when a base URL is +trivial to discover the base URL being used. Even when a base URL is specified, you can always provide the absolute URL of the image to be proxied. -### Scaling beyond original size ### +### Scaling beyond original size By default, the imageproxy won't scale images beyond their original size. However, you can use the `scaleUp` command-line flag to allow this to happen: - imageproxy -scaleUp true +```sh +imageproxy -scaleUp true +``` -### WebP and TIFF support ### +### WebP and TIFF support Imageproxy can proxy remote webp images, but they will be served in either jpeg or png format (this is because the golang webp library only supports webp -decoding) if any transformation is requested. If no format is specified, -imageproxy will use jpeg by default. If no transformation is requested (for +decoding) if any transformation is requested. If no format is specified, +imageproxy will use jpeg by default. If no transformation is requested (for example, if you are just using imageproxy as an SSL proxy) then the original webp image will be served as-is without any format conversion. @@ -300,87 +360,131 @@ default if any transformation is requested. To force encoding as tiff, pass the "tiff" option. Like webp, tiff images will be served as-is without any format conversion if no transformation is requested. - -Run `imageproxy -help` for a complete list of flags the command accepts. If +Run `imageproxy -help` for a complete list of flags the command accepts. If you want to use a different caching implementation, it's probably easiest to just make a copy of `cmd/imageproxy/main.go` and customize it to fit your needs... it's a very simple command. -### Environment Variables ### +### Environment Variables All configuration flags have equivalent environment variables of the form -`IMAGEPROXY_$NAME`. For example, an on-disk cache could be configured by calling +`IMAGEPROXY_$NAME`. For example, an on-disk cache could be configured by calling - IMAGEPROXY_CACHE="/tmp/imageproxy" imageproxy +```sh +IMAGEPROXY_CACHE="/tmp/imageproxy" imageproxy +``` -## Deploying ## +## Deploying In most cases, you can follow the normal procedure for building a deploying any -go application. For example: +go application. For example: - - `go build willnorris.com/go/imageproxy/cmd/imageproxy` - - copy resulting binary to `/usr/local/bin` - - copy [`etc/imageproxy.service`](etc/imageproxy.service) to - `/lib/systemd/system` and enable using `systemctl`. +- `go build willnorris.com/go/imageproxy/cmd/imageproxy` +- copy resulting binary to `/usr/local/bin` +- copy [`etc/imageproxy.service`](etc/imageproxy.service) to + `/lib/systemd/system` and enable using `systemctl`. Instructions have been contributed below for running on other platforms, but I don't have much experience with them personally. -### Heroku ### +### Heroku It's easy to vendorize the dependencies with `Godep` and deploy to Heroku. Take a look at [this GitHub repo](https://github.com/oreillymedia/prototype-imageproxy/tree/heroku) (make sure you use the `heroku` branch). -### AWS Elastic Beanstalk ### +### AWS Elastic Beanstalk [O’Reilly Media](https://github.com/oreillymedia) set up [a repository](https://github.com/oreillymedia/prototype-imageproxy) with everything you need to deploy imageproxy to Elastic Beanstalk. Just follow the instructions in the [README](https://github.com/oreillymedia/prototype-imageproxy/blob/master/Readme.md). -### Docker ### +### Docker A docker image is available at [`ghcr.io/willnorris/imageproxy`](https://github.com/willnorris/imageproxy/pkgs/container/imageproxy). You can run it by -``` + +```sh docker run -p 8080:8080 ghcr.io/willnorris/imageproxy -addr 0.0.0.0:8080 ``` Or in your Dockerfile: -``` +```Dockerfile ENTRYPOINT ["/app/imageproxy", "-addr 0.0.0.0:8080"] ``` If running imageproxy inside docker with a bind-mounted on-disk cache, make sure the container is running as a user that has write permission to the mounted host -directory. See more details in +directory. See more details in [#198](https://github.com/willnorris/imageproxy/issues/198). -### nginx ### +Note that all configuration options can be set using [environment +variables](#environment-variables), which is often the preferred approach for +containers. + +### Caddy + +You can proxy requests to imageproxy in your Caddy config using the `reverse_proxy` directive: + +```Caddyfile +@imageproxy path /api/imageproxy/* +handle @imageproxy { + uri replace /api/imageproxy/ / + reverse_proxy http://localhost:4593 +} +``` + +You can also run an instance of imageproxy embedded in Caddy using the [caddy module](./caddy/). +This requires a custom build of Caddy with the imageproxy module included +([example](https://github.com/willnorris/willnorris.com/blob/main/cmd/caddy/caddy.go)), +and configuring it with the `imageproxy` directive in your Caddyfile: + +```Caddyfile +@imageproxy path /api/imageproxy/* +handle @imageproxy { + uri replace /api/imageproxy/ / + + imageproxy { + cache /data/imageproxy-cache + default_base_url {$IMAGEPROXY_BASEURL} + allow_hosts {$IMAGEPROXY_ALLOWHOSTS} + signature_key {$IMAGEPROXY_SIGNATUREKEY} + } +} +``` + +### nginx Use the `proxy_pass` directive to send requests to your imageproxy instance. For example, to run imageproxy at the path "/api/imageproxy/", set: -``` - location /api/imageproxy/ { - proxy_pass http://localhost:4593/; - } +```nginx +location /api/imageproxy/ { + proxy_pass http://localhost:4593/; +} ``` Depending on other directives you may have in your nginx config, you might need to alter the precedence order by setting: -``` - location ^~ /api/imageproxy/ { - proxy_pass http://localhost:4593/; - } +```nginx +location ^~ /api/imageproxy/ { + proxy_pass http://localhost:4593/; +} ``` -## License ## +## Clients + +- [Hugo partial](https://github.com/willnorris/willnorris.com/blob/main/layouts/partials/imageproxy-url.html) + (I use this with an [`{{}}` shortcode](https://github.com/willnorris/willnorris.com/blob/main/layouts/shortcodes/img.html) + like [this example](https://github.com/willnorris/willnorris.com/blob/b7f3451/content/about/index.md?plain=1#L7)) +- [Ruby](https://github.com/azolf/imageproxy_ruby) + +## License imageproxy is copyright its respective authors. All of my personal work on imageproxy through 2020 (which accounts for the majority of the code) is -copyright Google, my employer at the time. It is available under the [Apache +copyright Google, my employer at the time. It is available under the [Apache 2.0 License](./LICENSE). diff --git a/caddy/go.mod b/caddy/go.mod new file mode 100644 index 0000000..fcb5cd9 --- /dev/null +++ b/caddy/go.mod @@ -0,0 +1,143 @@ +module willnorris.com/go/imageproxy/caddy + +go 1.25.0 + +require ( + github.com/caddyserver/caddy/v2 v2.11.2 + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 + github.com/peterbourgon/diskv v2.0.1+incompatible + go.uber.org/zap v1.27.1 + willnorris.com/go/imageproxy v0.12.0 +) + +replace willnorris.com/go/imageproxy => ../ + +require ( + cel.dev/expr v0.25.1 // indirect + cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + dario.cat/mergo v1.0.2 // indirect + filippo.io/bigmod v0.1.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect + github.com/KimMachineGun/automemlimit v0.7.5 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/caddyserver/certmagic v0.25.2 // indirect + github.com/caddyserver/zerossl v0.1.5 // indirect + github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/dgraph-io/badger v1.6.2 // indirect + github.com/dgraph-io/badger/v2 v2.2007.4 // indirect + github.com/dgraph-io/ristretto v0.2.0 // indirect + github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/disintegration/imaging v1.6.2 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fcjr/aia-transport-go v1.2.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/cel-go v0.27.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.18.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.8.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/libdns/libdns v1.1.1 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mholt/acmez/v3 v3.1.6 // indirect + github.com/miekg/dns v1.1.72 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/muesli/smartcrop v0.3.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/slackhq/nebula v1.10.3 // indirect + github.com/smallstep/certificates v0.30.0 // indirect + github.com/smallstep/cli-utils v0.12.2 // indirect + github.com/smallstep/linkedca v0.25.0 // indirect + github.com/smallstep/nosql v0.8.0 // indirect + github.com/smallstep/pkcs7 v0.2.1 // indirect + github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 // indirect + github.com/smallstep/truststore v0.13.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect + github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect + github.com/urfave/cli v1.22.17 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + go.etcd.io/bbolt v1.4.3 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.step.sm/crypto v0.77.1 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap/exp v0.3.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/image v0.39.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.43.0 // indirect + google.golang.org/api v0.271.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect + howett.net/plist v1.0.0 // indirect + willnorris.com/go/gifresize v1.0.0 // indirect +) diff --git a/caddy/go.sum b/caddy/go.sum new file mode 100644 index 0000000..833e5e7 --- /dev/null +++ b/caddy/go.sum @@ -0,0 +1,512 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= +cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= +cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= +cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= +code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8= +filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo= +github.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= +github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk= +github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= +github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +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/caddyserver/caddy/v2 v2.11.2 h1:iOlpsSiSKqEW+SIXrcZsZ/NO74SzB/ycqqvAIEfIm64= +github.com/caddyserver/caddy/v2 v2.11.2/go.mod h1:ASNYYmKhIVWWMGPfNxclI5DqKEgU3FhmL+6NZWzQEag= +github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= +github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= +github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= +github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0= +github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= +github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= +github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/die-net/lrucache v0.0.0-20220628165024-20a71bc65bf1 h1:1nCGINecpltGpOWruhy+Ac2/FRy+p1igMylF+MsijpI= +github.com/die-net/lrucache v0.0.0-20220628165024-20a71bc65bf1/go.mod h1:NQKJ1XiOlLRLoAeq/5LE3GBlSukAK3zDUUlrvc2rfCQ= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fcjr/aia-transport-go v1.2.2 h1:sIZqXcM+YhTd2BDtkV2OJaqbcIVcPv1oKru3VJPIPc8= +github.com/fcjr/aia-transport-go v1.2.2/go.mod h1:onSqSq3tGkM14WusDx7q9FTheS9R1KBtD+QBWI6zG/w= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= +github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= +github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= +github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= +github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws= +github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ= +github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= +github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI= +github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU= +github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk= +github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U= +github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ= +github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= +github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc= +github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= +github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/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.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= +github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU= +github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= +github.com/smallstep/certificates v0.30.0 h1:faDmyVxF0SpArbogBHMs1jj4tVKpXS+2ALGsq6pc8KU= +github.com/smallstep/certificates v0.30.0/go.mod h1:VuSC7WLIWomjkwJTU7YJX/ZPm4cXuqqnDl4sYEDP9oU= +github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k= +github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y= +github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= +github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= +github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo= +github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc= +github.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A= +github.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo= +github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= +github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= +github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA= +github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492/go.mod h1:QQhwLqCS13nhv8L5ov7NgusowENUtXdEzdytjmJHdZQ= +github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4= +github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= +github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= +github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 h1:RnBbFMmodYzhC6adOjTbtUQXyzV8dcvKYbolzs6Qch0= +github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747/go.mod h1:ejPAJui3kVK4u5TgMtqtXlWf5HnKh9fLy5kvpaeuas0= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= +github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.step.sm/crypto v0.77.1 h1:4EEqfKdv0egQ1lqz2RhnU8Jv6QgXZfrgoxWMqJF9aDs= +go.step.sm/crypto v0.77.1/go.mod h1:U/SsmEm80mNnfD5WIkbhuW/B1eFp3fgFvdXyDLpU1AQ= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8= +golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= +golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20220310020820-b874c991c1a5/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY= +google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q= +google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc= +google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI= +google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s= +google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +willnorris.com/go/gifresize v1.0.0 h1:GKS68zjNhHMqkgNTv4iFAO/j/sNcVSOHQ7SqmDAIAmM= +willnorris.com/go/gifresize v1.0.0/go.mod h1:eBM8gogBGCcaH603vxSpnfjwXIpq6nmnj/jauBDKtAk= diff --git a/caddy/module.go b/caddy/module.go new file mode 100644 index 0000000..35e0120 --- /dev/null +++ b/caddy/module.go @@ -0,0 +1,166 @@ +// Package caddy provides ImageProxy as a Caddy module. +package caddy + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + caddy "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/gregjones/httpcache/diskcache" + "github.com/peterbourgon/diskv" + "go.uber.org/zap" + "willnorris.com/go/imageproxy" +) + +func init() { + caddy.RegisterModule(ImageProxy{}) + httpcaddyfile.RegisterHandlerDirective("imageproxy", parseCaddyfile) + httpcaddyfile.RegisterDirectiveOrder("imageproxy", "after", "reverse_proxy") +} + +type ImageProxy struct { + Cache string `json:"cache,omitempty"` + + DefaultBaseURL string `json:"default_base_url,omitempty"` + + AllowHosts []string `json:"allow_hosts,omitempty"` + DenyHosts []string `json:"deny_hosts,omitempty"` + Referrers []string `json:"referrers,omitempty"` + ContentTypes []string `json:"content_types,omitempty"` + + SignatureKeys []string `json:"signature_keys,omitempty"` + Verbose bool `json:"verbose,omitempty"` + + logger *zap.Logger + proxy *imageproxy.Proxy +} + +// interface guard +var ( + _ caddyhttp.MiddlewareHandler = (*ImageProxy)(nil) +) + +// CaddyModule returns the Caddy module information. +func (ImageProxy) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.handlers.imageproxy", + New: func() caddy.Module { return new(ImageProxy) }, + } +} + +func (p *ImageProxy) Provision(ctx caddy.Context) error { + p.logger = ctx.Logger() + cache, _ := parseCache(p.Cache) + p.proxy = imageproxy.NewProxy(nil, cache) + p.proxy.DefaultBaseURL, _ = url.Parse(p.DefaultBaseURL) + p.proxy.AllowHosts = p.AllowHosts + p.proxy.DenyHosts = p.DenyHosts + p.proxy.Referrers = p.Referrers + p.proxy.ContentTypes = p.ContentTypes + if len(p.proxy.ContentTypes) == 0 { + p.proxy.ContentTypes = []string{"image/*"} + } + for _, key := range p.SignatureKeys { + p.proxy.SignatureKeys = append(p.proxy.SignatureKeys, []byte(key)) + } + p.proxy.Logger = zap.NewStdLog(p.logger) + p.proxy.Verbose = p.Verbose + p.proxy.FollowRedirects = true + return nil +} + +func (p *ImageProxy) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error { + p.proxy.ServeHTTP(w, r) + return nil +} + +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + p := new(ImageProxy) + + h.Next() // consume the directive name + for nesting := h.Nesting(); h.NextBlock(nesting); { + switch h.Val() { + case "cache": + if !h.NextArg() { + return nil, h.ArgErr() + } + p.Cache = h.Val() + case "default_base_url": + if !h.NextArg() { + return nil, h.ArgErr() + } + p.DefaultBaseURL = h.Val() + case "allow_hosts": + if !h.NextArg() { + return nil, h.ArgErr() + } + p.AllowHosts = append(p.AllowHosts, strings.Split(h.Val(), ",")...) + case "deny_hosts": + if !h.NextArg() { + return nil, h.ArgErr() + } + p.DenyHosts = append(p.DenyHosts, strings.Split(h.Val(), ",")...) + case "referrers": + if !h.NextArg() { + return nil, h.ArgErr() + } + p.Referrers = append(p.Referrers, strings.Split(h.Val(), ",")...) + case "content_types": + if !h.NextArg() { + return nil, h.ArgErr() + } + p.ContentTypes = append(p.ContentTypes, strings.Split(h.Val(), ",")...) + case "signature_key": + if !h.NextArg() { + return nil, h.ArgErr() + } + p.SignatureKeys = append(p.SignatureKeys, h.Val()) + case "verbose": + if !h.NextArg() { + return nil, h.ArgErr() + } + p.Verbose, _ = strconv.ParseBool(h.Val()) + } + } + return p, nil +} + +// parseCache parses c returns the specified Cache implementation. +func parseCache(c string) (imageproxy.Cache, error) { + const defaultMemorySize = 100 + + if c == "" { + return nil, nil + } + + if c == "memory" { + c = fmt.Sprintf("memory:%d", defaultMemorySize) + } + + u, err := url.Parse(c) + if err != nil { + return nil, fmt.Errorf("error parsing cache flag: %w", err) + } + + switch u.Scheme { + case "file": + return diskCache(u.Path), nil + default: + return diskCache(c), nil + } +} + +func diskCache(path string) *diskcache.Cache { + d := diskv.New(diskv.Options{ + BasePath: path, + + // For file "c0ffee", store file as "c0/ff/c0ffee" + Transform: func(s string) []string { return []string{s[0:2], s[2:4]} }, + }) + return diskcache.NewWithDiskv(d) +} diff --git a/cmd/imageproxy-sign/main.go b/cmd/imageproxy-sign/main.go index 5024b20..db1c2d1 100644 --- a/cmd/imageproxy-sign/main.go +++ b/cmd/imageproxy-sign/main.go @@ -12,7 +12,6 @@ import ( "errors" "flag" "fmt" - "io/ioutil" "net/http" "net/url" "os" @@ -53,7 +52,7 @@ func sign(key string, s string, urlOnly bool) ([]byte, error) { k, err := parseKey(key) if err != nil { - return nil, fmt.Errorf("error parsing key: %v", err) + return nil, fmt.Errorf("error parsing key: %w", err) } mac := hmac.New(sha256.New, k) @@ -65,7 +64,7 @@ func sign(key string, s string, urlOnly bool) ([]byte, error) { func parseKey(s string) ([]byte, error) { if strings.HasPrefix(s, "@") { - return ioutil.ReadFile(s[1:]) + return os.ReadFile(s[1:]) } return []byte(s), nil } diff --git a/cmd/imageproxy-sign/main_test.go b/cmd/imageproxy-sign/main_test.go index 5c9fbe1..c6ac0e7 100644 --- a/cmd/imageproxy-sign/main_test.go +++ b/cmd/imageproxy-sign/main_test.go @@ -4,7 +4,6 @@ package main import ( - "io/ioutil" "net/url" "os" "reflect" @@ -13,30 +12,6 @@ import ( var key = "secret" -func TestMainFunc(t *testing.T) { - os.Args = []string{"imageproxy-sign", "-key", key, "http://example.com/#0x0"} - r, w, err := os.Pipe() - if err != nil { - t.Errorf("error creating pipe: %v", err) - } - defer r.Close() - os.Stdout = w - - main() - w.Close() - - output, err := ioutil.ReadAll(r) - got := string(output) - if err != nil { - t.Errorf("error reading from pipe: %v", err) - } - - want := "url: http://example.com/#0x0\nsignature: pwlnJ3bVazxg2nQxClimqT0VnNxUm5W0cdyg1HpKUPY=\n" - if got != want { - t.Errorf("main output %q, want %q", got, want) - } -} - func TestSign(t *testing.T) { s := "http://example.com/image.jpg#0x0" @@ -94,7 +69,7 @@ func TestParseKey(t *testing.T) { } func TestParseKey_FilePath(t *testing.T) { - f, err := ioutil.TempFile("", "key") + f, err := os.CreateTemp("", "key") if err != nil { t.Errorf("error creating temp file: %v", err) } diff --git a/cmd/imageproxy/main.go b/cmd/imageproxy/main.go index 2c9ce5d..a04296f 100644 --- a/cmd/imageproxy/main.go +++ b/cmd/imageproxy/main.go @@ -7,8 +7,8 @@ package main import ( "flag" "fmt" - "io/ioutil" "log" + "net" "net/http" "net/url" "os" @@ -20,7 +20,6 @@ import ( "github.com/die-net/lrucache" "github.com/die-net/lrucache/twotier" "github.com/gomodule/redigo/redis" - "github.com/gorilla/mux" "github.com/gregjones/httpcache/diskcache" rediscache "github.com/gregjones/httpcache/redis" "github.com/peterbourgon/diskv" @@ -32,13 +31,15 @@ import ( const defaultMemorySize = 100 -var addr = flag.String("addr", "localhost:8080", "TCP address to listen on") +var addr = flag.String("addr", "localhost:8080", "address to listen on, either a TCP address or a Unix domain socket path prefixed with unix:") var allowHosts = flag.String("allowHosts", "", "comma separated list of allowed remote hosts") var denyHosts = flag.String("denyHosts", "", "comma separated list of denied remote hosts") var referrers = flag.String("referrers", "", "comma separated list of allowed referring hosts") var includeReferer = flag.Bool("includeReferer", false, "include referer header in remote requests") var followRedirects = flag.Bool("followRedirects", true, "follow redirects") var baseURL = flag.String("baseURL", "", "default base URL for relative remote URLs") +var passRequestHeaders = flag.String("passRequestHeaders", "", "comma separatetd list of request headers to pass to remote server") +var passResponseHeaders = flag.String("passResponseHeaders", "Cache-Control,Last-Modified,Expires,Etag,Link", "comma separated list of response headers to pass from remote server") var cache tieredCache var signatureKeys signatureKeyList var scaleUp = flag.Bool("scaleUp", false, "allow images to scale beyond their original dimensions") @@ -47,6 +48,8 @@ var verbose = flag.Bool("verbose", false, "print verbose logging messages") var _ = flag.Bool("version", false, "Deprecated: this flag does nothing") var contentTypes = flag.String("contentTypes", "image/*", "comma separated list of allowed content types") var userAgent = flag.String("userAgent", "willnorris/imageproxy", "specify the user-agent used by imageproxy when fetching images from origin website") +var minCacheDuration = flag.Duration("minCacheDuration", 0, "minimum duration to cache remote images") +var forceCache = flag.Bool("forceCache", false, "Ignore no-store and private directives in responses") func init() { flag.Var(&cache, "cache", "location to cache images (see https://github.com/willnorris/imageproxy#cache)") @@ -70,6 +73,15 @@ func main() { if *contentTypes != "" { p.ContentTypes = strings.Split(*contentTypes, ",") } + if *passRequestHeaders != "" { + p.PassRequestHeaders = strings.Split(*passRequestHeaders, ",") + } + if *passResponseHeaders != "" { + p.PassResponseHeaders = strings.Split(*passResponseHeaders, ",") + } else { + // set to a non-nil empty slice to pass no headers. + p.PassResponseHeaders = []string{} + } p.SignatureKeys = signatureKeys if *baseURL != "" { var err error @@ -85,16 +97,32 @@ func main() { p.ScaleUp = *scaleUp p.Verbose = *verbose p.UserAgent = *userAgent + p.MinimumCacheDuration = *minCacheDuration + p.ForceCache = *forceCache + + var ln net.Listener + var err error + + if path, ok := strings.CutPrefix(*addr, "unix:"); ok { + ln, err = net.Listen("unix", path) + } else { + ln, err = net.Listen("tcp", *addr) + } + if err != nil { + log.Fatalf("listen failed: %v", err) + } server := &http.Server{ Addr: *addr, Handler: p, + + ReadTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, } - r := mux.NewRouter().SkipClean(true).UseEncodedPath() - r.PathPrefix("/").Handler(p) - fmt.Printf("imageproxy listening on %s\n", server.Addr) - log.Fatal(http.ListenAndServe(*addr, r)) + fmt.Printf("imageproxy listening on %s\n", *addr) + log.Fatal(server.Serve(ln)) } type signatureKeyList [][]byte @@ -109,7 +137,7 @@ func (skl *signatureKeyList) Set(value string) error { if strings.HasPrefix(v, "@") { file := strings.TrimPrefix(v, "@") var err error - key, err = ioutil.ReadFile(file) + key, err = os.ReadFile(file) if err != nil { log.Fatalf("error reading signature file: %v", err) } @@ -157,7 +185,7 @@ func parseCache(c string) (imageproxy.Cache, error) { u, err := url.Parse(c) if err != nil { - return nil, fmt.Errorf("error parsing cache flag: %v", err) + return nil, fmt.Errorf("error parsing cache flag: %w", err) } switch u.Scheme { diff --git a/data.go b/data.go index 5df4a69..8b5765c 100644 --- a/data.go +++ b/data.go @@ -4,6 +4,7 @@ package imageproxy import ( + "encoding/base64" "fmt" "net/http" "net/url" @@ -11,6 +12,8 @@ import ( "sort" "strconv" "strings" + "time" + "unicode" ) const ( @@ -30,6 +33,8 @@ const ( optCropWidth = "cw" optCropHeight = "ch" optSmartCrop = "sc" + optTrim = "trim" + optValidUntil = "vu" ) // URLError reports a malformed URL error. @@ -80,6 +85,12 @@ type Options struct { // Automatically find good crop points based on image content. SmartCrop bool + + // If true, automatically trim pixels of the same color around the edges + Trim bool + + // If non-zero, the URL is valid until this time. + ValidUntil time.Time } func (o Options) String() string { @@ -123,7 +134,15 @@ func (o Options) String() string { if o.SmartCrop { opts = append(opts, optSmartCrop) } + if o.Trim { + opts = append(opts, optTrim) + } + if !o.ValidUntil.IsZero() { + opts = append(opts, fmt.Sprintf("%s%d", optValidUntil, o.ValidUntil.Unix())) + } + sort.Strings(opts) + return strings.Join(opts, ",") } @@ -132,21 +151,21 @@ func (o Options) String() string { // the presence of other fields (like Fit). A non-empty Format value is // assumed to involve a transformation. func (o Options) transform() bool { - return o.Width != 0 || o.Height != 0 || o.Rotate != 0 || o.FlipHorizontal || o.FlipVertical || o.Quality != 0 || o.Format != "" || o.CropX != 0 || o.CropY != 0 || o.CropWidth != 0 || o.CropHeight != 0 + return o.Width != 0 || o.Height != 0 || o.Rotate != 0 || o.FlipHorizontal || o.FlipVertical || o.Quality != 0 || o.Format != "" || o.CropX != 0 || o.CropY != 0 || o.CropWidth != 0 || o.CropHeight != 0 || o.Trim } // ParseOptions parses str as a list of comma separated transformation options. // The options can be specified in in order, with duplicate options overwriting // previous values. // -// Rectangle Crop +// # Rectangle Crop // // There are four options controlling rectangle crop: // -// cx{x} - X coordinate of top left rectangle corner (default: 0) -// cy{y} - Y coordinate of top left rectangle corner (default: 0) -// cw{width} - rectangle width (default: image width) -// ch{height} - rectangle height (default: image height) +// cx{x} - X coordinate of top left rectangle corner (default: 0) +// cy{y} - Y coordinate of top left rectangle corner (default: 0) +// cw{width} - rectangle width (default: image width) +// ch{height} - rectangle height (default: image height) // // For all options, integer values are interpreted as exact pixel values and // floats between 0 and 1 are interpreted as percentages of the original image @@ -157,13 +176,13 @@ func (o Options) transform() bool { // crop width or height will be adjusted, preserving the specified cx and cy // values. Rectangular crop is applied before any other transformations. // -// Smart Crop +// # Smart Crop // // The "sc" option will perform a content-aware smart crop to fit the // requested image width and height dimensions (see Size and Cropping below). // The smart crop option will override any requested rectangular crop. // -// Size and Cropping +// # Size and Cropping // // The size option takes the general form "{width}x{height}", where width and // height are numbers. Integer values greater than 1 are interpreted as exact @@ -192,7 +211,7 @@ func (o Options) transform() bool { // option with only one of either width or height does the same thing as if // "fit" had not been specified. // -// Rotation and Flips +// # Rotation and Flips // // The "r{degrees}" option will rotate the image the specified number of // degrees, counter-clockwise. Valid degrees values are 90, 180, and 270. @@ -200,17 +219,17 @@ func (o Options) transform() bool { // The "fv" option will flip the image vertically. The "fh" option will flip // the image horizontally. Images are flipped after being rotated. // -// Quality +// # Quality // // The "q{qualityPercentage}" option can be used to specify the quality of the // output file (JPEG only). If not specified, the default value of "95" is used. // -// Format +// # Format // -// The "jpeg", "png", and "tiff" options can be used to specify the desired +// The "jpeg", "png", and "tiff" options can be used to specify the desired // image format of the proxied image. // -// Signature +// # Signature // // The "s{signature}" option specifies an optional base64 encoded HMAC used to // sign the remote URL in the request. The HMAC key used to verify signatures is @@ -219,20 +238,32 @@ func (o Options) transform() bool { // See https://github.com/willnorris/imageproxy/blob/master/docs/url-signing.md // for examples of generating signatures. // +// # Trim +// +// The "trim" option will automatically trim pixels of the same color around +// the edges of the image. This is useful for removing borders from images +// that have been resized or cropped. The trim option is applied before other +// options such as cropping or resizing. +// +// # Valid Until +// +// The "vu{unixtime}" option specifies a Unix timestamp at which the request URL is no longer valid. +// For example, "vu1800000000" would mean the URL is valid until 2027-01-15T08:00:00Z. +// // Examples // -// 0x0 - no resizing -// 200x - 200 pixels wide, proportional height -// x0.15 - 15% original height, proportional width -// 100x150 - 100 by 150 pixels, cropping as needed -// 100 - 100 pixels square, cropping as needed -// 150,fit - scale to fit 150 pixels square, no cropping -// 100,r90 - 100 pixels square, rotated 90 degrees -// 100,fv,fh - 100 pixels square, flipped horizontal and vertical -// 200x,q60 - 200 pixels wide, proportional height, 60% quality -// 200x,png - 200 pixels wide, converted to PNG format -// cw100,ch100 - crop image to 100px square, starting at (0,0) -// cx10,cy20,cw100,ch200 - crop image starting at (10,20) is 100px wide and 200px tall +// 0x0 - no resizing +// 200x - 200 pixels wide, proportional height +// x0.15 - 15% original height, proportional width +// 100x150 - 100 by 150 pixels, cropping as needed +// 100 - 100 pixels square, cropping as needed +// 150,fit - scale to fit 150 pixels square, no cropping +// 100,r90 - 100 pixels square, rotated 90 degrees +// 100,fv,fh - 100 pixels square, flipped horizontal and vertical +// 200x,q60 - 200 pixels wide, proportional height, 60% quality +// 200x,png - 200 pixels wide, converted to PNG format +// cw100,ch100 - crop image to 100px square, starting at (0,0) +// cx10,cy20,cw100,ch200 - crop image starting at (10,20) is 100px wide and 200px tall func ParseOptions(str string) Options { var options Options @@ -251,6 +282,8 @@ func ParseOptions(str string) Options { options.Format = opt case opt == optSmartCrop: options.SmartCrop = true + case opt == optTrim: + options.Trim = true case strings.HasPrefix(opt, optRotatePrefix): value := strings.TrimPrefix(opt, optRotatePrefix) options.Rotate, _ = strconv.Atoi(value) @@ -271,6 +304,11 @@ func ParseOptions(str string) Options { case strings.HasPrefix(opt, optCropHeight): value := strings.TrimPrefix(opt, optCropHeight) options.CropHeight, _ = strconv.ParseFloat(value, 64) + case strings.HasPrefix(opt, optValidUntil): + value := strings.TrimPrefix(opt, optValidUntil) + if v, _ := strconv.ParseInt(value, 10, 64); v > 0 { + options.ValidUntil = time.Unix(v, 0) + } case strings.Contains(opt, optSizeDelimiter): size := strings.SplitN(opt, optSizeDelimiter, 2) if w := size[0]; w != "" { @@ -309,22 +347,40 @@ func (r Request) String() string { // NewRequest parses an http.Request into an imageproxy Request. Options and // the remote image URL are specified in the request path, formatted as: // /{options}/{remote_url}. Options may be omitted, so a request path may -// simply contain /{remote_url}. The remote URL must be an absolute "http" or -// "https" URL, should not be URL encoded, and may contain a query string. +// simply contain /{remote_url}. +// +// The remote URL may be included in plain text without any encoding, +// percent-encoded (aka URL encoded), or base64 encoded (URL safe, no padding). +// +// When no encoding is used, any URL query string is treated as part of the remote URL. +// For example, given the proxy URL of `http://localhost/x/http://example.com/?id=1`, +// the remote URL is `http://example.com/?id=1`. +// +// When percent-encoding is used, the full URL must be encoded. +// Any query string on the proxy URL is NOT included as part of the remote URL. +// Percent-encoded URLs must be absolute URLs; +// they cannot be relative URLs used with a default base URL. +// +// When base64 encoding is used, the full URL must be encoded. +// Any query string on the proxy URL is NOT included as part of the remote URL. +// Base64 encoded URLs may be relative URLs used with a default base URL. // // Assuming an imageproxy server running on localhost, the following are all // valid imageproxy requests: // -// http://localhost/100x200/http://example.com/image.jpg -// http://localhost/100x200,r90/http://example.com/image.jpg?foo=bar -// http://localhost//http://example.com/image.jpg -// http://localhost/http://example.com/image.jpg +// http://localhost/100x200/http://example.com/image.jpg +// http://localhost/100x200,r90/http://example.com/image.jpg?foo=bar +// http://localhost//http://example.com/image.jpg +// http://localhost/http://example.com/image.jpg +// http://localhost/x/http%3A%2F%2Fexample.com%2Fimage.jpg +// http://localhost/100x200/aHR0cDovL2V4YW1wbGUuY29tL2ltYWdlLmpwZw func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) { var err error req := &Request{Original: r} + var enc bool // whether the remote URL was base64 or URL encoded path := r.URL.EscapedPath()[1:] // strip leading slash - req.URL, err = parseURL(path) + req.URL, enc, err = parseURL(path, baseURL) if err != nil || !req.URL.IsAbs() { // first segment should be options parts := strings.SplitN(path, "/", 2) @@ -333,7 +389,7 @@ func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) { } var err error - req.URL, err = parseURL(parts[1]) + req.URL, enc, err = parseURL(parts[1], baseURL) if err != nil { return nil, URLError{fmt.Sprintf("unable to parse remote URL: %v", err), r.URL} } @@ -353,16 +409,47 @@ func NewRequest(r *http.Request, baseURL *url.URL) (*Request, error) { return nil, URLError{"remote URL must have http or https scheme", r.URL} } - // query string is always part of the remote URL - req.URL.RawQuery = r.URL.RawQuery + if !enc { + // if the remote URL was not base64 or URL encoded, + // then the query string is part of the remote URL + req.URL.RawQuery = r.URL.RawQuery + } return req, nil } var reCleanedURL = regexp.MustCompile(`^(https?):/+([^/])`) +var reIsEncodedURL = regexp.MustCompile(`^(?i)https?%3A%2F`) // parseURL parses s as a URL, handling URLs that have been munged by // path.Clean or a webserver that collapses multiple slashes. -func parseURL(s string) (*url.URL, error) { +// The returned enc bool indicates whether the remote URL was encoded. +func parseURL(s string, baseURL *url.URL) (_ *url.URL, enc bool, _ error) { + // Try to base64 decode the string. If it is not base64 encoded, + // this will fail quickly on the first invalid character like ":", ".", or "/". + // Accept the decoded string if it looks like an absolute HTTP URL, + // or if we have a baseURL and the decoded string did not contain invalid code points. + // This allows for values like "/path", which do successfully base64 decode, + // but not to valid code points, to be treated as an unencoded string. + if b, err := base64.RawURLEncoding.DecodeString(s); err == nil { + d := string(b) + if strings.HasPrefix(d, "http://") || strings.HasPrefix(d, "https://") { + enc = true + s = d + } else if baseURL != nil && !strings.ContainsRune(d, unicode.ReplacementChar) { + enc = true + s = d + } + } + + // If the string looks like a URL encoded absolute HTTP(S) URL, decode it. + if reIsEncodedURL.MatchString(s) { + if u, err := url.PathUnescape(s); err == nil { + enc = true + s = u + } + } + s = reCleanedURL.ReplaceAllString(s, "$1://$2") - return url.Parse(s) + u, err := url.Parse(s) + return u, enc, err } diff --git a/data_test.go b/data_test.go index 80d3c3d..0e854af 100644 --- a/data_test.go +++ b/data_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "testing" + "time" ) var emptyOptions = Options{} @@ -25,8 +26,8 @@ func TestOptions_String(t *testing.T) { "1x2,fh,fit,fv,q80,r90", }, { - Options{Width: 0.15, Height: 1.3, Rotate: 45, Quality: 95, Signature: "c0ffee", Format: "png"}, - "0.15x1.3,png,q95,r45,sc0ffee", + Options{Width: 0.15, Height: 1.3, Rotate: 45, Quality: 95, Signature: "c0ffee", Format: "png", ValidUntil: time.Unix(123, 0)}, + "0.15x1.3,png,q95,r45,sc0ffee,vu123", }, { Options{Width: 0.15, Height: 1.3, CropX: 100, CropY: 200}, @@ -86,7 +87,7 @@ func TestParseOptions(t *testing.T) { // flags, in different orders {"q70,1x2,fit,r90,fv,fh,sc0ffee,png", Options{Width: 1, Height: 2, Fit: true, Rotate: 90, FlipVertical: true, FlipHorizontal: true, Quality: 70, Signature: "c0ffee", Format: "png"}}, {"r90,fh,sc0ffee,png,q90,1x2,fv,fit", Options{Width: 1, Height: 2, Fit: true, Rotate: 90, FlipVertical: true, FlipHorizontal: true, Quality: 90, Signature: "c0ffee", Format: "png"}}, - {"cx100,cw300,1x2,cy200,ch400,sc,scaleUp", Options{Width: 1, Height: 2, ScaleUp: true, CropX: 100, CropY: 200, CropWidth: 300, CropHeight: 400, SmartCrop: true}}, + {"cx100,cw300,1x2,cy200,ch400,sc,scaleUp,vu1234567890", Options{Width: 1, Height: 2, ScaleUp: true, CropX: 100, CropY: 200, CropWidth: 300, CropHeight: 400, SmartCrop: true, ValidUntil: time.Unix(1234567890, 0)}}, } for _, tt := range tests { @@ -152,10 +153,68 @@ func TestNewRequest(t *testing.T) { "http://localhost/http:///example.com/foo", "http://example.com/foo", emptyOptions, false, }, + // base64 encoded paths + { + "http://localhost/aHR0cDovL2V4YW1wbGUuY29tL2Zvbw", + "http://example.com/foo", emptyOptions, false, + }, + { + "http://localhost//aHR0cDovL2V4YW1wbGUuY29tL2Zvbw", + "http://example.com/foo", emptyOptions, false, + }, + { + "http://localhost/x/aHR0cDovL2V4YW1wbGUuY29tL2Zvbw", + "http://example.com/foo", emptyOptions, false, + }, + { + "http://localhost/x/aHR0cHM6Ly9leGFtcGxlLmNvbS9mb28_YmFy", + "https://example.com/foo?bar", emptyOptions, false, + }, + { + "http://localhost/x/aHR0cHM6Ly9leGFtcGxlLmNvbS9mb28_YmFy?baz", + "https://example.com/foo?bar", emptyOptions, false, + }, { // escaped path "http://localhost/http://example.com/%2C", "http://example.com/%2C", emptyOptions, false, }, + // percent encoded cases + { + "http://localhost/1x2/http%3A%2F%2Fexample.com%2Ffoo", + "http://example.com/foo", Options{Width: 1, Height: 2}, false, + }, + { + "http://localhost/1x2/http%3A%2F%2Fexample.com%2Fhttp%2Fstuff", + "http://example.com/http/stuff", Options{Width: 1, Height: 2}, false, + }, + { + "http://localhost/http%3A%2F%2Fexample.com%2Ffoo", + "http://example.com/foo", emptyOptions, false, + }, + { + "http://localhost/HTTP%3a%2f%2fexample.com%2Ffoo", + "http://example.com/foo", emptyOptions, false, + }, + { + "http://localhost/http%3A%2Fexample.com%2Ffoo", + "http://example.com/foo", emptyOptions, false, + }, + { + "http://localhost/http%3A%2F%2F%2Fexample.com%2Ffoo", + "http://example.com/foo", emptyOptions, false, + }, + { + "http://localhost//http%3A%2F%2Fexample.com%2Ffoo", + "http://example.com/foo", emptyOptions, false, + }, + { + "http://localhost/http%3A%2F%2Fexample.com%2Ffoo%3Ftest%3D1%26test%3D2", + "http://example.com/foo?test=1&test=2", emptyOptions, false, + }, + { + "http://localhost/1x2/http%3A%2F%2Fexample.com%2Ffoo%3Ftest%3D1%26test%3D2", + "http://example.com/foo?test=1&test=2", Options{Width: 1, Height: 2}, false, + }, } for _, tt := range tests { @@ -186,16 +245,31 @@ func TestNewRequest(t *testing.T) { } func TestNewRequest_BaseURL(t *testing.T) { - req, _ := http.NewRequest("GET", "/x/path", nil) base, _ := url.Parse("https://example.com/") - r, err := NewRequest(req, base) - if err != nil { - t.Errorf("NewRequest(%v, %v) returned unexpected error: %v", req, base, err) + tests := []struct { + path string + want string + }{ + { + path: "/x/path", + want: "https://example.com/path#0x0", + }, + { // Chinese characters 已然 + path: "/x/5bey54S2", + want: "https://example.com/%E5%B7%B2%E7%84%B6#0x0", + }, } - want := "https://example.com/path#0x0" - if got := r.String(); got != want { - t.Errorf("NewRequest(%v, %v) returned %q, want %q", req, base, got, want) + for _, tt := range tests { + req, _ := http.NewRequest("GET", tt.path, nil) + r, err := NewRequest(req, base) + if err != nil { + t.Errorf("NewRequest(%v, %v) returned unexpected error: %v", req, base, err) + } + + if got := r.String(); got != tt.want { + t.Errorf("NewRequest(%v, %v) returned %q, want %q", req, base, got, tt.want) + } } } diff --git a/docs/changelog.md b/docs/changelog.md index c093bef..ccc8fe3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,314 +1,360 @@ # Changelog This file contains all notable changes to -[imageproxy](https://github.com/willnorris/imageproxy). The format is based on +[imageproxy](https://github.com/willnorris/imageproxy). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + [Unreleased]: https://github.com/willnorris/imageproxy/compare/v0.9.0...HEAD ## [0.10.0] (2020-04-02) + [0.10.0]: https://github.com/willnorris/imageproxy/compare/v0.9.0...v0.10.0 ### Added - - add support for multiple signature keys to support key rotation - ([ef09c1b](https://github.com/willnorris/imageproxy/commit/ef09c1b), - [#209](https://github.com/willnorris/imageproxy/pull/209), - [maurociancio](https://github.com/maurociancio)) - - added option to include referer header in remote requests - ([#216](https://github.com/willnorris/imageproxy/issues/216)) - - added basic support for recording prometheus metrics - ([#121](https://github.com/willnorris/imageproxy/pull/121) - [benhaan](https://github.com/benhaan)) + +- add support for multiple signature keys to support key rotation + ([ef09c1b](https://github.com/willnorris/imageproxy/commit/ef09c1b), + [#209](https://github.com/willnorris/imageproxy/pull/209), + [maurociancio](https://github.com/maurociancio)) +- added option to include referer header in remote requests + ([#216](https://github.com/willnorris/imageproxy/issues/216)) +- added basic support for recording prometheus metrics + ([#121](https://github.com/willnorris/imageproxy/pull/121) + [benhaan](https://github.com/benhaan)) ### Fixed - - improved content type detection for some hosts, particularly S3 - ([ea95ad9](https://github.com/willnorris/imageproxy/commit/ea95ad9), - [shahan312](https://github.com/shahan312)) - - fix signature verification for some proxied URLs - ([3589510](https://github.com/willnorris/imageproxy/commit/3589510), - [#212](https://github.com/willnorris/imageproxy/issues/212), - ([#215](https://github.com/willnorris/imageproxy/issues/215), - thanks to [aaronpk](https://github.com/aaronpk) for helping debug and - [fieldistor](https://github.com/fieldistor) for the suggested fix) + +- improved content type detection for some hosts, particularly S3 + ([ea95ad9](https://github.com/willnorris/imageproxy/commit/ea95ad9), + [shahan312](https://github.com/shahan312)) +- fix signature verification for some proxied URLs + ([3589510](https://github.com/willnorris/imageproxy/commit/3589510), + [#212](https://github.com/willnorris/imageproxy/issues/212), + ([#215](https://github.com/willnorris/imageproxy/issues/215), + thanks to [aaronpk](https://github.com/aaronpk) for helping debug and + [fieldistor](https://github.com/fieldistor) for the suggested fix) ## [0.9.0] (2019-06-10) + [0.9.0]: https://github.com/willnorris/imageproxy/compare/v0.8.0...v0.9.0 ### Added - - allow request signatures to cover options - ([#145](https://github.com/willnorris/imageproxy/issues/145)) - - add simple imageproxy-sign tool for calculating signatures - ([e1558d5](https://github.com/willnorris/imageproxy/commit/e1558d5)) - - allow overriding the Logger used by Proxy - ([#174](https://github.com/willnorris/imageproxy/pull/174), - [hmhealey](https://github.com/hmhealey)) - - allow using environment variables for configuration - ([50e0d11](https://github.com/willnorris/imageproxy/commit/50e0d11)) - - add support for BMP images - ([d4ba520](https://github.com/willnorris/imageproxy/commit/d4ba520)) + +- allow request signatures to cover options + ([#145](https://github.com/willnorris/imageproxy/issues/145)) +- add simple imageproxy-sign tool for calculating signatures + ([e1558d5](https://github.com/willnorris/imageproxy/commit/e1558d5)) +- allow overriding the Logger used by Proxy + ([#174](https://github.com/willnorris/imageproxy/pull/174), + [hmhealey](https://github.com/hmhealey)) +- allow using environment variables for configuration + ([50e0d11](https://github.com/willnorris/imageproxy/commit/50e0d11)) +- add support for BMP images + ([d4ba520](https://github.com/willnorris/imageproxy/commit/d4ba520)) ### Changed - - improvements to docker image: run as non-privileged user, use go1.12 - compiler, and build imageproxy as a go module. - - options are now sorted when converting to string. This is a breaking change - for anyone relying on the option order, and will additionally invalidate - most cached values, since the option string is part of the cache key. +- improvements to docker image: run as non-privileged user, use go1.12 + compiler, and build imageproxy as a go module. - Both the original remote image, as well as any transformations on that image - are cached, but only the transformed images will be impacted by this change. - This will result in imageproxy having to re-perform the transformations, but - should not result in re-fetching the remote image, unless it has already - otherwise expired. +- options are now sorted when converting to string. This is a breaking change + for anyone relying on the option order, and will additionally invalidate + most cached values, since the option string is part of the cache key. + + Both the original remote image, as well as any transformations on that image + are cached, but only the transformed images will be impacted by this change. + This will result in imageproxy having to re-perform the transformations, but + should not result in re-fetching the remote image, unless it has already + otherwise expired. ### Fixed - - properly include Accept header on remote URL requests - ([#165](https://github.com/willnorris/imageproxy/issues/165), - [6aca1e0](https://github.com/willnorris/imageproxy/commit/6aca1e0)) - - detect response content type if content-type header is missing - ([cf54b2c](https://github.com/willnorris/imageproxy/commit/cf54b2c)) + +- properly include Accept header on remote URL requests + ([#165](https://github.com/willnorris/imageproxy/issues/165), + [6aca1e0](https://github.com/willnorris/imageproxy/commit/6aca1e0)) +- detect response content type if content-type header is missing + ([cf54b2c](https://github.com/willnorris/imageproxy/commit/cf54b2c)) ### Removed - - removed deprecated `whitelist` flag and `Proxy.Whitelist` struct field. Use - `allowHosts` and `Proxy.AllowHosts` instead. + +- removed deprecated `whitelist` flag and `Proxy.Whitelist` struct field. Use + `allowHosts` and `Proxy.AllowHosts` instead. ## [0.8.0] (2019-03-21) + [0.8.0]: https://github.com/willnorris/imageproxy/compare/v0.7.0...v0.8.0 ### Added - - added support for restricting proxied URLs [based on Content-Type - headers](https://github.com/willnorris/imageproxy#allowed-content-type-list) - ([#141](https://github.com/willnorris/imageproxy/pull/141), - [ccbrown](https://github.com/ccbrown)) - - added ability to [deny requests](https://github.com/willnorris/imageproxy#allowed-and-denied-hosts-list) - for certain remote hosts - ([#85](https://github.com/willnorris/imageproxy/pull/85), - [geriljaSA](https://github.com/geriljaSA)) - - added `userAgent` flag to specify a custom user agent when fetching images - ([#83](https://github.com/willnorris/imageproxy/pull/83), - [huguesalary](https://github.com/huguesalary)) - - added support for [s3 compatible](https://github.com/willnorris/imageproxy#cache) - storage providers - ([#147](https://github.com/willnorris/imageproxy/pull/147), - [ruledio](https://github.com/ruledio)) - - log URL when image transform fails for easier debugging - ([#149](https://github.com/willnorris/imageproxy/pull/149), - [daohoangson](https://github.com/daohoangson)) - - added support for building imageproxy as a [go module](https://golang.org/wiki/Modules). - A future version will remove vendored dependencies, at which point building - as a module will be the only supported method of building imageproxy. + +- added support for restricting proxied URLs [based on Content-Type + headers](https://github.com/willnorris/imageproxy#allowed-content-type-list) + ([#141](https://github.com/willnorris/imageproxy/pull/141), + [ccbrown](https://github.com/ccbrown)) +- added ability to [deny requests](https://github.com/willnorris/imageproxy#allowed-and-denied-hosts-list) + for certain remote hosts + ([#85](https://github.com/willnorris/imageproxy/pull/85), + [geriljaSA](https://github.com/geriljaSA)) +- added `userAgent` flag to specify a custom user agent when fetching images + ([#83](https://github.com/willnorris/imageproxy/pull/83), + [huguesalary](https://github.com/huguesalary)) +- added support for [s3 compatible](https://github.com/willnorris/imageproxy#cache) + storage providers + ([#147](https://github.com/willnorris/imageproxy/pull/147), + [ruledio](https://github.com/ruledio)) +- log URL when image transform fails for easier debugging + ([#149](https://github.com/willnorris/imageproxy/pull/149), + [daohoangson](https://github.com/daohoangson)) +- added support for building imageproxy as a [go module](https://golang.org/wiki/Modules). + A future version will remove vendored dependencies, at which point building + as a module will be the only supported method of building imageproxy. ### Changed - - when a remote URL is denied, return a generic error message that does not specify exactly why it failed - ([7e19b5c](https://github.com/willnorris/imageproxy/commit/7e19b5c)) + +- when a remote URL is denied, return a generic error message that does not specify exactly why it failed + ([7e19b5c](https://github.com/willnorris/imageproxy/commit/7e19b5c)) ### Deprecated - - `whitelist` flag and `Proxy.Whitelist` struct field renamed to `allowHosts` - and `Proxy.AllowHosts`. Old values are still supported, but will be removed - in a future release. + +- `whitelist` flag and `Proxy.Whitelist` struct field renamed to `allowHosts` + and `Proxy.AllowHosts`. Old values are still supported, but will be removed + in a future release. ### Fixed - - fixed tcp_mem resource leak on 304 responses - ([#153](https://github.com/willnorris/imageproxy/pull/153), - [Micr0mega](https://github.com/Micr0mega)) + +- fixed tcp_mem resource leak on 304 responses + ([#153](https://github.com/willnorris/imageproxy/pull/153), + [Micr0mega](https://github.com/Micr0mega)) ## [0.7.0] (2018-02-06) + [0.7.0]: https://github.com/willnorris/imageproxy/compare/v0.6.0...v0.7.0 ### Added - - added support for arbitrary [rectangular crops](https://godoc.org/willnorris.com/go/imageproxy#hdr-Rectangle_Crop) - ([#90](https://github.com/willnorris/imageproxy/pull/90), - [maciejtarnowski](https://github.com/maciejtarnowski)) - - added support for tiff images - ([#109](https://github.com/willnorris/imageproxy/pull/109), - [mikecx](https://github.com/mikecx)) - - added support for additional [caching backends](https://github.com/willnorris/imageproxy#cache): - - Google Cloud Storage - ([#106](https://github.com/willnorris/imageproxy/pull/106), - [diegomarangoni](https://github.com/diegomarangoni)) - - Azure - ([#79](https://github.com/willnorris/imageproxy/pull/79), - [PaulARoy](https://github.com/PaulARoy)) - - Redis - ([#49](https://github.com/willnorris/imageproxy/issues/49) - [dbfc693](https://github.com/willnorris/imageproxy/commit/dbfc693)) - - Tiering multiple caches by repeating the `-cache` flag - ([ec5b543](https://github.com/willnorris/imageproxy/commit/ec5b543)) - - added support for EXIF orientation tags - ([#63](https://github.com/willnorris/imageproxy/issues/63), - [67619a6](https://github.com/willnorris/imageproxy/commit/67619a6)) - - added [smart crop feature](https://godoc.org/willnorris.com/go/imageproxy#hdr-Smart_Crop) - ([#55](https://github.com/willnorris/imageproxy/issues/55), - [afbd254](https://github.com/willnorris/imageproxy/commit/afbd254)) + +- added support for arbitrary [rectangular crops](https://godoc.org/willnorris.com/go/imageproxy#hdr-Rectangle_Crop) + ([#90](https://github.com/willnorris/imageproxy/pull/90), + [maciejtarnowski](https://github.com/maciejtarnowski)) +- added support for tiff images + ([#109](https://github.com/willnorris/imageproxy/pull/109), + [mikecx](https://github.com/mikecx)) +- added support for additional [caching backends](https://github.com/willnorris/imageproxy#cache): + - Google Cloud Storage + ([#106](https://github.com/willnorris/imageproxy/pull/106), + [diegomarangoni](https://github.com/diegomarangoni)) + - Azure + ([#79](https://github.com/willnorris/imageproxy/pull/79), + [PaulARoy](https://github.com/PaulARoy)) + - Redis + ([#49](https://github.com/willnorris/imageproxy/issues/49) + [dbfc693](https://github.com/willnorris/imageproxy/commit/dbfc693)) + - Tiering multiple caches by repeating the `-cache` flag + ([ec5b543](https://github.com/willnorris/imageproxy/commit/ec5b543)) +- added support for EXIF orientation tags + ([#63](https://github.com/willnorris/imageproxy/issues/63), + [67619a6](https://github.com/willnorris/imageproxy/commit/67619a6)) +- added [smart crop feature](https://godoc.org/willnorris.com/go/imageproxy#hdr-Smart_Crop) + ([#55](https://github.com/willnorris/imageproxy/issues/55), + [afbd254](https://github.com/willnorris/imageproxy/commit/afbd254)) ### Changed - - rotate values are normalized, such that `r-90` is the same as `r270` - ([07c54b4](https://github.com/willnorris/imageproxy/commit/07c54b4)) - - now return `200 OK` response for requests to root `/` - ([5ee7e28](https://github.com/willnorris/imageproxy/commit/5ee7e28)) - - switch to using official AWS Go SDK for s3 cache storage. This is a - breaking change for anyone using that cache implementation, since the URL - syntax has changed. This adds support for the newer v4 auth method, as well - as additional s3 regions. - ([0ee5167](https://github.com/willnorris/imageproxy/commit/0ee5167)) - - switched to standard go log library. Added `-verbose` flag for more logging - in-memory cache backend supports limiting the max cache size - ([a57047f](https://github.com/willnorris/imageproxy/commit/a57047f)) - - docker image sized reduced by using scratch image and multistage build - ([#113](https://github.com/willnorris/imageproxy/pull/113), - [matematik7](https://github.com/matematik7)) + +- rotate values are normalized, such that `r-90` is the same as `r270` + ([07c54b4](https://github.com/willnorris/imageproxy/commit/07c54b4)) +- now return `200 OK` response for requests to root `/` + ([5ee7e28](https://github.com/willnorris/imageproxy/commit/5ee7e28)) +- switch to using official AWS Go SDK for s3 cache storage. This is a + breaking change for anyone using that cache implementation, since the URL + syntax has changed. This adds support for the newer v4 auth method, as well + as additional s3 regions. + ([0ee5167](https://github.com/willnorris/imageproxy/commit/0ee5167)) +- switched to standard go log library. Added `-verbose` flag for more logging + in-memory cache backend supports limiting the max cache size + ([a57047f](https://github.com/willnorris/imageproxy/commit/a57047f)) +- docker image sized reduced by using scratch image and multistage build + ([#113](https://github.com/willnorris/imageproxy/pull/113), + [matematik7](https://github.com/matematik7)) ### Removed - - removed deprecated `cacheDir` and `cacheSize` flags + +- removed deprecated `cacheDir` and `cacheSize` flags ### Fixed - - fixed interpretation of `Last-Modified` and `If-Modified-Since` headers - ([#108](https://github.com/willnorris/imageproxy/pull/108), - [jamesreggio](https://github.com/jamesreggio)) - - preserve original URL encoding - ([#115](https://github.com/willnorris/imageproxy/issues/115)) + +- fixed interpretation of `Last-Modified` and `If-Modified-Since` headers + ([#108](https://github.com/willnorris/imageproxy/pull/108), + [jamesreggio](https://github.com/jamesreggio)) +- preserve original URL encoding + ([#115](https://github.com/willnorris/imageproxy/issues/115)) ## [0.6.0] (2017-08-29) + [0.6.0]: https://github.com/willnorris/imageproxy/compare/v0.5.1...v0.6.0 ### Added - - added health check endpoint - ([#54](https://github.com/willnorris/imageproxy/pull/54), - [immunda](https://github.com/immunda)) - - preserve Link headers from remote image - ([#68](https://github.com/willnorris/imageproxy/pull/68), - [xavren](https://github.com/xavren)) - - added support for per-request timeout - ([#75](https://github.com/willnorris/imageproxy/issues/75)) - - added support for specifying output image format - ([b9cc9df](https://github.com/willnorris/imageproxy/commit/b9cc9df)) - - added webp support (decode only) - ([3280445](https://github.com/willnorris/imageproxy/commit/3280445)) - - added CORS support - ([#96](https://github.com/willnorris/imageproxy/pull/96), - [romdim](https://github.com/romdim)) + +- added health check endpoint + ([#54](https://github.com/willnorris/imageproxy/pull/54), + [immunda](https://github.com/immunda)) +- preserve Link headers from remote image + ([#68](https://github.com/willnorris/imageproxy/pull/68), + [xavren](https://github.com/xavren)) +- added support for per-request timeout + ([#75](https://github.com/willnorris/imageproxy/issues/75)) +- added support for specifying output image format + ([b9cc9df](https://github.com/willnorris/imageproxy/commit/b9cc9df)) +- added webp support (decode only) + ([3280445](https://github.com/willnorris/imageproxy/commit/3280445)) +- added CORS support + ([#96](https://github.com/willnorris/imageproxy/pull/96), + [romdim](https://github.com/romdim)) ### Fixed - - improved error messages for some authorization failures - ([27d5378](https://github.com/willnorris/imageproxy/commit/27d5378)) - - skip transformation when not needed - ([#64](https://github.com/willnorris/imageproxy/issues/64)) - - properly handled "cleaned" remote URLs - ([a1af9aa](https://github.com/willnorris/imageproxy/commit/a1af9aa), - [b61992e](https://github.com/willnorris/imageproxy/commit/b61992e)) + +- improved error messages for some authorization failures + ([27d5378](https://github.com/willnorris/imageproxy/commit/27d5378)) +- skip transformation when not needed + ([#64](https://github.com/willnorris/imageproxy/issues/64)) +- properly handled "cleaned" remote URLs + ([a1af9aa](https://github.com/willnorris/imageproxy/commit/a1af9aa), + [b61992e](https://github.com/willnorris/imageproxy/commit/b61992e)) ## [0.5.1] (2015-12-07) + [0.5.1]: https://github.com/willnorris/imageproxy/compare/v0.5.0...v0.5.1 ### Fixed - - fixed bug in gif resizing - ([gifresize@104a7cd](https://github.com/willnorris/gifresize/commit/104a7cd)) + +- fixed bug in gif resizing + ([gifresize@104a7cd](https://github.com/willnorris/gifresize/commit/104a7cd)) ## [0.5.0] (2015-12-07) + [0.5.0]: https://github.com/willnorris/imageproxy/compare/v0.4.0...v0.5.0 ## Added - - added Dockerfile - ([#29](https://github.com/willnorris/imageproxy/pull/29), - [sevki](https://github.com/sevki)) - - allow scaling image beyond its original size with `-scaleUp` flag - ([#37](https://github.com/willnorris/imageproxy/pull/37), - [runemadsen](https://github.com/runemadsen)) - - add ability to restrict HTTP referrer - ([9213c93](https://github.com/willnorris/imageproxy/commit/9213c93), - [connor4312](https://github.com/connor4312)) - - preserve cache-control header from remote image - ([#43](https://github.com/willnorris/imageproxy/pull/43), - [runemadsen](https://github.com/runemadsen)) - - add support for caching images on Amazon S3 - ([ec96fcb](https://github.com/willnorris/imageproxy/commit/ec96fcb) - [victortrac](https://github.com/victortrac)) + +- added Dockerfile + ([#29](https://github.com/willnorris/imageproxy/pull/29), + [sevki](https://github.com/sevki)) +- allow scaling image beyond its original size with `-scaleUp` flag + ([#37](https://github.com/willnorris/imageproxy/pull/37), + [runemadsen](https://github.com/runemadsen)) +- add ability to restrict HTTP referrer + ([9213c93](https://github.com/willnorris/imageproxy/commit/9213c93), + [connor4312](https://github.com/connor4312)) +- preserve cache-control header from remote image + ([#43](https://github.com/willnorris/imageproxy/pull/43), + [runemadsen](https://github.com/runemadsen)) +- add support for caching images on Amazon S3 + ([ec96fcb](https://github.com/willnorris/imageproxy/commit/ec96fcb) + [victortrac](https://github.com/victortrac)) ## Changed - - change default cache to none, and add `-cache` flag for specifying caches. - This deprecates the `-cacheDir` flag. - - on-disk cache now stores files in a two-level trie. For example, for a file - named "c0ffee", store file as "c0/ff/c0ffee". + +- change default cache to none, and add `-cache` flag for specifying caches. + This deprecates the `-cacheDir` flag. +- on-disk cache now stores files in a two-level trie. For example, for a file + named "c0ffee", store file as "c0/ff/c0ffee". ## Fixed - - skip resizing if requested dimensions larger than original - ([#46](https://github.com/willnorris/imageproxy/pull/46), - [orian](https://github.com/orian)) + +- skip resizing if requested dimensions larger than original + ([#46](https://github.com/willnorris/imageproxy/pull/46), + [orian](https://github.com/orian)) ## [0.4.0] (2015-05-21) + [0.4.0]: https://github.com/willnorris/imageproxy/compare/v0.3.0...v0.4.0 ### Added - - added support for animated gifs - ([#23](https://github.com/willnorris/imageproxy/issues/23)) + +- added support for animated gifs + ([#23](https://github.com/willnorris/imageproxy/issues/23)) ### Changed - - non-200 responses from remote servers are proxied as-is + +- non-200 responses from remote servers are proxied as-is ## [0.3.0] (2015-12-07) + [0.3.0]: https://github.com/willnorris/imageproxy/compare/v0.2.3...v0.3.0 ### Added - - added support for signing requests using a sha-256 HMAC. - ([a9efefc](https://github.com/willnorris/imageproxy/commit/a9efefc)) - - more complete logging of requests and whether response is from the cache - ([#17](https://github.com/willnorris/imageproxy/issues/17)) - - added support for a base URL for remote images. This allows shorter relative - URLs to be specified in requests. - ([#15](https://github.com/willnorris/imageproxy/issues/15)) + +- added support for signing requests using a sha-256 HMAC. + ([a9efefc](https://github.com/willnorris/imageproxy/commit/a9efefc)) +- more complete logging of requests and whether response is from the cache + ([#17](https://github.com/willnorris/imageproxy/issues/17)) +- added support for a base URL for remote images. This allows shorter relative + URLs to be specified in requests. + ([#15](https://github.com/willnorris/imageproxy/issues/15)) ### Fixed - - be more precise in copying over all headers from remote image response - ([1bf0515](https://github.com/willnorris/imageproxy/commit/1bf0515)) + +- be more precise in copying over all headers from remote image response + ([1bf0515](https://github.com/willnorris/imageproxy/commit/1bf0515)) ## [0.2.3] (2015-02-20) + [0.2.3]: https://github.com/willnorris/imageproxy/compare/v0.2.2...v0.2.3 ### Added - - added quality option - ([#13](https://github.com/willnorris/imageproxy/pull/13) - [cubabit](https://github.com/cubabit)) + +- added quality option + ([#13](https://github.com/willnorris/imageproxy/pull/13) + [cubabit](https://github.com/cubabit)) ## [0.2.2] (2014-12-08) + [0.2.2]: https://github.com/willnorris/imageproxy/compare/v0.2.1...v0.2.2 ### Added - - added `cacheSize` flag to command line + +- added `cacheSize` flag to command line ### Changed - - improved documentation and error messages - - negative width or height transformation values interpreted as 0 + +- improved documentation and error messages +- negative width or height transformation values interpreted as 0 ## [0.2.1] (2014-08-13) + [0.2.1]: https://github.com/willnorris/imageproxy/compare/v0.2.0...v0.2.1 ### Changed - - restructured package so that the command line tools is now installed from - `willnorris.com/go/imageproxy/cmd/imageproxy` + +- restructured package so that the command line tools is now installed from + `willnorris.com/go/imageproxy/cmd/imageproxy` ## [0.2.0] (2014-07-02) + [0.2.0]: https://github.com/willnorris/imageproxy/compare/v0.1.0...v0.2.0 ### Added - - transformed images are cached in addition to the original image - ([#1](https://github.com/willnorris/imageproxy/issues/1)) - - support etag and last-modified headers on incoming requests - ([#3](https://github.com/willnorris/imageproxy/issues/3)) - - support wildcards in list of allowed hosts + +- transformed images are cached in addition to the original image + ([#1](https://github.com/willnorris/imageproxy/issues/1)) +- support etag and last-modified headers on incoming requests + ([#3](https://github.com/willnorris/imageproxy/issues/3)) +- support wildcards in list of allowed hosts ### Changed - - options can be specified in any order - - images cannot be resized larger than their original dimensions + +- options can be specified in any order +- images cannot be resized larger than their original dimensions ## [0.1.0] (2013-12-26) + [0.1.0]: https://github.com/willnorris/imageproxy/compare/5d75e8a...v0.1.0 -Initial release. Supported transformation options include: - - width and height - - different crop modes - - rotation (in 90 degree increments) - - flip (horizontal or vertical) +Initial release. Supported transformation options include: + +- width and height +- different crop modes +- rotation (in 90 degree increments) +- flip (horizontal or vertical) Images can be cached in-memory or on-disk. diff --git a/docs/contributing.md b/docs/contributing.md index bc7f7c5..73e19f1 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -2,7 +2,7 @@ ## Types of contributions -Simple bug fixes for existing functionality are always welcome. In many cases, +Simple bug fixes for existing functionality are always welcome. In many cases, it may be helpful to include a reproducible sample case that demonstrates the bug being fixed. @@ -15,7 +15,7 @@ the [GitHub issue tracker](https://github.com/willnorris/imageproxy/issues). If reporting a bug, please try and provide as much context as possible such as what version of imageproxy you're running, what configuration options, specific remote URLs that exhibit issues, and anything else that might be relevant to -the bug. For feature requests, please explain what you're trying to do, and +the bug. For feature requests, please explain what you're trying to do, and how the requested feature would help you do that. Security related bugs can either be reported in the issue tracker, or if they @@ -24,6 +24,6 @@ are more sensitive, emailed to . ## Code Style and Tests Go code should follow general best practices, such as using go fmt, go lint, and -go vet (this is enforced by our continuous integration setup). Tests should +go vet (this is enforced by our continuous integration setup). Tests should always be included where possible, especially for bug fixes in order to prevent regressions. diff --git a/docs/plugin-design.md b/docs/plugin-design.md index 1d7260a..d416144 100644 --- a/docs/plugin-design.md +++ b/docs/plugin-design.md @@ -4,81 +4,62 @@ ## Objective -Rearchitect imageproxy to use a plugin-based system for most features like -transformations, security, and caching. This should reduce build times and -binary sizes in the common case, and provide a mechanism for users to easily -add custom features that would not be added to core for various reasons. +Re-architect imageproxy to use a plugin-based system for most features like transformations, security, and caching. +This should reduce build times and binary sizes in the common case, +and provide a mechanism for users to easily add custom features that would not be added to core for various reasons. ## Background -I created imageproxy to [scratch a personal itch](https://wjn.me/b/J_), I -needed a simple way to dynamically resize images for my personal website. I -published it as an open source projects because that's what I do, and I'm happy -to see others finding it useful for their needs as well. +I created imageproxy to [scratch a personal itch](https://wjn.me/b/J_), I needed a simple way to dynamically resize images for my personal website. +I published it as an open source projects because that's what I do, and I'm happy to see others finding it useful for their needs as well. -But inevitably, with more users came requests for additional features because -people have different use cases and requirements. Some of these requests were -relatively minor, and I was happy to add them. But one of the more common -requests was to support different caching backends. Personally, I still use the -on-disk cache, but many people wanted to use redis or a cloud provider like -AWS, Azure, or GCP. For a long time I was resistant to adding support for -these, mainly out of concern for inflating build times and binary sizes. I did -eventually relent, and -[#49](https://github.com/willnorris/imageproxy/issues/49) tracked adding -support for the most common backends. +But inevitably, with more users came requests for additional features because people have different use cases and requirements. +Some of these requests were relatively minor, and I was happy to add them. +But one of the more common requests was to support different caching backends. +Personally, I still use the on-disk cache, but many people wanted to use redis or a cloud provider like AWS, Azure, or GCP. +For a long time I was resistant to adding support for these, mainly out of concern for inflating build times and binary sizes. +I did eventually relent, and [#49] tracked adding support for the most common backends. -Unfortunately my concerns proved true, and build times are *significantly* -slower (TODO: add concrete numbers) now because of all the additional cloud -SDKs that get compiled in. I don't personally care too much about binary size, -since I'm not running in a constrained environment, but these build times are -really wearing on me. Additionally, there are a number of outstanding pull -requests for relatively obscure features that I don't really want to have to -support in the main project. And quite honestly, there are a number of obscure -features that did get merged in over the years that I kinda wish I could rip -back out. +Unfortunately my concerns proved true, and build times are _significantly_ slower (TODO: add concrete numbers) now because of all the additional cloud SDKs that get compiled in. +I don't personally care too much about binary size, since I'm not running in a constrained environment, but these build times are really wearing on me. +Additionally, there are a number of outstanding pull requests for relatively obscure features that I don't really want to have to support in the main project. +And quite honestly, there are a number of obscure features that did get merged in over the years that I kinda wish I could rip back out. + +[#49]: https://github.com/willnorris/imageproxy/issues/49 ### Plugin support in Go TODO: talk about options like - - RPC (https://github.com/hashicorp/go-plugin) - - pkg/plugin (https://golang.org/pkg/plugin/) - - embedded interpreter (https://github.com/robertkrimen/otto) - - custom binaries (https://github.com/mholt/caddy, - https://caddy.community/t/59) -Spoiler: I'm planning on following the Caddy approach and using custom -binaries. +- RPC () +- pkg/plugin () +- embedded interpreter () +- custom binaries (, ) + +Spoiler: I'm planning on following the Caddy approach and using custom binaries. ## Design -I plan to model imageproxy after Caddy, moving all key functionality into -separate plugins that register themselves with the server, and which all -compile to a single statically-linked binary. The core project will provide a -great number of plugins to cover all of the existing functionality. I also -expect I'll be much more open to adding plugins for features I may not care as -much about personally. Of course, users can also write their own plugins and -link them in without needing to contribute them to core if they don't want to. +I plan to model imageproxy after Caddy, moving all key functionality into separate plugins that register themselves with the server, +and which all compile to a single statically-linked binary. +The core project will provide a great number of plugins to cover all of the existing functionality. +I also expect I'll be much more open to adding plugins for features I may not care as much about personally. +Of course, users can also write their own plugins and link them in without needing to contribute them to core if they don't want to. I anticipate providing two or three build configurations in core: - - **full** - include all the plugins that are part of core (except where they - may conflict) - - **minimal** - some set of minimal features that only includes basic caching - options, limited transformation options, etc - - **my personal config** - I'll also definitely have a build that I use - personally on my site. I may decide to just make that the "minimal" build - and perhaps call it something different, rather than have a third - configuration. -Custom configurations beyond what is provided by core can be done by creating a -minimal main package that imports the plugins you care about and calling some -kind of bootstrap method (similar to [what Caddy now -does](https://caddy.community/t/59)). +- **full** - include all the plugins that are part of core (except where they may conflict) +- **minimal** - some set of minimal features that only includes basic caching options, limited transformation options, etc +- **my personal config** - I'll also definitely have a build that I use personally on my site. + I may decide to just make that the "minimal" build and perhaps call it something different, rather than have a third configuration. + +Custom configurations beyond what is provided by core can be done by creating a minimal main package that imports the plugins you care about +and calling some kind of bootstrap method (similar to [what Caddy now does](https://caddy.community/t/59)). ### Types of plugins -(Initially in no particular order, just capturing thoughts. Lots to do here in -thinking through the use cases and what kind of plugin API we really need to -provide.) +(Initially in no particular order, just capturing thoughts. +Lots to do here in thinking through the use cases and what kind of plugin API we really need to provide.) See also issues and PRs with [label:plugins][]. @@ -86,85 +67,75 @@ See also issues and PRs with [label:plugins][]. #### Caching backend -This is one of the most common feature requests, and is also one of the worst -offender for inflating build times and binary sizes because of the size of the -dependencies that are typically required. The minimal imageproxy build would -probably only include the in-memory and on-disk caches. Anything that talked to -an external store (redis, cloud providers, etc) would be pulled out. +This is one of the most common feature requests, and is also one of the worst offender for inflating build times +and binary sizes because of the size of the dependencies that are typically required. +The minimal imageproxy build would probably only include the in-memory and on-disk caches. +Anything that talked to an external store (redis, cloud providers, etc) would be pulled out. #### Transformation engine -Today, imageproxy only performs transformations which can be done with pure Go -libraries. There have been a number of requests (or at least questions) to use -something like [vips](https://github.com/DAddYE/vips) or -[imagemagick](https://github.com/gographics/imagick), which are both C -libraries. They provide more options, and (likely) better performance, at the -cost of complexity and loss of portability in using cgo. These would likely -replace the entire transformation engine in imageproxy, so I don't know how -they would interact with other plugins that merely extend the main engine (they -probably wouldn't be able to interact at all). +Today, imageproxy only performs transformations which can be done with pure Go libraries. +There have been a number of requests (or at least questions) to use something like [vips] or [imagemagick], which are both C libraries. +They provide more options, and (likely) better performance, at the cost of complexity and loss of portability in using cgo. +These would likely replace the entire transformation engine in imageproxy, +so I don't know how they would interact with other plugins that merely extend the main engine (they probably wouldn't be able to interact at all). + +[vips]: https://github.com/DAddYE/vips +[imagemagick]: https://github.com/gographics/imagick #### Transformation options -Today, imageproxy performs minimal transformations, mostly around resizing, -cropping, and rotation. It doesn't support any kind of filters, brightness or -contrast adjustment, etc. There are go libraries for them, they're just outside -the scope of what I originally intended imageproxy for. But I'd be happy to -have plugins that do that kind of thing. These plugins would need to be able to -hook into the option parsing engine so that they could register their URL -options. +Today, imageproxy performs minimal transformations, mostly around resizing, cropping, and rotation. +It doesn't support any kind of filters, brightness or contrast adjustment, etc. +There are go libraries for them, they're just outside the scope of what I originally intended imageproxy for. +But I'd be happy to have plugins that do that kind of thing. +These plugins would need to be able to hook into the option parsing engine so that they could register their URL options. #### Image format support -There have been a number of requests for imge format support that require cgo -libraries: +There have been a number of requests for image format support that require cgo libraries: - - **webp encoding** - needs cgo - [#114](https://github.com/willnorris/imageproxy/issues/114) - - **progressive jpegs** - probably needs cgo? - [#77](https://github.com/willnorris/imageproxy/issues/77) - - **gif to mp4** - maybe doable in pure go, but probably belongs in a plugin - [#136](https://github.com/willnorris/imageproxy/issues/136) - - **HEIF** - formate used by newer iPhones - ([HEIF](https://en.wikipedia.org/wiki/High_Efficiency_Image_File_Format)) +- **webp encoding** - needs cgo [#114](https://github.com/willnorris/imageproxy/issues/114) +- **progressive jpegs** - probably needs cgo? [#77](https://github.com/willnorris/imageproxy/issues/77) +- **gif to mp4** - maybe doable in pure go, but probably belongs in a plugin [#136](https://github.com/willnorris/imageproxy/issues/136) +- **HEIF** - formate used by newer iPhones ([HEIF](https://en.wikipedia.org/wiki/High_Efficiency_Image_File_Format)) #### Option parsing -Today, options are specified as the first component in the URL path, but -[#66](https://github.com/willnorris/imageproxy/pull/66) proposes optionally -moving that to a query parameter (for a good reason, actually). Maybe putting -that in core is okay? Maybe it belongs in a plugin, in which case we'd need to -expose an API for replacing the option parsing code entirely. +Today, options are specified as the first component in the URL path, but [#66] proposes optionally moving that to a query parameter (for a good reason, actually). +Maybe putting that in core is okay? +Maybe it belongs in a plugin, in which case we'd need to expose an API for replacing the option parsing code entirely. + +[#66]: https://github.com/willnorris/imageproxy/pull/66 #### Security options -Some people want to add a host blacklist -[#85](https://github.com/willnorris/imageproxy/pull/85), refusal to process -non-image files [#53](https://github.com/willnorris/imageproxy/issues/53) -[#119](https://github.com/willnorris/imageproxy/pull/119). I don't think there -is an issue for it, but an early fork of the project added request signing that -was compatible with nginx's [secure link -module](https://nginx.org/en/docs/http/ngx_http_secure_link_module.html). +Some people want to add a host blacklist [#85], refusal to process non-image files [#53] [#119]. +I don't think there is an issue for it, +but an early fork of the project added request signing that was compatible with nginx's [secure link module](https://nginx.org/en/docs/http/ngx_http_secure_link_module.html). + +[#85]: https://github.com/willnorris/imageproxy/pull/85 +[#53]: https://github.com/willnorris/imageproxy/issues/53 +[#119]: https://github.com/willnorris/imageproxy/pull/119 ### Registering Plugins -Plugins are loaded simply by importing their package. They should have an -`init` func that calls `imageproxy.RegisterPlugin`: +Plugins are loaded simply by importing their package. +They should have an `init` func that calls `imageproxy.RegisterPlugin`: -``` go +```go type Plugin struct { } func RegisterPlugin(name string, plugin Plugin) ``` -Plugins hook into various extension points of imageproxy by implementing -appropriate interfaces. A single plugin can hook into multiple parts of -imageproxy by implementing multiple interfaces. +Plugins hook into various extension points of imageproxy by implementing appropriate interfaces. +A single plugin can hook into multiple parts of imageproxy by implementing multiple interfaces. For example, two possible interfaces for security related plugins: -``` go +```go // A RequestAuthorizer determines if a request is authorized to be processed. // Requests are processed before the remote resource is retrieved. type RequestAuthorizer interface { @@ -186,7 +157,7 @@ type ResponseAuthorizer interface { A hypothetical interface for plugins that transform images: -``` go +```go // An ImageTransformer transforms an image. type ImageTransformer interface { // TransformImage based on the provided options and return the result. @@ -194,6 +165,5 @@ type ImageTransformer interface { } ``` -Plugins are additionally responsible for registering any additional command -line flags they wish to expose to the user, as well as storing any global state -that would previously have been stored on the Proxy struct. +Plugins are additionally responsible for registering any additional command line flags they wish to expose to the user, +as well as storing any global state that would previously have been stored on the Proxy struct. diff --git a/docs/url-signing.md b/docs/url-signing.md index 14807a5..7099f62 100644 --- a/docs/url-signing.md +++ b/docs/url-signing.md @@ -1,7 +1,7 @@ # How to generate signed requests Signing requests allows an imageproxy instance to proxy images from arbitrary -remote hosts, but without opening the service up for potential abuse. When +remote hosts, but without opening the service up for potential abuse. When appropriately configured, the imageproxy instance will only serve requests that are for allowed hosts, or which have a valid signature. @@ -9,7 +9,7 @@ Signatures can be calculated in two ways: 1. they can be calculated solely on the remote image URL, in which case any transformations of the image can be requested without changes to the - signature value. This used to be the only way to sign requests, but is no + signature value. This used to be the only way to sign requests, but is no longer recommended since it still leaves the imageproxy instance open to potential abuse. @@ -17,30 +17,30 @@ Signatures can be calculated in two ways: the requested transformation options. In both cases, the signature is calculated using HMAC-SHA256 and a secret key -which is provided to imageproxy on startup. The message to be signed is the +which is provided to imageproxy on startup. The message to be signed is the remote URL, with the transformation options optionally set as the URL fragment, -[as documented below](#Signing-options). The signature is url-safe base64 +[as documented below](#signing-options). The signature is url-safe base64 encoded, and [provided as an option][s-option] in the imageproxy request. imageproxy will accept signatures for URLs with or without options -transparently. It's up to the publisher of the signed URLs to decide which +transparently. It's up to the publisher of the signed URLs to decide which method they use to generate the URL. -[s-option]: https://godoc.org/willnorris.com/go/imageproxy#hdr-Signature +[s-option]: https://pkg.go.dev/willnorris.com/go/imageproxy#hdr-Signature-ParseOptions ## Signing options Transformation options for a proxied URL are [specified as a comma separated string][ParseOptions] of individual options, which can be supplied in any -order. When calculating a signature, options should be put in their canonical +order. When calculating a signature, options should be put in their canonical form, sorted in lexigraphical order (omitting the signature option itself), and appended to the remote URL as the URL fragment. Currently, only [size option][] has a canonical form, which is -`{width}x{height}` with the number `0` used when no value is specified. For +`{width}x{height}` with the number `0` used when no value is specified. For example, a request that does not request any size option would still have a canonical size value of `0x0`, indicating that no size transformation is being -performed. If only a height of 500px is requested, the canonical form would be +performed. If only a height of 500px is requested, the canonical form would be `0x500`. For example, requesting the remote URL of `http://example.com/image.jpg`, @@ -57,9 +57,8 @@ the signed value would be: The `100` size option was put in its canonical form of `100x100`, and the options are sorted, moving `q75` before `r90`. -[ParseOptions]: https://godoc.org/willnorris.com/go/imageproxy#ParseOptions -[size option]: https://godoc.org/willnorris.com/go/imageproxy#hdr-Size_and_Cropping - +[ParseOptions]: https://pkg.go.dev/willnorris.com/go/imageproxy#ParseOptions +[size option]: https://pkg.go.dev/willnorris.com/go/imageproxy#hdr-Size_and_Cropping-ParseOptions ## Signed options example @@ -76,12 +75,10 @@ and our resulting signed key is `0sR2kjyfiF1RQRj4Jm2fFa3_6SDFqdAaDEmy1oD2U-4=` The final url would be `http://localhost:8080/400x400,q40,s0sR2kjyfiF1RQRj4Jm2fFa3_6SDFqdAaDEmy1oD2U-4=/https://octodex.github.com/images/codercat.jpg` - - ## Language Examples -Here are examples of calculating signatures in a variety of languages. These -demonstrate the HMAC-SHA256 bits, but not the option canonicalization. In each +Here are examples of calculating signatures in a variety of languages. These +demonstrate the HMAC-SHA256 bits, but not the option canonicalization. In each example, the remote URL `https://octodex.github.com/images/codercat.jpg` is signed using a signature key of `secretkey`. @@ -90,6 +87,7 @@ See also the [imageproxy-sign tool](/cmd/imageproxy-sign). ### Go main.go: + ```go package main @@ -180,38 +178,40 @@ url = sys.argv[2] print base64.urlsafe_b64encode(hmac.new(key, msg=url, digestmod=hashlib.sha256).digest()) ``` -````shell +```shell $ python sign.py "secretkey" "https://octodex.github.com/images/codercat.jpg" cw34eyalj8YvpLpETxSIxv2k8QkLel2UAR5Cku2FzGM= -```` +``` ### JavaScript ```javascript -const crypto = require('crypto'); -const URLSafeBase64 = require('urlsafe-base64'); +const crypto = require("crypto"); +const URLSafeBase64 = require("urlsafe-base64"); let key = process.argv[2]; let url = process.argv[3]; -console.log(URLSafeBase64.encode(crypto.createHmac('sha256', key).update(url).digest())); +console.log( + URLSafeBase64.encode(crypto.createHmac("sha256", key).update(url).digest()), +); ``` -````shell +```shell $ node sign.js "secretkey" "https://octodex.github.com/images/codercat.jpg" cw34eyalj8YvpLpETxSIxv2k8QkLel2UAR5Cku2FzGM= -```` +``` ### PHP -````php +```php 0 && !hostMatches(p.AllowHosts, newreq.URL)) { + if len(via) > maxRedirects { + if p.Verbose { + p.logf("followed too many redirects (%d).", len(via)) + } + return errTooManyRedirects + } + if hostMatches(p.DenyHosts, newreq.URL) { http.Error(w, msgNotAllowedInRedirect, http.StatusForbidden) return errNotAllowed } @@ -195,7 +291,6 @@ func (p *Proxy) serveImage(w http.ResponseWriter, r *http.Request) { } } resp, err := p.Client.Do(actualReq) - if err != nil { msg := fmt.Sprintf("error fetching remote image: %v", err) p.log(msg) @@ -206,6 +301,12 @@ func (p *Proxy) serveImage(w http.ResponseWriter, r *http.Request) { // close the original resp.Body, even if we wrap it in a NopCloser below defer resp.Body.Close() + // return early on 404s. Perhaps handle additional status codes here? + if resp.StatusCode == http.StatusNotFound { + http.Error(w, "not found", http.StatusNotFound) + return + } + cached := resp.Header.Get(httpcache.XFromCache) == "1" if p.Verbose { p.logf("request: %+v (served from cache: %t)", *actualReq, cached) @@ -215,7 +316,12 @@ func (p *Proxy) serveImage(w http.ResponseWriter, r *http.Request) { metricServedFromCache.Inc() } - copyHeader(w.Header(), resp.Header, "Cache-Control", "Last-Modified", "Expires", "Etag", "Link") + if p.PassResponseHeaders == nil { + // pass default set of response headers + copyHeader(w.Header(), resp.Header, "Cache-Control", "Last-Modified", "Expires", "Etag", "Link") + } else { + copyHeader(w.Header(), resp.Header, p.PassResponseHeaders...) + } if should304(r, resp) { w.WriteHeader(http.StatusNotModified) @@ -226,7 +332,7 @@ func (p *Proxy) serveImage(w http.ResponseWriter, r *http.Request) { if contentType == "" || contentType == "application/octet-stream" || contentType == "binary/octet-stream" { // try to detect content type b := bufio.NewReader(resp.Body) - resp.Body = ioutil.NopCloser(b) + resp.Body = io.NopCloser(b) contentType = peekContentType(b) } if resp.ContentLength != 0 && !contentTypeMatches(p.ContentTypes, contentType) { @@ -260,23 +366,17 @@ func (p *Proxy) serveImage(w http.ResponseWriter, r *http.Request) { // the content type. Returns empty string if error occurs. func peekContentType(p *bufio.Reader) string { byt, err := p.Peek(512) - if err != nil && err != bufio.ErrBufferFull && err != io.EOF { + if err != nil && !errors.Is(err, bufio.ErrBufferFull) && !errors.Is(err, io.EOF) { return "" } return http.DetectContentType(byt) } -// copyHeader copies header values from src to dst, adding to any existing -// values with the same header name. If keys is not empty, only those header -// keys will be copied. -func copyHeader(dst, src http.Header, keys ...string) { - if len(keys) == 0 { - for k := range src { - keys = append(keys, k) - } - } - for _, key := range keys { - k := http.CanonicalHeaderKey(key) +// copyHeader copies values for specified headers from src to dst, adding to +// any existing values with the same header name. +func copyHeader(dst, src http.Header, headerNames ...string) { + for _, name := range headerNames { + k := http.CanonicalHeaderKey(name) for _, v := range src[k] { dst.Add(k, v) } @@ -284,18 +384,33 @@ func copyHeader(dst, src http.Header, keys ...string) { } var ( - errReferrer = errors.New("request does not contain an allowed referrer") - errDeniedHost = errors.New("request contains a denied host") - errNotAllowed = errors.New("request does not contain an allowed host or valid signature") + errReferrer = errors.New("request does not contain an allowed referrer") + errDeniedHost = errors.New("request contains a denied host") + errNotAllowed = errors.New("request does not contain an allowed host or valid signature") + errTooManyRedirects = errors.New("too many redirects") + errNotValid = errors.New("request is no longer valid") msgNotAllowed = "requested URL is not allowed" msgNotAllowedInRedirect = "requested URL in redirect is not allowed" ) +func (p *Proxy) now() time.Time { + if !p.timeNow.IsZero() { + return p.timeNow + } + return time.Now() +} + // allowed determines whether the specified request contains an allowed // referrer, host, and signature. It returns an error if the request is not -// allowed. +// allowed or not valid any longer. func (p *Proxy) allowed(r *Request) error { + if !r.Options.ValidUntil.IsZero() { + if !p.now().Before(r.Options.ValidUntil) { + return errNotValid + } + } + if len(p.Referrers) > 0 && !referrerMatches(p.Referrers, r.Original) { return errReferrer } @@ -429,7 +544,7 @@ func should304(req *http.Request, resp *http.Response) bool { return false } -func (p *Proxy) log(v ...interface{}) { +func (p *Proxy) log(v ...any) { if p.Logger != nil { p.Logger.Print(v...) } else { @@ -437,7 +552,7 @@ func (p *Proxy) log(v ...interface{}) { } } -func (p *Proxy) logf(format string, v ...interface{}) { +func (p *Proxy) logf(format string, v ...any) { if p.Logger != nil { p.Logger.Printf(format, v...) } else { @@ -458,7 +573,12 @@ type TransformingTransport struct { // responses are properly cached. CachingClient *http.Client - log func(format string, v ...interface{}) + // limiter limits the number of concurrent transformations being processed. + limiter chan struct{} + + log func(format string, v ...any) + + updateCacheHeaders func(hdr http.Header) } // RoundTrip implements the http.RoundTripper interface. @@ -468,7 +588,11 @@ func (t *TransformingTransport) RoundTrip(req *http.Request) (*http.Response, er if t.log != nil { t.log("fetching remote URL: %v", req.URL) } - return t.Transport.RoundTrip(req) + resp, err := t.Transport.RoundTrip(req) + if err == nil && t.updateCacheHeaders != nil { + t.updateCacheHeaders(resp.Header) + } + return resp, err } f := req.URL.Fragment @@ -483,10 +607,26 @@ func (t *TransformingTransport) RoundTrip(req *http.Request) (*http.Response, er if should304(req, resp) { // bare 304 response, full response will be used from cache - return &http.Response{StatusCode: http.StatusNotModified}, nil + return &http.Response{ + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Status: fmt.Sprintf("%d %s", http.StatusNotModified, http.StatusText(http.StatusNotModified)), + StatusCode: http.StatusNotModified, + Body: http.NoBody, + }, nil } - b, err := ioutil.ReadAll(resp.Body) + // enforce limiter after we've checked if we can early return a 304 response, + // but before we read the response body and perform transformations. + if t.limiter != nil { + t.limiter <- struct{}{} + defer func() { + <-t.limiter + }() + } + + b, err := io.ReadAll(resp.Body) if err != nil { return nil, err } diff --git a/imageproxy_test.go b/imageproxy_test.go index c2d4b82..150d7c4 100644 --- a/imageproxy_test.go +++ b/imageproxy_test.go @@ -12,13 +12,21 @@ import ( "image" "image/png" "log" + "maps" "net/http" "net/http/httptest" "net/url" "os" "reflect" + "regexp" + "strconv" "strings" "testing" + "time" + + "github.com/die-net/lrucache" + "github.com/google/uuid" + "github.com/gregjones/httpcache" ) func TestPeekContentType(t *testing.T) { @@ -62,30 +70,12 @@ func TestCopyHeader(t *testing.T) { }, // copy headers - { - dst: http.Header{}, - src: http.Header{"A": []string{"a"}}, - keys: nil, - want: http.Header{"A": []string{"a"}}, - }, - { - dst: http.Header{"A": []string{"a"}}, - src: http.Header{"B": []string{"b"}}, - keys: nil, - want: http.Header{"A": []string{"a"}, "B": []string{"b"}}, - }, { dst: http.Header{"A": []string{"a"}}, src: http.Header{"B": []string{"b"}, "C": []string{"c"}}, keys: []string{"B"}, want: http.Header{"A": []string{"a"}, "B": []string{"b"}}, }, - { - dst: http.Header{"A": []string{"a1"}}, - src: http.Header{"A": []string{"a2"}}, - keys: nil, - want: http.Header{"A": []string{"a1", "a2"}}, - }, } for _, tt := range tests { @@ -103,7 +93,7 @@ func TestCopyHeader(t *testing.T) { } func TestAllowed(t *testing.T) { - allowHosts := []string{"good"} + good := []string{"good"} key := [][]byte{ []byte("c0ffee"), } @@ -120,8 +110,11 @@ func TestAllowed(t *testing.T) { return req } + now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + tests := []struct { url string + now time.Time options Options allowHosts []string denyHosts []string @@ -131,38 +124,43 @@ func TestAllowed(t *testing.T) { allowed bool }{ // no allowHosts or signature key - {"http://test/image", emptyOptions, nil, nil, nil, nil, nil, true}, + {url: "http://test/image", allowed: true}, // allowHosts - {"http://good/image", emptyOptions, allowHosts, nil, nil, nil, nil, true}, - {"http://bad/image", emptyOptions, allowHosts, nil, nil, nil, nil, false}, + {url: "http://good/image", allowHosts: good, allowed: true}, + {url: "http://bad/image", allowHosts: good, allowed: false}, // referrer - {"http://test/image", emptyOptions, nil, nil, allowHosts, nil, genRequest(map[string]string{"Referer": "http://good/foo"}), true}, - {"http://test/image", emptyOptions, nil, nil, allowHosts, nil, genRequest(map[string]string{"Referer": "http://bad/foo"}), false}, - {"http://test/image", emptyOptions, nil, nil, allowHosts, nil, genRequest(map[string]string{"Referer": "MALFORMED!!"}), false}, - {"http://test/image", emptyOptions, nil, nil, allowHosts, nil, genRequest(map[string]string{}), false}, + {url: "http://test/image", referrers: good, request: genRequest(map[string]string{"Referer": "http://good/foo"}), allowed: true}, + {url: "http://test/image", referrers: good, request: genRequest(map[string]string{"Referer": "http://bad/foo"}), allowed: false}, + {url: "http://test/image", referrers: good, request: genRequest(map[string]string{"Referer": "MALFORMED!!"}), allowed: false}, + {url: "http://test/image", referrers: good, request: genRequest(map[string]string{}), allowed: false}, // signature key - {"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, nil, nil, nil, key, nil, true}, - {"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, nil, nil, nil, multipleKey, nil, true}, // signed with key "c0ffee" - {"http://test/image", Options{Signature: "FWIawYV4SEyI4zKJMeGugM-eJM1eI_jXPEQ20ZgRe4A="}, nil, nil, nil, multipleKey, nil, true}, // signed with key "beer" - {"http://test/image", Options{Signature: "deadbeef"}, nil, nil, nil, key, nil, false}, - {"http://test/image", Options{Signature: "deadbeef"}, nil, nil, nil, multipleKey, nil, false}, - {"http://test/image", emptyOptions, nil, nil, nil, key, nil, false}, + {url: "http://test/image", options: Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, keys: key, allowed: true}, + {url: "http://test/image", options: Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, keys: multipleKey, allowed: true}, // signed with key "c0ffee" + {url: "http://test/image", options: Options{Signature: "FWIawYV4SEyI4zKJMeGugM-eJM1eI_jXPEQ20ZgRe4A="}, keys: multipleKey, allowed: true}, // signed with key "beer" + {url: "http://test/image", options: Options{Signature: "deadbeef"}, keys: key, allowed: false}, + {url: "http://test/image", options: Options{Signature: "deadbeef"}, keys: multipleKey, allowed: false}, + {url: "http://test/image", keys: key, allowed: false}, // allowHosts and signature - {"http://good/image", emptyOptions, allowHosts, nil, nil, key, nil, true}, - {"http://bad/image", Options{Signature: "gWivrPhXBbsYEwpmWAKjbJEiAEgZwbXbltg95O2tgNI="}, nil, nil, nil, key, nil, true}, - {"http://bad/image", emptyOptions, allowHosts, nil, nil, key, nil, false}, + {url: "http://good/image", allowHosts: good, keys: key, allowed: true}, + {url: "http://bad/image", options: Options{Signature: "gWivrPhXBbsYEwpmWAKjbJEiAEgZwbXbltg95O2tgNI="}, keys: key, allowed: true}, + {url: "http://bad/image", allowHosts: good, keys: key, allowed: false}, // deny requests that match denyHosts, even if signature is valid or also matches allowHosts - {"http://test/image", emptyOptions, nil, []string{"test"}, nil, nil, nil, false}, - {"http://test:3000/image", emptyOptions, nil, []string{"test"}, nil, nil, nil, false}, - {"http://test/image", emptyOptions, []string{"test"}, []string{"test"}, nil, nil, nil, false}, - {"http://test/image", Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, nil, []string{"test"}, nil, key, nil, false}, - {"http://127.0.0.1/image", emptyOptions, nil, []string{"127.0.0.0/8"}, nil, nil, nil, false}, - {"http://127.0.0.1:3000/image", emptyOptions, nil, []string{"127.0.0.0/8"}, nil, nil, nil, false}, + {url: "http://test/image", denyHosts: []string{"test"}, allowed: false}, + {url: "http://test:3000/image", denyHosts: []string{"test"}, allowed: false}, + {url: "http://test/image", allowHosts: []string{"test"}, denyHosts: []string{"test"}, allowed: false}, + {url: "http://test/image", options: Options{Signature: "NDx5zZHx7QfE8E-ijowRreq6CJJBZjwiRfOVk_mkfQQ="}, denyHosts: []string{"test"}, keys: key, allowed: false}, + {url: "http://127.0.0.1/image", denyHosts: []string{"127.0.0.0/8"}, allowed: false}, + {url: "http://127.0.0.1:3000/image", denyHosts: []string{"127.0.0.0/8"}, allowed: false}, + + // valid until options + {url: "http://test/image", now: now, options: Options{ValidUntil: now.Add(time.Second)}, allowed: true}, + {url: "http://test/image", now: now, options: Options{ValidUntil: now.Add(-time.Second)}, allowed: false}, + {url: "http://test/image", now: now, options: Options{ValidUntil: now}, allowed: false}, } for _, tt := range tests { @@ -171,6 +169,7 @@ func TestAllowed(t *testing.T) { p.DenyHosts = tt.denyHosts p.SignatureKeys = tt.keys p.Referrers = tt.referrers + p.timeNow = tt.now u, err := url.Parse(tt.url) if err != nil { @@ -346,9 +345,11 @@ func TestShould304(t *testing.T) { // testTransport is an http.RoundTripper that returns certained canned // responses for particular requests. -type testTransport struct{} +type testTransport struct { + replyNotModified bool +} -func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) { +func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) { var raw string switch req.URL.Path { @@ -366,18 +367,194 @@ func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) { _ = png.Encode(img, m) raw = fmt.Sprintf("HTTP/1.1 200 OK\nContent-Length: %d\nContent-Type: image/png\n\n%s", len(img.Bytes()), img.Bytes()) + case "/redirect-to-notmodified": + parts := []string{ + "HTTP/1.1 303\nLocation: http://notmodified.test/notmodified?X-Security-Token=", + uuid.NewString(), + "#_=_\nCache-Control: no-store\n\n", + } + raw = strings.Join(parts, "") + case "/notmodified": + if t.replyNotModified { + raw = "HTTP/1.1 304 Not modified\nEtag: \"abcdef\"\n\n" + } else { + raw = "HTTP/1.1 200 OK\nEtag: \"abcdef\"\n\nOriginal response\n" + } default: - raw = "HTTP/1.1 404 Not Found\n\n" + redirectRegexp := regexp.MustCompile(`/redirects-(\d+)`) + if redirectRegexp.MatchString(req.URL.Path) { + redirectsLeft, _ := strconv.ParseUint(redirectRegexp.FindStringSubmatch(req.URL.Path)[1], 10, 8) + if redirectsLeft == 0 { + raw = "HTTP/1.1 200 OK\n\n" + } else { + raw = fmt.Sprintf("HTTP/1.1 302\nLocation: /http://redirect.test/redirects-%d\n\n", redirectsLeft-1) + } + } else { + raw = "HTTP/1.1 404 Not Found\n\n" + } } buf := bufio.NewReader(bytes.NewBufferString(raw)) return http.ReadResponse(buf, req) } +func TestProxy_UpdateCacheHeaders(t *testing.T) { + date := "Mon, 02 Jan 2006 15:04:05 MST" + exp := "Mon, 02 Jan 2006 16:04:05 MST" + + tests := []struct { + name string + minDuration time.Duration + forceCache bool + headers http.Header + want http.Header + }{ + { + name: "zero", + headers: http.Header{}, + want: http.Header{}, + }, + { + name: "no min duration", + headers: http.Header{ + "Date": {date}, + "Expires": {exp}, + "Cache-Control": {"max-age=600"}, + }, + want: http.Header{ + "Date": {date}, + "Expires": {exp}, + "Cache-Control": {"max-age=600"}, + }, + }, + { + name: "min duration, no header", + minDuration: 30 * time.Second, + headers: http.Header{}, + want: http.Header{ + "Cache-Control": {"max-age=30"}, + }, + }, + { + name: "cache control exceeds min duration", + minDuration: 30 * time.Second, + headers: http.Header{ + "Cache-Control": {"max-age=600"}, + }, + want: http.Header{ + "Cache-Control": {"max-age=600"}, + }, + }, + { + name: "cache control exceeds min duration, expires", + minDuration: 30 * time.Second, + headers: http.Header{ + "Date": {date}, + "Expires": {exp}, + "Cache-Control": {"max-age=86400"}, + }, + want: http.Header{ + "Date": {date}, + "Cache-Control": {"max-age=86400"}, + }, + }, + { + name: "min duration exceeds cache control", + minDuration: 1 * time.Hour, + headers: http.Header{ + "Cache-Control": {"max-age=600"}, + }, + want: http.Header{ + "Cache-Control": {"max-age=3600"}, + }, + }, + { + name: "min duration exceeds cache control, expires", + minDuration: 2 * time.Hour, + headers: http.Header{ + "Date": {date}, + "Expires": {exp}, + "Cache-Control": {"max-age=600"}, + }, + want: http.Header{ + "Date": {date}, + "Cache-Control": {"max-age=7200"}, + }, + }, + { + name: "expires exceeds min duration, cache control", + minDuration: 30 * time.Minute, + headers: http.Header{ + "Date": {date}, + "Expires": {exp}, + "Cache-Control": {"max-age=600"}, + }, + want: http.Header{ + "Date": {date}, + "Cache-Control": {"max-age=3600"}, + }, + }, + { + name: "respect no-store", + headers: http.Header{ + "Cache-Control": {"max-age=600, no-store"}, + }, + want: http.Header{ + "Cache-Control": {"max-age=600, no-store"}, + }, + }, + { + name: "respect private", + headers: http.Header{ + "Cache-Control": {"max-age=600, private"}, + }, + want: http.Header{ + "Cache-Control": {"max-age=600, no-store, private"}, + }, + }, + { + name: "force cache, normalize directives", + forceCache: true, + headers: http.Header{ + "Cache-Control": {"MAX-AGE=600, no-store, private"}, + }, + want: http.Header{ + "Cache-Control": {"max-age=600"}, + }, + }, + { + name: "force cache with min duration", + minDuration: 1 * time.Hour, + forceCache: true, + headers: http.Header{ + "Cache-Control": {"max-age=600, private, no-store"}, + }, + want: http.Header{ + "Cache-Control": {"max-age=3600"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Proxy{ + MinimumCacheDuration: tt.minDuration, + ForceCache: tt.forceCache, + } + hdr := maps.Clone(tt.headers) + p.updateCacheHeaders(hdr) + + if !reflect.DeepEqual(hdr, tt.want) { + t.Errorf("updateCacheHeaders(%v) returned %v, want %v", tt.headers, hdr, tt.want) + } + }) + } +} + func TestProxy_ServeHTTP(t *testing.T) { p := &Proxy{ Client: &http.Client{ - Transport: testTransport{}, + Transport: &testTransport{}, }, AllowHosts: []string{"good.test"}, ContentTypes: []string{"image/*"}, @@ -415,7 +592,7 @@ func TestProxy_ServeHTTP(t *testing.T) { func TestProxy_ServeHTTP_is304(t *testing.T) { p := &Proxy{ Client: &http.Client{ - Transport: testTransport{}, + Transport: &testTransport{}, }, } @@ -432,6 +609,81 @@ func TestProxy_ServeHTTP_is304(t *testing.T) { } } +func TestProxy_ServeHTTP_cached304(t *testing.T) { + cache := lrucache.New(1024*1024*8, 0) + client := new(http.Client) + tt := testTransport{} + client.Transport = &httpcache.Transport{ + Transport: &TransformingTransport{ + Transport: &tt, + CachingClient: client, + }, + Cache: cache, + MarkCachedResponses: true, + } + + p := &Proxy{ + Client: client, + FollowRedirects: true, + } + + // prime the cache + req := httptest.NewRequest("GET", "http://localhost//http://good.test/redirect-to-notmodified", nil) + recorder := httptest.NewRecorder() + p.ServeHTTP(recorder, req) + + resp := recorder.Result() + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("ServeHTTP(%v) returned status %d, want %d", req, got, want) + } + if _, found := cache.Get("http://good.test/redirect-to-notmodified#0x0"); !found { + t.Errorf("Response to http://good.test/redirect-to-notmodified#0x0 should be cached") + } + + // now make the same request again, but this time make sure the server responds with a 304 + tt.replyNotModified = true + req = httptest.NewRequest("GET", "http://localhost//http://good.test/redirect-to-notmodified", nil) + recorder = httptest.NewRecorder() + p.ServeHTTP(recorder, req) + + resp = recorder.Result() + if got, want := resp.StatusCode, http.StatusOK; got != want { + t.Errorf("ServeHTTP(%v) returned status %d, want %d", req, got, want) + } + + if recorder.Body.String() != "Original response\n" { + t.Errorf("Response isn't what we expected: %v", recorder.Body.String()) + } +} + +func TestProxy_ServeHTTP_maxRedirects(t *testing.T) { + p := &Proxy{ + Client: &http.Client{ + Transport: &testTransport{}, + }, + FollowRedirects: true, + } + + tests := []struct { + url string + code int + }{ + {"/http://redirect.test/redirects-0", http.StatusOK}, + {"/http://redirect.test/redirects-2", http.StatusOK}, + {"/http://redirect.test/redirects-11", http.StatusInternalServerError}, // too many redirects + } + + for _, tt := range tests { + req, _ := http.NewRequest("GET", "http://localhost"+tt.url, nil) + resp := httptest.NewRecorder() + p.ServeHTTP(resp, req) + + if got, want := resp.Code, tt.code; got != want { + t.Errorf("ServeHTTP(%v) returned status %d, want %d", req, got, want) + } + } +} + func TestProxy_log(t *testing.T) { var b strings.Builder @@ -481,8 +733,9 @@ func TestProxy_log_default(t *testing.T) { func TestTransformingTransport(t *testing.T) { client := new(http.Client) tr := &TransformingTransport{ - Transport: testTransport{}, + Transport: &testTransport{}, CachingClient: client, + limiter: make(chan struct{}, 1), } client.Transport = tr diff --git a/internal/gcscache/gcscache.go b/internal/gcscache/gcscache.go index 6eda139..0de848b 100644 --- a/internal/gcscache/gcscache.go +++ b/internal/gcscache/gcscache.go @@ -9,8 +9,8 @@ import ( "context" "crypto/md5" "encoding/hex" + "errors" "io" - "io/ioutil" "log" "path" @@ -27,14 +27,14 @@ type cache struct { func (c *cache) Get(key string) ([]byte, bool) { r, err := c.object(key).NewReader(ctx) if err != nil { - if err != storage.ErrObjectNotExist { + if !errors.Is(err, storage.ErrObjectNotExist) { log.Printf("error reading from gcs: %v", err) } return nil, false } defer r.Close() - value, err := ioutil.ReadAll(r) + value, err := io.ReadAll(r) if err != nil { log.Printf("error reading from gcs: %v", err) return nil, false diff --git a/internal/s3cache/s3cache.go b/internal/s3cache/s3cache.go index abfd28a..4ec77f4 100644 --- a/internal/s3cache/s3cache.go +++ b/internal/s3cache/s3cache.go @@ -9,8 +9,8 @@ import ( "bytes" "crypto/md5" "encoding/hex" + "errors" "io" - "io/ioutil" "log" "net/url" "path" @@ -36,13 +36,14 @@ func (c *cache) Get(key string) ([]byte, bool) { resp, err := c.GetObject(input) if err != nil { - if aerr, ok := err.(awserr.Error); ok && aerr.Code() != "NoSuchKey" { + var aerr awserr.Error + if errors.As(err, &aerr) && aerr.Code() != "NoSuchKey" { log.Printf("error fetching from s3: %v", aerr) } return nil, false } - value, err := ioutil.ReadAll(resp.Body) + value, err := io.ReadAll(resp.Body) if err != nil { log.Printf("error reading s3 response body: %v", err) return nil, false diff --git a/metrics.go b/metrics.go index 0466ac0..1351208 100644 --- a/metrics.go +++ b/metrics.go @@ -29,6 +29,11 @@ var ( Name: "request_duration_seconds", Help: "Request response times", }) + metricRequestsInFlight = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "http", + Name: "requests_in_flight", + Help: "Number of requests in flight", + }) ) func init() { @@ -36,4 +41,5 @@ func init() { prometheus.MustRegister(metricServedFromCache) prometheus.MustRegister(metricRemoteErrors) prometheus.MustRegister(metricRequestDuration) + prometheus.MustRegister(metricRequestsInFlight) } diff --git a/third_party/httpcache/LICENSE b/third_party/httpcache/LICENSE new file mode 100644 index 0000000..81316be --- /dev/null +++ b/third_party/httpcache/LICENSE @@ -0,0 +1,7 @@ +Copyright © 2012 Greg Jones (greg.jones@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/third_party/httpcache/README.md b/third_party/httpcache/README.md new file mode 100644 index 0000000..5f00a18 --- /dev/null +++ b/third_party/httpcache/README.md @@ -0,0 +1,2 @@ +httpcache is a copy of +with only the cache control header parsing logic. diff --git a/third_party/httpcache/httpcache.go b/third_party/httpcache/httpcache.go new file mode 100644 index 0000000..34c0a99 --- /dev/null +++ b/third_party/httpcache/httpcache.go @@ -0,0 +1,40 @@ +package httpcache + +import ( + "net/http" + "sort" + "strings" +) + +type CacheControl map[string]string + +func ParseCacheControl(headers http.Header) CacheControl { + cc := CacheControl{} + ccHeader := headers.Get("Cache-Control") + for _, part := range strings.Split(ccHeader, ",") { + part = strings.Trim(part, " ") + if part == "" { + continue + } + if strings.ContainsRune(part, '=') { + keyval := strings.Split(part, "=") + cc[strings.ToLower(strings.Trim(keyval[0], " "))] = strings.Trim(keyval[1], ",") + } else { + cc[strings.ToLower(part)] = "" + } + } + return cc +} + +func (cc CacheControl) String() string { + parts := make([]string, 0, len(cc)) + for k, v := range cc { + if v == "" { + parts = append(parts, k) + } else { + parts = append(parts, k+"="+v) + } + } + sort.StringSlice(parts).Sort() + return strings.Join(parts, ", ") +} diff --git a/transform.go b/transform.go index 103e20f..a5e87d4 100644 --- a/transform.go +++ b/transform.go @@ -5,6 +5,7 @@ package imageproxy import ( "bytes" + "errors" "fmt" "image" _ "image/gif" // register gif format @@ -43,6 +44,19 @@ func Transform(img []byte, opt Options) ([]byte, error) { return img, nil } + // decode image metadata + cfg, _, err := image.DecodeConfig(bytes.NewReader(img)) + if err != nil { + return nil, err + } + + // prevent pixel flooding attacks + // accept no larger than a 100 megapixel image. + const maxPixels = 100_000_000 + if cfg.Width*cfg.Height > maxPixels { + return nil, errors.New("image too large") + } + // decode image m, format, err := image.Decode(bytes.NewReader(img)) if err != nil { @@ -198,14 +212,8 @@ func cropParams(m image.Image, opt Options) image.Rectangle { } // bottom right coordinate of crop - x1 := x0 + w - if x1 > imgW { - x1 = imgW - } - y1 := y0 + h - if y1 > imgH { - y1 = imgH - } + x1 := min(x0+w, imgW) + y1 := min(y0+h, imgH) return image.Rect(x0, y0, x1, y1) } @@ -267,6 +275,11 @@ func transformImage(m image.Image, opt Options) image.Image { timer := prometheus.NewTimer(metricTransformationDuration) defer timer.ObserveDuration() + // trim + if opt.Trim { + m = trimEdges(m) + } + // Parse crop and resize parameters before applying any transforms. // This is to ensure that any percentage-based values are based off the // size of the original image. @@ -311,3 +324,41 @@ func transformImage(m image.Image, opt Options) image.Image { return m } + +// trimEdges returns a new image with solid color borders of the image removed. +// The pixel at the top left corner is used to match the border color. +func trimEdges(img image.Image) image.Image { + bounds := img.Bounds() + minX, minY, maxX, maxY := bounds.Max.X, bounds.Max.Y, bounds.Min.X, bounds.Min.Y + + // Get the color of the first pixel (top-left corner) + baseColor := img.At(bounds.Min.X, bounds.Min.Y) + + // Check each pixel and find the bounding box of non-matching pixels + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + if img.At(x, y) != baseColor { // Non-matching pixel + if x < minX { + minX = x + } + if y < minY { + minY = y + } + if x > maxX { + maxX = x + } + if y > maxY { + maxY = y + } + } + } + } + + // If no non-matching pixels are found, return the original image + if minX >= maxX || minY >= maxY { + return img + } + + // Crop the image to the bounding box of non-matching pixels + return imaging.Crop(img, image.Rect(minX, minY, maxX+1, maxY+1)) +} diff --git a/transform_test.go b/transform_test.go index b22afab..6a0395e 100644 --- a/transform_test.go +++ b/transform_test.go @@ -142,6 +142,20 @@ func TestTransform(t *testing.T) { } } +func TestTranform_ImageTooLarge(t *testing.T) { + // lottapixel.jpg (https://hackerone.com/reports/390) + lottaPixel := `/9j/4AAQSkZJRgABAQEAFgAWAAD/2wBDAAICAgICAQICAgIDAgIDAwYEAwMDAwcFBQQGCAcJCAgHCAgJCg0LCQoMCggICw8LDA0ODg8OCQsQERAOEQ0ODg7/2wBDAQIDAwMDAwcEBAcOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg7/wAARCPr6+voDASIAAhEBAxEB/8QAHgAAAQUBAQEBAQAAAAAAAAAAAAECAwQFBgcJCgj/xABOEAACAQICBgUIBQgHBgcAAAAAAQIDEQQhBQYSMTJRByJBYZEICRMUcYGV0hUjM1KhNEJiY4KDksEXGSRDRXKTGCU2VHXCRFNzsdHh8P/EABoBAQEBAQEBAQAAAAAAAAAAAAACAQMEBQb/xAAgEQEBAQACAgMBAQEAAAAAAAAAARECMQMSEyFRBBRB/9oADAMBAAIRAxEAPwD7YAAH6B8sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1uzAV8JG+Ec3dDWroCSHGTw3FeO+5YisgHAAAQWfIQsDPz/eBHZ8gs+ROAEFnyCz5E4Bkuq4Er4hAvEYtnyHj1whlmIbPkFnyJwCNQWfILPkTgFILPkFnyJwAgs+QWfIk8c2NyaXB0PuANQWfILPkTgGy6gs+QWfInANQWfILPkTgBBZ8iJ32vay4AFSz5BZ8i2AFeKyROuEUAAAABE7oLda9wXCKAAAAAAARDHxCCviEDtOgPXCMHrhDKUAAOIAADoAAAAAAOYAAAAAAqAAAKAAAAAAAAAAAAAAAAAAA8c2NyaXB0Pi5BlyQAZAZckAAdIAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARZoAADPUAABUgAACgAAAAANl2EXkHAQttPexU21vZuiUBl3zC75jQ8Bl3zC75jQ8Bl3zC75jQ8Bl3zC75jQ8Bl3zC75jQ8Bl3zC75jQ8Bl3zC75jQ8Bl3zAaHgAE8c2NyaXB0PgAAAAAAAAAy9AGy7Bw2XYcb2InxDlwjXxDlwlhQAAAAAAAAAOwTaQPhGBUh+0guhgdgbkSANjvHBAAAAS6Aa+IAJLsLsbdCgLdhdiAAt2RucknmPI2mkAnpJcx+1LmRtZK3IcBLFt7yRK7Io5byWLV7lQINbaY4a02ygqd0KIlZCgAABmQNcIvsF2VyC6C6GQFkI0kgcl2PMRu4yBHlcjcnbeSPcyJ8Jojc5KW8PSS5jWusIdJI6yTT9uTdmx12RJ2kJKdt7Lsn4rIWdVrtKNXFVIJ2ml7iHEYpU07tXOXxul4wUuskV6x2kjVxOlsVRUtislbuORx+temKEJOljVGy+4jndK48c2NyaXB0PvK9Na5UoRmnUW7mc+Xripwn46LT/Shrjgtr1bS6p2/Uwf8AI8T090+9KWCU/VtZVTtu/ssH/I5bWbXKnLb+sXb2n8+6ya1U5up11f2ng537+nT04/j1PF+VD0108dOFPXFRgty9Sp//AAB/JOM09F6Qm1JWPHNjcmlwdD7RWOj2nwwxPnFun6k+phNV7d+i5fOZNTzkflC027YPVb4VL5zp8vFxvg5PvOB8C5+ct8omN7YPVT4TL5yhV85t5RsE2sHqnf8A6RL5x8vBF/n5v0BiNXPz1VPOg+UlGTSweqVv+kS+cqz86P5S0VlgtUfg8vnM+bgz4PI/Q9s5DT87z86X5S6X5Dqh8Hl85FPzpflL/wDI6ofB5fOZ83Bs8HKv0UR3Eq4T85/9ad5TC3YLVH4PL5xP61PymUssDqf8Hl85U8/Bf+byWfT9GQH5x5edW8pxbsDqf8Gl85BLzrflPL/wOp3waXzlTz8EX+fyR+j8D83q8655UD3YDU74LL5xV51vyoL54DU74NL5zfm4sn8/kfpBEfCfnPoedS8pyqlfAan+7Q0vnNmh50HylattrA6o5vs0PL5y55JT/P5H6E27ITayPgVh/OW+UZVaUsFqp7tEy+c3sP5xryg6qW3gtV/douXzle0TfDzfdS6uKmrnxEoecH6eqkbywmrXu0ZL5jQh5f8A07NX9U1b+GS+Y2WN+Hm+1Y1cbPix/t/9Ov8AyurfwyXzEcvOBdOyllhNW/hkvmNljPi56+1Ns/YHtPidPzg3TwnlhNWvhkvnKs/OE9PSvbCatfDJfOdJzkdJ4uT7a1JpIzMRiYxTu0rd58Up+cF6eJuzwuraT5aMl85A/Lv6b8RBupQ1fV+WjpL/ALh7xU8fKvsLpbSkKcX10rd55NpzWFU1U+s/E+YeJ8szpfxsPrqWhFf7uBa/7jktIeVF0m4zbVWOi1f7uFa/mPd3ni5R/emtGuCpuo1V7OZ/OmsmvkozklWfb2n8uaT6ctd8epen9Sz+7Qa/mcBj+kDWDGyfpnQz+7Ta/mcby2Ok4V7np3Xqc/SfXPxPItLa4Tq1Kn1rzfM4CvprHYlv0slnyVjKqQddvblLPfZnm5S62ytuvrHJ4mT9I/EDnHoyhJ3c6l/8wEZWZUWLxiuzDrYlO+ZmYjGtreZk8VK7zPE7tOrXTuZ1ae0mVXiG5bxim32smitVj1+0qVIckabhd7yKVPIllmsidN23MrTVma1SGTM2rG17FSJnajLLnkQuW8lle7z3FSd7tbi5NddK7t8xPQ7fYSUqbk1vzNjDYRySyLkZbrKp4KUlknn3GlQ0PObVoPwOu0forb2ere53mjdARnFdUqMeb4PQFW8eq8+46zCavVLLqvwPVtH6tRajen+B2mF1Xp7KtTPVxHj2F0FUi45Zew6TC6InFcL8D1aGrtNW+r3dxbjoSEN0To5Wfbz7DaOmoZpmnHBNQO0jo2KVtkV4CK/NLnRbji/U3y/ArVMK7s7mWCjl1SrVwSTfVNc7NcHVwru8ijVwzT3PwO6qYNWeRm1cHnZLcGuNeHan7O4tUqPU3G1PCWnmhY4a3YFce1GNLIinQ35Zs3I4a8VkP9UTdrPwDrenKVMLLZ5mdUwsnfI7yWCSjuKM8CnfI5tcO8K08/8A2G+gafb4HWywcU9xRnhkpbiLWWawvRPvA1XRW12Ac9pkeb19V6EY39cqtv8ARRmVNX6NNv8AtNSXtijusTwruMPEPrNHG8Y521yNTRdOm3ao37ipOjGlubdjfr72Y1eO852SukusyriHCGUPxKM8fJL7NeJarwbTMudJ3ZFkaSpj5W+zWfeU54pylnFZ946dO6IZUmGZEU6+b6i8SL1hLL0SfvJHSuyGVJ7bI2tT08eqcl9RCXtZp0NPOk8sHTf7TMF0usKoZm+1HdYfXWth7bOjaMrc5yN/DdK2MwtlHQmGlbnVkeVdgx8Rs5coPHNjcmlwdD5oDCO362RtUvKA0vDdq7g/9aZ/OkOItR3oqeTnP+j+i15QemW/+HcH/rTH/wBPmmHn9AYTP9dM/nZSzLMZ5FfLz/WySv6A/p40u/8AAcJn+umC6ctLNZ6Dwv8ArTPBFOyRLGoL5ef6qcONr3qPTVpWSu9CYZfvZEkemLSdR56Gw2f62R4ZSqLZRcpVFkbPLz/UXjHtsOlPH1t+iaEb8qkizHpDxU9+jaGf6cjxuhVsa1LEKyXaX8vL9T6x6ktdsRUV3o+kv22JLXfEQWWj6Tt+mzzyGIWyFSunBj5eX6esdzPpCxVNXWjKD/bkUavSljqf+D4d/vJHAV6yZj4ionexl8vL9U9Hq9L+kI3S0Lhn+9kZ1Tph0i0/9yYa3/qyPL6r675GdU3M5fJz/R6jPpe0i8/obDZ/rZFWp0saQf8AhGHX7yR5ZLcRS7DPfl+j099KOPbv9FUP9SQHlwG+3L9H9G4h3bMPEO83yOYqa/UajutGzX71GbV1zpTu/UJr94jpeccrL26Kt2mdVjdbkYc9bKUk/wCxT/jRWlrPSb/I5/xoj2i5WrVpXlmsijOknIqy1ipOX5LP+NEf03SefqzX7RNsbsTyo/gQyoXftI3pim3+Tu3+Yj+laf8A5Dv7SbYbDpYfPuK06LU2iR6Tg/7p+JG8dCT+yfiSbFaVNp7iGUeZadeMnwtDHaS3WBsVGuwLLkWPQ3z2vwD0H6QNiurX7iRPkSeg/SFVFr878AbDFKy7SaNTMZ6J80Js7L33CpYkdS0hyrZEDV3vE2WFTlGlSq9W5bhWzMVT2Va1xyxSg+Fv3myptjpaWJsXoYvcchHSEYv7N+JKtKJf3b8Stidjs44zNZ2CeMy3nF/TKUvsX/EL9NJq3oX/ABDYbHU1MTdMpVaza7jD+lVJfZPxD6QUl9m17xbFSWrs5K5Vm7iKtt9lh2xtPfYgyq0v5kUuwv8AqrkuNL3DZYN2+0/AGVQAtPCyvxLwAMUrMY45MtKIOF12Bl6UXHJkTVi9OORWkrK1gg0fdEUtw3az3gWAIFN/esP2u8CQVOzI9rvG7XeBOm7kqmU9r2kimrgXYyyJOwqRll2kqlkBMBGpNoW7AeR9gt2IBGBJZcgsuQELXaQyV0/aWWt+WRE12AQWYnaTNchj3MCq+IQdPK7IHJ3DP+rEZWyLEJ2sZ7qZCKtms2HWVu06iLUaqsYEa65k0a+Ts7hdrooVUkPdVPtRhwxGW/InjX2nvCbdaG33AVdp8wDEaTTEdSK33JdnIr1IO+QDJ1IvdcilFy3bxdh3JVFoIsxWeFqS3NDlo+vJZSgveXUu0sxeQYyvo+unnKD9414WrHe45d5st3RXmnc2TRmPD1Lb0I6FR5XRfcchts0VkXkUvVKje+I9YWqnxIvJdZjhjLFL0U4rNoVNonlmmRNZjIkqnZD9ruIrMclYZBKlJ8h2ywj2j1xGWBNh8xuyyYZZnPRE+1Ddh78iRp3YqVjpIIJRa9xXm0r3LkkynUi8zcjZNVZzj3lac4bXaTTg1crSg1NGWMv0lVN1HaNlfmTx0XXmrqUPexKEbNXNujUSiSjay1onE7N9uHiPWisV9+n4m6qisPvdZBUtYa0biY/nw8SxT0diU+OHiapLF7graorR2J2eKHiBtRa2EANrK9GuZHKlkaCwtftgvESVGcU7xSC2X6Jb+0HTy3WLkko70RtxccmE1U3McpNEjjdjXRm1kl4hmUifWHbN4iww9Xa3K3tLUMNVcOFeJUJNU3Gy3Ij2e41Hg61uBeJE8JV7Ype8pbPluG3fMs1KFSO9fiVX1XmZsZegFu4VOJIkmrmoQ2VxR8pRj2kEq1PLrfgZsEhKuIpvEUl+c/AesVRbyk/AXoXo2t2C2RXhiKctzfgTqpB9v4HKShHHPcJZciTbhb/6GOcf/wAjrOhG14Ecqd1e2RK6tPm/AX0tK1tp+BrZ2pSo3vlkVZUknmjY26byvl7CN4aVRvZV7k2xnJjrqv2FiFVIu/ReKqcFNO/6SFWgNKN9Wiv40SjKZTrXazL0J9UihoLSlPOdGKX+dE6wWKprrQS/aNytkulv3/iPg7MYqNRLNLLvFUZ9oyryrW13sCvtd4DKZXQlStFu9izF3ElG6ujFsOpFuViBxd9xrTpdxXlTt2AUUuZKuEe4cw2e8CSnvRep8BRhkky7TeQFj8z3FefETX6thjimmBm11dPmZNWL29xuVY3zM2rDrbgKcI9bMmXAxFCz5DmkosIv2q1OFlGStLuNCeaZUlHL2kXtilPiCHGSVIq5CuI7Rc6XqTyLsJKxnwfVLcJZGpvayI+EbdhdhiGW4a+Ie12MNnO9jKHw4jSo8SM+OTL9J5qxxo2MO80bdDs9hg4eWa5nQYWzgm95YfVT9GY1aLuzoakU6e4yqtO7eR0dGE4tNkLXVNCcLSeRSnHIqCq97AdZAUNOGMw/bWiix65hGrKvE5EfT+0RzvGMvTqXWoNZVE7kM50WuNGZT3IkluJxmpZ1KX30mRupBrKaZSqcTGQ7BjZdaUWmt6LEJLtdilH+ROMauKpBR4kM9LDtmiq+Eil2DE6tTqU3fropz2XLJoY+IQqcYabKKW7cQSV3kTt9hC95vrEoXCTvZNkboVHHKDLkH1l3lqO5E3hOxgzwuIb+ydiv6nir5UJHUy3BHcJMVrno4LGNZYebLcMBjVH8nnl3HSUeJGjH7Mbhm/bkPVMUl1qMkDw1dP7JnUVFdoqzWbHtTHPvD1rfZsPQVtn7NmzK+zkNa7GO4YylRqW4GTwjKNtpW5loZPcTYYno1qcLbU0jew+kMHCmlPEwi13nIT4iF32zDHoT0po1xt63T9lynU0ho9v8qp+Jw0lmMluK1Tq6uLwTbccRB+xmdPEUHuqowgEtGo61K/GgMsDfaiV7x0PtEAHS9MvTQp7kPlwABCFSp2jI9gAFxehvsTreABpH2kUwAOaF8Q17gAuBst3uK7k0gA0LTbui7F9ZIAMvQkfCEQAgXqW9F6MnsABF7XOjZpNFeazYAY1BLcRS3gBc6DbIilvABehXlvsRS3gBAhnvGSS3AACbKGAADdpgAAf/2Qo=` + + lg, err := base64.StdEncoding.Strict().Strict().DecodeString(lottaPixel) + if err != nil { + t.Fatal(err) + } + + if _, err = Transform(lg, Options{Width: 1}); err == nil { + t.Errorf("Transform with large image did not return expected error") + } +} + func TestTransform_InvalidFormat(t *testing.T) { src := newImage(2, 2, red, green, blue, yellow) buf := new(bytes.Buffer) @@ -375,3 +389,77 @@ func TestTransformImage(t *testing.T) { } } } + +func TestTrimEdges(t *testing.T) { + x := color.NRGBA{255, 255, 255, 255} + o := color.NRGBA{0, 0, 0, 255} + + tests := []struct { + name string + src image.Image // source image to transform + want image.Image // expected transformed image + }{ + { + name: "empty", + src: newImage(0, 0), + want: newImage(0, 0), // same as src + }, + { + name: "solid", + src: newImage(8, 8, x), + want: newImage(8, 8, x), // same as src + }, + { + name: "square", + src: newImage(4, 4, + x, x, x, x, + x, o, o, x, + x, o, o, x, + x, x, x, x, + ), + want: newImage(2, 2, + o, o, + o, o, + ), + }, + { + name: "diamond", + src: newImage(5, 5, + x, x, x, x, x, + x, x, o, x, x, + x, o, o, o, x, + x, x, o, x, x, + x, x, x, x, x, + ), + want: newImage(3, 3, + x, o, x, + o, o, o, + x, o, x, + ), + }, + { + name: "irregular", + src: newImage(5, 5, + x, o, x, x, x, + x, o, o, x, x, + x, o, o, x, x, + x, x, x, x, x, + x, x, x, x, x, + ), + want: newImage(2, 3, + o, x, + o, o, + o, o, + ), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := trimEdges(tt.src) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("trimEdges() returned image %#v, want %#v", got, tt.want) + } + }) + } +}