├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Caddyfile ├── LICENSE ├── README.md ├── app.go ├── cleaner.go ├── configuration.go ├── configuration.json ├── dispatch.go ├── examples ├── Caddyfile-embedded-olric ├── Caddyfile-file-configuration-olric ├── Caddyfile-minimal ├── Caddyfile-not-distributed ├── Caddyfile-remote-olric-cluster ├── configuration-embedded-olric.json ├── configuration-file-configuration-olric.json ├── configuration-not-distributed.json └── configuration-remote-olric-cluster.json ├── fixtures └── cache-tests │ ├── Caddyfile │ └── README.md ├── go.mod ├── go.sum ├── httpcache.go └── httpcache_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | # Default is true, cancels jobs for other platforms in the matrix if one fails 15 | fail-fast: false 16 | matrix: 17 | # The Windows build currently fail because of https://github.com/golang/go/issues/40795, and because xcaddy isn't compatible with the known workaround 18 | #os: [ ubuntu-latest, macos-latest, windows-latest ] 19 | os: [ ubuntu-latest, macos-latest ] 20 | go: [ '1.21' ] 21 | 22 | runs-on: ${{ matrix.os }} 23 | 24 | steps: 25 | - name: Install Go 26 | uses: actions/setup-go@v4 27 | with: 28 | go-version: ${{ matrix.go }} 29 | 30 | - name: Checkout code 31 | uses: actions/checkout@v3 32 | 33 | - name: Print Go version and environment 34 | id: vars 35 | run: | 36 | printf "Using go at: $(which go)\n" 37 | printf "Go version: $(go version)\n" 38 | printf "\n\nGo environment:\n\n" 39 | go env 40 | printf "\n\nSystem environment:\n\n" 41 | env 42 | # Calculate the short SHA1 hash of the git commit 43 | echo "::set-output name=short_sha::$(git rev-parse --short HEAD)" 44 | echo "::set-output name=go_cache::$(go env GOCACHE)" 45 | 46 | - name: Cache the build cache 47 | uses: actions/cache@v2 48 | with: 49 | path: ${{ steps.vars.outputs.go_cache }} 50 | key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }} 51 | restore-keys: | 52 | ${{ runner.os }}-${{ matrix.go }}-go-ci 53 | 54 | - name: Install xcaddy 55 | run: | 56 | go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest 57 | 58 | - name: Build 59 | env: 60 | CGO_ENABLED: 0 61 | run: | 62 | xcaddy build 63 | 64 | - name: Run tests 65 | run: | 66 | go test -v -coverprofile="cover-profile.out" -race ./... 67 | 68 | golangci: 69 | name: Lint 70 | 71 | runs-on: ubuntu-latest 72 | 73 | steps: 74 | - uses: actions/checkout@v3 75 | - uses: actions/setup-go@v4 76 | with: 77 | go-version: '1.21' 78 | - name: golangci-lint 79 | uses: golangci/golangci-lint-action@v3 80 | with: 81 | args: --timeout 5m 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /caddy 2 | vendor/ 3 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | order cache before rewrite 3 | debug 4 | log { 5 | level debug 6 | } 7 | cache { 8 | allowed_http_verbs GET POST 9 | api { 10 | prometheus 11 | souin 12 | } 13 | cdn { 14 | dynamic 15 | strategy hard 16 | } 17 | regex { 18 | exclude /test2.* 19 | } 20 | ttl 1000s 21 | timeout { 22 | backend 10s 23 | cache 100ms 24 | } 25 | default_cache_control public 26 | } 27 | } 28 | 29 | :4443 30 | respond "Hello World!" 31 | 32 | @match path /test1* 33 | @match2 path /test2* 34 | @matchdefault path /default 35 | @souin-api path /souin-api* 36 | 37 | cache @match { 38 | ttl 5s 39 | } 40 | 41 | cache @match2 { 42 | ttl 50s 43 | } 44 | 45 | cache @matchdefault { 46 | ttl 5s 47 | } 48 | 49 | route /badger-configuration { 50 | cache { 51 | ttl 15s 52 | badger { 53 | configuration { 54 | Dir /tmp/badger-configuration 55 | ValueDir match2 56 | ValueLogFileSize 16777216 57 | MemTableSize 4194304 58 | ValueThreshold 524288 59 | } 60 | } 61 | } 62 | respond "Hello badger" 63 | } 64 | 65 | route /etcd { 66 | cache { 67 | ttl 5s 68 | etcd { 69 | configuration { 70 | Endpoints etcd1:2379 etcd2:2379 etcd3:2379 71 | AutoSyncInterval 1s 72 | DialTimeout 1s 73 | DialKeepAliveTime 1s 74 | DialKeepAliveTimeout 1s 75 | MaxCallSendMsgSize 10000000 76 | MaxCallRecvMsgSize 10000000 77 | Username john 78 | Password doe 79 | RejectOldCluster false 80 | PermitWithoutStream false 81 | } 82 | } 83 | } 84 | respond "Hello etcd" 85 | } 86 | 87 | route /etcd-configuration { 88 | cache { 89 | ttl 5s 90 | etcd { 91 | configuration { 92 | Endpoints etcd:2379 etcd:2379 93 | AutoSyncInterval 1s 94 | DialTimeout 1s 95 | DialKeepAliveTime 1s 96 | DialKeepAliveTimeout 1s 97 | MaxCallSendMsgSize 10000000 98 | MaxCallRecvMsgSize 10000000 99 | RejectOldCluster false 100 | PermitWithoutStream false 101 | } 102 | } 103 | } 104 | respond "Hello etcd" 105 | } 106 | 107 | route /nuts-configuration { 108 | cache { 109 | ttl 15s 110 | nuts { 111 | configuration { 112 | Dir /tmp/nuts-configuration 113 | EntryIdxMode 1 114 | RWMode 0 115 | SegmentSize 1024 116 | NodeNum 42 117 | SyncEnable true 118 | StartFileLoadingMode 1 119 | } 120 | } 121 | } 122 | respond "Hello nuts" 123 | } 124 | 125 | route /redis { 126 | cache { 127 | ttl 5s 128 | redis { 129 | configuration { 130 | Network my-network 131 | Addr 127.0.0.1:6379 132 | Username user 133 | Password password 134 | DB 1 135 | MaxRetries 1 136 | MinRetryBackoff 5s 137 | MaxRetryBackoff 5s 138 | DialTimeout 5s 139 | ReadTimeout 5s 140 | WriteTimeout 5s 141 | PoolFIFO true 142 | PoolSize 99999 143 | PoolTimeout 10s 144 | MinIdleConns 100 145 | MaxIdleConns 100 146 | ConnMaxIdleTime 5s 147 | ConnMaxLifetime 5s 148 | } 149 | } 150 | } 151 | respond "Hello redis" 152 | } 153 | 154 | route /redis-configuration { 155 | cache { 156 | ttl 5s 157 | redis { 158 | configuration { 159 | Addr 127.0.0.1:6379 160 | DB 0 161 | MaxRetries 1 162 | MinRetryBackoff 5s 163 | MaxRetryBackoff 5s 164 | DialTimeout 5s 165 | ReadTimeout 5s 166 | WriteTimeout 5s 167 | PoolFIFO true 168 | PoolSize 99999 169 | PoolTimeout 10s 170 | MinIdleConns 100 171 | MaxIdleConns 100 172 | ConnMaxIdleTime 5s 173 | ConnMaxLifetime 5s 174 | } 175 | } 176 | } 177 | respond "Hello redis" 178 | } 179 | 180 | route /redis-url { 181 | cache { 182 | ttl 5s 183 | redis { 184 | url 127.0.0.1:6379 185 | } 186 | } 187 | respond "Hello redis url" 188 | } 189 | 190 | route /vary { 191 | cache { 192 | ttl 15s 193 | } 194 | header Vary X-Something 195 | respond "Hello {http.request.header.X-Something}" 196 | } 197 | 198 | route /cache-s-maxage { 199 | cache 200 | header Cache-Control "s-maxage=10" 201 | respond "Hello, s-maxage!" 202 | } 203 | 204 | route /cache-maxage { 205 | cache 206 | header Cache-Control "max-age=5" 207 | respond "Hello, max-age!" 208 | } 209 | 210 | route /cache-maxstale { 211 | cache { 212 | ttl 3s 213 | stale 5s 214 | } 215 | header Cache-Control "max-age=5" 216 | respond "Hello, max-age!" 217 | } 218 | 219 | route /not-modified { 220 | cache { 221 | ttl 5s 222 | } 223 | reverse_proxy 127.0.0.1:9000 224 | } 225 | 226 | route /no-reverse-proxy { 227 | cache 228 | reverse_proxy 127.0.0.1:9000 229 | } 230 | 231 | route /surrogate-keys { 232 | cache 233 | header Surrogate-Key "KEY-{http.request.header.X-Surrogate-Key-Suffix}" 234 | header Vary X-Surrogate-Key-Suffix,Accept-Encoding 235 | respond "Hello {http.request.header.X-Surrogate-Key-Suffix}" 236 | } 237 | 238 | route /another-cache-status-name { 239 | cache { 240 | cache_name Another 241 | } 242 | } 243 | 244 | route /backend-timeout { 245 | cache { 246 | timeout { 247 | backend 1s 248 | cache 1ms 249 | } 250 | } 251 | reverse_proxy 127.0.0.1:8081 252 | } 253 | 254 | route /stream { 255 | cache 256 | reverse_proxy 127.0.0.1:81 257 | } 258 | 259 | route /gzip { 260 | cache 261 | encode { 262 | gzip 263 | minimum_length 5 264 | } 265 | header Content-Type text/plain 266 | respond "Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip. Hello, gzip." 267 | } 268 | 269 | route /custom-key/without-* { 270 | cache { 271 | cache_keys { 272 | body { 273 | disable_body 274 | } 275 | host { 276 | disable_host 277 | } 278 | method { 279 | disable_method 280 | } 281 | everything-with-content-type { 282 | disable_method 283 | headers Content-Type 284 | } 285 | } 286 | } 287 | respond "Hello to the authenticated user." 288 | } 289 | 290 | route /must-revalidate { 291 | cache { 292 | ttl 5s 293 | stale 5s 294 | } 295 | header Cache-Control "must-revalidate" 296 | reverse_proxy 127.0.0.1:81 297 | } 298 | 299 | route /cache-authorization { 300 | cache { 301 | cache_keys { 302 | /.+ { 303 | headers Authorization 304 | } 305 | } 306 | } 307 | header Souin-Cache-Control public 308 | respond "Hello to the authenticated user." 309 | } 310 | 311 | route /bypass { 312 | cache { 313 | mode bypass 314 | } 315 | 316 | header Cache-Control "no-store" 317 | respond "Hello bypass" 318 | } 319 | 320 | route /bypass_request { 321 | cache { 322 | mode bypass_request 323 | } 324 | 325 | respond "Hello bypass_request" 326 | } 327 | 328 | route /bypass_response { 329 | cache { 330 | mode bypass_response 331 | } 332 | 333 | header Cache-Control "no-cache, no-store" 334 | respond "Hello bypass_response" 335 | } 336 | 337 | route /strict_request { 338 | cache { 339 | mode strict 340 | } 341 | 342 | respond "Hello strict" 343 | } 344 | 345 | route /strict_response { 346 | cache { 347 | mode strict 348 | } 349 | 350 | header Cache-Control "no-cache, no-store" 351 | respond "Hello strict" 352 | } 353 | 354 | cache @souin-api { 355 | } 356 | 357 | # ESI part 358 | route /esi-include { 359 | cache 360 | header Content-Type text/html 361 | respond "

ESI INCLUDE

" 362 | } 363 | 364 | route /alt-esi-include { 365 | cache 366 | header Content-Type text/html 367 | respond "

ALTERNATE ESI INCLUDE

" 368 | } 369 | 370 | route /esi { 371 | cache 372 | header Content-Type text/html 373 | respond `` 374 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Caddy Module: http.handlers.cache 2 | ================================ 3 | 4 | This is a distributed HTTP cache module for Caddy based on [Souin](https://github.com/darkweak/souin) cache. 5 | 6 | > [!WARNING] 7 | > Since `v1.7.0` Souin (the development repository that cache-handler is based on) implements only one storage. If you need a specific storage you have to take it from [the storages repository](https://github.com/darkweak/storages) and add it either in your code, during the build otherwise. 8 | (e.g. with otter using caddy) You have to build your caddy module with the desired storage `xcaddy build --with github.com/caddyserver/cache-handler --with github.com/darkweak/storages/otter/caddy` and configure otter in your Caddyfile/JSON configuration file. 9 | See the [documentation about the storages](https://docs.souin.io/docs/storages). 10 | 11 | ## Features 12 | 13 | * [RFC 7234](https://httpwg.org/specs/rfc7234.html) compliant HTTP Cache. 14 | * Sets [the `Cache-Status` HTTP Response Header](https://httpwg.org/http-extensions/draft-ietf-httpbis-cache-header.html) 15 | * REST API to purge the cache and list stored resources. 16 | * ESI tags processing (using the [go-esi package](https://github.com/darkweak/go-esi)). 17 | * Builtin support for distributed cache. 18 | 19 | ## Minimal Configuration 20 | Using the minimal configuration the responses will be cached for `120s` 21 | ```caddy 22 | { 23 | cache 24 | } 25 | 26 | example.com { 27 | cache 28 | reverse_proxy your-app:8080 29 | } 30 | ``` 31 | 32 | ## Global Option Syntax 33 | Here are all the available options for the global options 34 | ```caddy 35 | { 36 | log { 37 | level debug 38 | } 39 | cache { 40 | allowed_http_verbs GET POST PATCH 41 | api { 42 | basepath /some-basepath 43 | prometheus 44 | souin { 45 | basepath /souin-changed-endpoint-path 46 | } 47 | } 48 | badger { 49 | path the_path_to_a_file.json 50 | } 51 | cache_keys { 52 | .*\.css { 53 | disable_body 54 | disable_host 55 | disable_method 56 | disable_query 57 | headers X-Token Authorization 58 | hide 59 | } 60 | } 61 | cache_name Another 62 | cdn { 63 | api_key XXXX 64 | dynamic 65 | email darkweak@protonmail.com 66 | hostname domain.com 67 | network your_network 68 | provider fastly 69 | strategy soft 70 | service_id 123456_id 71 | zone_id anywhere_zone 72 | } 73 | etcd { 74 | configuration { 75 | # Your etcd configuration here 76 | } 77 | } 78 | key { 79 | disable_body 80 | disable_host 81 | disable_method 82 | headers Content-Type Authorization 83 | } 84 | log_level debug 85 | mode bypass 86 | nuts { 87 | path /path/to/the/storage 88 | } 89 | olric { 90 | url url_to_your_cluster:3320 91 | path the_path_to_a_file.yaml 92 | configuration { 93 | # Your olric configuration here 94 | } 95 | } 96 | regex { 97 | exclude /test2.* 98 | } 99 | stale 200s 100 | ttl 1000s 101 | default_cache_control no-store 102 | } 103 | } 104 | 105 | :4443 106 | respond "Hello World!" 107 | ``` 108 | 109 | ## Cache directive Syntax 110 | Here are all the available options for the directive options 111 | 112 | ``` 113 | @match path /path 114 | 115 | handle @match { 116 | cache { 117 | cache_name ChangeName 118 | cache_keys { 119 | (host1|host2).*\.css { 120 | disable_body 121 | disable_host 122 | disable_method 123 | disable_query 124 | headers X-Token Authorization 125 | } 126 | } 127 | cdn { 128 | api_key XXXX 129 | dynamic 130 | email darkweak@protonmail.com 131 | hostname domain.com 132 | network your_network 133 | provider fastly 134 | strategy soft 135 | service_id 123456_id 136 | zone_id anywhere_zone 137 | } 138 | key { 139 | disable_body 140 | disable_host 141 | disable_method 142 | disable_query 143 | headers Content-Type Authorization 144 | } 145 | log_level debug 146 | regex { 147 | exclude /test2.* 148 | } 149 | stale 200s 150 | ttl 1000s 151 | default_cache_control no-store 152 | } 153 | } 154 | ``` 155 | 156 | ## Provider Syntax 157 | 158 | ### Badger 159 | The badger provider must have either the path or the configuration directive. 160 | ``` 161 | badger-path.com { 162 | cache { 163 | badger { 164 | path /tmp/badger/first-match 165 | } 166 | } 167 | } 168 | ``` 169 | ``` 170 | badger-configuration.com { 171 | cache { 172 | badger { 173 | configuration { 174 | # Required value 175 | ValueDir 176 | 177 | # Optional 178 | SyncWrites 179 | NumVersionsToKeep 180 | ReadOnly 181 | Compression 182 | InMemory 183 | MetricsEnabled 184 | MemTableSize 185 | BaseTableSize 186 | BaseLevelSize 187 | LevelSizeMultiplier 188 | TableSizeMultiplier 189 | MaxLevels 190 | VLogPercentile 191 | ValueThreshold 192 | NumMemtables 193 | BlockSize 194 | BloomFalsePositive 195 | BlockCacheSize 196 | IndexCacheSize 197 | NumLevelZeroTables 198 | NumLevelZeroTablesStall 199 | ValueLogFileSize 200 | ValueLogMaxEntries 201 | NumCompactors 202 | CompactL0OnClose 203 | LmaxCompaction 204 | ZSTDCompressionLevel 205 | VerifyValueChecksum 206 | EncryptionKey 207 | EncryptionKeyRotationDuration 208 | BypassLockGuard 209 | ChecksumVerificationMode 210 | DetectConflicts 211 | NamespaceOffset 212 | } 213 | } 214 | } 215 | } 216 | ``` 217 | 218 | ### Etcd 219 | The etcd provider must have the configuration directive. 220 | ``` 221 | etcd-configuration.com { 222 | cache { 223 | etcd { 224 | configuration { 225 | Endpoints etcd1:2379 etcd2:2379 etcd3:2379 226 | AutoSyncInterval 1s 227 | DialTimeout 1s 228 | DialKeepAliveTime 1s 229 | DialKeepAliveTimeout 1s 230 | MaxCallSendMsgSize 10000000 231 | MaxCallRecvMsgSize 10000000 232 | Username john 233 | Password doe 234 | RejectOldCluster false 235 | PermitWithoutStream false 236 | } 237 | } 238 | } 239 | } 240 | ``` 241 | 242 | ### NutsDB 243 | The nutsdb provider must have either the path or the configuration directive. 244 | ``` 245 | nuts-path.com { 246 | cache { 247 | nuts { 248 | path /tmp/nuts-path 249 | } 250 | } 251 | } 252 | ``` 253 | ``` 254 | nuts-configuration.com { 255 | cache { 256 | nuts { 257 | configuration { 258 | Dir /tmp/nuts-configuration 259 | EntryIdxMode 1 260 | RWMode 0 261 | SegmentSize 1024 262 | NodeNum 42 263 | SyncEnable true 264 | StartFileLoadingMode 1 265 | } 266 | } 267 | } 268 | } 269 | ``` 270 | 271 | ### Olric 272 | The olric provider must have either the url directive to work as client mode. 273 | ``` 274 | olric-url.com { 275 | cache { 276 | olric { 277 | url olric:3320 278 | } 279 | } 280 | } 281 | ``` 282 | 283 | The olric provider must have either the path or the configuration directive to work as embedded mode. 284 | ``` 285 | olric-path.com { 286 | cache { 287 | olric { 288 | path /path/to/olricd.yml 289 | } 290 | } 291 | } 292 | ``` 293 | ``` 294 | olric-configuration.com { 295 | cache { 296 | nuts { 297 | configuration { 298 | Dir /tmp/nuts-configuration 299 | EntryIdxMode 1 300 | RWMode 0 301 | SegmentSize 1024 302 | NodeNum 42 303 | SyncEnable true 304 | StartFileLoadingMode 1 305 | } 306 | } 307 | } 308 | } 309 | ``` 310 | 311 | ### Redis 312 | The redis provider must have either the URL or the configuration directive. 313 | 314 | ``` 315 | redis-url.com { 316 | cache { 317 | redis { 318 | url 127.0.0.1:6379 319 | } 320 | } 321 | } 322 | ``` 323 | 324 | You can also use the configuration. Refer to the [Souin docs](https://docs.souin.io/docs/storages/redis/) 325 | or [rueidis client options](https://github.com/redis/rueidis/blob/main/rueidis.go#L56) to define your config as key value. 326 | 327 | What does these directives mean? 328 | | Key | Description | Value example | 329 | |:------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------| 330 | | `allowed_http_verbs` | The HTTP verbs allowed to be cached | `GET POST PATCH`

`(default: GET HEAD)` | 331 | | `api` | The cache-handler API cache management | | 332 | | `api.basepath` | BasePath for all APIs to avoid conflicts | `/your-non-conflict-route`

