├── .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 |
--------------------------------------------------------------------------------