`(default: /souin-api)` | 333 | | `api.prometheus` | Enable the Prometheus metrics | | 334 | | `api.souin.basepath` | Souin API basepath | `/another-souin-api-route`

`(default: /souin)` | 335 | | `badger` | Configure the Badger cache storage | | 336 | | `badger.path` | Configure Badger with a file | `/anywhere/badger_configuration.json` | 337 | | `badger.configuration` | Configure Badger directly in the Caddyfile or your JSON caddy configuration | [See the Badger configuration for the options](https://dgraph.io/docs/badger/get-started/) | 338 | | `cache_name` | Override the cache name to use in the Cache-Status response header | `Another` `Caddy` `Cache-Handler` `Souin` | 339 | | `cache_keys` | Define the key generation rules for each URI matching the key regexp | | 340 | | `cache_keys.{your regexp}` | Regexp that the URI should match to override the key generation | `.+\.css` | 341 | | `cache_keys.{your regexp}` | Regexp that the URI should match to override the key generation | `.+\.css` | 342 | | `cache_keys.{your regexp}.disable_body` | Disable the body part in the key matching the regexp (GraphQL context) | `true`

`(default: false)` | 343 | | `cache_keys.{your regexp}.disable_host` | Disable the host part in the key matching the regexp | `true`

`(default: false)` | 344 | | `cache_keys.{your regexp}.disable_method` | Disable the method part in the key matching the regexp | `true`

`(default: false)` | 345 | | `cache_keys.{your regexp}.disable_query` | Disable the query string part in the key matching the regexp | `true`

`(default: false)` | 346 | | `cache_keys.{your regexp}.headers` | Add headers to the key matching the regexp | `Authorization Content-Type X-Additional-Header` | 347 | | `cache_keys.{your regexp}.hide` | Prevent the key from being exposed in the `Cache-Status` HTTP response header | `true`

`(default: false)` | 348 | | `cdn` | The CDN management, if you use any cdn to proxy your requests Souin will handle that | | 349 | | `cdn.provider` | The provider placed before Souin | `akamai`

`fastly`

`souin` | 350 | | `cdn.api_key` | The api key used to access to the provider | `XXXX` | 351 | | `cdn.dynamic` | Enable the dynamic keys returned by your backend application | `(default: true)` | 352 | | `cdn.email` | The api key used to access to the provider if required, depending the provider | `XXXX` | 353 | | `cdn.hostname` | The hostname if required, depending the provider | `domain.com` | 354 | | `cdn.network` | The network if required, depending the provider | `your_network` | 355 | | `cdn.strategy` | The strategy to use to purge the cdn cache, soft will keep the content as a stale resource | `hard`

`(default: soft)` | 356 | | `cdn.service_id` | The service id if required, depending the provider | `123456_id` | 357 | | `cdn.zone_id` | The zone id if required, depending the provider | `anywhere_zone` | 358 | | `default_cache_control` | Set the default value of `Cache-Control` response header if not set by upstream (Souin treats empty `Cache-Control` as `public` if omitted) | `no-store` | 359 | | `key` | Override the key generation with the ability to disable unecessary parts | | 360 | | `key.disable_body` | Disable the body part in the key (GraphQL context) | `true`

`(default: false)` | 361 | | `key.disable_host` | Disable the host part in the key | `true`

`(default: false)` | 362 | | `key.disable_method` | Disable the method part in the key | `true`

`(default: false)` | 363 | | `key.disable_query` | Disable the query string part in the key | `true`

`(default: false)` | 364 | | `key.disable_scheme` | Disable the scheme string part in the key | `true`

`(default: false)` | 365 | | `key.hash` | Hash the key before store it in the storage to get smaller keys | `true`

`(default: false)` | 366 | | `key.headers` | Add headers to the key matching the regexp | `Authorization Content-Type X-Additional-Header` | 367 | | `key.hide` | Prevent the key from being exposed in the `Cache-Status` HTTP response header | `true`

`(default: false)` | 368 | | `key.template` | Use caddy templates to create the key (when this option is enabled, disable_* directives are skipped) | `KEY-{http.request.uri.path}-{http.request.uri.query}` | 369 | | `max_cacheable_body_bytes` | Set the maximum size (in bytes) for a response body to be cached (unlimited if omited) | `1048576` (1MB) | 370 | | `mode` | Bypass the RFC respect | One of `bypass` `bypass_request` `bypass_response` `strict` (default `strict`) | 371 | | `nuts` | Configure the Nuts cache storage | | 372 | | `nuts.path` | Set the Nuts file path storage | `/anywhere/nuts/storage` | 373 | | `nuts.configuration` | Configure Nuts directly in the Caddyfile or your JSON caddy configuration | [See the Nuts configuration for the options](https://github.com/nutsdb/nutsdb#default-options) | 374 | | `etcd` | Configure the Etcd cache storage | | 375 | | `etcd.configuration` | Configure Etcd directly in the Caddyfile or your JSON caddy configuration | [See the Etcd configuration for the options](https://pkg.go.dev/go.etcd.io/etcd/clientv3#Config) | 376 | | `olric` | Configure the Olric cache storage | | 377 | | `olric.path` | Configure Olric with a file | `/anywhere/olric_configuration.json` | 378 | | `olric.configuration` | Configure Olric directly in the Caddyfile or your JSON caddy configuration | [See the Olric configuration for the options](https://github.com/buraksezer/olric/blob/master/cmd/olricd/olricd.yaml/) | 379 | | `otter` | Configure the Otter cache storage | | 380 | | `otter.configuration` | Configure Otter directly in the Caddyfile or your JSON caddy configuration | | 381 | | `otter.configuration.size` | Set the size of the pool in Otter | `999999` (default `10000`) | 382 | | `redis` | Configure the Redis cache storage | | 383 | | `redis.url` | Set the Redis url storage | `localhost:6379` | 384 | | `redis.configuration` | Configure Redis directly in the Caddyfile or your JSON caddy configuration | [See the Nuts configuration for the options](https://github.com/nutsdb/nutsdb#default-options) | 385 | | `regex.exclude` | The regex used to prevent paths being cached | `^[A-z]+.*$` | 386 | | `stale` | The stale duration | `25m` | 387 | | `storers` | Storers chain to fallback if a previous one is unreachable or don't have the resource | `otter nuts badger redis` | 388 | | `timeout` | The timeout configuration | | 389 | | `timeout.backend` | The timeout duration to consider the backend as unreachable | `10s` | 390 | | `timeout.cache` | The timeout duration to consider the cache provider as unreachable | `10ms` | 391 | | `ttl` | The TTL duration | `120s` | 392 | | `log_level` | The log level | `One of DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, FATAL it's case insensitive` | 393 | 394 | Other resources 395 | --------------- 396 | You can find an example for the [Caddyfile](Caddyfile) or the [JSON file](configuration.json). 397 | See the [Souin](https://github.com/darkweak/souin) configuration for the full configuration, and its associated [Caddyfile](https://github.com/darkweak/souin/blob/master/plugins/caddy/Caddyfile) 398 | 399 | ### Development and Stable Versions 400 | 401 | The **Souin** repository serves as the development version, where new features are introduced and tested. Once these features have been thoroughly stabilized, they are integrated into the **cache-handler** repository through a dependency update. This ensures that **cache-handler** remains the stable and reliable version for production use. 402 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package httpcache 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/caddyserver/caddy/v2" 7 | "github.com/darkweak/souin/configurationtypes" 8 | "github.com/darkweak/souin/pkg/storage/types" 9 | "github.com/darkweak/souin/pkg/surrogate/providers" 10 | "github.com/darkweak/storages/core" 11 | ) 12 | 13 | // SouinApp contains the whole Souin necessary items 14 | type SouinApp struct { 15 | DefaultCache 16 | // The provider to use. 17 | Storers []types.Storer 18 | // Surrogate storage to support th econfiguration reload without surrogate-key data loss. 19 | SurrogateStorage providers.SurrogateInterface 20 | // Cache-key tweaking. 21 | CacheKeys configurationtypes.CacheKeys `json:"cache_keys,omitempty"` 22 | // API endpoints enablers. 23 | API configurationtypes.API `json:"api,omitempty"` 24 | // Logger level, fallback on caddy's one when not redefined. 25 | LogLevel string `json:"log_level,omitempty"` 26 | } 27 | 28 | func init() { 29 | caddy.RegisterModule(SouinApp{}) 30 | } 31 | 32 | // Start will start the App 33 | func (s SouinApp) Start() error { 34 | core.ResetRegisteredStorages() 35 | _, _ = up.Delete(stored_providers_key) 36 | _, _ = up.LoadOrStore(stored_providers_key, newStorageProvider()) 37 | if s.DefaultCache.GetTTL() == 0 { 38 | return errors.New("Invalid/Incomplete default cache declaration") 39 | } 40 | return nil 41 | } 42 | 43 | // Stop will stop the App 44 | func (s SouinApp) Stop() error { 45 | return nil 46 | } 47 | 48 | // CaddyModule implements caddy.ModuleInfo 49 | func (s SouinApp) CaddyModule() caddy.ModuleInfo { 50 | return caddy.ModuleInfo{ 51 | ID: moduleName, 52 | New: func() caddy.Module { return new(SouinApp) }, 53 | } 54 | } 55 | 56 | var ( 57 | _ caddy.App = (*SouinApp)(nil) 58 | _ caddy.Module = (*SouinApp)(nil) 59 | ) 60 | -------------------------------------------------------------------------------- /cleaner.go: -------------------------------------------------------------------------------- 1 | package httpcache 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | const stored_providers_key = "STORED_PROVIDERS_KEY" 8 | const coalescing_key = "COALESCING" 9 | const surrogate_key = "SURROGATE" 10 | 11 | type storage_providers struct { 12 | list map[interface{}]bool 13 | sync.RWMutex 14 | } 15 | 16 | func newStorageProvider() *storage_providers { 17 | return &storage_providers{ 18 | list: make(map[interface{}]bool), 19 | RWMutex: sync.RWMutex{}, 20 | } 21 | } 22 | 23 | func (s *storage_providers) Add(key interface{}) { 24 | s.RWMutex.Lock() 25 | defer s.RWMutex.Unlock() 26 | 27 | s.list[key] = true 28 | } 29 | 30 | func (s *SouinCaddyMiddleware) Cleanup() error { 31 | s.logger.Debug("Cleanup...") 32 | td := []interface{}{} 33 | sp, _ := up.LoadOrStore(stored_providers_key, newStorageProvider()) 34 | stored_providers := sp.(*storage_providers) 35 | up.Range(func(key, _ interface{}) bool { 36 | if key != stored_providers_key && key != coalescing_key && key != surrogate_key { 37 | if !stored_providers.list[key] { 38 | td = append(td, key) 39 | } 40 | } 41 | 42 | return true 43 | }) 44 | 45 | for _, v := range td { 46 | s.logger.Debugf("Cleaning %v\n", v) 47 | _, _ = up.Delete(v) 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /configuration.go: -------------------------------------------------------------------------------- 1 | package httpcache 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 10 | "github.com/darkweak/souin/configurationtypes" 11 | "github.com/darkweak/storages/core" 12 | ) 13 | 14 | // DefaultCache the struct 15 | type DefaultCache struct { 16 | // Allowed HTTP verbs to be cached by the system. 17 | AllowedHTTPVerbs []string `json:"allowed_http_verbs"` 18 | // Badger provider configuration. 19 | Badger configurationtypes.CacheProvider `json:"badger"` 20 | // The cache name to use in the Cache-Status response header. 21 | CacheName string `json:"cache_name"` 22 | CDN configurationtypes.CDN `json:"cdn"` 23 | // The default Cache-Control header value if none set by the upstream server. 24 | DefaultCacheControl string `json:"default_cache_control"` 25 | // The maximum body size (in bytes) to be stored into cache. 26 | MaxBodyBytes uint64 `json:"max_cacheable_body_bytes"` 27 | // Redis provider configuration. 28 | Distributed bool `json:"distributed"` 29 | // Headers to add to the cache key if they are present. 30 | Headers []string `json:"headers"` 31 | // Configure the global key generation. 32 | Key configurationtypes.Key `json:"key"` 33 | // Mode defines if strict or bypass. 34 | Mode string `json:"mode"` 35 | // Olric provider configuration. 36 | Olric configurationtypes.CacheProvider `json:"olric"` 37 | // Redis provider configuration. 38 | Redis configurationtypes.CacheProvider `json:"redis"` 39 | // Etcd provider configuration. 40 | Etcd configurationtypes.CacheProvider `json:"etcd"` 41 | // Nats provider configuration. 42 | Nats configurationtypes.CacheProvider `json:"nats"` 43 | // NutsDB provider configuration. 44 | Nuts configurationtypes.CacheProvider `json:"nuts"` 45 | // Otter provider configuration. 46 | Otter configurationtypes.CacheProvider `json:"otter"` 47 | // Regex to exclude cache. 48 | Regex configurationtypes.Regex `json:"regex"` 49 | // Storage providers chaining and order. 50 | Storers []string `json:"storers"` 51 | // Time before cache or backend access timeout. 52 | Timeout configurationtypes.Timeout `json:"timeout"` 53 | // Time to live. 54 | TTL configurationtypes.Duration `json:"ttl"` 55 | // SimpleFS provider configuration. 56 | SimpleFS configurationtypes.CacheProvider `json:"simplefs"` 57 | // Stale time to live. 58 | Stale configurationtypes.Duration `json:"stale"` 59 | // Disable the coalescing system. 60 | DisableCoalescing bool `json:"disable_coalescing"` 61 | } 62 | 63 | // GetAllowedHTTPVerbs returns the allowed verbs to cache 64 | func (d *DefaultCache) GetAllowedHTTPVerbs() []string { 65 | return d.AllowedHTTPVerbs 66 | } 67 | 68 | // GetBadger returns the Badger configuration 69 | func (d *DefaultCache) GetBadger() configurationtypes.CacheProvider { 70 | return d.Badger 71 | } 72 | 73 | // GetCacheName returns the cache name to use in the Cache-Status response header 74 | func (d *DefaultCache) GetCacheName() string { 75 | return d.CacheName 76 | } 77 | 78 | // GetCDN returns the CDN configuration 79 | func (d *DefaultCache) GetCDN() configurationtypes.CDN { 80 | return d.CDN 81 | } 82 | 83 | // GetDistributed returns if it uses Olric or not as provider 84 | func (d *DefaultCache) GetDistributed() bool { 85 | return d.Distributed 86 | } 87 | 88 | // GetHeaders returns the default headers that should be cached 89 | func (d *DefaultCache) GetHeaders() []string { 90 | return d.Headers 91 | } 92 | 93 | // GetKey returns the default Key generation strategy 94 | func (d *DefaultCache) GetKey() configurationtypes.Key { 95 | return d.Key 96 | } 97 | 98 | // GetEtcd returns etcd configuration 99 | func (d *DefaultCache) GetEtcd() configurationtypes.CacheProvider { 100 | return d.Etcd 101 | } 102 | 103 | // GetMode returns mdoe configuration 104 | func (d *DefaultCache) GetMode() string { 105 | return d.Mode 106 | } 107 | 108 | // GetNats returns nats configuration 109 | func (d *DefaultCache) GetNats() configurationtypes.CacheProvider { 110 | return d.Nats 111 | } 112 | 113 | // GetNuts returns nuts configuration 114 | func (d *DefaultCache) GetNuts() configurationtypes.CacheProvider { 115 | return d.Nuts 116 | } 117 | 118 | // GetOtter returns otter configuration 119 | func (d *DefaultCache) GetOtter() configurationtypes.CacheProvider { 120 | return d.Otter 121 | } 122 | 123 | // GetOlric returns olric configuration 124 | func (d *DefaultCache) GetOlric() configurationtypes.CacheProvider { 125 | return d.Olric 126 | } 127 | 128 | // GetRedis returns redis configuration 129 | func (d *DefaultCache) GetRedis() configurationtypes.CacheProvider { 130 | return d.Redis 131 | } 132 | 133 | // GetRegex returns the regex that shouldn't be cached 134 | func (d *DefaultCache) GetRegex() configurationtypes.Regex { 135 | return d.Regex 136 | } 137 | 138 | // GetSimpleFS returns simplefs configuration 139 | func (d *DefaultCache) GetSimpleFS() configurationtypes.CacheProvider { 140 | return d.SimpleFS 141 | } 142 | 143 | // GetStorers returns the chianed storers 144 | func (d *DefaultCache) GetStorers() []string { 145 | return d.Storers 146 | } 147 | 148 | // GetTimeout returns the backend and cache timeouts 149 | func (d *DefaultCache) GetTimeout() configurationtypes.Timeout { 150 | return d.Timeout 151 | } 152 | 153 | // GetTTL returns the default TTL 154 | func (d *DefaultCache) GetTTL() time.Duration { 155 | return d.TTL.Duration 156 | } 157 | 158 | // GetStale returns the stale duration 159 | func (d *DefaultCache) GetStale() time.Duration { 160 | return d.Stale.Duration 161 | } 162 | 163 | // GetDefaultCacheControl returns the configured default cache control value 164 | func (d *DefaultCache) GetDefaultCacheControl() string { 165 | return d.DefaultCacheControl 166 | } 167 | 168 | // GetMaxBodyBytes returns the maximum body size (in bytes) to be cached 169 | func (d *DefaultCache) GetMaxBodyBytes() uint64 { 170 | return d.MaxBodyBytes 171 | } 172 | 173 | // IsCoalescingDisable returns if the coalescing is disabled 174 | func (d *DefaultCache) IsCoalescingDisable() bool { 175 | return d.DisableCoalescing 176 | } 177 | 178 | // Configuration holder 179 | type Configuration struct { 180 | // Default cache to fallback on when none are redefined. 181 | DefaultCache DefaultCache 182 | // API endpoints enablers. 183 | API configurationtypes.API 184 | // Cache keys configuration. 185 | CacheKeys configurationtypes.CacheKeys `json:"cache_keys"` 186 | // Override the ttl depending the cases. 187 | URLs map[string]configurationtypes.URL 188 | // Logger level, fallback on caddy's one when not redefined. 189 | LogLevel string 190 | // SurrogateKeys contains the surrogate keys to use with a predefined mapping 191 | SurrogateKeys map[string]configurationtypes.SurrogateKeys 192 | logger core.Logger 193 | } 194 | 195 | // GetUrls get the urls list in the configuration 196 | func (c *Configuration) GetUrls() map[string]configurationtypes.URL { 197 | return c.URLs 198 | } 199 | 200 | // GetDefaultCache get the default cache 201 | func (c *Configuration) GetPluginName() string { 202 | return "caddy" 203 | } 204 | 205 | // GetDefaultCache get the default cache 206 | func (c *Configuration) GetDefaultCache() configurationtypes.DefaultCacheInterface { 207 | return &c.DefaultCache 208 | } 209 | 210 | // GetAPI get the default cache 211 | func (c *Configuration) GetAPI() configurationtypes.API { 212 | return c.API 213 | } 214 | 215 | // GetLogLevel get the log level 216 | func (c *Configuration) GetLogLevel() string { 217 | return c.LogLevel 218 | } 219 | 220 | // GetLogger get the logger 221 | func (c *Configuration) GetLogger() core.Logger { 222 | return c.logger 223 | } 224 | 225 | // SetLogger set the logger 226 | func (c *Configuration) SetLogger(l core.Logger) { 227 | c.logger = l 228 | } 229 | 230 | // GetYkeys get the ykeys list 231 | func (c *Configuration) GetYkeys() map[string]configurationtypes.SurrogateKeys { 232 | return nil 233 | } 234 | 235 | // GetSurrogateKeys get the surrogate keys list 236 | func (c *Configuration) GetSurrogateKeys() map[string]configurationtypes.SurrogateKeys { 237 | return nil 238 | } 239 | 240 | // GetCacheKeys get the cache keys rules to override 241 | func (c *Configuration) GetCacheKeys() configurationtypes.CacheKeys { 242 | return c.CacheKeys 243 | } 244 | 245 | var _ configurationtypes.AbstractConfigurationInterface = (*Configuration)(nil) 246 | 247 | func parseCaddyfileRecursively(h *caddyfile.Dispenser) interface{} { 248 | input := make(map[string]interface{}) 249 | for nesting := h.Nesting(); h.NextBlock(nesting); { 250 | val := h.Val() 251 | if val == "}" || val == "{" { 252 | continue 253 | } 254 | args := h.RemainingArgs() 255 | if len(args) == 1 { 256 | input[val] = args[0] 257 | } else if len(args) > 1 { 258 | input[val] = args 259 | } else { 260 | input[val] = parseCaddyfileRecursively(h) 261 | } 262 | } 263 | 264 | return input 265 | } 266 | 267 | func parseBadgerConfiguration(c map[string]interface{}) map[string]interface{} { 268 | for k, v := range c { 269 | switch k { 270 | case "Dir", "ValueDir": 271 | c[k] = v 272 | case "SyncWrites", "ReadOnly", "InMemory", "MetricsEnabled", "CompactL0OnClose", "LmaxCompaction", "VerifyValueChecksum", "BypassLockGuard", "DetectConflicts": 273 | c[k] = true 274 | case "NumVersionsToKeep", "NumGoroutines", "MemTableSize", "BaseTableSize", "BaseLevelSize", "LevelSizeMultiplier", "TableSizeMultiplier", "MaxLevels", "ValueThreshold", "NumMemtables", "BlockSize", "BlockCacheSize", "IndexCacheSize", "NumLevelZeroTables", "NumLevelZeroTablesStall", "ValueLogFileSize", "NumCompactors", "ZSTDCompressionLevel", "ChecksumVerificationMode", "NamespaceOffset": 275 | c[k], _ = strconv.Atoi(v.(string)) 276 | case "Compression", "ValueLogMaxEntries": 277 | c[k], _ = strconv.ParseUint(v.(string), 10, 32) 278 | case "VLogPercentile", "BloomFalsePositive": 279 | c[k], _ = strconv.ParseFloat(v.(string), 64) 280 | case "EncryptionKey": 281 | c[k] = []byte(v.(string)) 282 | case "EncryptionKeyRotationDuration": 283 | c[k], _ = time.ParseDuration(v.(string)) 284 | } 285 | } 286 | 287 | return c 288 | } 289 | 290 | func parseRedisConfiguration(c map[string]interface{}) map[string]interface{} { 291 | for k, v := range c { 292 | switch k { 293 | case "Addrs", "InitAddress": 294 | if s, ok := v.(string); ok { 295 | c[k] = []string{s} 296 | } else { 297 | c[k] = v 298 | } 299 | case "Username", "Password", "ClientName", "ClientSetInfo", "ClientTrackingOptions", "SentinelUsername", "SentinelPassword", "MasterName", "IdentitySuffix": 300 | c[k] = v 301 | case "SendToReplicas", "ShuffleInit", "ClientNoTouch", "DisableRetry", "DisableCache", "AlwaysPipelining", "AlwaysRESP2", "ForceSingleClient", "ReplicaOnly", "ClientNoEvict", "ContextTimeoutEnabled", "PoolFIFO", "ReadOnly", "RouteByLatency", "RouteRandomly", "DisableIndentity": 302 | c[k] = true 303 | case "SelectDB", "CacheSizeEachConn", "RingScaleEachConn", "ReadBufferEachConn", "WriteBufferEachConn", "BlockingPoolSize", "PipelineMultiplex", "DB", "Protocol", "MaxRetries", "PoolSize", "MinIdleConns", "MaxIdleConns", "MaxActiveConns", "MaxRedirects": 304 | if v == false { 305 | c[k] = 0 306 | } else if v == true { 307 | c[k] = 1 308 | } else { 309 | c[k], _ = strconv.Atoi(v.(string)) 310 | } 311 | case "ConnWriteTimeout", "MaxFlushDelay", "MinRetryBackoff", "MaxRetryBackoff", "DialTimeout", "ReadTimeout", "WriteTimeout", "PoolTimeout", "ConnMaxIdleTime", "ConnMaxLifetime": 312 | c[k], _ = time.ParseDuration(v.(string)) 313 | case "MaxVersion", "MinVersion": 314 | strV, _ := v.(string) 315 | if strings.HasPrefix(strV, "TLS") { 316 | strV = strings.Trim(strings.TrimPrefix(strV, "TLS"), " ") 317 | } 318 | 319 | switch strV { 320 | case "0x0300", "SSLv3": 321 | c[k] = 0x0300 322 | case "0x0301", "1.0": 323 | c[k] = 0x0301 324 | case "0x0302", "1.1": 325 | c[k] = 0x0302 326 | case "0x0303", "1.2": 327 | c[k] = 0x0303 328 | case "0x0304", "1.3": 329 | c[k] = 0x0304 330 | } 331 | case "TLSConfig": 332 | c[k] = parseRedisConfiguration(v.(map[string]interface{})) 333 | } 334 | } 335 | 336 | return c 337 | } 338 | 339 | func parseSimpleFSConfiguration(c map[string]interface{}) map[string]interface{} { 340 | for k, v := range c { 341 | switch k { 342 | case "path": 343 | c[k] = v 344 | case "size": 345 | if v == false { 346 | c[k] = 0 347 | } else if v == true { 348 | c[k] = 1 349 | } else { 350 | c[k], _ = strconv.Atoi(v.(string)) 351 | } 352 | } 353 | } 354 | 355 | return c 356 | } 357 | 358 | func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isGlobal bool) error { 359 | for h.Next() { 360 | for nesting := h.Nesting(); h.NextBlock(nesting); { 361 | rootOption := h.Val() 362 | switch rootOption { 363 | case "allowed_http_verbs": 364 | allowed := cfg.DefaultCache.AllowedHTTPVerbs 365 | allowed = append(allowed, h.RemainingArgs()...) 366 | cfg.DefaultCache.AllowedHTTPVerbs = allowed 367 | case "api": 368 | if !isGlobal { 369 | return h.Err("'api' block must be global") 370 | } 371 | apiConfiguration := configurationtypes.API{} 372 | for nesting := h.Nesting(); h.NextBlock(nesting); { 373 | directive := h.Val() 374 | switch directive { 375 | case "basepath": 376 | apiConfiguration.BasePath = h.RemainingArgs()[0] 377 | case "debug": 378 | apiConfiguration.Debug = configurationtypes.APIEndpoint{} 379 | apiConfiguration.Debug.Enable = true 380 | for nesting := h.Nesting(); h.NextBlock(nesting); { 381 | directive := h.Val() 382 | switch directive { 383 | case "basepath": 384 | apiConfiguration.Debug.BasePath = h.RemainingArgs()[0] 385 | default: 386 | return h.Errf("unsupported debug directive: %s", directive) 387 | } 388 | } 389 | case "prometheus": 390 | apiConfiguration.Prometheus = configurationtypes.APIEndpoint{} 391 | apiConfiguration.Prometheus.Enable = true 392 | for nesting := h.Nesting(); h.NextBlock(nesting); { 393 | directive := h.Val() 394 | switch directive { 395 | case "basepath": 396 | apiConfiguration.Prometheus.BasePath = h.RemainingArgs()[0] 397 | default: 398 | return h.Errf("unsupported prometheus directive: %s", directive) 399 | } 400 | } 401 | case "souin": 402 | apiConfiguration.Souin = configurationtypes.APIEndpoint{} 403 | apiConfiguration.Souin.Enable = true 404 | for nesting := h.Nesting(); h.NextBlock(nesting); { 405 | directive := h.Val() 406 | switch directive { 407 | case "basepath": 408 | apiConfiguration.Souin.BasePath = h.RemainingArgs()[0] 409 | default: 410 | return h.Errf("unsupported souin directive: %s", directive) 411 | } 412 | } 413 | default: 414 | return h.Errf("unsupported api directive: %s", directive) 415 | } 416 | } 417 | cfg.API = apiConfiguration 418 | case "badger": 419 | provider := configurationtypes.CacheProvider{Found: true} 420 | for nesting := h.Nesting(); h.NextBlock(nesting); { 421 | directive := h.Val() 422 | switch directive { 423 | case "path": 424 | urlArgs := h.RemainingArgs() 425 | provider.Path = urlArgs[0] 426 | case "configuration": 427 | provider.Configuration = parseCaddyfileRecursively(h) 428 | provider.Configuration = parseBadgerConfiguration(provider.Configuration.(map[string]interface{})) 429 | default: 430 | return h.Errf("unsupported badger directive: %s", directive) 431 | } 432 | } 433 | cfg.DefaultCache.Badger = provider 434 | case "cache_keys": 435 | CacheKeys := cfg.CacheKeys 436 | if CacheKeys == nil { 437 | CacheKeys = make(configurationtypes.CacheKeys, 0) 438 | } 439 | for nesting := h.Nesting(); h.NextBlock(nesting); { 440 | rg := h.Val() 441 | ck := configurationtypes.Key{} 442 | 443 | for nesting := h.Nesting(); h.NextBlock(nesting); { 444 | directive := h.Val() 445 | switch directive { 446 | case "disable_body": 447 | ck.DisableBody = true 448 | case "disable_host": 449 | ck.DisableHost = true 450 | case "disable_method": 451 | ck.DisableMethod = true 452 | case "disable_query": 453 | ck.DisableQuery = true 454 | case "disable_scheme": 455 | ck.DisableScheme = true 456 | case "template": 457 | ck.Template = h.RemainingArgs()[0] 458 | case "hash": 459 | ck.Hash = true 460 | case "hide": 461 | ck.Hide = true 462 | case "headers": 463 | ck.Headers = h.RemainingArgs() 464 | default: 465 | return h.Errf("unsupported cache_keys (%s) directive: %s", rg, directive) 466 | } 467 | } 468 | 469 | CacheKeys = append(CacheKeys, configurationtypes.CacheKey{configurationtypes.RegValue{Regexp: regexp.MustCompile(rg)}: ck}) 470 | } 471 | cfg.CacheKeys = CacheKeys 472 | case "cache_name": 473 | args := h.RemainingArgs() 474 | cfg.DefaultCache.CacheName = args[0] 475 | case "cdn": 476 | cdn := configurationtypes.CDN{ 477 | Dynamic: true, 478 | } 479 | for nesting := h.Nesting(); h.NextBlock(nesting); { 480 | directive := h.Val() 481 | switch directive { 482 | case "api_key": 483 | cdn.APIKey = h.RemainingArgs()[0] 484 | case "dynamic": 485 | cdn.Dynamic = true 486 | args := h.RemainingArgs() 487 | if len(args) > 0 { 488 | cdn.Dynamic, _ = strconv.ParseBool(args[0]) 489 | } 490 | case "email": 491 | cdn.Email = h.RemainingArgs()[0] 492 | case "hostname": 493 | cdn.Hostname = h.RemainingArgs()[0] 494 | case "network": 495 | cdn.Network = h.RemainingArgs()[0] 496 | case "provider": 497 | cdn.Provider = h.RemainingArgs()[0] 498 | case "service_id": 499 | cdn.ServiceID = h.RemainingArgs()[0] 500 | case "strategy": 501 | cdn.Strategy = h.RemainingArgs()[0] 502 | case "zone_id": 503 | cdn.ZoneID = h.RemainingArgs()[0] 504 | default: 505 | return h.Errf("unsupported cdn directive: %s", directive) 506 | } 507 | } 508 | cfg.DefaultCache.CDN = cdn 509 | case "default_cache_control": 510 | args := h.RemainingArgs() 511 | cfg.DefaultCache.DefaultCacheControl = strings.Join(args, " ") 512 | case "max_cacheable_body_bytes": 513 | args := h.RemainingArgs() 514 | maxBodyBytes, err := strconv.ParseUint(args[0], 10, 64) 515 | if err != nil { 516 | return h.Errf("unsupported max_cacheable_body_bytes: %s", args) 517 | } else { 518 | cfg.DefaultCache.MaxBodyBytes = maxBodyBytes 519 | } 520 | case "etcd": 521 | cfg.DefaultCache.Distributed = true 522 | provider := configurationtypes.CacheProvider{Found: true} 523 | for nesting := h.Nesting(); h.NextBlock(nesting); { 524 | directive := h.Val() 525 | switch directive { 526 | case "configuration": 527 | provider.Configuration = parseCaddyfileRecursively(h) 528 | default: 529 | return h.Errf("unsupported etcd directive: %s", directive) 530 | } 531 | } 532 | cfg.DefaultCache.Etcd = provider 533 | case "headers": 534 | cfg.DefaultCache.Headers = append(cfg.DefaultCache.Headers, h.RemainingArgs()...) 535 | case "key": 536 | config_key := configurationtypes.Key{} 537 | for nesting := h.Nesting(); h.NextBlock(nesting); { 538 | directive := h.Val() 539 | switch directive { 540 | case "disable_body": 541 | config_key.DisableBody = true 542 | case "disable_host": 543 | config_key.DisableHost = true 544 | case "disable_method": 545 | config_key.DisableMethod = true 546 | case "disable_query": 547 | config_key.DisableQuery = true 548 | case "disable_scheme": 549 | config_key.DisableScheme = true 550 | case "template": 551 | config_key.Template = h.RemainingArgs()[0] 552 | case "hash": 553 | config_key.Hash = true 554 | case "hide": 555 | config_key.Hide = true 556 | case "headers": 557 | config_key.Headers = h.RemainingArgs() 558 | default: 559 | return h.Errf("unsupported key directive: %s", directive) 560 | } 561 | } 562 | cfg.DefaultCache.Key = config_key 563 | case "log_level": 564 | args := h.RemainingArgs() 565 | cfg.LogLevel = args[0] 566 | case "mode": 567 | args := h.RemainingArgs() 568 | if len(args) > 1 { 569 | return h.Errf("mode must contains only one arg: %s given", args) 570 | } 571 | cfg.DefaultCache.Mode = args[0] 572 | case "nats": 573 | provider := configurationtypes.CacheProvider{Found: true} 574 | for nesting := h.Nesting(); h.NextBlock(nesting); { 575 | directive := h.Val() 576 | switch directive { 577 | case "url": 578 | urlArgs := h.RemainingArgs() 579 | provider.URL = urlArgs[0] 580 | case "configuration": 581 | provider.Configuration = parseCaddyfileRecursively(h) 582 | default: 583 | return h.Errf("unsupported nats directive: %s", directive) 584 | } 585 | } 586 | cfg.DefaultCache.Nats = provider 587 | case "nuts": 588 | provider := configurationtypes.CacheProvider{Found: true} 589 | for nesting := h.Nesting(); h.NextBlock(nesting); { 590 | directive := h.Val() 591 | switch directive { 592 | case "url": 593 | urlArgs := h.RemainingArgs() 594 | provider.URL = urlArgs[0] 595 | case "path": 596 | urlArgs := h.RemainingArgs() 597 | provider.Path = urlArgs[0] 598 | case "configuration": 599 | provider.Configuration = parseCaddyfileRecursively(h) 600 | default: 601 | return h.Errf("unsupported nuts directive: %s", directive) 602 | } 603 | } 604 | cfg.DefaultCache.Nuts = provider 605 | case "otter": 606 | provider := configurationtypes.CacheProvider{Found: true} 607 | for nesting := h.Nesting(); h.NextBlock(nesting); { 608 | directive := h.Val() 609 | switch directive { 610 | case "configuration": 611 | provider.Configuration = parseCaddyfileRecursively(h) 612 | default: 613 | return h.Errf("unsupported otter directive: %s", directive) 614 | } 615 | } 616 | cfg.DefaultCache.Otter = provider 617 | case "olric": 618 | cfg.DefaultCache.Distributed = true 619 | provider := configurationtypes.CacheProvider{Found: true} 620 | for nesting := h.Nesting(); h.NextBlock(nesting); { 621 | directive := h.Val() 622 | switch directive { 623 | case "url": 624 | urlArgs := h.RemainingArgs() 625 | provider.URL = urlArgs[0] 626 | case "path": 627 | urlArgs := h.RemainingArgs() 628 | provider.Path = urlArgs[0] 629 | case "configuration": 630 | provider.Configuration = parseCaddyfileRecursively(h) 631 | default: 632 | return h.Errf("unsupported olric directive: %s", directive) 633 | } 634 | } 635 | cfg.DefaultCache.Olric = provider 636 | case "redis": 637 | cfg.DefaultCache.Distributed = true 638 | provider := configurationtypes.CacheProvider{Found: true} 639 | for nesting := h.Nesting(); h.NextBlock(nesting); { 640 | directive := h.Val() 641 | switch directive { 642 | case "url": 643 | urlArgs := h.RemainingArgs() 644 | provider.URL = urlArgs[0] 645 | case "path": 646 | urlArgs := h.RemainingArgs() 647 | provider.Path = urlArgs[0] 648 | case "configuration": 649 | provider.Configuration = parseCaddyfileRecursively(h) 650 | provider.Configuration = parseRedisConfiguration(provider.Configuration.(map[string]interface{})) 651 | default: 652 | return h.Errf("unsupported redis directive: %s", directive) 653 | } 654 | } 655 | cfg.DefaultCache.Redis = provider 656 | case "regex": 657 | for nesting := h.Nesting(); h.NextBlock(nesting); { 658 | directive := h.Val() 659 | switch directive { 660 | case "exclude": 661 | cfg.DefaultCache.Regex.Exclude = h.RemainingArgs()[0] 662 | default: 663 | return h.Errf("unsupported regex directive: %s", directive) 664 | } 665 | } 666 | case "simplefs": 667 | provider := configurationtypes.CacheProvider{Found: true} 668 | for nesting := h.Nesting(); h.NextBlock(nesting); { 669 | directive := h.Val() 670 | switch directive { 671 | case "path": 672 | urlArgs := h.RemainingArgs() 673 | provider.Path = urlArgs[0] 674 | case "configuration": 675 | provider.Configuration = parseCaddyfileRecursively(h) 676 | provider.Configuration = parseSimpleFSConfiguration(provider.Configuration.(map[string]interface{})) 677 | default: 678 | return h.Errf("unsupported simplefs directive: %s", directive) 679 | } 680 | } 681 | cfg.DefaultCache.SimpleFS = provider 682 | case "stale": 683 | args := h.RemainingArgs() 684 | stale, err := time.ParseDuration(args[0]) 685 | if err == nil { 686 | cfg.DefaultCache.Stale.Duration = stale 687 | } 688 | case "storers": 689 | args := h.RemainingArgs() 690 | cfg.DefaultCache.Storers = args 691 | case "timeout": 692 | timeout := configurationtypes.Timeout{} 693 | for nesting := h.Nesting(); h.NextBlock(nesting); { 694 | directive := h.Val() 695 | switch directive { 696 | case "backend": 697 | d := configurationtypes.Duration{} 698 | ttl, err := time.ParseDuration(h.RemainingArgs()[0]) 699 | if err == nil { 700 | d.Duration = ttl 701 | } 702 | timeout.Backend = d 703 | case "cache": 704 | d := configurationtypes.Duration{} 705 | ttl, err := time.ParseDuration(h.RemainingArgs()[0]) 706 | if err == nil { 707 | d.Duration = ttl 708 | } 709 | timeout.Cache = d 710 | default: 711 | return h.Errf("unsupported timeout directive: %s", directive) 712 | } 713 | } 714 | cfg.DefaultCache.Timeout = timeout 715 | case "ttl": 716 | args := h.RemainingArgs() 717 | ttl, err := time.ParseDuration(args[0]) 718 | if err == nil { 719 | cfg.DefaultCache.TTL.Duration = ttl 720 | } 721 | case "disable_coalescing": 722 | cfg.DefaultCache.DisableCoalescing = true 723 | default: 724 | return h.Errf("unsupported root directive: %s", rootOption) 725 | } 726 | } 727 | } 728 | 729 | return nil 730 | } 731 | -------------------------------------------------------------------------------- /dispatch.go: -------------------------------------------------------------------------------- 1 | package httpcache 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/caddyserver/caddy/v2" 9 | ) 10 | 11 | func (s *SouinCaddyMiddleware) parseStorages(ctx caddy.Context) { 12 | if s.Configuration.DefaultCache.Badger.Found { 13 | e := dispatchStorage(ctx, "badger", s.Configuration.DefaultCache.Badger, s.Configuration.DefaultCache.GetStale()) 14 | if e != nil { 15 | s.logger.Errorf("Error during Badger init, did you include the Badger storage (--with github.com/darkweak/storages/badger/caddy)? %v", e) 16 | } else { 17 | badger := s.Configuration.DefaultCache.Badger 18 | dir := "" 19 | vdir := "" 20 | if c := badger.Configuration; c != nil { 21 | p, ok := c.(map[string]interface{}) 22 | if ok { 23 | if d, ok := p["Dir"]; ok { 24 | dir = fmt.Sprint(d) 25 | vdir = fmt.Sprint(d) 26 | } 27 | if d, ok := p["ValueDir"]; ok { 28 | vdir = fmt.Sprint(d) 29 | } 30 | } 31 | } 32 | s.Configuration.DefaultCache.Badger.Uuid = fmt.Sprintf( 33 | "BADGER-%s-%s-%s", 34 | dir, 35 | vdir, 36 | s.Configuration.DefaultCache.GetStale(), 37 | ) 38 | } 39 | } 40 | if s.Configuration.DefaultCache.Etcd.Found { 41 | e := dispatchStorage(ctx, "etcd", s.Configuration.DefaultCache.Etcd, s.Configuration.DefaultCache.GetStale()) 42 | if e != nil { 43 | s.logger.Errorf("Error during Etcd init, did you include the Etcd storage (--with github.com/darkweak/storages/etcd/caddy)? %v", e) 44 | } else { 45 | etcd := s.Configuration.DefaultCache.Etcd 46 | endpoints := etcd.URL 47 | username := "" 48 | password := "" 49 | if c := etcd.Configuration; c != nil { 50 | p, ok := c.(map[string]interface{}) 51 | if ok { 52 | if d, ok := p["Endpoints"]; ok { 53 | endpoints = fmt.Sprint(d) 54 | } 55 | if d, ok := p["Username"]; ok { 56 | username = fmt.Sprint(d) 57 | } 58 | if d, ok := p["Password"]; ok { 59 | password = fmt.Sprint(d) 60 | } 61 | } 62 | } 63 | s.Configuration.DefaultCache.Etcd.Uuid = fmt.Sprintf( 64 | "ETCD-%s-%s-%s-%s", 65 | endpoints, 66 | username, 67 | password, 68 | s.Configuration.DefaultCache.GetStale(), 69 | ) 70 | } 71 | } 72 | if s.Configuration.DefaultCache.Nats.Found { 73 | e := dispatchStorage(ctx, "nats", s.Configuration.DefaultCache.Nats, s.Configuration.DefaultCache.GetStale()) 74 | if e != nil { 75 | s.logger.Errorf("Error during Nats init, did you include the Nats storage (--with github.com/darkweak/storages/nats/caddy)? %v", e) 76 | } else { 77 | s.Configuration.DefaultCache.Nats.Uuid = fmt.Sprintf("NATS-%s-%s", s.Configuration.DefaultCache.Nats.URL, s.Configuration.DefaultCache.GetStale()) 78 | } 79 | } 80 | if s.Configuration.DefaultCache.Nuts.Found { 81 | e := dispatchStorage(ctx, "nuts", s.Configuration.DefaultCache.Nuts, s.Configuration.DefaultCache.GetStale()) 82 | if e != nil { 83 | s.logger.Errorf("Error during Nuts init, did you include the Nuts storage (--with github.com/darkweak/storages/nuts/caddy)? %v", e) 84 | } else { 85 | nuts := s.Configuration.DefaultCache.Nuts 86 | dir := "/tmp/souin-nuts" 87 | if c := nuts.Configuration; c != nil { 88 | p, ok := c.(map[string]interface{}) 89 | if ok { 90 | if d, ok := p["Dir"]; ok { 91 | dir = fmt.Sprint(d) 92 | } 93 | } 94 | } else if nuts.Path != "" { 95 | dir = nuts.Path 96 | } 97 | s.Configuration.DefaultCache.Nuts.Uuid = fmt.Sprintf("NUTS-%s-%s", dir, s.Configuration.DefaultCache.GetStale()) 98 | } 99 | } 100 | if s.Configuration.DefaultCache.Olric.Found { 101 | e := dispatchStorage(ctx, "olric", s.Configuration.DefaultCache.Olric, s.Configuration.DefaultCache.GetStale()) 102 | if e != nil { 103 | s.logger.Errorf("Error during Olric init, did you include the Olric storage (--with github.com/darkweak/storages/olric/caddy)? %v", e) 104 | } else { 105 | s.Configuration.DefaultCache.Olric.Uuid = fmt.Sprintf("OLRIC-%s-%s", s.Configuration.DefaultCache.Olric.URL, s.Configuration.DefaultCache.GetStale()) 106 | } 107 | } 108 | if s.Configuration.DefaultCache.Otter.Found { 109 | e := dispatchStorage(ctx, "otter", s.Configuration.DefaultCache.Otter, s.Configuration.DefaultCache.GetStale()) 110 | if e != nil { 111 | s.logger.Errorf("Error during Otter init, did you include the Otter storage (--with github.com/darkweak/storages/otter/caddy)? %v", e) 112 | } else { 113 | s.Configuration.DefaultCache.Otter.Uuid = fmt.Sprintf("OTTER-%s", s.Configuration.DefaultCache.GetStale()) 114 | } 115 | } 116 | if s.Configuration.DefaultCache.Redis.Found { 117 | e := dispatchStorage(ctx, "redis", s.Configuration.DefaultCache.Redis, s.Configuration.DefaultCache.GetStale()) 118 | if e != nil { 119 | s.logger.Errorf("Error during Redis init, did you include the Redis storage (--with github.com/darkweak/storages/redis/caddy or github.com/darkweak/storages/go-redis/caddy)? %v", e) 120 | } else { 121 | redis := s.Configuration.DefaultCache.Redis 122 | address := redis.URL 123 | username := "" 124 | dbname := "0" 125 | cname := "souin-redis" 126 | if c := redis.Configuration; c != nil { 127 | p, ok := c.(map[string]interface{}) 128 | if ok { 129 | // shared between go-redis and rueidis 130 | if d, ok := p["Username"]; ok { 131 | username = fmt.Sprint(d) 132 | } 133 | if d, ok := p["ClientName"]; ok { 134 | cname = fmt.Sprint(d) 135 | } 136 | 137 | // rueidis 138 | if d, ok := p["InitAddress"]; ok { 139 | elements := make([]string, 0) 140 | 141 | for _, elt := range d.([]interface{}) { 142 | elements = append(elements, elt.(string)) 143 | } 144 | 145 | address = strings.Join(elements, ",") 146 | } 147 | if d, ok := p["SelectDB"]; ok { 148 | dbname = fmt.Sprint(d) 149 | } 150 | 151 | // go-redis 152 | if d, ok := p["Addrs"]; ok { 153 | elements := make([]string, 0) 154 | 155 | for _, elt := range d.([]interface{}) { 156 | elements = append(elements, elt.(string)) 157 | } 158 | 159 | address = strings.Join(elements, ",") 160 | } 161 | if d, ok := p["DB"]; ok { 162 | dbname = fmt.Sprint(d) 163 | } 164 | } 165 | } 166 | s.Configuration.DefaultCache.Redis.Uuid = fmt.Sprintf( 167 | "REDIS-%s-%s-%s-%s-%s", 168 | address, 169 | username, 170 | dbname, 171 | cname, 172 | s.Configuration.DefaultCache.GetStale(), 173 | ) 174 | } 175 | } 176 | if s.Configuration.DefaultCache.SimpleFS.Found { 177 | e := dispatchStorage(ctx, "simplefs", s.Configuration.DefaultCache.SimpleFS, s.Configuration.DefaultCache.GetStale()) 178 | if e != nil { 179 | s.logger.Errorf("Error during SimpleFS init, did you include the SimpleFS storage (--with github.com/darkweak/storages/simplefs/caddy)? %v", e) 180 | } else { 181 | simplefs := s.Configuration.DefaultCache.SimpleFS 182 | path := simplefs.Path 183 | size := "0" 184 | if c := simplefs.Configuration; c != nil { 185 | p, ok := c.(map[string]interface{}) 186 | if ok { 187 | if d, ok := p["path"]; path == "" && ok { 188 | path = fmt.Sprint(d) 189 | } 190 | if d, ok := p["size"]; ok { 191 | size = fmt.Sprint(d) 192 | } 193 | } 194 | } 195 | 196 | if path == "" { 197 | path, _ = os.Getwd() 198 | } 199 | 200 | s.Configuration.DefaultCache.SimpleFS.Uuid = fmt.Sprintf( 201 | "SIMPLEFS-%s-%s", 202 | path, 203 | size, 204 | ) 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /examples/Caddyfile-embedded-olric: -------------------------------------------------------------------------------- 1 | { 2 | order cache before rewrite 3 | cache { 4 | headers Content-Type Authorization 5 | log_level info 6 | olric { 7 | path path/to/olric.yml 8 | } 9 | ttl 1000s 10 | } 11 | } 12 | 13 | 80 14 | 15 | @match path /test1* 16 | @match2 path /test2* 17 | @matchdefault path /default 18 | 19 | cache @match { 20 | ttl 30s 21 | headers Cookie 22 | } 23 | 24 | cache @match2 { 25 | ttl 50s 26 | headers Authorization 27 | } 28 | 29 | cache @matchdefault { 30 | ttl 25s 31 | } 32 | 33 | cache * { 34 | } 35 | -------------------------------------------------------------------------------- /examples/Caddyfile-file-configuration-olric: -------------------------------------------------------------------------------- 1 | { 2 | order cache before rewrite 3 | cache { 4 | headers Content-Type Authorization 5 | log_level info 6 | olric { 7 | path path/to/olric.yml 8 | } 9 | ttl 1000s 10 | } 11 | } 12 | 13 | 80 14 | 15 | @match path /test1* 16 | @match2 path /test2* 17 | @matchdefault path /default 18 | 19 | cache @match { 20 | ttl 30s 21 | headers Cookie 22 | } 23 | 24 | cache @match2 { 25 | ttl 50s 26 | headers Authorization 27 | } 28 | 29 | cache @matchdefault { 30 | ttl 25s 31 | } 32 | 33 | cache * { 34 | } 35 | -------------------------------------------------------------------------------- /examples/Caddyfile-minimal: -------------------------------------------------------------------------------- 1 | { 2 | order cache before rewrite 3 | cache # Enables the cache for all requests for 2 minutes by default and stores in the badger provider 4 | } 5 | 6 | :80 7 | -------------------------------------------------------------------------------- /examples/Caddyfile-not-distributed: -------------------------------------------------------------------------------- 1 | { 2 | order cache before rewrite 3 | cache { 4 | headers Content-Type Authorization 5 | log_level info 6 | ttl 1000s 7 | } 8 | } 9 | 10 | 80 11 | 12 | @match path /test1* 13 | @match2 path /test2* 14 | @matchdefault path /default 15 | 16 | cache @match { 17 | ttl 30s 18 | headers Cookie 19 | } 20 | 21 | cache @match2 { 22 | ttl 50s 23 | headers Authorization 24 | } 25 | 26 | cache @matchdefault { 27 | ttl 25s 28 | } 29 | 30 | cache * { 31 | } 32 | -------------------------------------------------------------------------------- /examples/Caddyfile-remote-olric-cluster: -------------------------------------------------------------------------------- 1 | { 2 | order cache before rewrite 3 | cache { 4 | headers Content-Type Authorization 5 | log_level info 6 | olric { 7 | url olric:3320 8 | } 9 | ttl 1000s 10 | } 11 | } 12 | 13 | 80 14 | 15 | @match path /test1* 16 | @match2 path /test2* 17 | @matchdefault path /default 18 | 19 | cache @match { 20 | ttl 30s 21 | headers Cookie 22 | } 23 | 24 | cache @match2 { 25 | ttl 50s 26 | headers Authorization 27 | } 28 | 29 | cache @matchdefault { 30 | ttl 25s 31 | } 32 | 33 | cache * { 34 | } 35 | -------------------------------------------------------------------------------- /examples/configuration-embedded-olric.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": { 3 | "cache": { 4 | "headers": [ 5 | "Content-Type", 6 | "Authorization" 7 | ], 8 | "log_level": "info", 9 | "olric": { 10 | "path": "path/to/olric.yml" 11 | }, 12 | "ttl": "1000s" 13 | }, 14 | "http": { 15 | "servers": { 16 | "": { 17 | "listen": [":80"], 18 | "routes": [ 19 | { 20 | "match": [ 21 | { 22 | "header": { 23 | "Content-Type": ["*"] 24 | }, 25 | "path": [ 26 | "/a*" 27 | ] 28 | } 29 | ], 30 | "handle": [ 31 | { 32 | "handler": "cache", 33 | "ttl": "30s" 34 | } 35 | ] 36 | }, 37 | { 38 | "match": [ 39 | { 40 | "header": { 41 | "Content-Type": ["*"] 42 | }, 43 | "path": [ 44 | "/b*" 45 | ] 46 | } 47 | ], 48 | "handle": [ 49 | { 50 | "handler": "cache", 51 | "headers": [] 52 | } 53 | ] 54 | }, 55 | { 56 | "match": [ 57 | { 58 | "header": { 59 | "Content-Type": ["*"] 60 | }, 61 | "path": [ 62 | "*" 63 | ] 64 | } 65 | ], 66 | "handle": [ 67 | { 68 | "handler": "cache" 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /examples/configuration-file-configuration-olric.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": { 3 | "cache": { 4 | "headers": [ 5 | "Content-Type", 6 | "Authorization" 7 | ], 8 | "log_level": "info", 9 | "olric": { 10 | "path": "path/to/olric.yml" 11 | }, 12 | "ttl": "1000s" 13 | }, 14 | "http": { 15 | "servers": { 16 | "": { 17 | "listen": [":80"], 18 | "routes": [ 19 | { 20 | "match": [ 21 | { 22 | "header": { 23 | "Content-Type": ["*"] 24 | }, 25 | "path": [ 26 | "/a*" 27 | ] 28 | } 29 | ], 30 | "handle": [ 31 | { 32 | "handler": "cache", 33 | "ttl": "30s" 34 | } 35 | ] 36 | }, 37 | { 38 | "match": [ 39 | { 40 | "header": { 41 | "Content-Type": ["*"] 42 | }, 43 | "path": [ 44 | "/b*" 45 | ] 46 | } 47 | ], 48 | "handle": [ 49 | { 50 | "handler": "cache", 51 | "headers": [] 52 | } 53 | ] 54 | }, 55 | { 56 | "match": [ 57 | { 58 | "header": { 59 | "Content-Type": ["*"] 60 | }, 61 | "path": [ 62 | "*" 63 | ] 64 | } 65 | ], 66 | "handle": [ 67 | { 68 | "handler": "cache" 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /examples/configuration-not-distributed.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": { 3 | "cache": { 4 | "headers": [ 5 | "Content-Type", 6 | "Authorization" 7 | ], 8 | "log_level": "info", 9 | "ttl": "1000s" 10 | }, 11 | "http": { 12 | "servers": { 13 | "": { 14 | "listen": [":80"], 15 | "routes": [ 16 | { 17 | "match": [ 18 | { 19 | "header": { 20 | "Content-Type": ["*"] 21 | }, 22 | "path": [ 23 | "/a*" 24 | ] 25 | } 26 | ], 27 | "handle": [ 28 | { 29 | "handler": "cache", 30 | "ttl": "30s" 31 | } 32 | ] 33 | }, 34 | { 35 | "match": [ 36 | { 37 | "header": { 38 | "Content-Type": ["*"] 39 | }, 40 | "path": [ 41 | "/b*" 42 | ] 43 | } 44 | ], 45 | "handle": [ 46 | { 47 | "handler": "cache", 48 | "headers": [] 49 | } 50 | ] 51 | }, 52 | { 53 | "match": [ 54 | { 55 | "header": { 56 | "Content-Type": ["*"] 57 | }, 58 | "path": [ 59 | "*" 60 | ] 61 | } 62 | ], 63 | "handle": [ 64 | { 65 | "handler": "cache" 66 | } 67 | ] 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/configuration-remote-olric-cluster.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": { 3 | "cache": { 4 | "headers": [ 5 | "Content-Type", 6 | "Authorization" 7 | ], 8 | "log_level": "info", 9 | "olric": { 10 | "url": "olric:3320" 11 | }, 12 | "ttl": "1000s" 13 | }, 14 | "http": { 15 | "servers": { 16 | "": { 17 | "listen": [":80"], 18 | "routes": [ 19 | { 20 | "match": [ 21 | { 22 | "header": { 23 | "Content-Type": ["*"] 24 | }, 25 | "path": [ 26 | "/a*" 27 | ] 28 | } 29 | ], 30 | "handle": [ 31 | { 32 | "handler": "cache", 33 | "ttl": "30s" 34 | } 35 | ] 36 | }, 37 | { 38 | "match": [ 39 | { 40 | "header": { 41 | "Content-Type": ["*"] 42 | }, 43 | "path": [ 44 | "/b*" 45 | ] 46 | } 47 | ], 48 | "handle": [ 49 | { 50 | "handler": "cache", 51 | "headers": [] 52 | } 53 | ] 54 | }, 55 | { 56 | "match": [ 57 | { 58 | "header": { 59 | "Content-Type": ["*"] 60 | }, 61 | "path": [ 62 | "*" 63 | ] 64 | } 65 | ], 66 | "handle": [ 67 | { 68 | "handler": "cache" 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /fixtures/cache-tests/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | debug 3 | experimental_http3 4 | cache { 5 | olric_config fixtures/olricd.yaml 6 | } 7 | } 8 | 9 | localhost 10 | 11 | route * { 12 | cache 13 | reverse_proxy 127.0.0.1:8000 14 | } 15 | -------------------------------------------------------------------------------- /fixtures/cache-tests/README.md: -------------------------------------------------------------------------------- 1 | # Cache-Tests 2 | 3 | Setup https://github.com/http-tests/cache-tests 4 | 5 | 1) run the server `npm run server` 6 | 2) run the tests `NODE_TLS_REJECT_UNAUTHORIZED=0 npm run cli --base=http://localhost --silent > results/caddy-cache-handler.json` 7 | 3) to check the results you may want to add this to the `results/index.mjs`: 8 | 9 | ``` 10 | { 11 | file: 'caddy-cache-handler.json', 12 | name: 'Caddy', 13 | type: 'rev-proxy', 14 | version: 'dev' 15 | } 16 | ``` 17 | 18 | 4) Open https://localhost 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/caddyserver/cache-handler 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/caddyserver/caddy/v2 v2.8.4 7 | github.com/darkweak/souin v1.7.5 8 | github.com/darkweak/storages/core v0.0.11 9 | ) 10 | 11 | require ( 12 | filippo.io/edwards25519 v1.1.0 // indirect 13 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect 14 | github.com/BurntSushi/toml v1.3.2 // indirect 15 | github.com/Masterminds/goutils v1.1.1 // indirect 16 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 17 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 18 | github.com/Microsoft/go-winio v0.6.0 // indirect 19 | github.com/alecthomas/chroma/v2 v2.13.0 // indirect 20 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 21 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/caddyserver/certmagic v0.21.3 // indirect 24 | github.com/caddyserver/zerossl v0.1.3 // indirect 25 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 26 | github.com/cespare/xxhash v1.1.0 // indirect 27 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 28 | github.com/chzyer/readline v1.5.1 // indirect 29 | github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect 30 | github.com/darkweak/go-esi v0.0.5 // indirect 31 | github.com/dgraph-io/badger v1.6.2 // indirect 32 | github.com/dgraph-io/badger/v2 v2.2007.4 // indirect 33 | github.com/dgraph-io/ristretto v0.1.1 // indirect 34 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect 35 | github.com/dlclark/regexp2 v1.11.0 // indirect 36 | github.com/dustin/go-humanize v1.0.1 // indirect 37 | github.com/felixge/httpsnoop v1.0.4 // indirect 38 | github.com/fxamacker/cbor/v2 v2.6.0 // indirect 39 | github.com/go-chi/chi/v5 v5.0.12 // indirect 40 | github.com/go-jose/go-jose/v3 v3.0.3 // indirect 41 | github.com/go-kit/kit v0.13.0 // indirect 42 | github.com/go-kit/log v0.2.1 // indirect 43 | github.com/go-logfmt/logfmt v0.6.0 // indirect 44 | github.com/go-logr/logr v1.4.1 // indirect 45 | github.com/go-logr/stdr v1.2.2 // indirect 46 | github.com/go-sql-driver/mysql v1.7.1 // indirect 47 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 48 | github.com/golang/glog v1.2.0 // indirect 49 | github.com/golang/protobuf v1.5.4 // indirect 50 | github.com/golang/snappy v0.0.4 // indirect 51 | github.com/google/cel-go v0.20.1 // indirect 52 | github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect 53 | github.com/google/go-tpm v0.9.0 // indirect 54 | github.com/google/go-tspi v0.3.0 // indirect 55 | github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect 56 | github.com/google/uuid v1.6.0 // indirect 57 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect 58 | github.com/huandu/xstrings v1.3.3 // indirect 59 | github.com/imdario/mergo v0.3.13 // indirect 60 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 61 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 62 | github.com/jackc/pgconn v1.14.3 // indirect 63 | github.com/jackc/pgio v1.0.0 // indirect 64 | github.com/jackc/pgpassfile v1.0.0 // indirect 65 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 66 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 67 | github.com/jackc/pgtype v1.14.0 // indirect 68 | github.com/jackc/pgx/v4 v4.18.3 // indirect 69 | github.com/klauspost/compress v1.17.8 // indirect 70 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 71 | github.com/libdns/libdns v0.2.2 // indirect 72 | github.com/manifoldco/promptui v0.9.0 // indirect 73 | github.com/mattn/go-colorable v0.1.13 // indirect 74 | github.com/mattn/go-isatty v0.0.20 // indirect 75 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 76 | github.com/mholt/acmez/v2 v2.0.1 // indirect 77 | github.com/miekg/dns v1.1.59 // indirect 78 | github.com/mitchellh/copystructure v1.2.0 // indirect 79 | github.com/mitchellh/go-ps v1.0.0 // indirect 80 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 81 | github.com/onsi/ginkgo/v2 v2.15.0 // indirect 82 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 83 | github.com/pires/go-proxyproto v0.7.0 // indirect 84 | github.com/pkg/errors v0.9.1 // indirect 85 | github.com/pquerna/cachecontrol v0.2.0 // indirect 86 | github.com/prometheus/client_golang v1.19.1 // indirect 87 | github.com/prometheus/client_model v0.5.0 // indirect 88 | github.com/prometheus/common v0.48.0 // indirect 89 | github.com/prometheus/procfs v0.12.0 // indirect 90 | github.com/quic-go/qpack v0.4.0 // indirect 91 | github.com/quic-go/quic-go v0.44.0 // indirect 92 | github.com/rs/xid v1.5.0 // indirect 93 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 94 | github.com/shopspring/decimal v1.2.0 // indirect 95 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 96 | github.com/sirupsen/logrus v1.9.3 // indirect 97 | github.com/slackhq/nebula v1.6.1 // indirect 98 | github.com/smallstep/certificates v0.26.1 // indirect 99 | github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect 100 | github.com/smallstep/nosql v0.6.1 // indirect 101 | github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect 102 | github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect 103 | github.com/smallstep/truststore v0.13.0 // indirect 104 | github.com/spf13/cast v1.4.1 // indirect 105 | github.com/spf13/cobra v1.8.0 // indirect 106 | github.com/spf13/pflag v1.0.5 // indirect 107 | github.com/stoewer/go-strcase v1.2.0 // indirect 108 | github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect 109 | github.com/urfave/cli v1.22.14 // indirect 110 | github.com/x448/float16 v0.8.4 // indirect 111 | github.com/yuin/goldmark v1.7.1 // indirect 112 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect 113 | github.com/zeebo/blake3 v0.2.3 // indirect 114 | go.etcd.io/bbolt v1.3.9 // indirect 115 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 116 | go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 // indirect 117 | go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect 118 | go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect 119 | go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect 120 | go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect 121 | go.opentelemetry.io/otel v1.24.0 // indirect 122 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect 123 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect 124 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 125 | go.opentelemetry.io/otel/sdk v1.21.0 // indirect 126 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 127 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 128 | go.step.sm/cli-utils v0.9.0 // indirect 129 | go.step.sm/crypto v0.45.0 // indirect 130 | go.step.sm/linkedca v0.20.1 // indirect 131 | go.uber.org/automaxprocs v1.5.3 // indirect 132 | go.uber.org/mock v0.4.0 // indirect 133 | go.uber.org/multierr v1.11.0 // indirect 134 | go.uber.org/zap v1.27.0 // indirect 135 | go.uber.org/zap/exp v0.2.0 // indirect 136 | golang.org/x/crypto v0.23.0 // indirect 137 | golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect 138 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect 139 | golang.org/x/mod v0.17.0 // indirect 140 | golang.org/x/net v0.25.0 // indirect 141 | golang.org/x/sync v0.7.0 // indirect 142 | golang.org/x/sys v0.20.0 // indirect 143 | golang.org/x/term v0.20.0 // indirect 144 | golang.org/x/text v0.15.0 // indirect 145 | golang.org/x/time v0.5.0 // indirect 146 | golang.org/x/tools v0.21.0 // indirect 147 | google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae // indirect 148 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect 149 | google.golang.org/grpc v1.63.2 // indirect 150 | google.golang.org/protobuf v1.34.2 // indirect 151 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 152 | gopkg.in/yaml.v3 v3.0.1 // indirect 153 | howett.net/plist v1.0.0 // indirect 154 | ) 155 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= 2 | cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg= 3 | cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= 4 | cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= 5 | cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= 6 | cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= 7 | cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= 8 | cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 9 | cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= 10 | cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= 11 | cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY= 12 | cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc= 13 | cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= 14 | cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= 15 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 16 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 17 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= 18 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 19 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 20 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 21 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 22 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 23 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 24 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 25 | github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= 26 | github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 27 | github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= 28 | github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= 29 | github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= 30 | github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= 31 | github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= 32 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 33 | github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= 34 | github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 35 | github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= 36 | github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI= 37 | github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= 38 | github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 39 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 40 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 41 | github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= 42 | github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 43 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 44 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= 45 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= 46 | github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= 47 | github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= 48 | github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A= 49 | github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs= 50 | github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE= 51 | github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI= 52 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= 53 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= 54 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= 55 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= 56 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= 57 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= 58 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 59 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 60 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= 61 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= 62 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= 63 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= 64 | github.com/aws/aws-sdk-go-v2/service/kms v1.31.1 h1:5wtyAwuUiJiM3DHYeGZmP5iMonM7DFBWAEaaVPHYZA0= 65 | github.com/aws/aws-sdk-go-v2/service/kms v1.31.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ= 66 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs= 67 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= 68 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw= 69 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= 70 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns= 71 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= 72 | github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= 73 | github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 74 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 75 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 76 | github.com/caddyserver/caddy/v2 v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk= 77 | github.com/caddyserver/caddy/v2 v2.8.4/go.mod h1:vmDAHp3d05JIvuhc24LmnxVlsZmWnUwbP5WMjzcMPWw= 78 | github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0= 79 | github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI= 80 | github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= 81 | github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= 82 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 83 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 84 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 85 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 86 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 87 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 88 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 89 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 90 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 91 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 92 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 93 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 94 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 95 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 96 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 97 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 98 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 99 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 100 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 101 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 102 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 103 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 104 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 105 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 106 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 107 | github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= 108 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 109 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 110 | github.com/darkweak/go-esi v0.0.5 h1:b9LHI8Tz46R+i6p8avKPHAIBRQUCZDebNmKm5w/Zrns= 111 | github.com/darkweak/go-esi v0.0.5/go.mod h1:koCJqwum1u6mslyZuq/Phm6hfG1K3ZK5Y7jrUBTH654= 112 | github.com/darkweak/souin v1.7.5 h1:drNhZc0GhSbGcugiGfcYdLDTcx3DCZW6o13wwRj5o5Y= 113 | github.com/darkweak/souin v1.7.5/go.mod h1:PcP+hhvYOdqn4OmeScKKvit0TihYVYS1o154mhfWT/s= 114 | github.com/darkweak/storages/core v0.0.11 h1:IwvpAtkhOmxC5pIffJ8opW6erpTnIi5zqPveiAQs8ew= 115 | github.com/darkweak/storages/core v0.0.11/go.mod h1:ajTpB9IFLRIRY0EEFLjM5vtsrcNTh+TJK9yRxgG5/wY= 116 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 117 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 118 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 119 | github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= 120 | github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= 121 | github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= 122 | github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= 123 | github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= 124 | github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= 125 | github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= 126 | github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= 127 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 128 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= 129 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 130 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 131 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 132 | github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= 133 | github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 134 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 135 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 136 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 137 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 138 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 139 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 140 | github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= 141 | github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 142 | github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= 143 | github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 144 | github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= 145 | github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= 146 | github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 147 | github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= 148 | github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= 149 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 150 | github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= 151 | github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 152 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 153 | github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 154 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 155 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 156 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 157 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 158 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 159 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 160 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 161 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= 162 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 163 | github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 164 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 165 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 166 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 167 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 168 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 169 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 170 | github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= 171 | github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 172 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 173 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 174 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 175 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 176 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 177 | github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 178 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 179 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 180 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 181 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 182 | github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= 183 | github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= 184 | github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= 185 | github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= 186 | github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= 187 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 188 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 189 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 190 | github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= 191 | github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= 192 | github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98= 193 | github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY= 194 | github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= 195 | github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= 196 | github.com/google/pprof v0.0.0-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU= 197 | github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= 198 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 199 | github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= 200 | github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= 201 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 202 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 203 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 204 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= 205 | github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= 206 | github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= 207 | github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= 208 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= 209 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= 210 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 211 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 212 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 213 | github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= 214 | github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 215 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 216 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 217 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 218 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 219 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 220 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 221 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 222 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 223 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 224 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 225 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 226 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 227 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 228 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 229 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 230 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 231 | github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= 232 | github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= 233 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 234 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 235 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 236 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 237 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 238 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 239 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 240 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 241 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 242 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 243 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 244 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 245 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 246 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 247 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 248 | github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= 249 | github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 250 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 251 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 252 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 253 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 254 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 255 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 256 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 257 | github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= 258 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 259 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 260 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 261 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 262 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 263 | github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= 264 | github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 265 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 266 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 267 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 268 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 269 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 270 | github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= 271 | github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= 272 | github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 273 | github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= 274 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 275 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 276 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 277 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 278 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 279 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 280 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 281 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 282 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 283 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 284 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 285 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 286 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 287 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 288 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 289 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 290 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 291 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 292 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 293 | github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= 294 | github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= 295 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 296 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 297 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 298 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 299 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 300 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 301 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 302 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 303 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 304 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 305 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 306 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 307 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 308 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 309 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 310 | github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k= 311 | github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U= 312 | github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= 313 | github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= 314 | github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 315 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 316 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 317 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 318 | github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= 319 | github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= 320 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 321 | github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 322 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 323 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 324 | github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= 325 | github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= 326 | github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= 327 | github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= 328 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 329 | github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= 330 | github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= 331 | github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= 332 | github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 333 | github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= 334 | github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= 335 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 336 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 337 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 338 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 339 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 340 | github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k= 341 | github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= 342 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 343 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 344 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 345 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 346 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 347 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 348 | github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= 349 | github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= 350 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 351 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 352 | github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= 353 | github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= 354 | github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0= 355 | github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek= 356 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 357 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 358 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 359 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 360 | github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= 361 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 362 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 363 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 364 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 365 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 366 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 367 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 368 | github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= 369 | github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= 370 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 371 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 372 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 373 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 374 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 375 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 376 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 377 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 378 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 379 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 380 | github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM= 381 | github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI= 382 | github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= 383 | github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= 384 | github.com/smallstep/certificates v0.26.1 h1:FIUliEBcExSfJJDhRFA/s8aZgMIFuorexnRSKQd884o= 385 | github.com/smallstep/certificates v0.26.1/go.mod h1:OQMrW39IrGKDViKSHrKcgSQArMZ8c7EcjhYKK7mYqis= 386 | github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA= 387 | github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= 388 | github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y= 389 | github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y= 390 | github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh4tuqByDjRRKan2EvtnOfHr2zHJVg= 391 | github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y= 392 | github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw= 393 | github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU= 394 | github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4= 395 | github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= 396 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 397 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 398 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 399 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 400 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 401 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 402 | github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= 403 | github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 404 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 405 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 406 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 407 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 408 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 409 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 410 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 411 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 412 | github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= 413 | github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 414 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 415 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 416 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 417 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 418 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 419 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 420 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 421 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 422 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 423 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 424 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 425 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 426 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 427 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 428 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 429 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 430 | github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl1MRtyPXh5hqoxB5I7snOtTHgrn6HU= 431 | github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= 432 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 433 | github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= 434 | github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= 435 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 436 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 437 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 438 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 439 | github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 440 | github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U= 441 | github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 442 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= 443 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 444 | github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= 445 | github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 446 | github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= 447 | github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= 448 | github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= 449 | github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= 450 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 451 | go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= 452 | go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= 453 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 454 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 455 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= 456 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= 457 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 458 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 459 | go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 h1:s2RzYOAqHVgG23q8fPWYChobUoZM6rJZ98EnylJr66w= 460 | go.opentelemetry.io/contrib/propagators/autoprop v0.42.0/go.mod h1:Mv/tWNtZn+NbALDb2XcItP0OM3lWWZjAfSroINxfW+Y= 461 | go.opentelemetry.io/contrib/propagators/aws v1.17.0 h1:IX8d7l2uRw61BlmZBOTQFaK+y22j6vytMVTs9wFrO+c= 462 | go.opentelemetry.io/contrib/propagators/aws v1.17.0/go.mod h1:pAlCYRWff4uGqRXOVn3WP8pDZ5E0K56bEoG7a1VSL4k= 463 | go.opentelemetry.io/contrib/propagators/b3 v1.17.0 h1:ImOVvHnku8jijXqkwCSyYKRDt2YrnGXD4BbhcpfbfJo= 464 | go.opentelemetry.io/contrib/propagators/b3 v1.17.0/go.mod h1:IkfUfMpKWmynvvE0264trz0sf32NRTZL4nuAN9AbWRc= 465 | go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 h1:Zbpbmwav32Ea5jSotpmkWEl3a6Xvd4tw/3xxGO1i05Y= 466 | go.opentelemetry.io/contrib/propagators/jaeger v1.17.0/go.mod h1:tcTUAlmO8nuInPDSBVfG+CP6Mzjy5+gNV4mPxMbL0IA= 467 | go.opentelemetry.io/contrib/propagators/ot v1.17.0 h1:ufo2Vsz8l76eI47jFjuVyjyB3Ae2DmfiCV/o6Vc8ii0= 468 | go.opentelemetry.io/contrib/propagators/ot v1.17.0/go.mod h1:SbKPj5XGp8K/sGm05XblaIABgMgw2jDczP8gGeuaVLk= 469 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 470 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 471 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= 472 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= 473 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= 474 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= 475 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 476 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 477 | go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= 478 | go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= 479 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 480 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 481 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 482 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 483 | go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ= 484 | go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8= 485 | go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc= 486 | go.step.sm/crypto v0.45.0/go.mod h1:6IYlT0L2jfj81nVyCPpvA5cORy0EVHPhieSgQyuwHIY= 487 | go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU= 488 | go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw= 489 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 490 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 491 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 492 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 493 | go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= 494 | go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= 495 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 496 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 497 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 498 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 499 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 500 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 501 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 502 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 503 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 504 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 505 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 506 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 507 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 508 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 509 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 510 | go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= 511 | go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= 512 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 513 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 514 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 515 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 516 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 517 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 518 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 519 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 520 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 521 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 522 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 523 | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 524 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 525 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 526 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 527 | golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw= 528 | golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= 529 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 530 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 531 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 532 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 533 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 534 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 535 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 536 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 537 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 538 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 539 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 540 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 541 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 542 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 543 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 544 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 545 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 546 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 547 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 548 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 549 | golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= 550 | golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 551 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 552 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 553 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 554 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 555 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 556 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 557 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 558 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 559 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 560 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 561 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 562 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 563 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 564 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 565 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 566 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 567 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 568 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 569 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 570 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 571 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 572 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 573 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 574 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 575 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 576 | golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 577 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 578 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 579 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 580 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 581 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 582 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 583 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 584 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 585 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 586 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 587 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 588 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 589 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 590 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 591 | golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= 592 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 593 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 594 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 595 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 596 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 597 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 598 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 599 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 600 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 601 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 602 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 603 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 604 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 605 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 606 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 607 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 608 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 609 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 610 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 611 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 612 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 613 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 614 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 615 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 616 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 617 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 618 | golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= 619 | golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 620 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 621 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 622 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 623 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 624 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 625 | google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4= 626 | google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= 627 | google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= 628 | google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= 629 | google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk= 630 | google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= 631 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE= 632 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= 633 | google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= 634 | google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= 635 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 636 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 637 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 638 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 639 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 640 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 641 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 642 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 643 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 644 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 645 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 646 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 647 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 648 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 649 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 650 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 651 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 652 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 653 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 654 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 655 | howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= 656 | howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 657 | -------------------------------------------------------------------------------- /httpcache.go: -------------------------------------------------------------------------------- 1 | package httpcache 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/caddyserver/caddy/v2" 9 | "github.com/caddyserver/caddy/v2/caddyconfig" 10 | "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 11 | "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" 12 | "github.com/caddyserver/caddy/v2/modules/caddyhttp" 13 | "github.com/darkweak/souin/configurationtypes" 14 | "github.com/darkweak/souin/pkg/middleware" 15 | surrogates_providers "github.com/darkweak/souin/pkg/surrogate/providers" 16 | "github.com/darkweak/storages/core" 17 | ) 18 | 19 | const moduleName = "cache" 20 | 21 | var up = caddy.NewUsagePool() 22 | 23 | func init() { 24 | caddy.RegisterModule(SouinCaddyMiddleware{}) 25 | httpcaddyfile.RegisterGlobalOption(moduleName, parseCaddyfileGlobalOption) 26 | httpcaddyfile.RegisterHandlerDirective(moduleName, parseCaddyfileHandlerDirective) 27 | httpcaddyfile.RegisterDirectiveOrder(moduleName, httpcaddyfile.Before, "rewrite") 28 | } 29 | 30 | // SouinCaddyMiddleware allows the user to set up an HTTP cache system, 31 | // RFC-7234 compliant and supports the tag based cache purge, 32 | // distributed and not-distributed storage, key generation tweaking. 33 | type SouinCaddyMiddleware struct { 34 | *middleware.SouinBaseHandler 35 | logger core.Logger 36 | cacheKeys configurationtypes.CacheKeys 37 | Configuration Configuration 38 | // Logger level, fallback on caddy's one when not redefined. 39 | LogLevel string `json:"log_level,omitempty"` 40 | // Allowed HTTP verbs to be cached by the system. 41 | AllowedHTTPVerbs []string `json:"allowed_http_verbs,omitempty"` 42 | // Headers to add to the cache key if they are present. 43 | Headers []string `json:"headers,omitempty"` 44 | // Configure the Badger cache storage. 45 | Badger configurationtypes.CacheProvider `json:"badger,omitempty"` 46 | // Configure the global key generation. 47 | Key configurationtypes.Key `json:"key,omitempty"` 48 | // Override the cache key generation matching the pattern. 49 | CacheKeys configurationtypes.CacheKeys `json:"cache_keys,omitempty"` 50 | // Configure the Nats cache storage. 51 | Nats configurationtypes.CacheProvider `json:"nats,omitempty"` 52 | // Configure the Nuts cache storage. 53 | Nuts configurationtypes.CacheProvider `json:"nuts,omitempty"` 54 | // Configure the Otter cache storage. 55 | Otter configurationtypes.CacheProvider `json:"otter,omitempty"` 56 | // Enable the Etcd distributed cache storage. 57 | Etcd configurationtypes.CacheProvider `json:"etcd,omitempty"` 58 | // Enable the Redis distributed cache storage. 59 | Redis configurationtypes.CacheProvider `json:"redis,omitempty"` 60 | // Enable the Olric distributed cache storage. 61 | Olric configurationtypes.CacheProvider `json:"olric,omitempty"` 62 | // Time to live for a key, using time.duration. 63 | Timeout configurationtypes.Timeout `json:"timeout,omitempty"` 64 | // Time to live for a key, using time.duration. 65 | TTL configurationtypes.Duration `json:"ttl,omitempty"` 66 | // Configure the SimpleFS cache storage. 67 | SimpleFS configurationtypes.CacheProvider `json:"simplefs,omitempty"` 68 | // Time to live for a stale key, using time.duration. 69 | Stale configurationtypes.Duration `json:"stale,omitempty"` 70 | // Storage providers chaining and order. 71 | Storers []string `json:"storers,omitempty"` 72 | // The default Cache-Control header value if none set by the upstream server. 73 | DefaultCacheControl string `json:"default_cache_control,omitempty"` 74 | // The cache name to use in the Cache-Status response header. 75 | CacheName string `json:"cache_name,omitempty"` 76 | } 77 | 78 | // CaddyModule returns the Caddy module information. 79 | func (SouinCaddyMiddleware) CaddyModule() caddy.ModuleInfo { 80 | return caddy.ModuleInfo{ 81 | ID: "http.handlers.cache", 82 | New: func() caddy.Module { return new(SouinCaddyMiddleware) }, 83 | } 84 | } 85 | 86 | // ServeHTTP implements caddyhttp.MiddlewareHandler. 87 | func (s *SouinCaddyMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { 88 | return s.SouinBaseHandler.ServeHTTP(rw, r, func(w http.ResponseWriter, _ *http.Request) error { 89 | return next.ServeHTTP(w, r) 90 | }) 91 | } 92 | 93 | func (s *SouinCaddyMiddleware) configurationPropertyMapper() error { 94 | if s.Configuration.GetDefaultCache() == nil { 95 | defaultCache := DefaultCache{ 96 | Badger: s.Badger, 97 | Nats: s.Nats, 98 | Nuts: s.Nuts, 99 | SimpleFS: s.SimpleFS, 100 | Otter: s.Otter, 101 | Key: s.Key, 102 | DefaultCacheControl: s.DefaultCacheControl, 103 | CacheName: s.CacheName, 104 | Distributed: s.Olric.URL != "" || s.Olric.Path != "" || s.Olric.Configuration != nil || s.Etcd.Configuration != nil || s.Redis.URL != "" || s.Redis.Configuration != nil, 105 | Headers: s.Headers, 106 | Olric: s.Olric, 107 | Etcd: s.Etcd, 108 | Redis: s.Redis, 109 | Timeout: s.Timeout, 110 | TTL: s.TTL, 111 | Stale: s.Stale, 112 | Storers: s.Storers, 113 | } 114 | s.Configuration = Configuration{ 115 | CacheKeys: s.cacheKeys, 116 | DefaultCache: defaultCache, 117 | LogLevel: s.LogLevel, 118 | } 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func isProviderEmpty(c configurationtypes.CacheProvider) bool { 125 | return !c.Found 126 | } 127 | 128 | // FromApp to initialize configuration from App structure. 129 | func (s *SouinCaddyMiddleware) FromApp(app *SouinApp) error { 130 | if s.Configuration.GetDefaultCache() == nil { 131 | s.Configuration = Configuration{ 132 | URLs: make(map[string]configurationtypes.URL), 133 | } 134 | } 135 | 136 | if app.DefaultCache.GetTTL() == 0 { 137 | return nil 138 | } 139 | 140 | s.Configuration.API = app.API 141 | 142 | if s.Configuration.GetDefaultCache() == nil { 143 | s.Configuration.DefaultCache = DefaultCache{ 144 | AllowedHTTPVerbs: app.DefaultCache.AllowedHTTPVerbs, 145 | Headers: app.Headers, 146 | Key: app.Key, 147 | TTL: app.TTL, 148 | Stale: app.Stale, 149 | DefaultCacheControl: app.DefaultCacheControl, 150 | CacheName: app.CacheName, 151 | Timeout: app.Timeout, 152 | } 153 | return nil 154 | } 155 | if len(s.Configuration.CacheKeys) == 0 { 156 | s.Configuration.CacheKeys = configurationtypes.CacheKeys{} 157 | } 158 | if s.CacheKeys == nil { 159 | s.CacheKeys = app.CacheKeys 160 | } 161 | for _, cacheKey := range s.Configuration.CacheKeys { 162 | for k, v := range cacheKey { 163 | s.Configuration.CacheKeys = append( 164 | s.Configuration.CacheKeys, 165 | map[configurationtypes.RegValue]configurationtypes.Key{k: v}, 166 | ) 167 | } 168 | } 169 | 170 | dc := s.Configuration.DefaultCache 171 | appDc := app.DefaultCache 172 | s.Configuration.DefaultCache.AllowedHTTPVerbs = append(s.Configuration.DefaultCache.AllowedHTTPVerbs, appDc.AllowedHTTPVerbs...) 173 | s.Configuration.DefaultCache.CDN = app.DefaultCache.CDN 174 | if dc.Headers == nil { 175 | s.Configuration.DefaultCache.Headers = appDc.Headers 176 | } 177 | 178 | if s.Configuration.LogLevel == "" { 179 | s.Configuration.LogLevel = app.LogLevel 180 | } 181 | if dc.TTL.Duration == 0 { 182 | s.Configuration.DefaultCache.TTL = appDc.TTL 183 | } 184 | if dc.Stale.Duration == 0 { 185 | s.Configuration.DefaultCache.Stale = appDc.Stale 186 | } 187 | if len(dc.Storers) == 0 { 188 | s.Configuration.DefaultCache.Storers = appDc.Storers 189 | } 190 | if dc.Timeout.Backend.Duration == 0 { 191 | s.Configuration.DefaultCache.Timeout.Backend = appDc.Timeout.Backend 192 | } 193 | if dc.Mode == "" { 194 | s.Configuration.DefaultCache.Mode = appDc.Mode 195 | } 196 | if dc.Timeout.Cache.Duration == 0 { 197 | s.Configuration.DefaultCache.Timeout.Cache = appDc.Timeout.Cache 198 | } 199 | if !dc.Key.DisableBody && !dc.Key.DisableHost && !dc.Key.DisableMethod && !dc.Key.DisableQuery && !dc.Key.DisableScheme && !dc.Key.Hash && !dc.Key.Hide && len(dc.Key.Headers) == 0 && dc.Key.Template == "" { 200 | s.Configuration.DefaultCache.Key = appDc.Key 201 | } 202 | if dc.DefaultCacheControl == "" { 203 | s.Configuration.DefaultCache.DefaultCacheControl = appDc.DefaultCacheControl 204 | } 205 | if dc.MaxBodyBytes == 0 { 206 | s.Configuration.DefaultCache.MaxBodyBytes = appDc.MaxBodyBytes 207 | } 208 | if dc.CacheName == "" { 209 | s.Configuration.DefaultCache.CacheName = appDc.CacheName 210 | } 211 | if isProviderEmpty(dc.Badger) && isProviderEmpty(dc.Etcd) && isProviderEmpty(dc.Nats) && isProviderEmpty(dc.Nuts) && isProviderEmpty(dc.Olric) && isProviderEmpty(dc.Otter) && isProviderEmpty(dc.Redis) && isProviderEmpty(dc.SimpleFS) { 212 | s.Configuration.DefaultCache.Distributed = appDc.Distributed 213 | s.Configuration.DefaultCache.Olric = appDc.Olric 214 | s.Configuration.DefaultCache.Redis = appDc.Redis 215 | s.Configuration.DefaultCache.Etcd = appDc.Etcd 216 | s.Configuration.DefaultCache.Badger = appDc.Badger 217 | s.Configuration.DefaultCache.Nats = appDc.Nats 218 | s.Configuration.DefaultCache.Nuts = appDc.Nuts 219 | s.Configuration.DefaultCache.Otter = appDc.Otter 220 | s.Configuration.DefaultCache.SimpleFS = appDc.SimpleFS 221 | } 222 | if dc.Regex.Exclude == "" { 223 | s.Configuration.DefaultCache.Regex.Exclude = appDc.Regex.Exclude 224 | } 225 | 226 | return nil 227 | } 228 | 229 | func dispatchStorage(ctx caddy.Context, name string, provider configurationtypes.CacheProvider, stale time.Duration) error { 230 | b, _ := json.Marshal(core.Configuration{ 231 | Provider: core.CacheProvider{ 232 | Path: provider.Path, 233 | Configuration: provider.Configuration, 234 | URL: provider.URL, 235 | }, 236 | Stale: stale, 237 | }) 238 | _, e := ctx.LoadModuleByID("storages.cache."+name, b) 239 | 240 | return e 241 | } 242 | 243 | // Provision to do the provisioning part. 244 | func (s *SouinCaddyMiddleware) Provision(ctx caddy.Context) error { 245 | s.logger = ctx.Logger(s).Sugar() 246 | 247 | if err := s.configurationPropertyMapper(); err != nil { 248 | return err 249 | } 250 | 251 | s.Configuration.SetLogger(s.logger) 252 | ctxApp, _ := ctx.App(moduleName) 253 | app := ctxApp.(*SouinApp) 254 | 255 | if err := s.FromApp(app); err != nil { 256 | return err 257 | } 258 | 259 | s.parseStorages(ctx) 260 | 261 | bh := middleware.NewHTTPCacheHandler(&s.Configuration) 262 | surrogates, ok := up.LoadOrStore(surrogate_key, bh.SurrogateKeyStorer) 263 | if ok { 264 | bh.SurrogateKeyStorer = surrogates.(surrogates_providers.SurrogateInterface) 265 | } 266 | 267 | s.SouinBaseHandler = bh 268 | if len(app.Storers) == 0 { 269 | app.Storers = s.SouinBaseHandler.Storers 270 | } 271 | 272 | if app.SurrogateStorage == (surrogates_providers.SurrogateInterface)(nil) { 273 | app.SurrogateStorage = s.SouinBaseHandler.SurrogateKeyStorer 274 | } else { 275 | s.SouinBaseHandler.SurrogateKeyStorer = app.SurrogateStorage 276 | } 277 | 278 | return nil 279 | } 280 | 281 | func parseCaddyfileGlobalOption(h *caddyfile.Dispenser, _ interface{}) (interface{}, error) { 282 | souinApp := new(SouinApp) 283 | cfg := Configuration{ 284 | DefaultCache: DefaultCache{ 285 | AllowedHTTPVerbs: make([]string, 0), 286 | Distributed: false, 287 | Headers: []string{}, 288 | TTL: configurationtypes.Duration{ 289 | Duration: 120 * time.Second, 290 | }, 291 | DefaultCacheControl: "", 292 | CacheName: "", 293 | }, 294 | URLs: make(map[string]configurationtypes.URL), 295 | } 296 | 297 | err := parseConfiguration(&cfg, h, true) 298 | 299 | souinApp.DefaultCache = cfg.DefaultCache 300 | souinApp.API = cfg.API 301 | souinApp.CacheKeys = cfg.CacheKeys 302 | souinApp.LogLevel = cfg.LogLevel 303 | 304 | return httpcaddyfile.App{ 305 | Name: moduleName, 306 | Value: caddyconfig.JSON(souinApp, nil), 307 | }, err 308 | } 309 | func parseCaddyfileHandlerDirective(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { 310 | var s SouinCaddyMiddleware 311 | return &s, s.UnmarshalCaddyfile(h.Dispenser) 312 | } 313 | func (s *SouinCaddyMiddleware) UnmarshalCaddyfile(h *caddyfile.Dispenser) error { 314 | dc := DefaultCache{ 315 | AllowedHTTPVerbs: make([]string, 0), 316 | } 317 | s.Configuration = Configuration{ 318 | DefaultCache: dc, 319 | } 320 | 321 | return parseConfiguration(&s.Configuration, h, false) 322 | } 323 | 324 | // Interface guards 325 | var ( 326 | _ caddy.CleanerUpper = (*SouinCaddyMiddleware)(nil) 327 | _ caddy.Provisioner = (*SouinCaddyMiddleware)(nil) 328 | _ caddyhttp.MiddlewareHandler = (*SouinCaddyMiddleware)(nil) 329 | _ caddyfile.Unmarshaler = (*SouinCaddyMiddleware)(nil) 330 | ) 331 | -------------------------------------------------------------------------------- /httpcache_test.go: -------------------------------------------------------------------------------- 1 | package httpcache 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/caddyserver/caddy/v2/caddytest" 13 | ) 14 | 15 | func TestMinimal(t *testing.T) { 16 | tester := caddytest.NewTester(t) 17 | tester.InitServer(` 18 | { 19 | admin localhost:2999 20 | http_port 9080 21 | https_port 9443 22 | cache 23 | } 24 | localhost:9080 { 25 | route /cache-default { 26 | cache 27 | respond "Hello, default!" 28 | } 29 | }`, "caddyfile") 30 | 31 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, 200, "Hello, default!") 32 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/cache-default" { 33 | t.Errorf("unexpected Cache-Status header %v", resp1.Header) 34 | } 35 | 36 | resp2, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, 200, "Hello, default!") 37 | if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=119; key=GET-http-localhost:9080-/cache-default; detail=DEFAULT" { 38 | t.Errorf("unexpected Cache-Status header %v", resp2.Header.Get("Cache-Status")) 39 | } 40 | 41 | time.Sleep(2 * time.Second) 42 | resp3, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, 200, "Hello, default!") 43 | if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=117; key=GET-http-localhost:9080-/cache-default; detail=DEFAULT" { 44 | t.Errorf("unexpected Cache-Status header %v", resp3.Header.Get("Cache-Status")) 45 | } 46 | } 47 | 48 | func TestHead(t *testing.T) { 49 | tester := caddytest.NewTester(t) 50 | tester.InitServer(` 51 | { 52 | admin localhost:2999 53 | http_port 9080 54 | https_port 9443 55 | cache 56 | } 57 | localhost:9080 { 58 | route /cache-head { 59 | cache 60 | respond "Hello, HEAD!" 61 | } 62 | }`, "caddyfile") 63 | 64 | headReq, _ := http.NewRequest(http.MethodHead, "http://localhost:9080/cache-head", nil) 65 | resp1, _ := tester.AssertResponse(headReq, 200, "") 66 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=HEAD-http-localhost:9080-/cache-head" { 67 | t.Errorf("unexpected Cache-Status header %v", resp1.Header) 68 | } 69 | if resp1.Header.Get("Content-Length") != "12" { 70 | t.Errorf("unexpected Content-Length header %v", resp1.Header) 71 | } 72 | 73 | resp2, _ := tester.AssertResponse(headReq, 200, "") 74 | if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=119; key=HEAD-http-localhost:9080-/cache-head; detail=DEFAULT" { 75 | t.Errorf("unexpected Cache-Status header %v", resp2.Header) 76 | } 77 | if resp2.Header.Get("Content-Length") != "12" { 78 | t.Errorf("unexpected Content-Length header %v", resp2.Header) 79 | } 80 | } 81 | 82 | func TestQueryString(t *testing.T) { 83 | tester := caddytest.NewTester(t) 84 | tester.InitServer(` 85 | { 86 | admin localhost:2999 87 | http_port 9080 88 | https_port 9443 89 | cache { 90 | key { 91 | disable_query 92 | } 93 | } 94 | } 95 | localhost:9080 { 96 | route /query-string { 97 | cache { 98 | key { 99 | disable_query 100 | } 101 | } 102 | respond "Hello, query string!" 103 | } 104 | }`, "caddyfile") 105 | 106 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/query-string?query=string`, 200, "Hello, query string!") 107 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/query-string" { 108 | t.Errorf("unexpected Cache-Status header %v", resp1.Header) 109 | } 110 | } 111 | 112 | func TestMaxAge(t *testing.T) { 113 | tester := caddytest.NewTester(t) 114 | tester.InitServer(` 115 | { 116 | admin localhost:2999 117 | http_port 9080 118 | https_port 9443 119 | cache 120 | } 121 | localhost:9080 { 122 | route /cache-max-age { 123 | cache 124 | header Cache-Control "max-age=60" 125 | respond "Hello, max-age!" 126 | } 127 | }`, "caddyfile") 128 | 129 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/cache-max-age`, 200, "Hello, max-age!") 130 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/cache-max-age" { 131 | t.Errorf("unexpected Cache-Status header %v", resp1.Header) 132 | } 133 | 134 | resp2, _ := tester.AssertGetResponse(`http://localhost:9080/cache-max-age`, 200, "Hello, max-age!") 135 | if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=59; key=GET-http-localhost:9080-/cache-max-age; detail=DEFAULT" { 136 | t.Errorf("unexpected Cache-Status header %v", resp2.Header.Get("Cache-Status")) 137 | } 138 | 139 | time.Sleep(2 * time.Second) 140 | resp3, _ := tester.AssertGetResponse(`http://localhost:9080/cache-max-age`, 200, "Hello, max-age!") 141 | if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=57; key=GET-http-localhost:9080-/cache-max-age; detail=DEFAULT" { 142 | t.Errorf("unexpected Cache-Status header %v", resp3.Header.Get("Cache-Status")) 143 | } 144 | } 145 | 146 | func TestMaxStale(t *testing.T) { 147 | tester := caddytest.NewTester(t) 148 | tester.InitServer(` 149 | { 150 | admin localhost:2999 151 | http_port 9080 152 | https_port 9443 153 | cache { 154 | stale 5s 155 | } 156 | } 157 | localhost:9080 { 158 | route /cache-max-stale { 159 | cache 160 | header Cache-Control "max-age=3" 161 | respond "Hello, max-stale!" 162 | } 163 | }`, "caddyfile") 164 | 165 | maxStaleURL := "http://localhost:9080/cache-max-stale" 166 | 167 | resp1, _ := tester.AssertGetResponse(maxStaleURL, 200, "Hello, max-stale!") 168 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/cache-max-stale" { 169 | t.Errorf("unexpected Cache-Status header %v", resp1.Header) 170 | } 171 | 172 | resp2, _ := tester.AssertGetResponse(maxStaleURL, 200, "Hello, max-stale!") 173 | if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=2; key=GET-http-localhost:9080-/cache-max-stale; detail=DEFAULT" { 174 | t.Errorf("unexpected Cache-Status header %v", resp2.Header.Get("Cache-Status")) 175 | } 176 | 177 | time.Sleep(3 * time.Second) 178 | reqMaxStale, _ := http.NewRequest(http.MethodGet, maxStaleURL, nil) 179 | reqMaxStale.Header = http.Header{"Cache-Control": []string{"max-stale=3"}} 180 | resp3, _ := tester.AssertResponse(reqMaxStale, 200, "Hello, max-stale!") 181 | if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=-1; key=GET-http-localhost:9080-/cache-max-stale; detail=DEFAULT; fwd=stale" { 182 | t.Errorf("unexpected Cache-Status header %v", resp3.Header.Get("Cache-Status")) 183 | } 184 | 185 | time.Sleep(3 * time.Second) 186 | resp4, _ := tester.AssertResponse(reqMaxStale, 200, "Hello, max-stale!") 187 | if resp4.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/cache-max-stale" { 188 | t.Errorf("unexpected Cache-Status header %v", resp4.Header.Get("Cache-Status")) 189 | } 190 | } 191 | 192 | func TestSMaxAge(t *testing.T) { 193 | tester := caddytest.NewTester(t) 194 | tester.InitServer(` 195 | { 196 | admin localhost:2999 197 | http_port 9080 198 | https_port 9443 199 | cache { 200 | ttl 1000s 201 | } 202 | } 203 | localhost:9080 { 204 | route /cache-s-maxage { 205 | cache 206 | header Cache-Control "s-maxage=5" 207 | respond "Hello, s-maxage!" 208 | } 209 | }`, "caddyfile") 210 | 211 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/cache-s-maxage`, 200, "Hello, s-maxage!") 212 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/cache-s-maxage" { 213 | t.Errorf("unexpected Cache-Status header %v", resp1.Header.Get("Cache-Status")) 214 | } 215 | 216 | resp2, _ := tester.AssertGetResponse(`http://localhost:9080/cache-s-maxage`, 200, "Hello, s-maxage!") 217 | if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/cache-s-maxage; detail=DEFAULT" { 218 | t.Errorf("unexpected Cache-Status header with %v", resp2.Header.Get("Cache-Status")) 219 | } 220 | } 221 | 222 | func TestAgeHeader(t *testing.T) { 223 | tester := caddytest.NewTester(t) 224 | tester.InitServer(` 225 | { 226 | admin localhost:2999 227 | http_port 9080 228 | https_port 9443 229 | cache { 230 | ttl 1000s 231 | } 232 | } 233 | localhost:9080 { 234 | route /age-header { 235 | cache 236 | header Cache-Control "max-age=60" 237 | respond "Hello, Age header!" 238 | } 239 | }`, "caddyfile") 240 | 241 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/age-header`, 200, "Hello, Age header!") 242 | if resp1.Header.Get("Age") != "" { 243 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 244 | } 245 | 246 | resp2, _ := tester.AssertGetResponse(`http://localhost:9080/age-header`, 200, "Hello, Age header!") 247 | if resp2.Header.Get("Age") == "" { 248 | t.Error("Age header should be present") 249 | } 250 | if resp2.Header.Get("Age") != "1" { 251 | t.Error("Age header should be present") 252 | } 253 | 254 | time.Sleep(10 * time.Second) 255 | resp3, _ := tester.AssertGetResponse(`http://localhost:9080/age-header`, 200, "Hello, Age header!") 256 | if resp3.Header.Get("Age") != "11" { 257 | t.Error("Age header should be present") 258 | } 259 | } 260 | 261 | func TestKeyGeneration(t *testing.T) { 262 | tester := caddytest.NewTester(t) 263 | tester.InitServer(` 264 | { 265 | admin localhost:2999 266 | http_port 9080 267 | https_port 9443 268 | cache { 269 | ttl 1000s 270 | } 271 | } 272 | localhost:9080 { 273 | route /key-template-route { 274 | cache { 275 | key { 276 | template {method}-{host}-{path}-WITH_SUFFIX 277 | } 278 | } 279 | respond "Hello, template route!" 280 | } 281 | route /key-headers-route { 282 | cache { 283 | key { 284 | headers X-Header X-Internal 285 | } 286 | } 287 | respond "Hello, headers route!" 288 | } 289 | route /key-hash-route { 290 | cache { 291 | key { 292 | hash 293 | } 294 | } 295 | respond "Hello, hash route!" 296 | } 297 | }`, "caddyfile") 298 | 299 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/key-template-route`, 200, "Hello, template route!") 300 | if resp1.Header.Get("Age") != "" { 301 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 302 | } 303 | if !strings.Contains(resp1.Header.Get("Cache-Status"), "key=GET-localhost-/key-template-route-WITH_SUFFIX") { 304 | t.Errorf("unexpected Cache-Status header %v", resp1.Header.Get("Cache-Status")) 305 | } 306 | 307 | resp2, _ := tester.AssertGetResponse(`http://localhost:9080/key-template-route`, 200, "Hello, template route!") 308 | if resp2.Header.Get("Age") == "" { 309 | t.Error("Age header should be present") 310 | } 311 | if resp2.Header.Get("Age") != "1" { 312 | t.Error("Age header should be present") 313 | } 314 | if !strings.Contains(resp2.Header.Get("Cache-Status"), "key=GET-localhost-/key-template-route-WITH_SUFFIX") { 315 | t.Errorf("unexpected Cache-Status header %v", resp2.Header.Get("Cache-Status")) 316 | } 317 | 318 | rq, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/key-headers-route", nil) 319 | rq.Header = http.Header{ 320 | "X-Internal": []string{"my-value"}, 321 | } 322 | resp1, _ = tester.AssertResponse(rq, 200, "Hello, headers route!") 323 | if resp1.Header.Get("Age") != "" { 324 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 325 | } 326 | if !strings.Contains(resp1.Header.Get("Cache-Status"), "key=GET-http-localhost:9080-/key-headers-route--my-value") { 327 | t.Errorf("unexpected Cache-Status header %v", resp1.Header.Get("Cache-Status")) 328 | } 329 | 330 | rq.Header = http.Header{ 331 | "X-Header": []string{"first"}, 332 | "X-Internal": []string{"my-value"}, 333 | } 334 | resp1, _ = tester.AssertResponse(rq, 200, "Hello, headers route!") 335 | if resp1.Header.Get("Age") != "" { 336 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 337 | } 338 | if !strings.Contains(resp1.Header.Get("Cache-Status"), "key=GET-http-localhost:9080-/key-headers-route-first-my-value") { 339 | t.Errorf("unexpected Cache-Status header %v", resp1.Header.Get("Cache-Status")) 340 | } 341 | } 342 | 343 | func TestNotHandledRoute(t *testing.T) { 344 | tester := caddytest.NewTester(t) 345 | tester.InitServer(` 346 | { 347 | admin localhost:2999 348 | http_port 9080 349 | https_port 9443 350 | cache { 351 | ttl 1000s 352 | regex { 353 | exclude ".*handled" 354 | } 355 | } 356 | } 357 | localhost:9080 { 358 | route /not-handled { 359 | cache 360 | header Cache-Control "max-age=60" 361 | header Age "max-age=5" 362 | respond "Hello, Age header!" 363 | } 364 | }`, "caddyfile") 365 | 366 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/not-handled`, 200, "Hello, Age header!") 367 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=bypass; detail=EXCLUDED-REQUEST-URI" { 368 | t.Errorf("unexpected Cache-Status header value %v", resp1.Header.Get("Cache-Status")) 369 | } 370 | } 371 | 372 | func TestMaxBodyByte(t *testing.T) { 373 | tester := caddytest.NewTester(t) 374 | tester.InitServer(` 375 | { 376 | admin localhost:2999 377 | http_port 9080 378 | https_port 9443 379 | cache { 380 | ttl 5s 381 | max_cacheable_body_bytes 30 382 | } 383 | } 384 | localhost:9080 { 385 | route /max-body-bytes-stored { 386 | cache 387 | respond "Hello, Max body bytes stored!" 388 | } 389 | route /max-body-bytes-not-stored { 390 | cache 391 | respond "Hello, Max body bytes not stored due to the response length!" 392 | } 393 | }`, "caddyfile") 394 | 395 | respStored1, _ := tester.AssertGetResponse(`http://localhost:9080/max-body-bytes-stored`, 200, "Hello, Max body bytes stored!") 396 | respStored2, _ := tester.AssertGetResponse(`http://localhost:9080/max-body-bytes-stored`, 200, "Hello, Max body bytes stored!") 397 | if respStored1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/max-body-bytes-stored" { 398 | t.Errorf("unexpected Cache-Status header value %v", respStored1.Header.Get("Cache-Status")) 399 | } 400 | if respStored1.Header.Get("Age") != "" { 401 | t.Errorf("unexpected Age header %v", respStored1.Header.Get("Age")) 402 | } 403 | 404 | if respStored2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/max-body-bytes-stored; detail=DEFAULT" { 405 | t.Errorf("unexpected Cache-Status header value %v", respStored2.Header.Get("Cache-Status")) 406 | } 407 | if respStored2.Header.Get("Age") == "" { 408 | t.Error("Age header should be present") 409 | } 410 | 411 | respNotStored1, _ := tester.AssertGetResponse(`http://localhost:9080/max-body-bytes-not-stored`, 200, "Hello, Max body bytes not stored due to the response length!") 412 | respNotStored2, _ := tester.AssertGetResponse(`http://localhost:9080/max-body-bytes-not-stored`, 200, "Hello, Max body bytes not stored due to the response length!") 413 | if respNotStored1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; detail=UPSTREAM-RESPONSE-TOO-LARGE; key=GET-http-localhost:9080-/max-body-bytes-not-stored" { 414 | t.Errorf("unexpected Cache-Status header value %v", respNotStored1.Header.Get("Cache-Status")) 415 | } 416 | if respNotStored1.Header.Get("Age") != "" { 417 | t.Errorf("unexpected Age header %v", respNotStored1.Header.Get("Age")) 418 | } 419 | 420 | if respNotStored2.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; detail=UPSTREAM-RESPONSE-TOO-LARGE; key=GET-http-localhost:9080-/max-body-bytes-not-stored" { 421 | t.Errorf("unexpected Cache-Status header value %v", respNotStored2.Header.Get("Cache-Status")) 422 | } 423 | if respNotStored2.Header.Get("Age") != "" { 424 | t.Errorf("unexpected Age header %v", respNotStored2.Header.Get("Age")) 425 | } 426 | } 427 | 428 | func TestAuthenticatedRoute(t *testing.T) { 429 | tester := caddytest.NewTester(t) 430 | tester.InitServer(` 431 | { 432 | admin localhost:2999 433 | http_port 9080 434 | https_port 9443 435 | cache { 436 | ttl 1000s 437 | } 438 | } 439 | localhost:9080 { 440 | route /no-auth-bypass { 441 | cache 442 | respond "Hello, auth {http.request.header.Authorization}!" 443 | } 444 | route /auth-bypass { 445 | cache { 446 | key { 447 | headers Authorization Content-Type 448 | } 449 | } 450 | header Cache-Control "private, s-maxage=5" 451 | respond "Hello, auth bypass {http.request.header.Authorization}!" 452 | } 453 | route /auth-bypass-vary { 454 | cache { 455 | key { 456 | headers Authorization Content-Type 457 | } 458 | } 459 | header Cache-Control "private, s-maxage=5" 460 | header Vary "Content-Type, Authorization" 461 | respond "Hello, auth vary bypass {http.request.header.Authorization}!" 462 | } 463 | }`, "caddyfile") 464 | 465 | getRequestFor := func(endpoint, user string) *http.Request { 466 | rq, _ := http.NewRequest(http.MethodGet, "http://localhost:9080"+endpoint, nil) 467 | rq.Header = http.Header{"Authorization": []string{"Bearer " + user}, "Content-Type": []string{"text/plain"}} 468 | 469 | return rq 470 | } 471 | 472 | respNoAuthBypass, _ := tester.AssertResponse(getRequestFor("/no-auth-bypass", "Alice"), 200, "Hello, auth Bearer Alice!") 473 | if respNoAuthBypass.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; key=GET-http-localhost:9080-/no-auth-bypass; detail=PRIVATE-OR-AUTHENTICATED-RESPONSE" { 474 | t.Errorf("unexpected Cache-Status header %v", respNoAuthBypass.Header.Get("Cache-Status")) 475 | } 476 | 477 | respAuthBypassAlice1, _ := tester.AssertResponse(getRequestFor("/auth-bypass", "Alice"), 200, "Hello, auth bypass Bearer Alice!") 478 | if respAuthBypassAlice1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/auth-bypass-Bearer Alice-text/plain" { 479 | t.Errorf("unexpected Cache-Status header %v", respAuthBypassAlice1.Header.Get("Cache-Status")) 480 | } 481 | respAuthBypassAlice2, _ := tester.AssertResponse(getRequestFor("/auth-bypass", "Alice"), 200, "Hello, auth bypass Bearer Alice!") 482 | if respAuthBypassAlice2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/auth-bypass-Bearer Alice-text/plain; detail=DEFAULT" { 483 | t.Errorf("unexpected Cache-Status header %v", respAuthBypassAlice2.Header.Get("Cache-Status")) 484 | } 485 | 486 | respAuthBypassBob1, _ := tester.AssertResponse(getRequestFor("/auth-bypass", "Bob"), 200, "Hello, auth bypass Bearer Bob!") 487 | if respAuthBypassBob1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/auth-bypass-Bearer Bob-text/plain" { 488 | t.Errorf("unexpected Cache-Status header %v", respAuthBypassBob1.Header.Get("Cache-Status")) 489 | } 490 | respAuthBypassBob2, _ := tester.AssertResponse(getRequestFor("/auth-bypass", "Bob"), 200, "Hello, auth bypass Bearer Bob!") 491 | if respAuthBypassBob2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/auth-bypass-Bearer Bob-text/plain; detail=DEFAULT" { 492 | t.Errorf("unexpected Cache-Status header %v", respAuthBypassBob2.Header.Get("Cache-Status")) 493 | } 494 | 495 | respAuthVaryBypassAlice1, _ := tester.AssertResponse(getRequestFor("/auth-bypass-vary", "Alice"), 200, "Hello, auth vary bypass Bearer Alice!") 496 | if respAuthVaryBypassAlice1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/auth-bypass-vary-Bearer Alice-text/plain" { 497 | t.Errorf("unexpected Cache-Status header %v", respAuthVaryBypassAlice1.Header.Get("Cache-Status")) 498 | } 499 | respAuthVaryBypassAlice2, _ := tester.AssertResponse(getRequestFor("/auth-bypass-vary", "Alice"), 200, "Hello, auth vary bypass Bearer Alice!") 500 | if respAuthVaryBypassAlice2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/auth-bypass-vary-Bearer Alice-text/plain; detail=DEFAULT" { 501 | t.Errorf("unexpected Cache-Status header %v", respAuthVaryBypassAlice2.Header.Get("Cache-Status")) 502 | } 503 | } 504 | 505 | type testErrorHandler struct { 506 | iterator int 507 | } 508 | 509 | func (t *testErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 510 | t.iterator++ 511 | if t.iterator%2 == 0 { 512 | w.WriteHeader(http.StatusInternalServerError) 513 | return 514 | } 515 | 516 | w.Header().Set("Cache-Control", "must-revalidate") 517 | w.WriteHeader(http.StatusOK) 518 | _, _ = w.Write([]byte("Hello must-revalidate!")) 519 | } 520 | 521 | func TestMustRevalidate(t *testing.T) { 522 | tester := caddytest.NewTester(t) 523 | tester.InitServer(` 524 | { 525 | admin localhost:2999 526 | http_port 9080 527 | cache { 528 | ttl 5s 529 | stale 5s 530 | } 531 | } 532 | localhost:9080 { 533 | route /cache-default { 534 | cache 535 | reverse_proxy localhost:9081 536 | } 537 | }`, "caddyfile") 538 | 539 | go func() { 540 | errorHandler := testErrorHandler{} 541 | _ = http.ListenAndServe(":9081", &errorHandler) 542 | }() 543 | time.Sleep(time.Second) 544 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, http.StatusOK, "Hello must-revalidate!") 545 | resp2, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, http.StatusOK, "Hello must-revalidate!") 546 | time.Sleep(6 * time.Second) 547 | staleReq, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/cache-default", nil) 548 | staleReq.Header = http.Header{"Cache-Control": []string{"max-stale=3, stale-if-error=84600"}} 549 | resp3, _ := tester.AssertResponse(staleReq, http.StatusOK, "Hello must-revalidate!") 550 | 551 | if resp1.Header.Get("Cache-Control") != "must-revalidate" { 552 | t.Errorf("unexpected resp1 Cache-Control header %v", resp1.Header.Get("Cache-Control")) 553 | } 554 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/cache-default" { 555 | t.Errorf("unexpected resp1 Cache-Status header %v", resp1.Header.Get("Cache-Status")) 556 | } 557 | if resp1.Header.Get("Age") != "" { 558 | t.Errorf("unexpected resp1 Age header %v", resp1.Header.Get("Age")) 559 | } 560 | 561 | if resp2.Header.Get("Cache-Control") != "must-revalidate" { 562 | t.Errorf("unexpected resp2 Cache-Control header %v", resp2.Header.Get("Cache-Control")) 563 | } 564 | if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/cache-default; detail=DEFAULT" { 565 | t.Errorf("unexpected resp2 Cache-Status header %v", resp2.Header.Get("Cache-Status")) 566 | } 567 | if resp2.Header.Get("Age") != "1" { 568 | t.Errorf("unexpected resp2 Age header %v", resp2.Header.Get("Age")) 569 | } 570 | 571 | if resp3.Header.Get("Cache-Control") != "must-revalidate" { 572 | t.Errorf("unexpected resp3 Cache-Control header %v", resp3.Header.Get("Cache-Control")) 573 | } 574 | if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=-2; key=GET-http-localhost:9080-/cache-default; detail=DEFAULT; fwd=stale; fwd-status=500" { 575 | t.Errorf("unexpected resp3 Cache-Status header %v", resp3.Header.Get("Cache-Status")) 576 | } 577 | if resp3.Header.Get("Age") != "7" { 578 | t.Errorf("unexpected resp3 Age header %v", resp3.Header.Get("Age")) 579 | } 580 | 581 | resp4, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, http.StatusOK, "Hello must-revalidate!") 582 | if resp4.Header.Get("Cache-Control") != "must-revalidate" { 583 | t.Errorf("unexpected resp4 Cache-Control header %v", resp4.Header.Get("Cache-Control")) 584 | } 585 | if resp4.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/cache-default" { 586 | t.Errorf("unexpected resp4 Cache-Status header %v", resp4.Header.Get("Cache-Status")) 587 | } 588 | if resp4.Header.Get("Age") != "" { 589 | t.Errorf("unexpected resp4 Age header %v", resp4.Header.Get("Age")) 590 | } 591 | 592 | time.Sleep(6 * time.Second) 593 | staleReq, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/cache-default", nil) 594 | staleReq.Header = http.Header{"Cache-Control": []string{"max-stale=3"}} 595 | resp5, _ := tester.AssertResponse(staleReq, http.StatusGatewayTimeout, "") 596 | 597 | if resp5.Header.Get("Cache-Status") != "Souin; fwd=request; fwd-status=500; key=GET-http-localhost:9080-/cache-default; detail=REQUEST-REVALIDATION" { 598 | t.Errorf("unexpected resp5 Cache-Status header %v", resp4.Header.Get("Cache-Status")) 599 | } 600 | if resp5.Header.Get("Age") != "" { 601 | t.Errorf("unexpected resp5 Age header %v", resp4.Header.Get("Age")) 602 | } 603 | } 604 | 605 | type staleIfErrorHandler struct { 606 | iterator int 607 | } 608 | 609 | func (t *staleIfErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 610 | if t.iterator > 0 { 611 | w.WriteHeader(http.StatusInternalServerError) 612 | return 613 | } 614 | 615 | t.iterator++ 616 | w.Header().Set("Cache-Control", "stale-if-error=86400") 617 | w.WriteHeader(http.StatusOK) 618 | _, _ = w.Write([]byte("Hello stale-if-error!")) 619 | } 620 | 621 | func TestStaleIfError(t *testing.T) { 622 | tester := caddytest.NewTester(t) 623 | tester.InitServer(` 624 | { 625 | admin localhost:2999 626 | http_port 9080 627 | cache { 628 | ttl 5s 629 | stale 5s 630 | } 631 | } 632 | localhost:9080 { 633 | route /stale-if-error { 634 | cache 635 | reverse_proxy localhost:9085 636 | } 637 | }`, "caddyfile") 638 | 639 | go func() { 640 | staleIfErrorHandler := staleIfErrorHandler{} 641 | _ = http.ListenAndServe(":9085", &staleIfErrorHandler) 642 | }() 643 | time.Sleep(time.Second) 644 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusOK, "Hello stale-if-error!") 645 | resp2, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusOK, "Hello stale-if-error!") 646 | 647 | if resp1.Header.Get("Cache-Control") != "stale-if-error=86400" { 648 | t.Errorf("unexpected resp1 Cache-Control header %v", resp1.Header.Get("Cache-Control")) 649 | } 650 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/stale-if-error" { 651 | t.Errorf("unexpected resp1 Cache-Status header %v", resp1.Header.Get("Cache-Status")) 652 | } 653 | if resp1.Header.Get("Age") != "" { 654 | t.Errorf("unexpected resp1 Age header %v", resp1.Header.Get("Age")) 655 | } 656 | 657 | if resp2.Header.Get("Cache-Control") != "stale-if-error=86400" { 658 | t.Errorf("unexpected resp2 Cache-Control header %v", resp2.Header.Get("Cache-Control")) 659 | } 660 | if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/stale-if-error; detail=DEFAULT" { 661 | t.Errorf("unexpected resp2 Cache-Status header %v", resp2.Header.Get("Cache-Status")) 662 | } 663 | if resp2.Header.Get("Age") != "1" { 664 | t.Errorf("unexpected resp2 Age header %v", resp2.Header.Get("Age")) 665 | } 666 | 667 | time.Sleep(6 * time.Second) 668 | staleReq, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/stale-if-error", nil) 669 | staleReq.Header = http.Header{"Cache-Control": []string{"stale-if-error=86400"}} 670 | resp3, _ := tester.AssertResponse(staleReq, http.StatusOK, "Hello stale-if-error!") 671 | 672 | if resp3.Header.Get("Cache-Control") != "stale-if-error=86400" { 673 | t.Errorf("unexpected resp3 Cache-Control header %v", resp3.Header.Get("Cache-Control")) 674 | } 675 | if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=-2; key=GET-http-localhost:9080-/stale-if-error; detail=DEFAULT; fwd=stale; fwd-status=500" { 676 | t.Errorf("unexpected resp3 Cache-Status header %v", resp3.Header.Get("Cache-Status")) 677 | } 678 | if resp3.Header.Get("Age") != "7" { 679 | t.Errorf("unexpected resp3 Age header %v", resp3.Header.Get("Age")) 680 | } 681 | 682 | resp4, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusOK, "Hello stale-if-error!") 683 | 684 | if resp4.Header.Get("Cache-Status") != "Souin; hit; ttl=-2; key=GET-http-localhost:9080-/stale-if-error; detail=DEFAULT; fwd=stale; fwd-status=500" && 685 | resp4.Header.Get("Cache-Status") != "Souin; hit; ttl=-3; key=GET-http-localhost:9080-/stale-if-error; detail=DEFAULT; fwd=stale; fwd-status=500" { 686 | t.Errorf("unexpected resp4 Cache-Status header %v", resp4.Header.Get("Cache-Status")) 687 | } 688 | 689 | if resp4.Header.Get("Age") != "7" && resp4.Header.Get("Age") != "8" { 690 | t.Errorf("unexpected resp4 Age header %v", resp4.Header.Get("Age")) 691 | } 692 | 693 | time.Sleep(6 * time.Second) 694 | resp5, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusInternalServerError, "") 695 | 696 | if resp5.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; key=GET-http-localhost:9080-/stale-if-error; detail=UNCACHEABLE-STATUS-CODE" { 697 | t.Errorf("unexpected resp5 Cache-Status header %v", resp5.Header.Get("Cache-Status")) 698 | } 699 | 700 | if resp5.Header.Get("Age") != "" { 701 | t.Errorf("unexpected resp5 Age header %v", resp5.Header.Get("Age")) 702 | } 703 | } 704 | 705 | type testETagsHandler struct{} 706 | 707 | const etagValue = "AAA-BBB" 708 | 709 | func (t *testETagsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 710 | if strings.Contains(r.Header.Get("If-None-Match"), etagValue) { 711 | w.WriteHeader(http.StatusNotModified) 712 | 713 | return 714 | } 715 | w.Header().Set("ETag", etagValue) 716 | w.WriteHeader(http.StatusOK) 717 | _, _ = w.Write([]byte("Hello etag!")) 718 | } 719 | 720 | func Test_ETags(t *testing.T) { 721 | tester := caddytest.NewTester(t) 722 | tester.InitServer(` 723 | { 724 | admin localhost:2999 725 | http_port 9080 726 | cache { 727 | ttl 50s 728 | stale 50s 729 | } 730 | } 731 | localhost:9080 { 732 | route /etags { 733 | cache 734 | reverse_proxy localhost:9082 735 | } 736 | }`, "caddyfile") 737 | 738 | go func() { 739 | etagsHandler := testETagsHandler{} 740 | _ = http.ListenAndServe(":9082", &etagsHandler) 741 | }() 742 | _, _ = tester.AssertGetResponse(`http://localhost:9080/etags`, http.StatusOK, "Hello etag!") 743 | staleReq, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/etags", nil) 744 | staleReq.Header = http.Header{"If-None-Match": []string{etagValue}} 745 | _, _ = tester.AssertResponse(staleReq, http.StatusNotModified, "") 746 | staleReq.Header = http.Header{} 747 | _, _ = tester.AssertResponse(staleReq, http.StatusOK, "Hello etag!") 748 | staleReq.Header = http.Header{"If-None-Match": []string{etagValue}} 749 | _, _ = tester.AssertResponse(staleReq, http.StatusNotModified, "") 750 | staleReq.Header = http.Header{"If-None-Match": []string{"other"}} 751 | _, _ = tester.AssertResponse(staleReq, http.StatusOK, "Hello etag!") 752 | } 753 | 754 | type testHugeMaxAgeHandler struct{} 755 | 756 | func (t *testHugeMaxAgeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 757 | w.Header().Set("Cache-Control", "max-age=600") 758 | w.WriteHeader(http.StatusOK) 759 | _, _ = w.Write([]byte("Hello, huge max age!")) 760 | } 761 | 762 | func TestHugeMaxAgeHandler(t *testing.T) { 763 | tester := caddytest.NewTester(t) 764 | tester.InitServer(` 765 | { 766 | admin localhost:2999 767 | http_port 9080 768 | https_port 9443 769 | cache 770 | } 771 | localhost:9080 { 772 | route /huge-max-age { 773 | cache 774 | reverse_proxy localhost:9083 775 | } 776 | }`, "caddyfile") 777 | 778 | go func() { 779 | hugeMaxAgeHandler := testHugeMaxAgeHandler{} 780 | _ = http.ListenAndServe(":9083", &hugeMaxAgeHandler) 781 | }() 782 | time.Sleep(time.Second) 783 | 784 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/huge-max-age`, 200, "Hello, huge max age!") 785 | if resp1.Header.Get("Age") != "" { 786 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 787 | } 788 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/huge-max-age" { 789 | t.Error("Cache-Status header should be present") 790 | } 791 | 792 | resp2, _ := tester.AssertGetResponse(`http://localhost:9080/huge-max-age`, 200, "Hello, huge max age!") 793 | if resp2.Header.Get("Age") == "" { 794 | t.Error("Age header should be present") 795 | } 796 | if resp2.Header.Get("Age") != "1" { 797 | t.Error("Age header should be present") 798 | } 799 | if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=599; key=GET-http-localhost:9080-/huge-max-age; detail=DEFAULT" { 800 | t.Error("Cache-Status header should be present") 801 | } 802 | 803 | time.Sleep(2 * time.Second) 804 | resp3, _ := tester.AssertGetResponse(`http://localhost:9080/huge-max-age`, 200, "Hello, huge max age!") 805 | if resp3.Header.Get("Age") != "3" { 806 | t.Error("Age header should be present") 807 | } 808 | if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=597; key=GET-http-localhost:9080-/huge-max-age; detail=DEFAULT" { 809 | t.Error("Cache-Status header should be present") 810 | } 811 | } 812 | 813 | type testVaryHandler struct{} 814 | 815 | const variedHeader = "X-Varied" 816 | 817 | func (t *testVaryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 818 | time.Sleep(50 * time.Millisecond) 819 | w.Header().Set("Vary", variedHeader) 820 | w.Header().Set(variedHeader, r.Header.Get(variedHeader)) 821 | w.WriteHeader(http.StatusOK) 822 | _, _ = w.Write([]byte(fmt.Sprintf("Hello, vary %s!", r.Header.Get(variedHeader)))) 823 | } 824 | 825 | func TestVaryHandler(t *testing.T) { 826 | tester := caddytest.NewTester(t) 827 | tester.InitServer(` 828 | { 829 | admin localhost:2999 830 | http_port 9080 831 | https_port 9443 832 | cache 833 | } 834 | localhost:9080 { 835 | route /vary-multiple { 836 | cache 837 | reverse_proxy localhost:9084 838 | } 839 | }`, "caddyfile") 840 | 841 | go func() { 842 | varyHandler := testVaryHandler{} 843 | _ = http.ListenAndServe(":9084", &varyHandler) 844 | }() 845 | time.Sleep(time.Second) 846 | 847 | baseRq, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/vary-multiple", nil) 848 | 849 | rq1 := baseRq.Clone(context.Background()) 850 | rq1.Header.Set(variedHeader, "first") 851 | rq2 := baseRq.Clone(context.Background()) 852 | rq2.Header.Set(variedHeader, "second") 853 | rq3 := baseRq.Clone(context.Background()) 854 | rq3.Header.Set(variedHeader, "third") 855 | rq4 := baseRq.Clone(context.Background()) 856 | rq4.Header.Set(variedHeader, "fourth") 857 | 858 | requests := []*http.Request{ 859 | rq1, 860 | rq2, 861 | rq3, 862 | rq4, 863 | } 864 | 865 | var wg sync.WaitGroup 866 | resultMap := &sync.Map{} 867 | 868 | for i, rq := range requests { 869 | wg.Add(1) 870 | 871 | go func(r *http.Request, iteration int) { 872 | defer wg.Done() 873 | res, _ := tester.AssertResponse(r, 200, fmt.Sprintf("Hello, vary %s!", r.Header.Get(variedHeader))) 874 | resultMap.Store(iteration, res) 875 | }(rq, i) 876 | } 877 | 878 | wg.Wait() 879 | 880 | for i := 0; i < 4; i++ { 881 | if res, ok := resultMap.Load(i); !ok { 882 | t.Errorf("unexpected nil response for iteration %d", i) 883 | } else { 884 | rs, ok := res.(*http.Response) 885 | if !ok { 886 | t.Error("The object is not type of *http.Response") 887 | } 888 | 889 | if rs.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/vary-multiple" { 890 | t.Errorf("The response %d doesn't match the expected header: %s", i, rs.Header.Get("Cache-Status")) 891 | } 892 | } 893 | } 894 | 895 | for i, rq := range requests { 896 | wg.Add(1) 897 | 898 | go func(r *http.Request, iteration int) { 899 | defer wg.Done() 900 | res, _ := tester.AssertResponse(r, 200, fmt.Sprintf("Hello, vary %s!", r.Header.Get(variedHeader))) 901 | resultMap.Store(iteration, res) 902 | }(rq, i) 903 | } 904 | 905 | wg.Wait() 906 | 907 | checker := func(res any, ttl int) { 908 | rs, ok := res.(*http.Response) 909 | if !ok { 910 | t.Error("The object is not type of *http.Response") 911 | } 912 | 913 | nextTTL := ttl - 1 914 | if (rs.Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-/vary-multiple; detail=DEFAULT", ttl) || rs.Header.Get("Age") != fmt.Sprint(120-ttl)) && 915 | (rs.Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-/vary-multiple; detail=DEFAULT", nextTTL) || rs.Header.Get("Age") != fmt.Sprint(120-nextTTL)) { 916 | t.Errorf("The response doesn't match the expected header or age: %s => %s", rs.Header.Get("Cache-Status"), rs.Header.Get("Age")) 917 | } 918 | } 919 | 920 | if res, ok := resultMap.Load(0); !ok { 921 | t.Errorf("unexpected nil response for iteration %d", 0) 922 | } else { 923 | checker(res, 119) 924 | } 925 | 926 | if res, ok := resultMap.Load(1); !ok { 927 | t.Errorf("unexpected nil response for iteration %d", 1) 928 | } else { 929 | checker(res, 119) 930 | } 931 | 932 | if res, ok := resultMap.Load(2); !ok { 933 | t.Errorf("unexpected nil response for iteration %d", 2) 934 | } else { 935 | checker(res, 119) 936 | } 937 | 938 | if res, ok := resultMap.Load(3); !ok { 939 | t.Errorf("unexpected nil response for iteration %d", 3) 940 | } else { 941 | checker(res, 119) 942 | } 943 | } 944 | 945 | func TestESITags(t *testing.T) { 946 | tester := caddytest.NewTester(t) 947 | tester.InitServer(` 948 | { 949 | admin localhost:2999 950 | http_port 9080 951 | https_port 9443 952 | cache { 953 | ttl 1000s 954 | } 955 | } 956 | localhost:9080 { 957 | route /esi-include-1 { 958 | cache 959 | respond "esi-include-1 with some long content to ensure the compute works well. Also add some dummy text with some $pecial characters without recursive esi includes" 960 | } 961 | route /esi-include-2 { 962 | cache 963 | respond "esi-include-2" 964 | } 965 | route /esi-path { 966 | cache 967 | header Cache-Control "max-age=60" 968 | respond "Hello and !" 969 | } 970 | }`, "caddyfile") 971 | 972 | resp1, _ := tester.AssertGetResponse(`http://localhost:9080/esi-path`, 200, "Hello esi-include-1 with some long content to ensure the compute works well. Also add some dummy text with some $pecial characters without recursive esi includes and esi-include-2!") 973 | if resp1.Header.Get("Age") != "" { 974 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 975 | } 976 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/esi-path" { 977 | t.Errorf("unexpected Cache-Status header %v", resp1.Header.Get("Cache-Status")) 978 | } 979 | if resp1.Header.Get("Content-Length") != "180" { 980 | t.Errorf("unexpected Content-Length header %v", resp1.Header.Get("Content-Length")) 981 | } 982 | 983 | resp2, _ := tester.AssertGetResponse(`http://localhost:9080/esi-path`, 200, "Hello esi-include-1 with some long content to ensure the compute works well. Also add some dummy text with some $pecial characters without recursive esi includes and esi-include-2!") 984 | if resp2.Header.Get("Age") == "" { 985 | t.Error("Age header should be present") 986 | } 987 | if resp2.Header.Get("Age") != "1" { 988 | t.Error("Age header should be present") 989 | } 990 | 991 | resp3, _ := tester.AssertGetResponse(`http://localhost:9080/esi-include-1`, 200, "esi-include-1 with some long content to ensure the compute works well. Also add some dummy text with some $pecial characters without recursive esi includes") 992 | if resp3.Header.Get("Age") == "" { 993 | t.Error("Age header should be present") 994 | } 995 | if resp3.Header.Get("Cache-Status") == "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/esi-include-1" { 996 | t.Error("Cache-Status should be already stored") 997 | } 998 | 999 | resp4, _ := tester.AssertGetResponse(`http://localhost:9080/esi-include-2`, 200, "esi-include-2") 1000 | if resp4.Header.Get("Age") == "" { 1001 | t.Error("Age header should be present") 1002 | } 1003 | if resp4.Header.Get("Cache-Status") == "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/esi-include-2" { 1004 | t.Error("Cache-Status should be already stored") 1005 | } 1006 | } 1007 | 1008 | func TestCacheableStatusCode(t *testing.T) { 1009 | caddyTester := caddytest.NewTester(t) 1010 | caddyTester.InitServer(` 1011 | { 1012 | admin localhost:2999 1013 | http_port 9080 1014 | https_port 9443 1015 | cache { 1016 | ttl 10s 1017 | } 1018 | } 1019 | localhost:9080 { 1020 | cache 1021 | 1022 | respond /cache-200 "" 200 { 1023 | close 1024 | } 1025 | respond /cache-204 "" 204 { 1026 | close 1027 | } 1028 | respond /cache-301 "" 301 { 1029 | close 1030 | } 1031 | respond /cache-405 "" 405 { 1032 | close 1033 | } 1034 | }`, "caddyfile") 1035 | 1036 | cacheChecker := func(tester *caddytest.Tester, path string, expectedStatusCode int, expectedCached bool) { 1037 | resp1, _ := tester.AssertGetResponse("http://localhost:9080"+path, expectedStatusCode, "") 1038 | if resp1.Header.Get("Age") != "" { 1039 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 1040 | } 1041 | 1042 | cacheStatus := "Souin; fwd=uri-miss; " 1043 | if expectedCached { 1044 | cacheStatus += "stored; " 1045 | } else { 1046 | cacheStatus += "detail=UPSTREAM-ERROR-OR-EMPTY-RESPONSE; " 1047 | } 1048 | cacheStatus += "key=GET-http-localhost:9080-" + path 1049 | 1050 | if resp1.Header.Get("Cache-Status") != cacheStatus { 1051 | t.Errorf("unexpected first Cache-Status header %v", resp1.Header.Get("Cache-Status")) 1052 | } 1053 | 1054 | resp1, _ = tester.AssertGetResponse("http://localhost:9080"+path, expectedStatusCode, "") 1055 | 1056 | cacheStatus = "Souin; " 1057 | detail := "" 1058 | if expectedCached { 1059 | if resp1.Header.Get("Age") != "1" { 1060 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 1061 | } 1062 | cacheStatus += "hit; ttl=9; " 1063 | detail = "; detail=DEFAULT" 1064 | } else { 1065 | cacheStatus += "fwd=uri-miss; detail=UPSTREAM-ERROR-OR-EMPTY-RESPONSE; " 1066 | } 1067 | cacheStatus += "key=GET-http-localhost:9080-" + path + detail 1068 | 1069 | if resp1.Header.Get("Cache-Status") != cacheStatus { 1070 | t.Errorf("unexpected second Cache-Status header %v", resp1.Header.Get("Cache-Status")) 1071 | } 1072 | } 1073 | 1074 | cacheChecker(caddyTester, "/cache-200", 200, false) 1075 | cacheChecker(caddyTester, "/cache-204", 204, true) 1076 | cacheChecker(caddyTester, "/cache-301", 301, true) 1077 | cacheChecker(caddyTester, "/cache-405", 405, true) 1078 | } 1079 | 1080 | func TestExpires(t *testing.T) { 1081 | expiresValue := time.Now().Add(time.Hour * 24) 1082 | caddyTester := caddytest.NewTester(t) 1083 | caddyTester.InitServer(fmt.Sprintf(` 1084 | { 1085 | admin localhost:2999 1086 | http_port 9080 1087 | https_port 9443 1088 | cache { 1089 | ttl 10s 1090 | } 1091 | } 1092 | localhost:9080 { 1093 | route /expires-only { 1094 | cache 1095 | header Expires "%[1]s" 1096 | respond "Hello, expires-only!" 1097 | } 1098 | route /expires-with-max-age { 1099 | cache 1100 | header Expires "%[1]s" 1101 | header Cache-Control "max-age=60" 1102 | respond "Hello, expires-with-max-age!" 1103 | } 1104 | route /expires-with-s-maxage { 1105 | cache 1106 | header Expires "%[1]s" 1107 | header Cache-Control "s-maxage=5" 1108 | respond "Hello, expires-with-s-maxage!" 1109 | } 1110 | }`, expiresValue.Format(time.RFC1123)), "caddyfile") 1111 | 1112 | cacheChecker := func(tester *caddytest.Tester, path string, expectedBody string, expectedDuration int) { 1113 | resp1, _ := tester.AssertGetResponse("http://localhost:9080"+path, 200, expectedBody) 1114 | if resp1.Header.Get("Age") != "" { 1115 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 1116 | } 1117 | 1118 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-"+path { 1119 | t.Errorf("unexpected first Cache-Status header %v", resp1.Header.Get("Cache-Status")) 1120 | } 1121 | 1122 | resp1, _ = tester.AssertGetResponse("http://localhost:9080"+path, 200, expectedBody) 1123 | 1124 | if resp1.Header.Get("Age") != "1" { 1125 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 1126 | } 1127 | 1128 | if resp1.Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-%s; detail=DEFAULT", expectedDuration, path) { 1129 | t.Errorf( 1130 | "unexpected second Cache-Status header %v, expected %s", 1131 | resp1.Header.Get("Cache-Status"), 1132 | fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-%s; detail=DEFAULT", expectedDuration, path), 1133 | ) 1134 | } 1135 | } 1136 | 1137 | cacheChecker(caddyTester, "/expires-only", "Hello, expires-only!", int(time.Until(expiresValue).Seconds())-1) 1138 | cacheChecker(caddyTester, "/expires-with-max-age", "Hello, expires-with-max-age!", 59) 1139 | cacheChecker(caddyTester, "/expires-with-s-maxage", "Hello, expires-with-s-maxage!", 4) 1140 | } 1141 | 1142 | func TestComplexQuery(t *testing.T) { 1143 | caddyTester := caddytest.NewTester(t) 1144 | caddyTester.InitServer(` 1145 | { 1146 | admin localhost:2999 1147 | http_port 9080 1148 | https_port 9443 1149 | cache { 1150 | ttl 10s 1151 | } 1152 | } 1153 | localhost:9080 { 1154 | route /complex-query { 1155 | cache 1156 | respond "Hello, {query}!" 1157 | } 1158 | }`, "caddyfile") 1159 | 1160 | cacheChecker := func(tester *caddytest.Tester, query string, expectedDuration int) { 1161 | body := fmt.Sprintf("Hello, %s!", query) 1162 | resp1, _ := tester.AssertGetResponse("http://localhost:9080/complex-query?"+query, 200, body) 1163 | if resp1.Header.Get("Age") != "" { 1164 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 1165 | } 1166 | 1167 | if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/complex-query?"+query { 1168 | t.Errorf("unexpected first Cache-Status header %v", resp1.Header.Get("Cache-Status")) 1169 | } 1170 | 1171 | resp1, _ = tester.AssertGetResponse("http://localhost:9080/complex-query?"+query, 200, body) 1172 | 1173 | if resp1.Header.Get("Age") != "1" { 1174 | t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) 1175 | } 1176 | 1177 | if resp1.Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-/complex-query?%s; detail=DEFAULT", expectedDuration, query) { 1178 | t.Errorf( 1179 | "unexpected second Cache-Status header %v, expected %s", 1180 | resp1.Header.Get("Cache-Status"), 1181 | fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-/complex-query?%s; detail=DEFAULT", expectedDuration, query), 1182 | ) 1183 | } 1184 | } 1185 | 1186 | cacheChecker(caddyTester, "fields[]=id&pagination=true", 9) 1187 | cacheChecker(caddyTester, "fields[]=id&pagination=false", 9) 1188 | } 1189 | 1190 | func TestBypassWithExpiresAndRevalidate(t *testing.T) { 1191 | tester := caddytest.NewTester(t) 1192 | tester.InitServer(` 1193 | { 1194 | debug 1195 | admin localhost:2999 1196 | http_port 9080 1197 | https_port 9443 1198 | cache { 1199 | ttl 5s 1200 | stale 5s 1201 | mode bypass 1202 | } 1203 | } 1204 | localhost:9080 { 1205 | route /bypass-with-expires-and-revalidate { 1206 | cache 1207 | header Expires 0 1208 | header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" 1209 | respond "Hello, expires and revalidate!" 1210 | } 1211 | }`, "caddyfile") 1212 | 1213 | respStored1, _ := tester.AssertGetResponse(`http://localhost:9080/bypass-with-expires-and-revalidate`, 200, "Hello, expires and revalidate!") 1214 | if respStored1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/bypass-with-expires-and-revalidate" { 1215 | t.Errorf("unexpected Cache-Status header value %v", respStored1.Header.Get("Cache-Status")) 1216 | } 1217 | if respStored1.Header.Get("Age") != "" { 1218 | t.Errorf("unexpected Age header %v", respStored1.Header.Get("Age")) 1219 | } 1220 | 1221 | respStored2, _ := tester.AssertGetResponse(`http://localhost:9080/bypass-with-expires-and-revalidate`, 200, "Hello, expires and revalidate!") 1222 | if respStored2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/bypass-with-expires-and-revalidate; detail=DEFAULT" { 1223 | t.Errorf("unexpected Cache-Status header value %v", respStored2.Header.Get("Cache-Status")) 1224 | } 1225 | if respStored2.Header.Get("Age") == "" { 1226 | t.Error("Age header should be present") 1227 | } 1228 | 1229 | time.Sleep(5 * time.Second) 1230 | respStored3, _ := tester.AssertGetResponse(`http://localhost:9080/bypass-with-expires-and-revalidate`, 200, "Hello, expires and revalidate!") 1231 | if respStored3.Header.Get("Cache-Status") != "Souin; hit; ttl=-1; key=GET-http-localhost:9080-/bypass-with-expires-and-revalidate; detail=DEFAULT; fwd=stale" { 1232 | t.Errorf("unexpected Cache-Status header value %v", respStored3.Header.Get("Cache-Status")) 1233 | } 1234 | if respStored3.Header.Get("Age") == "" { 1235 | t.Error("Age header should be present") 1236 | } 1237 | 1238 | time.Sleep(5 * time.Second) 1239 | respStored4, _ := tester.AssertGetResponse(`http://localhost:9080/bypass-with-expires-and-revalidate`, 200, "Hello, expires and revalidate!") 1240 | if respStored4.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/bypass-with-expires-and-revalidate" { 1241 | t.Errorf("unexpected Cache-Status header value %v", respStored4.Header.Get("Cache-Status")) 1242 | } 1243 | if respStored4.Header.Get("Age") != "" { 1244 | t.Errorf("unexpected Age header %v", respStored4.Header.Get("Age")) 1245 | } 1246 | } 1247 | --------------------------------------------------------------------------------