Compare commits

..

379 Commits

Author SHA1 Message Date
Mohammed Al Sahaf 5245045f44 Merge branch 'master' into acme-database 2024-02-24 02:26:57 +03:00
Mohammed Al Sahaf 931656bd68 acmeserver: add policy field to define allow/deny rules (#5796)
* acmeserver: support specifying the allowed challenge types

* add caddyfile adapt tests

* acmeserver: add `policy` field to define allow/deny rules

* allow `omitempty` to work

* add caddyfile support for `policy`

* remove "uri domain" policy

* fmt the files

* add docs

* do not support `CommonName`; the field is deprecated

* r/DNSDomains/Domains/g

* Caddyfile docs

* add tests

* move `Policy` to top of file
2024-02-24 02:26:00 +03:00
Mohammed Al Sahaf 1a3ba2890b Merge branch 'master' into acme-database 2024-02-24 02:12:25 +03:00
Sam Ottenhoff da6a569e85 reverseproxy: cookie should be Secure and SameSite=None when TLS (#6115)
* reverseproxy: cookie should be Secure and SameSite=None when TLS

* Update modules/caddyhttp/reverseproxy/selectionpolicies_test.go

Co-authored-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>

---------

Co-authored-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>
2024-02-23 12:45:58 -07:00
Francis Lavoie 4512be49a9 caddytest: Rename adapt tests to *.caddyfiletest extension (#6119) 2024-02-21 00:37:40 +00:00
José Carlos Chávez f8143a3af1 tests: uses testing.TB interface for helper to be able to use test server in benchmarks. (#6103) 2024-02-20 22:04:14 +00:00
bbaa 8bbf8ec629 caddyfile: Assert having a space after heredoc marker to simply check (#6117) 2024-02-20 12:29:20 +00:00
Francis Lavoie 4284e39a17 chore: Update Chroma to get the new Caddyfile lexer (#6118) 2024-02-20 06:23:39 -05:00
WeidiDeng 53f7035299 reverseproxy: use context.WithoutCancel (#6116) 2024-02-19 20:25:02 -07:00
Aziz Rmadi b893c8c5f8 caddyfile: Reject directives in the place of site addresses (#6104)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-02-19 00:22:48 +00:00
Matt Holt 127788807f caddyhttp: Register post-shutdown callbacks (#5948) 2024-02-14 21:21:23 -07:00
Francis Lavoie 2c48dda109 caddyhttp: Only attempt to enable full duplex for HTTP/1.x (#6102) 2024-02-13 13:45:38 -05:00
Francis Lavoie 30d63648f5 caddyauth: Drop support for scrypt (#6091) 2024-02-12 19:33:54 +00:00
Mohammed Al Sahaf 21744b6c4c Revert "caddyfile: Reject long heredoc markers (#6098)" (#6100)
This reverts commit e7a534d0a3.
2024-02-12 18:06:22 +00:00
Francis Lavoie f9e11158bc caddyauth: Rename basicauth to basic_auth (#6092) 2024-02-12 17:34:23 +00:00
Francis Lavoie 91ec75441a logging: Inline Caddyfile syntax for ip_mask filter (#6094) 2024-02-12 17:15:35 +00:00
Francis Lavoie e7a534d0a3 caddyfile: Reject long heredoc markers (#6098)
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2024-02-11 13:30:14 -05:00
Mohammed Al Sahaf 998d165b45 simplify getting the *caddy.Replacer line
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-02-11 16:09:51 +03:00
Mohammed Al Sahaf f94affbc39 acmeserver: support additional database types beside bbolt 2024-02-11 12:49:14 +00:00
Francis Lavoie c78ebb3d6a chore: Rename CI jobs, run on M1 mac (#6089)
* Try macos-14 for fun

* Decouple OS names and VM names

* Shorten `cross-build-test` to `build`
2024-02-09 15:31:26 -07:00
Kévin Dunglas a6d9f9be5b Merge pull request #6081 from dunglas/fix/encode-match 2024-02-09 09:41:44 +01:00
Kévin Dunglas 2348ac897a update comment 2024-02-09 09:35:55 +01:00
Kévin Dunglas d3f23a8eeb improved list 2024-02-09 09:35:55 +01:00
Kévin Dunglas 60abd72c7a fix: add back text/* 2024-02-09 09:35:55 +01:00
Kévin Dunglas b8f729b88f fix: add more media types to the compressed by default list 2024-02-09 09:35:55 +01:00
Mohammed Al Sahaf e1aa862e6a acmeserver: support specifying the allowed challenge types (#5794)
* acmeserver: support specifying the allowed challenge types

* add caddyfile adapt tests

* introduce basic acme_server test

* skip acme test on unsuitable environments

* skip integration tests of ACME

* documentation

* add negative-scenario test for mismatched allowed challenges

* a bit more docs

* fix tests for ACME challenges

* appease the linter

* skip ACME tests on s390x

* enable ACME challenge tests on all machines

* Apply suggestions from code review

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-02-08 11:42:03 +03:00
Francis Lavoie 8c2a72ad07 matchers: Drop forwarded option from remote_ip matcher (#6085) 2024-02-07 10:09:29 -05:00
Francis Lavoie bde46211e3 caddyhttp: Test cases for %2F and %252F (#6084) 2024-02-07 05:13:17 -05:00
WeidiDeng bc1e63198d bump to golang 1.22 (#6083) 2024-02-07 02:13:58 -05:00
Aziz Rmadi feb07a7b59 fileserver: Browse can show symlink target if enabled (#5973)
* Added optional subdirective to browse allowing to reveal symlink paths.

* Update modules/caddyhttp/fileserver/browsetplcontext.go

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-02-06 04:31:26 +00:00
Aziz Rmadi a7479302fc core: Support NO_COLOR env var to disable log coloring (#6078) 2024-02-01 19:12:42 -07:00
dependabot[bot] 223f314331 build(deps): bump peter-evans/repository-dispatch from 2 to 3 (#6080)
Bumps [peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch) from 2 to 3.
- [Release notes](https://github.com/peter-evans/repository-dispatch/releases)
- [Commits](https://github.com/peter-evans/repository-dispatch/compare/v2...v3)

---
updated-dependencies:
- dependency-name: peter-evans/repository-dispatch
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 18:34:40 -05:00
Matthew Holt 1919c08ecc Update comment in setcap helper script 2024-01-31 12:59:26 -07:00
Matt Holt 57c5b921a4 caddytls: Make on-demand 'ask' permission modular (#6055)
* caddytls: Make on-demand 'ask' permission modular

This makes the 'ask' endpoint a module, which means that developers can
write custom plugins for granting permission for on-demand certificates.

Kicking myself that we didn't do it this way at the beginning, but who coulda known...

* Lint

* Error on conflicting config

* Fix bad merge

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-30 16:11:29 -07:00
Francis Lavoie e1b9a9d7b0 core: Add ctx.Slogger() which returns an slog logger (#5945) 2024-01-25 12:31:15 -07:00
Marten Seemann 697cc593a1 chore: Update quic-go to v0.41.0, bump Go minimum to 1.21 (#6043)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2024-01-25 13:58:19 -05:00
Yolan Romailler 2fe69a828f chore: enabling a few more linters (#5961)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-25 15:24:58 +00:00
bbaa c369df5c37 caddyfile: Correctly close the heredoc when the closing marker appears immediately (#6062) 2024-01-25 14:55:00 +00:00
bbaa 7c48b5fdbb caddyfile: Switch to slices.Equal for better performance (#6061) 2024-01-25 14:46:08 +00:00
Mohammed Al Sahaf e965b111cd tls: modularize trusted CA providers (#5784)
* tls: modularize client authentication trusted CA

* add `omitempty` to `CARaw`

* docs

* initial caddyfile support

* revert anything related to leaf cert validation

The certs are used differently than the CA pool flow

* complete caddyfile unmarshalling implementation

* Caddyfile syntax documentation

* enhance caddyfile parsing and documentation

Apply suggestions from code review

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* add client_auth caddyfile tests

* add caddyfile unmarshalling tests

* fix and add missed adapt tests

* fix rebase issue

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-25 11:44:41 +03:00
Francis Lavoie b9c40e7111 logging: Automatic wrap default for filter encoder (#5980)
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
2024-01-25 04:00:22 +00:00
Francis Lavoie f5344f8cad caddyhttp: Fix panic when request missing ClientIPVarKey (#6040) 2024-01-24 00:45:50 +00:00
Francis Lavoie 750d0b8331 caddyfile: Normalize & flatten all unmarshalers (#6037) 2024-01-23 19:36:59 -05:00
Mohammed Al Sahaf 54823f52bc cmd: reverseproxy: log: use caddy logger (#6042) 2024-01-23 10:52:02 -07:00
Aziz Rmadi ed7e3c906a matchers: query now ANDs multiple keys (#6054)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-22 02:36:44 +00:00
bbaa c0273f1f04 caddyfile: Add heredoc support to fmt command (#6056) 2024-01-22 02:24:49 +00:00
Kévin Dunglas dba556fe4b refactor: move automaxprocs init in caddycmd.Main() 2024-01-19 11:17:35 +01:00
Aziz Rmadi d9aded016c caddyfile: Allow heredoc blank lines (#6051) 2024-01-18 22:57:18 -05:00
Aziz Rmadi 4181c79a81 httpcaddyfile: Add optional status code argument to handle_errors directive (#5965)
Co-authored-by: Aziz Rmadi <azizrmadi@Azizs-MacBook-Air.local>
2024-01-16 01:24:17 -05:00
Francis Lavoie 5e2f1b5ced httpcaddyfile: Rewrite root and rewrite parsing to allow omitting matcher (#5844) 2024-01-15 09:57:08 -07:00
Francis Lavoie f3e849e49f fileserver: Implement caddyfile.Unmarshaler interface (#5850) 2024-01-13 21:32:44 +00:00
Bas Westerbaan f658fd05ac reverseproxy: Add tls_curves option to HTTP transport (#5851) 2024-01-13 20:56:23 +00:00
Nebez Briefkani cc0c0cf03e caddyhttp: Security enhancements for client IP parsing (#5805)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-13 20:46:37 +00:00
Aziz Rmadi 80acf1bf23 replacer: Fix escaped closing braces (#5995) 2024-01-13 20:24:03 +00:00
a c839a98ff5 filesystem: Globally declared filesystems, fs directive (#5833) 2024-01-13 20:12:43 +00:00
Mohammed Al Sahaf b359ca565c ci/cd: use the build tag nobadger to exclude badgerdb (#6031)
* ci/cd: use the build tag `nobadger` to exclude badgerdb

* upgrade github.com/google/certificate-transparency-go@master
2024-01-10 21:04:11 +03:00
Subhaditya Nath c2d889f85e httpcaddyfile: Fix redir <to> html (#6001) 2024-01-10 12:24:47 +00:00
Zach Galvin cb86319bd5 httpcaddyfile: Support client auth verifiers (#6022)
* Added verifier case

Update author

* Update verifier to match struct tag

* gci run
2024-01-09 23:14:51 +00:00
Rithvik Vibhu ed41c924cf tls: add reuse_private_keys (#6025) 2024-01-09 16:00:31 -07:00
Fred Cox d9ff7b1872 reverseproxy: Only change Content-Length when full request is buffered (#5830)
fixes: https://github.com/caddyserver/caddy/issues/5829

Signed-off-by: Fred Cox <mcfedr@gmail.com>
2024-01-09 12:59:30 -07:00
Aaron Brady 76611fa150 Switch Solaris-derivatives away from listen_unix (#6021)
Solaris 10 and Illumos are missing SO_REUSEPORT. Treat them more like
Windows (i.e. use the listener pool).
2024-01-06 05:09:20 -05:00
dependabot[bot] 8a50f191bf build(deps): bump actions/upload-artifact from 3 to 4 (#6013)
* build(deps): bump actions/upload-artifact from 3 to 4

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Disable compression

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2024-01-02 08:23:25 +00:00
dependabot[bot] 4f3f6e35e8 build(deps): bump actions/setup-go from 4 to 5 (#6012)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 07:13:31 +00:00
Mohammed Al Sahaf 787f6b257f chore: check against errors of io/fs instead of os (#6011)
* chore: replace `os.ErrNotExist` with `fs.ErrNotExist`

* check against permission error from `io/fs` package
2024-01-02 08:48:55 +03:00
networkException b568a10dd4 caddyhttp: support unix sockets in caddy respond command (#6010)
previously the `caddy respond` command would treat the argument
passed to --listen as a TCP socket address, iterating over a possible
port range.

this patch factors the server creation out into a separate function,
allowing this to be reused in case the listen address is a unix network
address.
2023-12-31 22:34:00 -05:00
Steffen Busch 8f9ffc587e fileserver: Add total file size to directory listing (#6003)
* browse: Add total file size to directory listing

* Apply suggestion to remove "in "

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-12-30 18:47:13 +00:00
Francis Lavoie f976c84d9e httpcaddyfile: Fix cert file decoding to load multiple PEM in one file (#5997) 2023-12-20 08:37:21 -07:00
dependabot[bot] 1bf72db6ff build(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#5994)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 16:11:51 -07:00
Kévin Dunglas d54dcf1598 cmd: use automaxprocs for better perf in containers (#5711)
* feat: use automaxprocs for better perf in containers

* better logs

* cs
2023-12-18 15:50:26 -07:00
Francis Lavoie 3248e4c89f logging: Add zap.Option support (#5944) 2023-12-18 20:48:34 +00:00
Francis Lavoie da7d8cb26d httpcaddyfile: Sort skip_hosts for deterministic JSON (#5990)
* httpcaddyfile: Sort skip_hosts for deterministic JSON

* Update caddyconfig/httpcaddyfile/httptype.go

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* Fix test

* Bah

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2023-12-18 12:54:52 -07:00
Tim Geoghegan 387545a895 metrics: Record request metrics on HTTP errors (#5979) 2023-12-15 20:14:00 +00:00
Aziz Rmadi b49ec05161 go.mod: Updated quic-go to v0.40.1 (#5983) 2023-12-14 22:42:01 -07:00
Kévin Dunglas b16aba5c27 fileserver: Enable compression for command by default (#5855)
* feat: enable compression for file-server

* refactor

* const

* Update help text

* Update modules/caddyhttp/fileserver/command.go

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-12-13 20:44:22 -07:00
David DeMoss 362f33daae fileserver: New --precompressed flag (#5880)
exposes the file_server precompressed functionality to be used with the
file-server command

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-12-13 20:26:20 -07:00
Francis Lavoie 3d7d60f7cf caddyhttp: Add uuid to access logs when used (#5859) 2023-12-13 15:40:15 -07:00
Mohammed Al Sahaf dc12bd9743 proxyprotocol: use github.com/pires/go-proxyproto (#5915)
* proxyprotocol: use github.com/pires/go-proxyproto

* Fix typo: r/generelly/generally

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* add config options for `Deny` CIDR and fallback policy

* use `netip` package & trust unix sockets

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-12-13 09:07:43 -07:00
Jens-Uwe Mager 56c6b3f673 cmd: Preserve LastModified date when exporting storage (#5968) 2023-12-13 09:06:06 -07:00
Aziz Rmadi cbbd1df904 core: Always make AppDataDir for InstanceID (#5976) 2023-12-13 07:39:10 -07:00
Benjamin Marwell 7d919af01b chore: cross-build for AIX (#5971) 2023-12-11 12:55:04 +00:00
Matt Holt 4a09cf0dc0 caddytls: Sync distributed storage cleaning (#5940)
* caddytls: Log out remote addr to detect abuse

* caddytls: Sync distributed storage cleaning

* Handle errors

* Update certmagic to fix tiny bug

* Split off port when logging remote IP

* Upgrade CertMagic
2023-12-07 11:00:02 -07:00
Andreas Kohn b24ae63ea6 caddytls: Context to DecisionFunc (#5923)
See https://github.com/caddyserver/certmagic/pull/255
2023-12-07 10:40:13 -07:00
Mohammed Al Sahaf 4173e2c77a tls: accept placeholders in string values of certificate loaders (#5963)
* tls: loader: accept placeholders in string values

* appease the linter
2023-12-04 09:23:15 -07:00
Matt Holt 18f34290d2 templates: Offically make templates extensible (#5939)
* templates: Offically make templates extensible

This supercedes #4757 (and #4568) by making template extensions
configurable.

The previous implementation was never documented AFAIK and had only
1 consumer, which I'll notify as a courtesy.

* templates: Add 'maybe' function for optional components

* Try to fix lint error
2023-11-28 09:39:14 -07:00
WeidiDeng 22eecdb90c http2 uses new round-robin scheduler (#5946) 2023-11-24 01:54:27 +00:00
WeidiDeng 4de2c1c65e panic when reading from backend failed to propagate stream error (#5952) 2023-11-23 03:18:18 -05:00
dlorenc 878d491834 chore: Bump otel to v1.21.0. (#5949)
Signed-off-by: Dan Lorenc <dlorenc@chainguard.dev>
2023-11-22 17:02:13 +03:00
WeidiDeng 96f638eaad httpredirectlistener: Only set read limit for when request is HTTP (#5917) 2023-11-20 12:31:36 +00:00
Matthew Holt 7e52db8280 fileserver: Add .m4v for browse template icon 2023-11-14 13:39:57 -07:00
Mohammed Al Sahaf 3b3d678714 Revert "caddyhttp: Use sync.Pool to reduce lengthReader allocations (#5848)" (#5924) 2023-11-01 13:17:02 -04:00
WeidiDeng ee358550e4 go.mod: update quic-go version to v0.40.0 (#5922) 2023-10-31 14:05:34 -04:00
Marten Seemann 3f55efcfde update quic-go to v0.39.3 (#5918) 2023-10-27 07:52:12 -04:00
WeidiDeng f71d779009 chore: Fix usage pool comment (#5916) 2023-10-25 23:05:20 -04:00
Mohammed Al Sahaf d949caf459 test: acmeserver: add smoke test for the ACME server directory (#5914) 2023-10-24 13:59:53 -04:00
Mariano Cano ac0ad4da84 Upgrade acmeserver to github.com/go-chi/chi/v5 (#5913)
This commit upgrades the router used in the acmeserver to
github.com/go-chi/chi/v5. In the latest release of step-ca, the router
used by certificates was upgraded to that version.

Fixes #5911

Signed-off-by: Mariano Cano <mariano.cano@gmail.com>
2023-10-23 21:02:11 -04:00
Francis Lavoie 4c10a05431 caddyhttp: Adjust scheme placeholder docs (#5910) 2023-10-22 17:47:16 -04:00
Matthew Holt fe2a02bf7a go.mod: Upgrade quic-go to v0.39.1 2023-10-20 15:23:35 -06:00
Ethan Brown (Domino) 9fc55a9792 go.mod: CVE-2023-45142 Update opentelemetry (#5908) 2023-10-20 21:15:48 +00:00
Francis Lavoie 4e8245df0b templates: Delete headers on httpError to reset to clean slate (#5905) 2023-10-18 16:43:14 -06:00
Francis Lavoie ac1f20b9e4 httpcaddyfile: Remove port from logger names (#5881)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-10-16 23:57:03 -06:00
Matt Holt 174c19a953 core: Apply SO_REUSEPORT to UDP sockets (#5725)
* core: Apply SO_REUSEPORT to UDP sockets

For some reason, 10 months ago when I implemented SO_REUSEPORT
for TCP, I didn't realize, or forgot, that it can be used for UDP too. It is a
much better solution than using deadline hacks to reuse a socket, at
least for TCP.

Then https://github.com/mholt/caddy-l4/issues/132 was posted,
in which we see that UDP servers never actually stopped when the
L4 app was stopped. I verified this using this command:

    $ nc -u 127.0.0.1 55353

combined with POSTing configs to the /load admin endpoint (which
alternated between an echo server and a proxy server so I could tell
which config was being used).

I refactored the code to use SO_REUSEPORT for UDP, but of course
we still need graceful reloads on all platforms, not just Unix, so I
also implemented a deadline hack similar to what we used for
TCP before. That implementation for TCP was not perfect, possibly
having a logical (not data) race condition; but for UDP so far it
seems to be working. Verified the same way I verified that SO_REUSEPORT
works.

I think this code is slightly cleaner and I'm fairly confident this code
is effective.

* Check error

* Fix return

* Fix var name

* implement Unwrap interface and clean up

* move unix packet conn to platform specific file

* implement Unwrap for unix packet conn

* Move sharedPacketConn into proper file

* Fix Windows

* move sharedPacketConn and fakeClosePacketConn to proper file

---------

Co-authored-by: Weidi Deng <weidi_deng@icloud.com>
2023-10-16 22:17:32 -06:00
Harish Shan c8559c4485 caddyhttp: Use sync.Pool to reduce lengthReader allocations (#5848)
* Use sync.Pool to reduce lengthReader allocations

Signed-off-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>

* Add defer putLengthReader to prevent leak

Signed-off-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>

* Cleanup in putLengthReader

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

---------

Signed-off-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-10-16 14:42:01 -06:00
Thanmay Nath 24b0ecc310 cmd: Add newline character to version string in CLI output (#5895) 2023-10-16 09:58:32 -06:00
WeidiDeng 7c82e265da core: quic listener will manage the underlying socket by itself (#5749)
* core: quic listener will manage the underlying socket by itself.

* format code

* rename sharedQUICTLSConfig to sharedQUICState, and it will now manage the number of active requests

* add comment

* strict unwrap type

* fix unwrap

* remove comment
2023-10-16 09:28:15 -06:00
Francis Lavoie 0900844c81 templates: Clarify include args docs, add .ClientIP (#5898) 2023-10-15 20:58:46 -04:00
Francis Lavoie 7984e6f6fd httpcaddyfile: Fix TLS automation policy merging with get_certificate (#5896) 2023-10-14 14:23:50 -06:00
Mohammed Al Sahaf d70608b656 cmd: upgrade: resolve symlink of the executable (#5891) 2023-10-13 17:19:22 -04:00
WeidiDeng 1f60328e17 caddyfile: Fix variadic placeholder false positive when token contains : (#5883) 2023-10-13 02:28:20 -04:00
Norman Soetbeer 0e204b730a admin: Respond with 4xx on non-existing config path (#5870)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-10-11 20:24:29 +00:00
Francis Lavoie fae195ac7e ci: Force the Go version for govulncheck (#5879) 2023-10-11 20:09:02 +00:00
Forza 130f6d1f83 fileserver: Set canonical URL on browse template (#5867)
* Browse.html: Add canonical URL and home-link

When contents are equal, but maybe just a sort order is different, it is good to add `<link rel="canonical" href="base-path/" />`. This helps search engines propeely index the page.

I also added a link to the home page with the name of `{{.Host}}` just above the bread crumbs to make the page clearer.

https://paste.tnonline.net/files/28Wun5CQZiqA_Screenshot_20231007_134435_Opera.png

* Update browse.html
2023-10-11 13:47:38 -06:00
Bas Westerbaan 289934f3d1 tls: Add X25519Kyber768Draft00 PQ "curve" behind build tag (#5852)
… when compiled with cfgo (https://github.com/cloudflare/go).
2023-10-11 13:45:37 -06:00
Matt Holt 3a3182fba3 reverseproxy: Add more debug logs (#5793)
* reverseproxy: Add more debug logs

This makes debug logging very noisy when reverse proxying, but I guess
that's the point.

This has shown to be useful in troubleshooting infrastructure issues.

* Update modules/caddyhttp/reverseproxy/streaming.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddyhttp/reverseproxy/streaming.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Add opt-in `trace_logs` option

* Rename to VerboseLogs

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-10-11 13:36:20 -06:00
Francis Lavoie e8b8d4a8cd reverseproxy: Fix least_conn policy regression (#5862) 2023-10-11 16:04:28 +00:00
Francis Lavoie a8586b05aa reverseproxy: Add logging for dynamic A upstreams (#5857) 2023-10-11 09:50:44 -06:00
Francis Lavoie 05dbe1c171 reverseproxy: Replace health header placeholders (#5861) 2023-10-11 09:50:28 -06:00
Francis Lavoie 33d8d2c6b5 httpcaddyfile: Sort TLS SNI matcher for deterministic JSON output (#5860)
* httpcaddyfile: Sort TLS SNI matcher, for deterministic adapt output

* Update caddyconfig/httpcaddyfile/httptype.go

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-10-11 09:47:07 -06:00
Francis Lavoie 9c419f1e1a cmd: Fix exiting with custom status code, add caddy -v (#5874)
* Simplify variables for commands

* Add --envfile support for adapt command

* Carry custom status code for commands to os.Exit()

* cmd: add `-v` and `--version` to root caddy command

* Add `--envfile` to `caddy environ`, extract flag parsing to func

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2023-10-11 09:46:18 -06:00
Fred Cox b245ecd325 reverseproxy: fix parsing Caddyfile fails for unlimited request/response buffers (#5828) 2023-10-11 04:42:40 -04:00
Francis Lavoie 2a6859a5e4 reverseproxy: Fix retries on "upstreams unavailable" error (#5841) 2023-10-10 22:07:20 +00:00
Đỗ Trọng Hải df99502977 httpcaddyfile: Enable TLS for catch-all site if tls directive is specified (#5808) 2023-10-10 21:46:39 +00:00
Christoph e0aaefab80 encode: Add application/wasm* to the default content types (#5869) 2023-10-10 21:18:37 +00:00
Kévin Dunglas fa5a579b60 fileserver: Add command shortcuts -l and -a (#5854) 2023-10-10 20:57:18 +00:00
Matthew Holt 88b4fbf244 go.mod: Upgrade dependencies incl. x/net/http
Possibly important for the HTTP/2 Rapid Reset issue.
2023-10-10 12:01:20 -06:00
Thanmay Nath 5653c36bc2 templates: Add dummy RemoteAddr to httpInclude request, proxy compatibility (#5845)
* Enhancement: Allow X-Forwarded-For Header in httpInclude Virtual Requests

The goal of this enhancement is to modify the funcHTTPInclude function in the Caddy codebase to include the X-Forwarded-For header in the virtual request. This change will enable reverse proxies to set the X-Forwarded-For header, ensuring that the client's IP address is correctly provided to the target endpoint. This modification is essential for applications that depend on the X-Forwarded-For header for various functionalities, such as authentication, logging, or content customization.

* Updated tplcontext.go - set `virtReq.RemoteAddr = "127.0.0.1"`

i have made the suggested changes

* Apply suggestions from code review

* Update modules/caddyhttp/templates/tplcontext.go

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-10-07 20:47:34 +00:00
Patrick Koenig 4feac4d83c reverseproxy: Allow fallthrough for response handlers without routes (#5780) 2023-10-05 23:15:26 -04:00
Kévin Dunglas 82c356f254 fix: caddytest.AssertResponseCode error message (#5853) 2023-10-02 20:55:09 +00:00
dependabot[bot] 1405683c2b build(deps): bump goreleaser/goreleaser-action from 4 to 5 (#5847)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 00:34:43 +00:00
dependabot[bot] 89c407aa34 build(deps): bump actions/checkout from 3 to 4 (#5846)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-01 20:13:54 -04:00
Matthew Holt 58ab3a01a0 caddyhttp: Use LimitedReader for HTTPRedirectListener 2023-09-26 07:32:46 -06:00
glowinthedark a306c5f769 fileserver: browse template SVG icons and UI tweaks (#5812)
* fileserver browse.html UI tweaks: folder-symlink icon, search

fileserver browse.html UI tweaks: folder-symlink icon, search

- ui - add folder-symlink SVG icon
- search: use `<input type="search">` instead of `text`
- fix npe with `sizebar.style.width` = null in grid mode

* tabify whitespace

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-09-15 19:05:45 -06:00
Pascal Vorwerk 1e0dea59ef reverseproxy: fix nil pointer dereference in AUpstreams.GetUpstreams (#5811)
fix a nil pointer dereference in AUpstreams.GetUpstreams when AUpstreams.Versions is not set (fixes caddyserver#5809)

Signed-off-by: Pascal Vorwerk <info@fossores.de>
2023-09-10 19:08:02 -04:00
Đỗ Trọng Hải 2cac3c5491 httpcaddyfile: fix placeholder shorthands in named routes (#5791)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-09-08 14:38:44 -04:00
Evan Van Dam f2ab7099db cmd: Prevent overwriting existing env vars with --envfile (#5803)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-09-07 02:19:24 +00:00
Đỗ Trọng Hải 50cea4e263 ci: Run govulncheck (#5790)
* feat(ci): check vuln Go mods in CI

* fix(ci): correct directive for govulncheck

* refactor(ci): move govulncheck to lint.yml

* refactor(lint): move govulncheck to different job
2023-09-05 11:31:25 -04:00
Paul Jeannot 1b73e3862d logging: query filter for array of strings (#5779)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-08-29 22:59:43 +00:00
Francis Lavoie c46ec3b500 logging: Clone array on log filters, prevent side-effects (#5786)
Fixes https://caddy.community/t/is-caddy-mutating-header-content-from-logging-settings/20947
2023-08-29 11:41:39 -06:00
Matthew Holt ed8bb13c5d fileserver: Export BrowseTemplate
This allows programs embedding Caddy to customize the browse template.
2023-08-29 09:34:20 -06:00
Mohammed Al Sahaf b7e472d548 ci: ensure short-sha is exported correctly on all platforms (#5781) 2023-08-25 16:06:44 +00:00
Francis Lavoie 7103ea096f caddyfile: Fix case where heredoc marker is empty after newline (#5769)
Fixes `panic: runtime error: slice bounds out of range [:3] with capacity 2`

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-08-24 03:27:57 +00:00
WeidiDeng 888c6d7e93 go.mod: Update quic-go to v0.38.0 (#5772)
* go.mod: Update quic-go to v0.38.0

* run "go mod tidy"

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-08-24 02:55:28 +00:00
Matt Holt b377208ede chore: Appease gosec linter (#5777)
These happen to be harmless memory aliasing
but I guess the linter can't know that and we
can't really prove it in general.
2023-08-23 20:47:54 -06:00
WeidiDeng 4776f62caa replacer: change timezone to UTC for "time.now.http" placeholders (#5774) 2023-08-22 02:41:25 -04:00
Francis Lavoie 38a7b6b3d0 caddyfile: Adjust error formatting (#5765) 2023-08-20 08:51:03 -06:00
Marten Seemann 84d5e1c5d6 update quic-go to v0.37.6 (#5767) 2023-08-19 23:34:15 +00:00
Karun Agarwal 288216e1fb httpcaddyfile: Stricter errors for site and upstream address schemes (#5757)
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-08-19 07:28:25 -04:00
Francis Lavoie 10053f7570 caddyfile: Loosen heredoc parsing (#5761) 2023-08-19 10:32:32 +00:00
Mohammed Al Sahaf 0a6d3333b2 fileserver: docs: clarify the ability to produce JSON array with browse (#5751) 2023-08-18 19:04:08 +00:00
guangwu 568fd2b286 fix package typo (#5764)
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
2023-08-18 08:20:46 -06:00
Matthew Holt f11c3c9f5a go.mod: Upgrade CertMagic and quic-go 2023-08-17 11:34:48 -06:00
Matt Holt 936ee918ee reverseproxy: Always return new upstreams (fix #5736) (#5752)
* reverseproxy: Always return new upstreams (fix #5736)

* Fix healthcheck logger race
2023-08-17 11:33:40 -06:00
Jacob Gadikian d6f86cccf5 ci: use gci linter (#5708)
* use gofmput to format code

* use gci to format imports

* reconfigure gci

* linter autofixes

* rearrange imports a little

* export GOOS=windows golangci-lint run ./... --fix
2023-08-14 09:41:15 -06:00
Matthew Holt 2d7d806fcf fileserver: Slightly more fitting icons 2023-08-11 20:53:11 -06:00
pistasjis d8135505d3 cmd: Require config for caddy validate (fix #5612) (#5614)
* Require config for caddy validate - fixes #5612

Signed-off-by: Pistasj <hi@pistasjis.net>

* Try making adjacent Caddyfile check its own function

Signed-off-by: Pistasj <hi@pistasjis.net>

* add Francis' suggestion

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Refactor

* Fix borked commit, sigh

---------

Signed-off-by: Pistasj <hi@pistasjis.net>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2023-08-09 17:40:37 +00:00
Matthew Holt 11166889c5 Fix tests
I thought Go ordered JSON objects when marshaling, but I guess not.
2023-08-09 11:25:59 -06:00
Matthew Holt 080db93817 caddytls: Update docs for on-demand config 2023-08-09 11:15:01 -06:00
Francis Lavoie a8492c064d fileserver: Don't repeat error for invalid method inside error context (#5705) 2023-08-09 17:12:09 +00:00
Matt Holt 6cdcc2a782 ci: Update to Go 1.21 (#5719)
* ci: Update to Go 1.21

* Bump quic-go to v0.37.4

* Check EnableFullDuplex err

* Linter bug suppression

See https://github.com/timakin/bodyclose/issues/52

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-08-09 12:34:28 -04:00
Aaron Dewes fbb0ecfa32 ci: Add riscv64 (64-bit RISC-V) to goreleaser (#5720)
This will add 64-bit RISC-V Linux prebuilts for Caddy.
2023-08-08 12:11:53 -06:00
Shyim 5b9c850ab3 go.mod: Upgrade golang.org/x/net to 0.14.0 (#5718) 2023-08-08 11:23:26 -06:00
Jacob Gadikian b32f265eca ci: Use gofumpt to format code (#5707) 2023-08-07 19:40:31 +00:00
Matthew Holt 431adc0980 templates: Fix httpInclude (fix #5698)
Allowable during feature freeze because this is a simple, non-invasive
bug fix only.
2023-08-07 12:53:21 -06:00
Matthew Holt a8cc5d1a7d go.mod: Upgrade to quic-go v0.37.3
Fixes #5680 once and for all! Hopefully :)

Thank you @marten-seemann for your excellent work!
2023-08-05 18:10:15 -06:00
Emily 8d304a4566 cmd: Split unix sockets for admin endpoint addresses (#5696)
* cmd: fix cli when admin endpoint uses new unix socket permission format

Fixes a bug where the following Caddyfile

```Caddyfile
{
	admin unix/admin.sock|0660
}
```

and `caddy reload --config Caddyfile`
would throw the following error instead of reloading it:

```
INFO    using provided configuration    {"config_file": "Caddyfile", "config_adapter": ""}
Error: sending configuration to instance: performing request: Post "http://127.0.0.1/load": dial unix admin.sock|0660: connect: no such file or directory
[ERROR] exit status 1
```

---

This bug also affected `caddy start` and `caddy stop`.

* Move splitter function to internal

---------

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
2023-08-06 00:09:16 +00:00
Mohammed Al Sahaf 65e33fc1ee reverseproxy: do not parse upstream address too early if it contains replaceble parts (#5695)
* reverseproxy: do not parse upstream address too early if it contains replaceble parts

* remove unused method

* cleanup

* accommodate partially replaceable port
2023-08-05 23:30:02 +02:00
WeidiDeng 9f34383c02 caddyfile: check that matched key is not a substring of the replacement key (#5685) 2023-08-04 10:44:38 -06:00
Mohammed Al Sahaf b07b198764 chore: use --clean instead of --rm-dist for goreleaser (#5691) 2023-08-04 16:08:54 +00:00
Matthew Holt 51b1bfb125 go.mod: Upgrade quic-go to v0.37.2 (fix #5680) 2023-08-03 18:44:03 -06:00
Matthew Holt c049bab458 fileserver: browse: Render SVG images in grid 2023-08-03 12:53:47 -06:00
WeidiDeng e2fc08bd34 reverseproxy: Fix hijack ordering which broke websockets (#5679) 2023-08-03 04:08:12 +00:00
Herman Slatman 4aa4f3ac70 httpcaddyfile: Fix string does not match ~[]E error (#5675)
Only happens for some people. Unable to confirm.
2023-08-03 00:41:37 +00:00
Francis Lavoie 1913930783 encode: Fix infinite recursion (#5672) 2023-08-02 18:21:11 -06:00
Francis Lavoie cd486c25d1 caddyhttp: Make use of http.ResponseController (#5654)
* caddyhttp: Make use of http.ResponseController

Also syncs the reverseproxy implementation with stdlib's which now uses ResponseController as well https://github.com/golang/go/commit/2449bbb5e614954ce9e99c8a481ea2ee73d72d61

* Enable full-duplex for HTTP/1.1

* Appease linter

* Add warning for builds with Go 1.20, so it's less surprising to users

* Improved godoc for EnableFullDuplex, copied text from stdlib

* Only wrap in encode if not already wrapped
2023-08-02 20:03:26 +00:00
Matthew Holt e198c605bd go.mod: Upgrade dependencies esp. smallstep/certificates
This prevents initialization of a .step folder when it's not used.
2023-08-02 11:48:59 -06:00
Matt Holt f66493efef core: Allow loopback hosts for admin endpoint (fix #5650) (#5664) 2023-08-02 11:13:52 -06:00
Francis Lavoie 5c51c1db2c httpcaddyfile: Allow hostnames & logger name overrides for log directive (#5643)
* httpcaddyfile: Allow `hostnames` override for log directive

* Implement access logger name overrides

* Fix panic & default logger clobbering edgecase
2023-08-02 03:13:46 -04:00
mmm444 da23501457 reverseproxy: Connection termination cleanup (#5663) 2023-08-01 14:01:12 +00:00
Matthew Holt 94749e119a go.mod: Use quic-go 0.37.1
Should fix panic in Go 1.21 where there was no RemoteAddr.
2023-07-31 16:31:17 -06:00
Omar Ramadan d7d16360d4 reverseproxy: Export ipVersions type (#5648)
allows AUpstreams to be instantiated externally
2023-07-25 12:50:21 -06:00
Matthew Holt 4df27a20c8 go.mod: Use latest CertMagic (v0.19.1)
Fixes race condition
2023-07-25 10:31:47 -06:00
Matthew Holt 18c309b5fa caddyhttp: Preserve original error (fix #5652) 2023-07-25 09:41:56 -06:00
ydylla e041962b66 fileserver: add lazy image loading (#5646) 2023-07-22 15:50:36 +00:00
Marten Seemann f45a6de20d go.mod: Update quic-go to v0.37.0, bump to Go 1.20 minimum (#5644)
* update quic-go to v0.37.0

* Bump to Go 1.20

* Bump golangci-lint version, yml syntax consistency

* Use skip-pkg-cache workaround

* Workaround needed for both?

* Seeding weakrand is no longer necessary

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-07-21 22:00:48 -06:00
Matt Holt b51dc5d5d0 core: Refine mutex during reloads (fix #5628) (#5645)
Separate currentCtxMu to protect currentCtx, and a new
rawCfgMu to protect rawCfg and synchronize loads.
2023-07-21 15:32:20 -06:00
bt90 f857b32d65 go.mod: update quic-go to v0.36.2 (#5636) 2023-07-17 14:16:43 -06:00
Matthew Holt 4e36b4c9d1 fileserver: Tweak grid view of browse template
All cells on row have same height.
Center-align vertically.
2023-07-17 11:18:40 -06:00
Mohammed Al Sahaf 27bc16abed fileserver: add export-template sub-command to file-server (#5630) 2023-07-13 15:54:48 -06:00
WeidiDeng bbe1952a59 caddyfile: Fix comparing if two tokens are on the same line (#5626)
* fix comparing if two tokens are on the same line

* compare tokens from copies when importing
2023-07-12 14:32:22 -06:00
Matt Holt 0e2c7e1d35 caddytls: Reuse certificate cache through reloads (#5623)
* caddytls: Don't purge cert cache on config reload

* Update CertMagic

This actually avoids reloading managed certs from storage
when already in the cache, d'oh.

* Fix bug; re-implement HasCertificateForSubject

* Update go.mod: CertMagic tag
2023-07-11 19:10:58 +00:00
Matt Holt 7ceef91295 Minor tweaks to security.md 2023-07-08 14:02:09 -06:00
Matthew Holt 5dec11f2a0 reverseproxy: Pointer receiver
This avoids copying the Upstream, which has an atomically-accessed value
in it.
2023-07-08 13:42:51 -06:00
Matthew Holt 66114cb155 caddyhttp: Trim dot/space only on Windows (fix #5613)
Follow-up to #2917. Path matcher needs to trim dots and spaces but only
on Windows.
2023-07-08 13:42:13 -06:00
Marten Seemann 7914ba3573 update quic-go to v0.36.1 (#5611) 2023-07-01 19:34:27 -04:00
Matthew Holt dfe17c33ef caddyconfig: Specify config adapter for HTTP loader (close #5607) 2023-06-30 20:04:32 -06:00
WeidiDeng 710824c3ce core: Embed net.UDPConn to gain optimizations (#5606)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-06-30 16:31:26 -06:00
Mohammed Al Sahaf d8ae801068 chore: remove deprecated property rlcp in goreleaser config (#5608) 2023-06-30 16:53:56 -04:00
Emily 119e8794bc core: Skip chmod for abstract unix sockets (#5596)
because those aren't real paths on the filesystem and thus can't be `chmod`ed
2023-06-24 18:25:02 -06:00
Emily 22927e278d core: Add optional unix socket file permissions (#4741)
* core: Add optional unix socket file permissions

This commit also changes the default unix socket file permissions to `u=w,g=,o=` (octal: `0200`).
It used to default to the shell's umask (usually `u=rwx,g=rx,o=rx`, octal: `0755`).

`/run/caddy.sock` -> `/run/caddy.sock` with `0200` default perms
`/run/caddy.sock|0222` -> `/run/caddy.sock` with `0222` perms

`|` instead of `:` is used as a separator, to account for the `:` in Windows drive letters (e.g. `C:\absolute\path.sock`)

Fun fact:
The old unix(7) man page (pre Jun 2016) stated a socket needs both read and write perms.
Turns out, only write perms are needed.
Corrected in https://github.com/mkerrisk/man-pages/commit/7578ea2f85b272363d22680d69e7d32f0b59c83b
Despite this, most implementations still default to read+write to this date.

* Add cases with Windows paths to test

* Require write perms for the owning user
2023-06-23 14:49:41 -06:00
Francis Lavoie 7a69ae7571 reverseproxy: Honor tls_except_port for active health checks (#5591) 2023-06-22 16:20:30 -06:00
Matthew Holt 2b2addebb8 Appease linter 2023-06-21 17:59:54 -06:00
Matthew Holt 9563666bfb Fix compile on Windows, hopefully 2023-06-21 17:47:23 -06:00
Matthew Holt 806341e089 core: Properly preserve unix sockets (fix #5568) 2023-06-21 17:16:01 -06:00
Matthew Holt 0468508e92 go.mod: Upgrade CertMagic for hotfix 2023-06-21 13:25:38 -06:00
Matthew Holt 415d1e7b6f go.mod: Upgrade some dependencies 2023-06-21 13:25:38 -06:00
Omer Demirok 1a36b06cd4 chore: upgrade otel (#5586) 2023-06-21 11:46:42 -06:00
Marten Seemann 398c12ae9b go.mod: Update quic-go to v0.36.0 (#5584) 2023-06-21 06:56:12 -04:00
Saber Haj Rabiee 361946eb0c reverseproxy: weighted_round_robin load balancing policy (#5579)
* added weighted round robin algorithm to load balancer

* added an adapt integration test for wrr and fixed a typo

* changed args format to Caddyfile args convention

* added provisioner and validator for wrr

* simplified the code and improved doc
2023-06-20 11:42:58 -06:00
mmm444 424ae0f420 reverseproxy: Experimental streaming timeouts (#5567)
* reverseproxy: WIP streaming timeouts

* More verbose logging by using the child logger

* reverseproxy: Implement streaming timeouts

* reverseproxy: Refactor cleanup

* reverseproxy: Avoid **time.Timer

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-06-19 15:54:43 -06:00
guangwu 4548b7de8e chore: remove refs of deprecated io/ioutil (#5576) 2023-06-16 21:27:57 -06:00
Francis Lavoie 3b19aa2b5a headers: Allow > to defer shortcut for replacements (#5574) 2023-06-15 17:18:55 -06:00
Dominik Roos 6a41b62e70 caddyhttp: Support custom network for HTTP/3 (#5573)
Allow registering a custom network mapping for HTTP/3. This is useful
if the original network for HTTP/1.1 and HTTP/2 is not a standard `unix`,
`tcp4`, or `tcp6` network. To keep backwards compatibility, we fall back
to `udp` if the original network is not registered in the mapping.

Fixes #5555
2023-06-13 19:33:39 -06:00
Corin Langosch 2ddb717144 reverseproxy: Fix parsing of source IP in case it's an ipv6 address (#5569) 2023-06-12 09:35:22 -06:00
365cent 56af1ceb32 fileserver: browse: Better grid layout (#5564)
* feat: better implementation of grid layout

* fix: vertical alignment
2023-06-05 07:39:57 +00:00
Matthew Holt 4ba03c9d38 caddytls: Clarify some JSON config docs 2023-06-04 22:15:50 -06:00
Cass C 078f130a51 cmd: Implement storage import/export (#5532)
* cmd: Implement 'storage import' and 'storage export' CLI commands.

These commands use the certmagic.Storage interface. In particular,
storage implementations should ensure that their List() functions
correctly enumerate all keys when called with an empty prefix and
recursive == true. Also, Stat() calls on keys holding values instead
of nested keys are expected to set KeyInfo.IsTerminal = true.

* remove errors.Join
2023-06-02 13:04:31 -06:00
Matthew Holt 9c180a5988 go.mod: Upgrade quic-go to 0.35.1 2023-06-01 11:28:33 -06:00
Marten Seemann 467b7e3a9c update quic-go to v0.35.0 (#5560) 2023-05-30 05:41:57 -04:00
kassienull 31d75acc9c templates: Add readFile action that does not evaluate templates (#5553)
* Create an includeRaw template function to include a file without parsing it as a template.

Some formatting fixes

* Rename to readFile, various docs adjustments

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-05-26 10:16:28 -06:00
WeidiDeng 9cde715525 caddyfile: Track import name instead of modifying filename (#5540)
* Merge branch 'master' into import_file_stack

* remove space in log key
2023-05-25 13:05:00 -06:00
Jonathan Davies 942fbb37ec core: Use SO_REUSEPORT_LB on FreeBSD (#5554)
to balance load between threads.
2023-05-23 10:56:00 -06:00
WeidiDeng cee4441cb1 caddyfile: Do not replace import tokens if they are part of a snippet (#5539)
* fix variadic placeholder in imported file which also imports

* fix tests.

* skip replacing args when imported token may be part of a snippet
2023-05-22 15:36:55 -06:00
Matt Holt 5bd9c49042 fileserver: Don't set Etag if mtime is 0 or 1 (close #5548) (#5550) 2023-05-22 14:17:15 -06:00
pistasjis cdd3884b32 fileserver: browse: minor tweaks for grid view, dark mode (#5545)
* Make grid entries take up full width on mobile and fix breadcrumb color issue in dark mode

Signed-off-by: Pistasj <odyssey346@disroot.org>

* Do mholt's suggestions

Signed-off-by: Pistasj <odyssey346@disroot.org>

---------

Signed-off-by: Pistasj <odyssey346@disroot.org>
2023-05-20 17:23:17 -06:00
Charles Duffy 2615c9c524 fileserver: Only set Etag if not already set (fix #5546) (#5547) 2023-05-20 17:21:43 -06:00
pistasjis 5336bc0fb6 fileserver: Fix file browser breadcrumb font (#5543)
Signed-off-by: Pistasj <odyssey346@disroot.org>
2023-05-19 11:08:47 -06:00
WeidiDeng 29452647d8 caddyhttp: Fix h3 shutdown (#5541)
* swap h3server close and listener close, avoid quic-listener not closing

* fix typo
2023-05-19 10:00:00 -06:00
Matthew Holt bd34cb6b4e fileserver: More filetypes for browse icons 2023-05-19 09:59:44 -06:00
pistasjis 2d236ead3e fileserver: Fix file browser footer in grid mode (#5536)
* Fix file browser footer in grid

Signed-off-by: Odyssey <odyssey346@disroot.org>

* Fix file browser footer while in grid mode

Signed-off-by: Pistasj <odyssey346@disroot.org>

* Do mholt's suggestions

Signed-off-by: Odyssey <odyssey346@disroot.org>

---------

Signed-off-by: Odyssey <odyssey346@disroot.org>
Signed-off-by: Pistasj <odyssey346@disroot.org>
2023-05-19 09:51:21 -06:00
Matthew Holt 38cb587e0f cmd: Avoid spammy log messages (fix #5538)
I forgot there are two calls to LoadConfig() here that needed replacing.
2023-05-17 16:13:15 -06:00
Matthew Holt ca14b6edd9 httpcaddyfile: Sort Caddyfile slice
Makes list deterministic. See #5538
2023-05-17 13:50:32 -06:00
Francis Lavoie cbf16f6d9e caddyhttp: Implement named routes, invoke directive (#5107)
* caddyhttp: Implement named routes, `invoke` directive

* gofmt

* Add experimental marker

* Adjust route compile comments
2023-05-16 15:27:52 +00:00
Tran Phong 13a37688dc rewrite: use escaped path, fix #5278 (#5504)
* use escaped path while rewriting

Signed-off-by: TP-O <letranphong2k1@gmail.com>

* restore line break

---------

Signed-off-by: TP-O <letranphong2k1@gmail.com>
2023-05-16 09:16:07 -06:00
Francis Lavoie e8352aef38 headers: Add > Caddyfile shortcut for enabling defer (#5535) 2023-05-16 01:18:13 -04:00
Matthew Holt 36546cd8b9 go.mod: Upgrade several dependencies 2023-05-15 16:56:27 -06:00
Francis Lavoie 75b690d248 reverseproxy: Expand port ranges to multiple upstreams in CLI + Caddyfile (#5494)
* reverseproxy: Expand port ranges to multiple upstreams in CLI + Caddyfile

* Add clarifying comment
2023-05-15 12:14:50 -06:00
Matt Holt 52d7335c2b fileserver: Use EscapedPath for browse (#5534)
* fileserver: Use EscapedPath for browse

Fix #5143

* Fixes if filter element is not present

* Remove extraneous line
2023-05-15 10:48:05 -06:00
Matt Holt 96919acc9d caddyhttp: Refactor cert Managers (fix #5415) (#5533) 2023-05-15 10:47:30 -06:00
Matthew Holt e96aafe1ca Slightly more helpful error message 2023-05-13 08:04:42 -06:00
Matt Holt a02ecb0f88 caddytls: Check for nil ALPN; close #5470 (#5473)
* Check for nil ALPN; close #5470

* Apply patch

* Actually I want to try this
2023-05-13 07:09:20 -06:00
Matthew Holt 5ebb7d496d cmd: Reduce spammy logs from --watch 2023-05-12 11:04:02 -06:00
jjiang-stripe cfc85ae8ca caddyhttp: Add a getter for Server.name (#5531) 2023-05-11 10:34:05 -06:00
Matt Holt faf0399e80 caddytls: Configurable fallback SNI (#5527)
* Initial implementation of fallback_sni

* Apply upstream patch
2023-05-10 14:29:29 -06:00
WeidiDeng 808b05c3b4 caddyhttp: Update quic's TLS configs after reload (#5517) (fix #4849)
* fix http3 outdated certificates after config reload

* delegate quic tls GetConfigForClient to another struct.

* change type and method names
fix lint

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-05-10 14:25:09 -06:00
Matthew Holt 12b2f22092 Add doc comment about changing admin endpoint 2023-05-09 20:05:27 -06:00
Yehonatan Ezron 571fc034d3 feature: watch include directory (#5521)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-05-08 22:49:16 +00:00
Mohammed Al Sahaf bef1a739db chore: remove deprecated linters (#5525) 2023-05-08 13:47:33 -06:00
Matthew Holt 0de6064c3b go.mod: Upgrade CertMagic again 2023-05-07 23:40:30 -06:00
Matthew Holt 774f228868 go.mod: Upgrade CertMagic 2023-05-06 11:30:27 -06:00
Francis Lavoie b19946f6af reverseproxy: Optimize base case for least_conn and random_choose policies (#5487)
When only a single request has the least amount of requests, there's no need to compute a random number, because the modulo of 1 will always be 0 anyways.
2023-05-05 20:53:48 -06:00
Francis Lavoie 335cd2e8a4 reverseproxy: Fix active health check header canonicalization, refactor (#5446) 2023-05-05 15:19:22 -06:00
Francis Lavoie 48598e1f2a reverseproxy: Add fallback for some policies, instead of always random (#5488) 2023-05-05 15:08:10 -06:00
Matthew Holt cdce452edc logging: Actually honor the SoftStart parameter 2023-05-04 16:30:34 -06:00
Matthew Holt f3e8b9d95f logging: Soft start for net writer (close #5520)
If enabled and there is an error when opening the net writer, ignore the
error and report it along with subsequent logs to stderr.
2023-05-04 16:29:03 -06:00
eanavitarte c8032867b1 fastcgi: Fix capture_stderr (#5515) 2023-05-04 00:40:49 +00:00
Francis Lavoie 3f20a7c9f3 acmeserver: Configurable resolvers, fix smallstep deprecations (#5500)
* acmeserver: Configurable `resolvers`, fix smallstep deprecations

* Improve default net/port

* Update proxy resolvers parsing to use the new function

* Update listeners.go

Co-authored-by: itsxaos <33079230+itsxaos@users.noreply.github.com>

---------

Co-authored-by: itsxaos <33079230+itsxaos@users.noreply.github.com>
2023-05-03 17:07:22 +00:00
Matthew Holt 1af419e7ec go.mod: Update some dependencies 2023-04-28 09:47:28 -06:00
Dave Henderson f0e3981774 logging: Add traceID field to access logs when tracing is active (#5507)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-04-27 02:46:41 +00:00
Kévin Dunglas 1c9ea0113d caddyhttp: Impl ResponseWriter.Unwrap(), prep for Go 1.20's ResponseController (#5509)
* feat: add support for ResponseWriter.Unwrap()

* cherry-pick Francis' code
2023-04-26 19:44:01 -04:00
Y.Horie 2b04e09fa7 reverseproxy: Fix reinitialize upstream healthy metrics (#5498)
Co-authored-by: Dávid Szabó <david.szabo97@gmail.com>
2023-04-25 09:59:26 -06:00
cui fliter 3443a8a056 fix some comments (#5508)
Signed-off-by: cui fliter <imcusg@gmail.com>
2023-04-25 09:54:42 -06:00
Stéphane Mourey 2943c41884 templates: Add fileStat function (#5497)
* Add isDir template function

* Update modules/caddyhttp/templates/tplcontext.go

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* Fix funcIsDir return value on error

* Fix funcIsDir return false when root file system not specified

* Add stat function, remove isDir function

* Remove isDir function (really)

* Rename stat to fileStat

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2023-04-24 10:36:37 -06:00
Francis Lavoie 53b6fab125 caddyfile: Stricter parsing, error for brace on new line (#5505) 2023-04-20 18:43:51 +00:00
Matthew Holt c6ac350a3b core: Return default logger if no modules loaded
Fix report from:
https://caddy.community/t/remote-caddyfile-invalid-memory-address-or-nil-pointer-dereference/19700/3
2023-04-20 10:27:40 -06:00
Francis Lavoie b301a3df70 celmatcher: Implement pkix.Name conversion to string (#5492) 2023-04-19 11:55:22 -04:00
Francis Lavoie 998c6e06a7 chore: Adjustments to CI caching (#5495) 2023-04-14 21:38:33 -04:00
Francis Lavoie 4636109ce1 reverseproxy: Remove deprecated lookup_srv (#5396) 2023-04-10 20:08:40 +00:00
Matt Holt 205b142614 cmd: Support ' quotes in envfile parsing (#5437) 2023-04-10 13:55:45 -06:00
Matt Holt ff35ba9ec3 Update contributing guidelines (#5466)
* Update contributing guidelines

* Request disclosure as a courtesy
2023-04-10 13:08:32 -06:00
WeidiDeng d8d87a378f caddyhttp: Serve http2 when listener wrapper doesn't return *tls.Conn (#4929)
* Serve http2 when listener wrapper doesn't return *tls.Conn

* close conn when h2server serveConn returns

* merge from upstream

* rebase from latest

* run New and Closed ConnState hook for h2 conns

* go fmt

* fix lint

* Add comments

* reorder import
2023-04-10 17:05:02 +00:00
Francis Lavoie f8b59e77f8 reverseproxy: Add query and client_ip_hash lb policies (#5468) 2023-04-04 03:31:47 +00:00
Matthew Holt 508cf2aa22 cmd: Create pidfile before config load (close #5477) 2023-04-03 11:57:16 -06:00
Kid f9bd2d3e92 fileserver: Add color-scheme meta tag (#5475) 2023-04-02 22:44:21 -04:00
dependabot[bot] b1366c7e46 build(deps): bump actions/setup-go from 3 to 4 (#5474)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 00:36:16 -04:00
Corin Langosch b6fe5d4b41 proxyprotocol: Add PROXY protocol support to reverse_proxy, add HTTP listener wrapper (#5424)
Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-03-31 17:44:53 -04:00
Francis Lavoie 66e571e687 reverseproxy: Add mention of which half a copyBuffer err comes from (#5472)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-03-31 15:46:29 -04:00
Francis Lavoie 2b3046de36 caddyhttp: Log request body bytes read (#5461) 2023-03-27 22:40:15 +00:00
Mohammed Al Sahaf 1aef807c71 log: Make sink logs encodable (#5441)
* log: make `sink` encodable

* deduplicate logger fields

* extract common fields into `BaseLog` and embed it into `SinkLog`

* amend godoc on `BaseLog` and `SinkLog`

* minor style change

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-03-27 21:41:24 +00:00
Francis Lavoie e16a886814 caddytls: Eval replacer on automation policy subjects (#5459)
Also renamed the field to SubjectsRaw, which can be considered a breaking change but I don't expect this to affect much.
2023-03-27 21:16:22 +00:00
黑墨水鱼 dd86171d67 headers: Support deleting all headers as first op (#5464)
* Delete all existing fields when fieldName is `*`

* Rearrange deletion before addition in headers

* Revert "Rearrange deletion before addition in headers"

This reverts commit 1b50eeeccc92ccd660c7896d8283c7d9e5d1fcb0.

* Treat deleting all headers as a special case

* Apply suggestions from code review

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-03-27 21:05:18 +00:00
Francis Lavoie f5a13a4ab4 replacer: Add HTTP time format (#5458) 2023-03-27 20:51:13 +00:00
Francis Lavoie 10b265d252 reverseproxy: Header up/down support for CLI command (#5460) 2023-03-27 20:35:31 +00:00
Francis Lavoie 05e9974570 caddyhttp: Determine real client IP if trusted proxies configured (#5104)
* caddyhttp: Determine real client IP if trusted proxies configured

* Support customizing client IP header

* Implement client_ip matcher, deprecate remote_ip's forwarded option
2023-03-27 20:22:59 +00:00
Francis Lavoie 330be2d8c7 httpcaddyfile: Adjust path matcher sorting to solve for specificity (#5462) 2023-03-27 15:43:44 -04:00
Matt Holt 0cc49c053f caddytls: Zero out throttle window first (#5443)
* caddytls: Zero out throttle window first

* Don't error for on-demand 

Fixes https://github.com/caddyserver/caddy/commit/b97c76fb4789b8da0b80f5a2c1c1c5bebba163b5

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-03-20 12:06:00 -06:00
Mohammed Al Sahaf a7db0cfe55 ci: add --yes to cosign arguments (#5440) 2023-03-17 10:36:59 -06:00
Trea Hauet 2182270a2c reverseproxy: Reset Content-Length to prevent FastCGI from hanging (#5435)
Fixes: https://github.com/caddyserver/caddy/issues/5420
2023-03-16 11:42:16 -06:00
Matthew Holt a7af7c486e caddytls: Allow on-demand w/o ask for internal-only 2023-03-14 10:29:27 -06:00
Matthew Holt b97c76fb47 caddytls: Require 'ask' endpoint for on-demand TLS 2023-03-14 10:02:44 -06:00
Matt Holt 6cc3cbbc69 fileserver: New file browse template (#5427)
* fileserver: New file browse template

* Redo extension/icon logic; minor color tweaks

* Fine-tune image display
2023-03-10 18:19:31 +00:00
Matthew Holt 9e943319b4 go.mod: Upgrade dependencies 2023-03-09 10:33:25 -07:00
Chris Reeves b420561737 tracing: Support autoprop from OTEL_PROPAGATORS (#5147)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-03-09 09:02:35 -07:00
Marten Seemann c05e3898b9 caddyhttp: Enable 0-RTT QUIC (#5425) 2023-03-09 08:58:31 -07:00
WeidiDeng b3f0cea2c3 encode: flush status code when hijacked. (#5419) 2023-03-06 09:13:48 -07:00
esell 94d41a9d86 fileserver: Remove trailing slash on fs filenames (#5417) 2023-03-03 14:45:17 -07:00
Matt Holt 99d47050e9 core: Eliminate unnecessary shutdown delay on Unix (#5413)
* core: Eliminate unnecessary shutdown delay on Unix

Fix #5393, alternate to #5405

* Comments, cleanup, adjust logs

* Fix build constraint
2023-03-03 04:00:18 +00:00
Francis Lavoie 85375861f6 caddyhttp: Fix vars_regexp matcher with placeholders (#5408)
Changed to match the `vars` matcher's logic for handling placeholders
2023-03-02 09:01:54 -07:00
Francis Lavoie f6bab8ba85 context: Rename func to AppIfConfigured (#5397) 2023-02-27 18:58:27 +00:00
Emily Lange 941eae5f61 reverseproxy: allow specifying ip version for dynamic a upstream (#5401)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-02-27 17:23:09 +00:00
Mohammed Al Sahaf 096971e313 ci/cd: ship tarballs with vendored deps (#5403) 2023-02-26 22:06:15 +00:00
Francis Lavoie f3379f650a caddyfile: Fix heredoc fuzz crasher, drop trailing newline (#5404)
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2023-02-26 16:56:48 -05:00
Francis Lavoie 960150bb03 caddyfile: Implement heredoc support (#5385) 2023-02-26 00:34:27 +00:00
Francis Lavoie 9e6919550b cmd: Expand cobra support, add short flags (#5379)
* cmd: Expand cobra support

* Convert commands to cobra, add short flags

* Fix version command typo

Co-authored-by: Emily Lange <git@indeednotjames.com>

* Apply suggestions from code review

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

---------

Co-authored-by: Emily Lange <git@indeednotjames.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-02-24 16:09:12 -07:00
Matthew Holt 167981d258 ci: Update minimum Go version to 1.19 2023-02-24 13:45:44 -07:00
Matthew Holt 8cb1bb4af3 go.mod: Upgrade quic-go to v0.33.0 (Go 1.19 min) 2023-02-24 13:35:56 -07:00
Mohammed Al Sahaf e3909cc385 reverseproxy: refactor HTTP transport layer (#5369)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Weidi Deng <weidi_deng@icloud.com>
2023-02-24 19:54:04 +00:00
Francis Lavoie be53e432fc caddytls: Relax the warning for on-demand (#5384) 2023-02-22 11:41:01 -07:00
Francis Lavoie 79de6df93d cmd: Strict unmarshal for validate (#5383) 2023-02-22 11:39:40 -07:00
WeidiDeng 8bc05e598d caddyfile: Implement variadics for import args placeholders (#5249)
* implement variadic placeholders
imported snippets reflect actual lines in file

* add import directive line number for imported snippets
add tests for parsing

* add realfile field to help debug import cycle detection.

* use file field to reflect import chain

* Switch syntax, deprecate old syntax, refactoring

- Moved the import args handling to a separate file
- Using {args[0:1]} syntax now
- Deprecate {args.*} syntax
- Use a replacer map for better control over the parsing
- Add plenty of warnings when invalid placeholders are detected
- Renaming variables, cleanup comments for readability
- More tests to cover edgecases I could think of
- Minor cleanup to snippet tracking in tokens, drop a redundant boolean field in tokens

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-02-16 17:08:36 -07:00
Emily Lange bf54892a73 cmd: make caddy fmt hints more clear (#5378) 2023-02-16 16:34:12 -07:00
Francis Lavoie 5ded580444 cmd: Adjust documentation for commands (#5377) 2023-02-16 09:14:07 -07:00
Matthew Holt 0db29e2ce9 go.mod: Upgrade acmez and x/net
x/net 0.7.0 contains a security patch apparently.
2023-02-14 12:08:31 -07:00
Matt Holt 4b119a475f reverseproxy: Don't buffer chunked requests (fix #5366) (#5367)
* reverseproxy: Don't buffer chunked requests (fix #5366)

Mostly reverts 845bc4d50b (#5289)

Adds warning for unsafe config.

Deprecates unsafe properties in favor of simpler, safer designed ones.

* Update modules/caddyhttp/reverseproxy/caddyfile.go

Co-authored-by: Y.Horie <u5.horie@gmail.com>

* Update modules/caddyhttp/reverseproxy/reverseproxy.go

Co-authored-by: Y.Horie <u5.horie@gmail.com>

* Update modules/caddyhttp/reverseproxy/reverseproxy.go

Co-authored-by: Y.Horie <u5.horie@gmail.com>

* Remove unused code

---------

Co-authored-by: Y.Horie <u5.horie@gmail.com>
2023-02-11 17:25:29 -07:00
Francis Lavoie 90798f3eea go.mod: Upgrade various dependencies (#5362)
* chore: Upgrade various dependencies

* Support CEL file matcher with no args

* Document `http.request.orig_uri.path.*`, reorder placeholders in docs

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-02-08 17:49:17 +00:00
Steffen Brüheim 536c28d4dc core: Support Windows absolute paths for UDS proxy upstreams (#5114)
* added some tests for parseUpstreamDialAddress

Test 4 fails because it produces "[[::1]]:80" instead of "[::1]:80"

* support absolute windows path in unix reverse proxy address

* make IsUnixNetwork public, support +h2c and reuse it
* add new tests
2023-02-08 10:05:09 -07:00
WeidiDeng c77a6bea66 reverseproxy: Log status code and byte count for websockets (#5140)
* log response size for websocket request

* record size when using hijack bufio.Writer
2023-02-06 16:14:59 -07:00
Francis Lavoie 12bcbe2c49 caddyhttp: Pluggable trusted proxy IP range sources (#5328)
* caddyhttp: Pluggable trusted proxy IP range sources

* Add request to the IPRangeSource interface
2023-02-06 12:44:11 -07:00
Matthew Holt f6f1d8fc89 Run go.mod tidy 2023-02-06 12:24:01 -07:00
Y.Horie 8d3a1b8bcb caddyauth: Use singleflight for basic auth (#5344)
* caddyauth: Add singleflight for basic auth

* Fixes #5338
* it occurred the thunder herd problem like this https://medium.com/@mhrlife/avoid-duplicate-requests-while-filling-cache-98c687879f59

* Update modules/caddyhttp/caddyauth/basicauth.go

Fix comment

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2023-02-06 18:29:03 +00:00
Francis Lavoie ac83b7e218 admin: Add CADDY_ADMIN env var to override the default (#5332) 2023-02-06 17:55:16 +00:00
Francis Lavoie e62b5fb586 chore: Build with Go 1.20, keep minimum at 1.18 for now (#5353) 2023-02-06 11:29:20 -05:00
Amis Shokoohi 94b8d56096 cmd: Add --envfile flag to validate command (#5350)
Fixes https://github.com/caddyserver/caddy/issues/5346
2023-01-31 16:27:35 -05:00
Amis Shokoohi 8c0b49bf03 cmd: fmt exit successfully after overwriting config file (#5351)
Fixes https://github.com/caddyserver/caddy/issues/5349
2023-01-31 11:24:44 -05:00
Francis Lavoie 201b9b41f9 chore: Fix warning "range variable captured by func literal" (#5348) 2023-01-31 03:07:57 -05:00
Matthew Holt 0a3efd1641 caddytls: Debug log for ask endpoint 2023-01-30 09:30:53 -07:00
Y.Horie d73660f7c3 httpcaddyfile: Add persist_config global option (#5339)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-01-27 23:31:37 -05:00
Francis Lavoie 7f2a93e6c3 caddyfile: Allow overriding server names (#5323) 2023-01-27 14:56:39 -05:00
Y.Horie e9d95ab29f reverseproxy: Add flag to short command to disable redirects (#5330)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Fixes undefined
2023-01-25 09:40:08 -05:00
David Frickert 962310204f tracing: Support placeholders in span name (#5329)
Fixes https://github.com/caddyserver/caddy/issues/5171
2023-01-25 02:26:44 -05:00
Brad Fitzpatrick 98867ac346 go.mod: bump tscert package to fix Tailscale 1.34+ on Windows (#5331)
As of Tailscale 1.34.0 on Windows, Tailscale now uses a named pipe to
connect to the local tailscale service.

This pulls in tailscale/tscert#5 as reported in tailscale/tscert#4.

(Sorry, we should've noticed this earlier!)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-01-24 20:01:54 -05:00
Y.Horie 5805b3ca11 cmd: caddy fmt return code is 1 if not formatted (#5297)
* cmd: Fix caddy fmt if input isn't formatted

* Fixes #5294
* return exit 1 with an error message

* cmd: Use formattingDifference for caddy fmt

* #5294
* expose caddyfile.formattingDifference
2023-01-21 21:28:37 -07:00
Y.Horie d6d7511699 httpcaddyfile: Warn on importing empty file; skip dotfiles (#5320)
* httpcaddyfile: Change the parse rules when empty file or dotfile with a glob.

* Fixes #5295
* Empty file should just log a warning, and result in no tokens.
* The last segment of the path is '*', it should skip any dotfiles.
* The last segment of the path is '.*', it should read all dotfiles in a dir.

* httpcaddyfile: Regard empty files as import files which include only white space.
2023-01-21 10:22:36 -07:00
Y.Horie 8d6870fd06 chore: Fix typo, coral -> cobra (#5325) 2023-01-21 10:27:58 -05:00
WeidiDeng c38a040e85 httpcaddyfile: Fix handle grouping inside route (#5315)
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2023-01-18 16:04:41 -05:00
Alexandre Vicenzi e8ad9b32c9 go.mod: Update golang.org/x/net to v0.5.0 (#5314) 2023-01-17 07:07:07 -05:00
Y.Horie 62e8b21724 chore: Fix caddyfile.replaceEnvVars return (#5311) 2023-01-17 06:57:42 -05:00
Francis Lavoie 223cbe3d0b caddyhttp: Add server-level trusted_proxies config (#5103) 2023-01-10 00:08:23 -05:00
Yannick Ihmels 66ce0c5c63 caddytls: Add test cases for Caddyfile tls options (#5293) 2023-01-09 15:18:12 -05:00
Y.Horie 845bc4d50b reverseproxy: Fix hanging for Transfer-Encoding: chunked (#5289)
* Fixes #5236
* enable request body buffering in reverse proxy
  when the request header has Transfer-Encoding: chunked
2023-01-09 00:13:34 -07:00
Emily Lange e450a7377b reverseproxy: Don't enable auto-https when --from flag is http (#5269) 2023-01-06 15:42:07 -05:00
Matt Holt d74f6fd967 reverseproxy: Set origreq in active health check (#5284)
* reverseproxy: Set origreq in active health check

Fix #5281

* Oops; dereference Request
2023-01-06 15:06:38 -05:00
Yannick Ihmels 55035d327a caddytls: Add dns_ttl config, improve Caddyfile tls options (#5287) 2023-01-06 14:44:00 -05:00
Matthew Holt 4e9ad50f65 fileserver: Add a couple test cases
With placeholders
2023-01-04 11:07:27 -07:00
Matt Holt 05a4637489 Update README.md
Attempt to fix logo that was appearing black in some browsers (perhaps due to CSP?).

Thanks to @IndeedNotJames for investigating! Hopefully this works.
2023-01-01 16:27:06 -07:00
Matt Holt bd74f94496 Update README.md
Update logo and fix test result badge
2022-12-31 10:10:32 -07:00
Francis Lavoie b40548ff61 ci: Fix goreleaser deprecation (#5270) 2022-12-28 13:11:39 -05:00
TAKAHASHI Shuuji 4e54e48409 ci: Update GitHub Actions to avoid set-output deprecation (#5271) 2022-12-28 12:05:42 -05:00
Mohammed Al Sahaf b166b90083 ci: exclude dependbot from running tests on s390x machine (#5266) 2022-12-22 14:13:47 -05:00
darkweak dac7cacd4d encode: Respect Cache-Control no-transform (#5257)
* encode: respect Cache-Control HTTP header no-transform

* encode: switch to strings.Contains
2022-12-20 13:26:53 -07:00
dependabot[bot] af93517c2d build(deps): bump goreleaser/goreleaser-action from 2 to 4 (#5264)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-19 19:47:33 -05:00
dependabot[bot] 3b724a2082 build(deps): bump actions/upload-artifact from 1 to 3 (#5262)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-19 19:14:43 -05:00
dependabot[bot] 329af5ced9 build(deps): bump actions/cache from 2 to 3 (#5263)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-19 18:56:52 -05:00
dependabot[bot] cd49847edb build(deps): bump peter-evans/repository-dispatch from 1 to 2 (#5261)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-19 18:44:54 -05:00
John Losito d3d76d6ac2 ci: Check for github action updates monthly (#5258) 2022-12-19 14:57:56 -07:00
Lukas Vogel c3b5b1811c cmd: Avoid panic when printing version without build info (#5210)
* version: don't panic if read build info doesn't work

If `debug.ReadBuildInfo()` doesn't return the build information we
should not try to access it. Especially if users only want to build with
the `CustomVersion` we should not assume access to
`debug.ReadBuildInfo()`.

The build environment where this isn't available for me is when building
with bazel.

* exit early
2022-12-19 14:23:45 -07:00
Emily Lange 4fe5e64e46 readme: white ZeroSSL text color in dark mode (#5259)
* readme: white ZeroSSL text color in dark mode

* fix: keep `valign` for GitHub mobile app
2022-12-19 13:01:30 -07:00
IndeedNotJames e10ed7b00d readme: darker variants of logos in dark mode (#5248) 2022-12-12 10:18:30 -07:00
Matthew Holt fac35db9dc go.mod: Update quic-go to v0.31.0
And fix a comment typo
2022-12-08 08:55:04 -07:00
Kyle McCullough bfaf2a8201 acme_server: Configurable default lifetime for issued certificates (#5232)
* acme_server: add certificate lifetime configuration option

Signed-off-by: Kyle McCullough <kylemcc@gmail.com>

* pki: allow intermediate cert lifetime to be configured

Signed-off-by: Kyle McCullough <kylemcc@gmail.com>

Signed-off-by: Kyle McCullough <kylemcc@gmail.com>
2022-12-06 00:12:26 -07:00
Mohammed Al Sahaf fef9cb3e05 caddytest: internalize init config into '.go' file (#5230) 2022-12-05 18:49:41 +00:00
Alban Lecocq d4a7d89f56 reverseproxy: Improve hostByHashing distribution (#5229)
* If upstreams are all using same host but with different ports
ie:
foobar:4001
foobar:4002
foobar:4003
...
Because fnv-1a has not a good enough avalanche effect
Then the hostByHashing result is not well balanced over
all upstreams

As last byte FNV input tend to affect few bits, the idea is to change
the concatenation order between the key and the upstream strings
So the upstream last byte have more impact on hash diffusion
2022-12-05 11:28:12 -07:00
Matthew Holt ae77a56ac8 Clarify some docs 2022-11-30 16:03:31 -07:00
bit 762b02789a admin: set certmagic cache logger (#5173)
same way it is set in modules/caddytls/tls.go
2022-11-23 20:49:22 -07:00
Mariano Cano 6f8fe01da1 caddypki: Use go.step.sm/crypto to generate the PKI (#5217)
This commit replaces the use of github.com/smallstep/cli to generate the
root and intermediate certificates and uses go.step.sm/crypto instead.

It also upgrades the version of github.com/smallstep/certificates to the
latest version.
2022-11-23 20:47:42 -07:00
bit ac96455a9a admin: fix certificate renewal for admin (#5169)
certmagic.New takes a template and returns pointer to the new config.
GetConfigForCert later must return a pointer to the new config not the
template.

fixes #5162
2022-11-23 11:48:37 -07:00
Francis Lavoie ee7c92ec9b reverseproxy: Mask the WS close message when we're the client (#5199)
* reverseproxy: Mask the WS close message when we're the client

* weakrand

* Bump golangci-lint version so path ignores work on Windows

* gofmt

* ugh, gofmt everything, I guess
2022-11-14 09:38:02 -07:00
Jonathan Garcia 33fdea8f26 caddypki: Prefer user-configured root instead of generating new one (#5189)
instead of generating a new root certificate at the default location
load the certificate from the configuration.
fixes: #5181
2022-11-08 12:13:46 -07:00
Ashish Kurmi 6efd1b3bb1 ci: set least privilged token for github actions for lint workflow (#5179)
* ci: set least privilged token for github actions

Signed-off-by: Ashish Kurmi <akurmi@stepsecurity.io>

* ci:reverting github actions permissions for all but lint workflow

Signed-off-by: Ashish Kurmi <akurmi@stepsecurity.io>
2022-11-06 08:01:36 +00:00
Alexander Graf 087f126cf4 caddyhttp: Canonicalize header field names (#5176) 2022-10-29 16:35:44 -04:00
Benjamin Chalmers 1fa4cb7ba1 caddytest: Increased sleep between retries to reduce flakey tests in CI (#5160)
* Incresed sleep between retries to reduce flakey tests in CI

* Also changed wait time for admin

* Modified time to make it more reliable

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2022-10-27 22:12:30 +00:00
Mohammed Al Sahaf f20a8e7aa0 cmd: replace deprecate func use (#5170) 2022-10-25 17:56:38 +03:00
Matthew Holt 798c4a3ba4 go.mod: Upgrade some dependencies
Quic-go 0.30 should be faster
2022-10-24 12:20:39 -06:00
Matthew Holt 817470dd66 httploader: Close resp body on bad status code
Related to #5158
2022-10-24 12:18:32 -06:00
Chris Lahaye bbe3663167 caddyconfig: Fix httploader leak from unused responses (#5159)
fixes #5158

Signed-off-by: Chris Lahaye <mail@chrislahaye.com>

Signed-off-by: Chris Lahaye <mail@chrislahaye.com>
2022-10-24 11:58:30 -06:00
XYenon ed503118dd caddyhttp: add placeholder {http.request.orig_uri.path.*} (#5161) 2022-10-24 11:57:50 -06:00
Matt Holt a3ae146cbd fileserver: Reject non-GET/HEAD requests (close #5166) (#5167)
* fileserver: Reject non-GET/HEAD requests (close #5166)

* Set Allow header according to RFC 9110 10.2.1
2022-10-24 10:23:57 -06:00
Matt Holt 4bf6cb4199 fileserver: Reject ADS and short name paths; trim trailing dots and spaces on Windows (#5148)
* fileserver: Reject ADS and short name paths

* caddyhttp: Trim trailing space and dot on Windows

Windows ignores trailing dots and spaces in filenames.

* Fix test

* Adjust path filters

* Revert Windows test

* Actually revert the test

* Just check for colons
2022-10-18 21:55:25 -06:00
Scott Mebberson 72e7edda1f map: Clarified how destination values should be formatted (#5156) 2022-10-18 18:14:53 -06:00
BakaFT a999b70727 cmd: Add missing \n to HelpTemplate (#5151) 2022-10-17 11:51:41 +03:00
Francis Lavoie 1cd594963e docs: Fix templates documentation, stray newline breaks godoc (#5149) 2022-10-16 12:25:44 -04:00
351 changed files with 23444 additions and 7910 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
[*] [*]
end_of_line = lf end_of_line = lf
[caddytest/integration/caddyfile_adapt/*.txt] [caddytest/integration/caddyfile_adapt/*.caddyfiletest]
indent_style = tab indent_style = tab
+16 -6
View File
@@ -1,7 +1,7 @@
Contributing to Caddy Contributing to Caddy
===================== =====================
Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be great without your involvement! Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be nearly as excellent without your involvement!
For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers. For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers.
@@ -35,19 +35,29 @@ Here are some of the expectations we have of contributors:
- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other. - **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other.
- **Write tests.** Tests are essential! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass. - **Write tests.** Good, automated tests are very valuable! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass.
- **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks or profiling. - **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks and profiling.
- **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft <diverging-commit>` then `git commit -s`. - **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft <diverging-commit>` then `git commit -s`.
- **Own your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged. - **Be responsible for and maintain your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged.
- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious. - **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious.
- **Pull requests may still get closed.** The longer a PR stays open and idle, the more likely it is to be closed. If we haven't reviewed it in a while, it probably means the change is not a priority. Please don't take this personally, we're trying to balance a lot of tasks! If nobody else has commented or reacted to the PR, it likely means your change is useful only to you. The reality is this happens quite a bit. We don't tend to accept PRs that aren't generally helpful. For these reasons or others, the PR may get closed even after a review. We are not obligated to accept all proposed changes, even if the best justification we can give is something vague like, "It doesn't sit right." Sometimes PRs are just the wrong thing or the wrong time. Because it is open source, you can always build your own modified version of Caddy with a change you need, even if we reject it in the official repo. - **Pull requests may still get closed.** The longer a PR stays open and idle, the more likely it is to be closed. If we haven't reviewed it in a while, it probably means the change is not a priority. Please don't take this personally, we're trying to balance a lot of tasks! If nobody else has commented or reacted to the PR, it likely means your change is useful only to you. The reality is this happens quite a lot. We don't tend to accept PRs that aren't generally helpful. For these reasons or others, the PR may get closed even after a review. We are not obligated to accept all proposed changes, even if the best justification we can give is something vague like, "It doesn't sit right." Sometimes PRs are just the wrong thing or the wrong time. Because it is open source, you can always build your own modified version of Caddy with a change you need, even if we reject it in the official repo. Plus, because Caddy is extensible, it's possible your feature could make a great plugin instead!
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base! - **You certify that you wrote and comprehend the code you submit.** The Caddy project welcomes original contributions that comply with [our CLA](https://cla-assistant.io/caddyserver/caddy), meaning that authors must be able to certify that they created or have rights to the code they are contributing. In addition, we require that code is not simply copy-pasted from Q/A sites or AI language models without full comprehension and rigorous testing. In other words: contributors are allowed to refer to communities for assistance and use AI tools such as language models for inspiration, but code which originates from or is assisted by these resources MUST be:
- Licensed for you to freely share
- Fully comprehended by you (be able to explain every line of code)
- Verified by automated tests when feasible, or thorough manual tests otherwise
We have found that current language models (LLMs, like ChatGPT) may understand code syntax and even problem spaces to an extent, but often fail in subtle ways to convey true knowledge and produce correct algorithms. Integrated tools such as GitHub Copilot and Sourcegraph Cody may be used for inspiration, but code generated by these tools still needs to meet our criteria for licensing, human comprehension, and testing. These tools may be used to help write code comments and tests as long as you can certify they are accurate and correct. Note that it is often more trouble than it's worth to certify that Copilot (for example) is not giving you code that is possibly plagiarised, unlicensed, or licensed with incompatible terms -- as the Caddy project cannot accept such contributions. If that's too difficult for you (or impossible), then we recommend using these resources only for inspiration and write your own code. Ultimately, you (the contributor) are responsible for the code you're submitting.
As a courtesy to reviewers, we kindly ask that you disclose when contributing code that was generated by an AI tool or copied from another website so we can be aware of what to look for in code review.
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base.
#### HOW TO MAKE A PULL REQUEST TO CADDY #### HOW TO MAKE A PULL REQUEST TO CADDY
+3 -3
View File
@@ -7,7 +7,7 @@ The Caddy project would like to make sure that it stays on top of all practicall
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 2.x | :white_check_mark: | | 2.x | ✔️ |
| 1.x | :x: | | 1.x | :x: |
| < 1.x | :x: | | < 1.x | :x: |
@@ -24,7 +24,7 @@ We do not accept reports if the steps imply or require a compromised system or t
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application. Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
Security bugs in code dependencies are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code. Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
## Reporting a Vulnerability ## Reporting a Vulnerability
@@ -42,7 +42,7 @@ We'll need enough information to verify the bug and make a patch. To speed thing
- Specific minimal steps to reproduce the issue from scratch - Specific minimal steps to reproduce the issue from scratch
- A working patch - A working patch
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl` instead of web browsers. Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl -v` instead of web browsers.
We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored. We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored.
+7
View File
@@ -0,0 +1,7 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
+39 -47
View File
@@ -18,45 +18,54 @@ jobs:
# Default is true, cancels jobs for other platforms in the matrix if one fails # Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false fail-fast: false
matrix: matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ] os:
go: [ '1.18', '1.19' ] - linux
- mac
- windows
go:
- '1.21'
- '1.22'
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.18' - go: '1.21'
GO_SEMVER: '~1.18.4' GO_SEMVER: '~1.21.0'
- go: '1.19' - go: '1.22'
GO_SEMVER: '~1.19.0' GO_SEMVER: '~1.22.0'
# Set some variables per OS, usable via ${{ matrix.VAR }} # Set some variables per OS, usable via ${{ matrix.VAR }}
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True') # SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
- os: ubuntu-latest - os: linux
OS_LABEL: ubuntu-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0 SUCCESS: 0
- os: macos-latest - os: mac
OS_LABEL: macos-14
CADDY_BIN_PATH: ./cmd/caddy/caddy CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0 SUCCESS: 0
- os: windows-latest - os: windows
OS_LABEL: windows-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy.exe CADDY_BIN_PATH: ./cmd/caddy/caddy.exe
SUCCESS: 'True' SUCCESS: 'True'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.OS_LABEL }}
steps: steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go - name: Install Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true check-latest: true
- name: Checkout code
uses: actions/checkout@v3
# These tools would be useful if we later decide to reinvestigate # These tools would be useful if we later decide to reinvestigate
# publishing test/coverage reports to some tool for easier consumption # publishing test/coverage reports to some tool for easier consumption
# - name: Install test and coverage analysis tools # - name: Install test and coverage analysis tools
@@ -64,10 +73,11 @@ jobs:
# go get github.com/axw/gocov/gocov # go get github.com/axw/gocov/gocov
# go get github.com/AlekSi/gocov-xml # go get github.com/AlekSi/gocov-xml
# go get -u github.com/jstemmer/go-junit-report # go get -u github.com/jstemmer/go-junit-report
# echo "::add-path::$(go env GOPATH)/bin" # echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Print Go version and environment - name: Print Go version and environment
id: vars id: vars
shell: bash
run: | run: |
printf "Using go at: $(which go)\n" printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n" printf "Go version: $(go version)\n"
@@ -77,24 +87,7 @@ jobs:
env env
printf "Git version: $(git version)\n\n" printf "Git version: $(git version)\n\n"
# Calculate the short SHA1 hash of the git commit # Calculate the short SHA1 hash of the git commit
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)" echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Cache the build cache
uses: actions/cache@v2
with:
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
~\AppData\Local\go-build
key: ${{ runner.os }}-${{ matrix.go }}-go-ci-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go }}-go-ci
- name: Get dependencies - name: Get dependencies
run: | run: |
@@ -106,13 +99,14 @@ jobs:
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
run: | run: |
go build -trimpath -ldflags="-w -s" -v go build -tags nobdger -trimpath -ldflags="-w -s" -v
- name: Publish Build Artifact - name: Publish Build Artifact
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v4
with: with:
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }} name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
path: ${{ matrix.CADDY_BIN_PATH }} path: ${{ matrix.CADDY_BIN_PATH }}
compression-level: 0
# Commented bits below were useful to allow the job to continue # Commented bits below were useful to allow the job to continue
# even if the tests fail, so we can publish the report separately # even if the tests fail, so we can publish the report separately
@@ -122,8 +116,8 @@ jobs:
# continue-on-error: true # continue-on-error: true
run: | run: |
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out # (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
go test -v -coverprofile="cover-profile.out" -short -race ./... go test -tags nobadger -v -coverprofile="cover-profile.out" -short -race ./...
# echo "::set-output name=status::$?" # echo "status=$?" >> $GITHUB_OUTPUT
# Relevant step if we reinvestigate publishing test/coverage reports # Relevant step if we reinvestigate publishing test/coverage reports
# - name: Prepare coverage reports # - name: Prepare coverage reports
@@ -135,7 +129,7 @@ jobs:
# To return the correct result even though we set 'continue-on-error: true' # To return the correct result even though we set 'continue-on-error: true'
# - name: Coerce correct build result # - name: Coerce correct build result
# if: matrix.os != 'windows-latest' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }} # if: matrix.os != 'windows' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }}
# run: | # run: |
# echo "step_test ${{ steps.step_test.outputs.status }}\n" # echo "step_test ${{ steps.step_test.outputs.status }}\n"
# exit 1 # exit 1
@@ -143,11 +137,11 @@ jobs:
s390x-test: s390x-test:
name: test (s390x on IBM Z) name: test (s390x on IBM Z)
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps: steps:
- name: Checkout code into the Go module directory - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Run Tests - name: Run Tests
run: | run: |
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
@@ -157,7 +151,7 @@ jobs:
# The environment is fresh, so there's no point in keeping accepting and adding the key. # The environment is fresh, so there's no point in keeping accepting and adding the key.
rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha" rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..." ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -tags nobadger -v ./..."
test_result=$? test_result=$?
# There's no need leaving the files around # There's no need leaving the files around
@@ -172,12 +166,10 @@ jobs:
goreleaser-check: goreleaser-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: checkout - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- uses: goreleaser/goreleaser-action@v2 - uses: goreleaser/goreleaser-action@v5
with: with:
version: latest version: latest
args: check args: check
env:
TAG: ${{ steps.vars.outputs.version_tag }}
+24 -23
View File
@@ -11,24 +11,40 @@ on:
- 2.* - 2.*
jobs: jobs:
cross-build-test: build:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd'] goos:
go: [ '1.19' ] - 'aix'
- 'android'
- 'linux'
- 'solaris'
- 'illumos'
- 'dragonfly'
- 'freebsd'
- 'openbsd'
- 'plan9'
- 'windows'
- 'darwin'
- 'netbsd'
go:
- '1.22'
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.19' - go: '1.22'
GO_SEMVER: '~1.19.0' GO_SEMVER: '~1.22.0'
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
steps: steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go - name: Install Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true check-latest: true
@@ -43,31 +59,16 @@ jobs:
printf "\n\nSystem environment:\n\n" printf "\n\nSystem environment:\n\n"
env env
- name: Cache the build cache
uses: actions/cache@v2
with:
# In order:
# * Module download cache
# * Build cache (Linux)
path: |
~/go/pkg/mod
~/.cache/go-build
key: cross-build-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
cross-build-go${{ matrix.go }}-${{ matrix.goos }}
- name: Checkout code into the Go module directory
uses: actions/checkout@v3
- name: Run Build - name: Run Build
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
GOOS: ${{ matrix.goos }} GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
shell: bash shell: bash
continue-on-error: true continue-on-error: true
working-directory: ./cmd/caddy working-directory: ./cmd/caddy
run: | run: |
GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null GOOS=$GOOS GOARCH=$GOARCH go build -tags nobadger -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "::warning ::$GOOS Build Failed" echo "::warning ::$GOOS Build Failed"
exit 0 exit 0
+41 -6
View File
@@ -10,26 +10,61 @@ on:
- master - master
- 2.* - 2.*
permissions:
contents: read
jobs: jobs:
# From https://github.com/golangci/golangci-lint-action # From https://github.com/golangci/golangci-lint-action
golangci: golangci:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint name: lint
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os:
runs-on: ${{ matrix.os }} - linux
- mac
- windows
include:
- os: linux
OS_LABEL: ubuntu-latest
- os: mac
OS_LABEL: macos-14
- os: windows
OS_LABEL: windows-latest
runs-on: ${{ matrix.OS_LABEL }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-go@v3 - uses: actions/setup-go@v5
with: with:
go-version: '~1.18.4' go-version: '~1.22.0'
check-latest: true check-latest: true
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
version: v1.47 version: v1.55
# Workaround for https://github.com/golangci/golangci-lint-action/issues/135
skip-pkg-cache: true
# Windows times out frequently after about 5m50s if we don't set a longer timeout. # Windows times out frequently after about 5m50s if we don't set a longer timeout.
args: --timeout 10m args: --timeout 10m
# Optional: show only new issues if it's a pull request. The default value is `false`. # Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true # only-new-issues: true
govulncheck:
runs-on: ubuntu-latest
steps:
- name: govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-input: '~1.22.0'
check-latest: true
+21 -31
View File
@@ -10,14 +10,16 @@ jobs:
name: Release name: Release
strategy: strategy:
matrix: matrix:
os: [ ubuntu-latest ] os:
go: [ '1.19' ] - ubuntu-latest
go:
- '1.21'
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.19' - go: '1.21'
GO_SEMVER: '~1.19.0' GO_SEMVER: '~1.21.0'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233 # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
@@ -29,19 +31,19 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Go - name: Install Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true check-latest: true
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
# Force fetch upstream tags -- because 65 minutes # Force fetch upstream tags -- because 65 minutes
# tl;dr: actions/checkout@v3 runs this line: # tl;dr: actions/checkout@v4 runs this line:
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/ # git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran: # which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
# git fetch --prune --unshallow # git fetch --prune --unshallow
@@ -61,8 +63,8 @@ jobs:
go env go env
printf "\n\nSystem environment:\n\n" printf "\n\nSystem environment:\n\n"
env env
echo "::set-output name=version_tag::${GITHUB_REF/refs\/tags\//}" echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
echo "::set-output name=short_sha::$(git rev-parse --short HEAD)" echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
# Add "pip install" CLI tools to PATH # Add "pip install" CLI tools to PATH
echo ~/.local/bin >> $GITHUB_PATH echo ~/.local/bin >> $GITHUB_PATH
@@ -74,10 +76,10 @@ jobs:
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"` TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"` TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"` TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
echo "::set-output name=tag_major::${TAG_MAJOR}" echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT
echo "::set-output name=tag_minor::${TAG_MINOR}" echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT
echo "::set-output name=tag_patch::${TAG_PATCH}" echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT
echo "::set-output name=tag_special::${TAG_SPECIAL}" echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT
# Cloudsmith CLI tooling for pushing releases # Cloudsmith CLI tooling for pushing releases
# See https://help.cloudsmith.io/docs/cli # See https://help.cloudsmith.io/docs/cli
@@ -94,18 +96,6 @@ jobs:
# tags are only accepted if signed by Matt's key # tags are only accepted if signed by Matt's key
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1 git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
- name: Cache the build cache
uses: actions/cache@v2
with:
# In order:
# * Module download cache
# * Build cache (Linux)
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go${{ matrix.go }}-release-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go${{ matrix.go }}-release
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@main uses: sigstore/cosign-installer@main
- name: Cosign version - name: Cosign version
@@ -116,10 +106,10 @@ jobs:
run: syft version run: syft version
# GoReleaser will take care of publishing those artifacts into the release # GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2 uses: goreleaser/goreleaser-action@v5
with: with:
version: latest version: latest
args: release --rm-dist --timeout 60m args: release --clean --timeout 60m
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.vars.outputs.version_tag }} TAG: ${{ steps.vars.outputs.version_tag }}
+4 -3
View File
@@ -10,14 +10,15 @@ jobs:
name: Release Published name: Release Published
strategy: strategy:
matrix: matrix:
os: [ ubuntu-latest ] os:
- ubuntu-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
# See https://github.com/peter-evans/repository-dispatch # See https://github.com/peter-evans/repository-dispatch
- name: Trigger event on caddyserver/dist - name: Trigger event on caddyserver/dist
uses: peter-evans/repository-dispatch@v1 uses: peter-evans/repository-dispatch@v3
with: with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }} token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/dist repository: caddyserver/dist
@@ -25,7 +26,7 @@ jobs:
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}' client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
- name: Trigger event on caddyserver/caddy-docker - name: Trigger event on caddyserver/caddy-docker
uses: peter-evans/repository-dispatch@v1 uses: peter-evans/repository-dispatch@v3
with: with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }} token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/caddy-docker repository: caddyserver/caddy-docker
+2
View File
@@ -11,6 +11,8 @@ Caddyfile.*
# build artifacts and helpers # build artifacts and helpers
cmd/caddy/caddy cmd/caddy/caddy
cmd/caddy/caddy.exe cmd/caddy/caddy.exe
cmd/caddy/tmp/*.exe
cmd/caddy/.env
# mac specific # mac specific
.DS_Store .DS_Store
+90 -20
View File
@@ -2,38 +2,81 @@ linters-settings:
errcheck: errcheck:
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.* ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
ignoretests: true ignoretests: true
gci:
sections:
- standard # Standard section: captures all standard packages.
- default # Default section: contains all imports that could not be matched to another section type.
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
# Skip generated files.
# Default: true
skip-generated: true
# Enable custom order of sections.
# If `true`, make the section order the same as the order of `sections`.
# Default: false
custom-order: true
exhaustive:
ignore-enum-types: reflect.Kind|svc.Cmd
linters: linters:
disable-all: true disable-all: true
enable: enable:
- asasalint
- asciicheck
- bidichk
- bodyclose - bodyclose
- deadcode - decorder
- dogsled
- dupl
- dupword
- durationcheck
- errcheck - errcheck
- errname
- exhaustive
- exportloopref
- gci
- gofmt - gofmt
- goimports - goimports
- gofumpt
- gosec - gosec
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- importas
- misspell - misspell
- prealloc - prealloc
- promlinter
- sloglint
- sqlclosecheck
- staticcheck - staticcheck
- structcheck - tenv
- testableexamples
- testifylint
- tparallel
- typecheck - typecheck
- unconvert - unconvert
- unused - unused
- varcheck - wastedassign
- whitespace
- zerologlint
# these are implicitly disabled: # these are implicitly disabled:
# - asciicheck # - containedctx
# - contextcheck
# - cyclop
# - depguard # - depguard
# - dogsled # - errchkjson
# - dupl # - errorlint
# - exhaustive # - exhaustruct
# - exportloopref # - execinquery
# - exhaustruct
# - forbidigo
# - forcetypeassert
# - funlen # - funlen
# - gci # - ginkgolinter
# - gocheckcompilerdirectives
# - gochecknoglobals # - gochecknoglobals
# - gochecknoinits # - gochecknoinits
# - gochecksumtype
# - gocognit # - gocognit
# - goconst # - goconst
# - gocritic # - gocritic
@@ -41,27 +84,47 @@ linters:
# - godot # - godot
# - godox # - godox
# - goerr113 # - goerr113
# - gofumpt
# - goheader # - goheader
# - golint
# - gomnd # - gomnd
# - gomoddirectives
# - gomodguard # - gomodguard
# - goprintffuncname # - goprintffuncname
# - interfacer # - gosmopolitan
# - grouper
# - inamedparam
# - interfacebloat
# - ireturn
# - lll # - lll
# - maligned # - loggercheck
# - maintidx
# - makezero
# - mirror
# - musttag
# - nakedret # - nakedret
# - nestif # - nestif
# - nilerr
# - nilnil
# - nlreturn # - nlreturn
# - noctx # - noctx
# - nolintlint # - nolintlint
# - nonamedreturns
# - nosprintfhostport
# - paralleltest
# - perfsprint
# - predeclared
# - protogetter
# - reassign
# - revive
# - rowserrcheck # - rowserrcheck
# - scopelint
# - sqlclosecheck
# - stylecheck # - stylecheck
# - tagalign
# - tagliatelle
# - testpackage # - testpackage
# - thelper
# - unparam # - unparam
# - whitespace # - usestdlibvars
# - varnamelen
# - wrapcheck
# - wsl # - wsl
run: run:
@@ -80,19 +143,26 @@ output:
issues: issues:
exclude-rules: exclude-rules:
# we aren't calling unknown URL # we aren't calling unknown URL
- text: "G107" # G107: Url provided to HTTP request as taint input - text: 'G107' # G107: Url provided to HTTP request as taint input
linters: linters:
- gosec - gosec
# as a web server that's expected to handle any template, this is totally in the hands of the user. # as a web server that's expected to handle any template, this is totally in the hands of the user.
- text: "G203" # G203: Use of unescaped data in HTML templates - text: 'G203' # G203: Use of unescaped data in HTML templates
linters: linters:
- gosec - gosec
# we're shelling out to known commands, not relying on user-defined input. # we're shelling out to known commands, not relying on user-defined input.
- text: "G204" # G204: Audit use of command execution - text: 'G204' # G204: Audit use of command execution
linters: linters:
- gosec - gosec
# the choice of weakrand is deliberate, hence the named import "weakrand" # the choice of weakrand is deliberate, hence the named import "weakrand"
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go - path: modules/caddyhttp/reverseproxy/selectionpolicies.go
text: "G404" # G404: Insecure random number source (rand) text: 'G404' # G404: Insecure random number source (rand)
linters: linters:
- gosec - gosec
- path: modules/caddyhttp/reverseproxy/streaming.go
text: 'G404' # G404: Insecure random number source (rand)
linters:
- gosec
- path: modules/logging/filters.go
linters:
- dupl
+64 -7
View File
@@ -4,7 +4,9 @@ before:
# This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/caddy directory # This is so we can run goreleaser on tag without Git complaining of being dirty. The main.go in cmd/caddy directory
# cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which # cannot be built within that directory due to changes necessary for the build causing Git to be dirty, which
# subsequently causes gorleaser to refuse running. # subsequently causes gorleaser to refuse running.
- rm -rf caddy-build caddy-dist - rm -rf caddy-build caddy-dist vendor
# vendor Caddy deps
- go mod vendor
- mkdir -p caddy-build - mkdir -p caddy-build
- cp cmd/caddy/main.go caddy-build/main.go - cp cmd/caddy/main.go caddy-build/main.go
- /bin/sh -c 'cd ./caddy-build && go mod init caddy' - /bin/sh -c 'cd ./caddy-build && go mod init caddy'
@@ -14,6 +16,8 @@ before:
# as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly # as of Go 1.16, `go` commands no longer automatically change go.{mod,sum}. We now have to explicitly
# run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation. # run `go mod tidy`. The `/bin/sh -c '...'` is because goreleaser can't find cd in PATH without shell invocation.
- /bin/sh -c 'cd ./caddy-build && go mod tidy' - /bin/sh -c 'cd ./caddy-build && go mod tidy'
# vendor the deps of the prepared to-build module
- /bin/sh -c 'cd ./caddy-build && go mod vendor'
- git clone --depth 1 https://github.com/caddyserver/dist caddy-dist - git clone --depth 1 https://github.com/caddyserver/dist caddy-dist
- mkdir -p caddy-dist/man - mkdir -p caddy-dist/man
- go mod download - go mod download
@@ -39,6 +43,7 @@ builds:
- arm64 - arm64
- s390x - s390x
- ppc64le - ppc64le
- riscv64
goarm: goarm:
- "5" - "5"
- "6" - "6"
@@ -50,14 +55,20 @@ builds:
goarch: ppc64le goarch: ppc64le
- goos: darwin - goos: darwin
goarch: s390x goarch: s390x
- goos: darwin
goarch: riscv64
- goos: windows - goos: windows
goarch: ppc64le goarch: ppc64le
- goos: windows - goos: windows
goarch: s390x goarch: s390x
- goos: windows
goarch: riscv64
- goos: freebsd - goos: freebsd
goarch: ppc64le goarch: ppc64le
- goos: freebsd - goos: freebsd
goarch: s390x goarch: s390x
- goos: freebsd
goarch: riscv64
- goos: freebsd - goos: freebsd
goarch: arm goarch: arm
goarm: "5" goarm: "5"
@@ -66,24 +77,71 @@ builds:
- -mod=readonly - -mod=readonly
ldflags: ldflags:
- -s -w - -s -w
tags:
- nobadger
signs: signs:
- cmd: cosign - cmd: cosign
signature: "${artifact}.sig" signature: "${artifact}.sig"
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem' certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
args: ["sign-blob", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"] args: ["sign-blob", "--yes", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"]
artifacts: all artifacts: all
sboms: sboms:
- artifacts: binary - artifacts: binary
documents: documents:
- '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{if .Arm}}v{{ .Arm }}{{end}}.sbom' - >-
{{ .ProjectName }}_
{{- .Version }}_
{{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_
{{- .Arch }}
{{- with .Arm }}v{{ . }}{{ end }}
{{- with .Mips }}_{{ . }}{{ end }}
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}.sbom
cmd: syft cmd: syft
args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"] args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"]
archives: archives:
- format_overrides: - id: default
format_overrides:
- goos: windows - goos: windows
format: zip format: zip
replacements: name_template: >-
darwin: mac {{ .ProjectName }}_
{{- .Version }}_
{{- if eq .Os "darwin" }}mac{{ else }}{{ .Os }}{{ end }}_
{{- .Arch }}
{{- with .Arm }}v{{ . }}{{ end }}
{{- with .Mips }}_{{ . }}{{ end }}
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}
# package the 'caddy-build' directory into a tarball,
# allowing users to build the exact same set of files as ours.
- id: source
meta: true
name_template: "{{ .ProjectName }}_{{ .Version }}_buildable-artifact"
files:
- src: LICENSE
dst: ./LICENSE
- src: README.md
dst: ./README.md
- src: AUTHORS
dst: ./AUTHORS
- src: ./caddy-build
dst: ./
source:
enabled: true
name_template: '{{ .ProjectName }}_{{ .Version }}_src'
format: 'tar.gz'
# Additional files/template/globs you want to add to the source archive.
#
# Default: empty.
files:
- vendor
checksum: checksum:
algorithm: sha512 algorithm: sha512
@@ -128,7 +186,6 @@ nfpms:
preremove: ./caddy-dist/scripts/preremove.sh preremove: ./caddy-dist/scripts/preremove.sh
postremove: ./caddy-dist/scripts/postremove.sh postremove: ./caddy-dist/scripts/postremove.sh
release: release:
github: github:
owner: caddyserver owner: caddyserver
+20 -8
View File
@@ -1,13 +1,19 @@
<p align="center"> <p align="center">
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a> <a href="https://caddyserver.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1128849/210187358-e2c39003-9a5e-4dd5-a783-6deb6483ee72.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/210187356-dfb7f1c5-ac2e-43aa-bb23-fc014280ae1f.svg">
<img src="https://user-images.githubusercontent.com/1128849/210187356-dfb7f1c5-ac2e-43aa-bb23-fc014280ae1f.svg" alt="Caddy" width="550">
</picture>
</a>
<br> <br>
<h3 align="center">a <a href="https://zerossl.com"><img src="https://caddyserver.com/resources/images/zerossl-logo.svg" height="28" valign="middle"></a> project</h3> <h3 align="center">a <a href="https://zerossl.com"><img src="https://user-images.githubusercontent.com/55066419/208327323-2770dc16-ec09-43a0-9035-c5b872c2ad7f.svg" height="28" style="vertical-align: -7.7px" valign="middle"></a> project</h3>
</p> </p>
<hr> <hr>
<h3 align="center">Every site on HTTPS</h3> <h3 align="center">Every site on HTTPS</h3>
<p align="center">Caddy is an extensible server platform that uses TLS by default.</p> <p align="center">Caddy is an extensible server platform that uses TLS by default.</p>
<p align="center"> <p align="center">
<a href="https://github.com/caddyserver/caddy/actions?query=workflow%3ACross-Platform"><img src="https://github.com/caddyserver/caddy/workflows/Cross-Platform/badge.svg"></a> <a href="https://github.com/caddyserver/caddy/actions/workflows/ci.yml"><img src="https://github.com/caddyserver/caddy/actions/workflows/ci.yml/badge.svg"></a>
<a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a> <a href="https://pkg.go.dev/github.com/caddyserver/caddy/v2"><img src="https://img.shields.io/badge/godoc-reference-%23007d9c.svg"></a>
<br> <br>
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a> <a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
@@ -40,7 +46,13 @@
<p align="center"> <p align="center">
<b>Powered by</b> <b>Powered by</b>
<br> <br>
<a href="https://github.com/caddyserver/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a> <a href="https://github.com/caddyserver/certmagic">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/55066419/206946718-740b6371-3df3-4d72-a822-47e4c48af999.png">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png">
<img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250">
</picture>
</a>
</p> </p>
@@ -58,7 +70,7 @@
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues - **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates - **Production-ready** after serving trillions of requests and managing millions of TLS certificates
- **Scales to hundreds of thousands of sites** as proven in production - **Scales to hundreds of thousands of sites** as proven in production
- **HTTP/1.1, HTTP/2, and HTTP/3** supported all by default - **HTTP/1.1, HTTP/2, and HTTP/3** all supported by default
- **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat - **Highly extensible** [modular architecture](https://caddyserver.com/docs/architecture) lets Caddy do anything without bloat
- **Runs anywhere** with **no external dependencies** (not even libc) - **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers - Written in Go, a language with higher **memory safety guarantees** than other servers
@@ -75,10 +87,10 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
Requirements: Requirements:
- [Go 1.18 or newer](https://golang.org/dl/) - [Go 1.21 or newer](https://golang.org/dl/)
### For development ### For development
_**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions in the next section._ _**Note:** These steps [will not embed proper version information](https://github.com/golang/go/issues/29228). For that, please follow the instructions in the next section._
```bash ```bash
@@ -185,4 +197,4 @@ Matthew Holt began developing Caddy in 2014 while studying computer science at B
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company. Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.
Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence. Debian package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that enables your organization to create, store and share packages in any format, to any place, with total confidence.
+66 -8
View File
@@ -46,6 +46,17 @@ import (
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
func init() {
// The hard-coded default `DefaultAdminListen` can be overidden
// by setting the `CADDY_ADMIN` environment variable.
// The environment variable may be used by packagers to change
// the default admin address to something more appropriate for
// that platform. See #5317 for discussion.
if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
DefaultAdminListen = env
}
}
// AdminConfig configures Caddy's API endpoint, which is used // AdminConfig configures Caddy's API endpoint, which is used
// to manage Caddy while it is running. // to manage Caddy while it is running.
type AdminConfig struct { type AdminConfig struct {
@@ -57,7 +68,14 @@ type AdminConfig struct {
// The address to which the admin endpoint's listener should // The address to which the admin endpoint's listener should
// bind itself. Can be any single network address that can be // bind itself. Can be any single network address that can be
// parsed by Caddy. Accepts placeholders. Default: localhost:2019 // parsed by Caddy. Accepts placeholders.
// Default: the value of the `CADDY_ADMIN` environment variable,
// or `localhost:2019` otherwise.
//
// Remember: When changing this value through a config reload,
// be sure to use the `--address` CLI flag to specify the current
// admin address if the currently-running admin endpoint is not
// the default address.
Listen string `json:"listen,omitempty"` Listen string `json:"listen,omitempty"`
// If true, CORS headers will be emitted, and requests to the // If true, CORS headers will be emitted, and requests to the
@@ -300,7 +318,32 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
// messages. If the requested URI does not include an Internet host // messages. If the requested URI does not include an Internet host
// name for the service being requested, then the Host header field MUST // name for the service being requested, then the Host header field MUST
// be given with an empty value." // be given with an empty value."
//
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
// Understandable, but frustrating. See:
// https://github.com/golang/go/issues/60374
// See also the discussion here:
// https://github.com/golang/go/issues/61431
//
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
// security checks, the infosec community assures me that it is secure to do
// so, because:
// 1) Browsers do not allow access to unix sockets
// 2) DNS is irrelevant to unix sockets
//
// I am not quite ready to trust either of those external factors, so instead
// of disabling Host/Origin checks, we now allow specific Host values when
// accessing the admin endpoint over unix sockets. I definitely don't trust
// DNS (e.g. I don't trust 'localhost' to always resolve to the local host),
// and IP shouldn't even be used, but if it is for some reason, I think we can
// at least be reasonably assured that 127.0.0.1 and ::1 route to the local
// machine, meaning that a hypothetical browser origin would have to be on the
// local machine as well.
uniqueOrigins[""] = struct{}{} uniqueOrigins[""] = struct{}{}
uniqueOrigins["127.0.0.1"] = struct{}{}
uniqueOrigins["::1"] = struct{}{}
} else { } else {
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
@@ -572,12 +615,13 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
} }
func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config { func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool) *certmagic.Config {
var cmCfg *certmagic.Config
if ident == nil { if ident == nil {
// user might not have configured identity; that's OK, we can still make a // user might not have configured identity; that's OK, we can still make a
// certmagic config, although it'll be mostly useless for remote management // certmagic config, although it'll be mostly useless for remote management
ident = new(IdentityConfig) ident = new(IdentityConfig)
} }
cmCfg := &certmagic.Config{ template := certmagic.Config{
Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity) Storage: DefaultStorage, // do not act as part of a cluster (this is for the server's local identity)
Logger: logger, Logger: logger,
Issuers: ident.issuers, Issuers: ident.issuers,
@@ -587,9 +631,11 @@ func (ident *IdentityConfig) certmagicConfig(logger *zap.Logger, makeCache bool)
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) { GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
return cmCfg, nil return cmCfg, nil
}, },
Logger: logger.Named("cache"),
}) })
} }
return certmagic.New(identityCertCache, *cmCfg) cmCfg = certmagic.New(identityCertCache, template)
return cmCfg
} }
// IdentityCredentials returns this instance's configured, managed identity credentials // IdentityCredentials returns this instance's configured, managed identity credentials
@@ -995,9 +1041,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
id := parts[2] id := parts[2]
// map the ID to the expanded path // map the ID to the expanded path
currentCtxMu.RLock() rawCfgMu.RLock()
expanded, ok := rawCfgIndex[id] expanded, ok := rawCfgIndex[id]
defer currentCtxMu.RUnlock() rawCfgMu.RUnlock()
if !ok { if !ok {
return APIError{ return APIError{
HTTPStatus: http.StatusNotFound, HTTPStatus: http.StatusNotFound,
@@ -1150,15 +1196,27 @@ traverseLoop:
} }
case http.MethodPut: case http.MethodPut:
if _, ok := v[part]; ok { if _, ok := v[part]; ok {
return fmt.Errorf("[%s] key already exists: %s", path, part) return APIError{
HTTPStatus: http.StatusConflict,
Err: fmt.Errorf("[%s] key already exists: %s", path, part),
}
} }
v[part] = val v[part] = val
case http.MethodPatch: case http.MethodPatch:
if _, ok := v[part]; !ok { if _, ok := v[part]; !ok {
return fmt.Errorf("[%s] key does not exist: %s", path, part) return APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
}
} }
v[part] = val v[part] = val
case http.MethodDelete: case http.MethodDelete:
if _, ok := v[part]; !ok {
return APIError{
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
}
}
delete(v, part) delete(v, part)
default: default:
return fmt.Errorf("unrecognized method %s", method) return fmt.Errorf("unrecognized method %s", method)
@@ -1300,7 +1358,7 @@ var (
// will get deleted before the process gracefully exits. // will get deleted before the process gracefully exits.
func PIDFile(filename string) error { func PIDFile(filename string) error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n") pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
err := os.WriteFile(filename, pid, 0600) err := os.WriteFile(filename, pid, 0o600)
if err != nil { if err != nil {
return err return err
} }
+6
View File
@@ -75,6 +75,12 @@ func TestUnsyncedConfigAccess(t *testing.T) {
path: "/bar/qq", path: "/bar/qq",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`, expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
}, },
{
method: "DELETE",
path: "/bar/qq",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
shouldErr: true,
},
{ {
method: "POST", method: "POST",
path: "/list", path: "/list",
+68 -29
View File
@@ -22,6 +22,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"log" "log"
"net/http" "net/http"
"os" "os"
@@ -34,10 +35,12 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/caddyserver/caddy/v2/notify"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2/internal/filesystems"
"github.com/caddyserver/caddy/v2/notify"
) )
// Config is the top (or beginning) of the Caddy configuration structure. // Config is the top (or beginning) of the Caddy configuration structure.
@@ -82,6 +85,9 @@ type Config struct {
storage certmagic.Storage storage certmagic.Storage
cancelFunc context.CancelFunc cancelFunc context.CancelFunc
// filesystems is a dict of filesystems that will later be loaded from and added to.
filesystems FileSystems
} }
// App is a thing that Caddy runs. // App is a thing that Caddy runs.
@@ -156,8 +162,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
return fmt.Errorf("method not allowed") return fmt.Errorf("method not allowed")
} }
currentCtxMu.Lock() rawCfgMu.Lock()
defer currentCtxMu.Unlock() defer rawCfgMu.Unlock()
if ifMatchHeader != "" { if ifMatchHeader != "" {
// expect the first and last character to be quotes // expect the first and last character to be quotes
@@ -257,8 +263,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
// readConfig traverses the current config to path // readConfig traverses the current config to path
// and writes its JSON encoding to out. // and writes its JSON encoding to out.
func readConfig(path string, out io.Writer) error { func readConfig(path string, out io.Writer) error {
currentCtxMu.RLock() rawCfgMu.RLock()
defer currentCtxMu.RUnlock() defer rawCfgMu.RUnlock()
return unsyncedConfigAccess(http.MethodGet, path, nil, out) return unsyncedConfigAccess(http.MethodGet, path, nil, out)
} }
@@ -305,7 +311,7 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err
// it as the new config, replacing any other current config. // it as the new config, replacing any other current config.
// It does NOT update the raw config state, as this is a // It does NOT update the raw config state, as this is a
// lower-level function; most callers will want to use Load // lower-level function; most callers will want to use Load
// instead. A write lock on currentCtxMu is required! If // instead. A write lock on rawCfgMu is required! If
// allowPersist is false, it will not be persisted to disk, // allowPersist is false, it will not be persisted to disk,
// even if it is configured to. // even if it is configured to.
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
@@ -314,7 +320,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
strippedCfgJSON := RemoveMetaFields(cfgJSON) strippedCfgJSON := RemoveMetaFields(cfgJSON)
var newCfg *Config var newCfg *Config
err := strictUnmarshalJSON(strippedCfgJSON, &newCfg) err := StrictUnmarshalJSON(strippedCfgJSON, &newCfg)
if err != nil { if err != nil {
return err return err
} }
@@ -340,8 +346,10 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
} }
// swap old context (including its config) with the new one // swap old context (including its config) with the new one
currentCtxMu.Lock()
oldCtx := currentCtx oldCtx := currentCtx
currentCtx = ctx currentCtx = ctx
currentCtxMu.Unlock()
// Stop, Cleanup each old app // Stop, Cleanup each old app
unsyncedStop(oldCtx) unsyncedStop(oldCtx)
@@ -354,13 +362,13 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
newCfg.Admin.Config.Persist == nil || newCfg.Admin.Config.Persist == nil ||
*newCfg.Admin.Config.Persist) { *newCfg.Admin.Config.Persist) {
dir := filepath.Dir(ConfigAutosavePath) dir := filepath.Dir(ConfigAutosavePath)
err := os.MkdirAll(dir, 0700) err := os.MkdirAll(dir, 0o700)
if err != nil { if err != nil {
Log().Error("unable to create folder for config autosave", Log().Error("unable to create folder for config autosave",
zap.String("dir", dir), zap.String("dir", dir),
zap.Error(err)) zap.Error(err))
} else { } else {
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600) err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0o600)
if err == nil { if err == nil {
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath)) Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
} else { } else {
@@ -443,6 +451,9 @@ func run(newCfg *Config, start bool) (Context, error) {
} }
} }
// create the new filesystem map
newCfg.filesystems = &filesystems.FilesystemMap{}
// prepare the new config for use // prepare the new config for use
newCfg.apps = make(map[string]App) newCfg.apps = make(map[string]App)
@@ -627,22 +638,35 @@ type ConfigLoader interface {
// stop the others. Stop should only be called // stop the others. Stop should only be called
// if not replacing with a new config. // if not replacing with a new config.
func Stop() error { func Stop() error {
currentCtxMu.RLock()
ctx := currentCtx
currentCtxMu.RUnlock()
rawCfgMu.Lock()
unsyncedStop(ctx)
currentCtxMu.Lock() currentCtxMu.Lock()
defer currentCtxMu.Unlock()
unsyncedStop(currentCtx)
currentCtx = Context{} currentCtx = Context{}
currentCtxMu.Unlock()
rawCfgJSON = nil rawCfgJSON = nil
rawCfgIndex = nil rawCfgIndex = nil
rawCfg[rawConfigKey] = nil rawCfg[rawConfigKey] = nil
rawCfgMu.Unlock()
return nil return nil
} }
// unsyncedStop stops cfg from running, but has // unsyncedStop stops ctx from running, but has
// no locking around cfg. It is a no-op if cfg is // no locking around ctx. It is a no-op if ctx has a
// nil. If any app returns an error when stopping, // nil cfg. If any app returns an error when stopping,
// it is logged and the function continues stopping // it is logged and the function continues stopping
// the next app. This function assumes all apps in // the next app. This function assumes all apps in
// cfg were successfully started first. // ctx were successfully started first.
//
// A lock on rawCfgMu is required, even though this
// function does not access rawCfg, that lock
// synchronizes the stop/start of apps.
func unsyncedStop(ctx Context) { func unsyncedStop(ctx Context) {
if ctx.cfg == nil { if ctx.cfg == nil {
return return
@@ -809,14 +833,19 @@ func ParseDuration(s string) (time.Duration, error) {
// regardless of storage configuration, since each instance is intended to // regardless of storage configuration, since each instance is intended to
// have its own unique ID. // have its own unique ID.
func InstanceID() (uuid.UUID, error) { func InstanceID() (uuid.UUID, error) {
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid") appDataDir := AppDataDir()
uuidFilePath := filepath.Join(appDataDir, "instance.uuid")
uuidFileBytes, err := os.ReadFile(uuidFilePath) uuidFileBytes, err := os.ReadFile(uuidFilePath)
if os.IsNotExist(err) { if errors.Is(err, fs.ErrNotExist) {
uuid, err := uuid.NewRandom() uuid, err := uuid.NewRandom()
if err != nil { if err != nil {
return uuid, err return uuid, err
} }
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0600) err = os.MkdirAll(appDataDir, 0o600)
if err != nil {
return uuid, err
}
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600)
return uuid, err return uuid, err
} else if err != nil { } else if err != nil {
return [16]byte{}, err return [16]byte{}, err
@@ -864,13 +893,21 @@ func Version() (simple, full string) {
// bi.Main... hopefully. // bi.Main... hopefully.
var module *debug.Module var module *debug.Module
bi, ok := debug.ReadBuildInfo() bi, ok := debug.ReadBuildInfo()
if ok { if !ok {
// find the Caddy module in the dependency list if CustomVersion != "" {
for _, dep := range bi.Deps { full = CustomVersion
if dep.Path == ImportPath { simple = CustomVersion
module = dep return
break }
} full = "unknown"
simple = "unknown"
return
}
// find the Caddy module in the dependency list
for _, dep := range bi.Deps {
if dep.Path == ImportPath {
module = dep
break
} }
} }
if module != nil { if module != nil {
@@ -961,14 +998,12 @@ type CtxKey string
// This group of variables pertains to the current configuration. // This group of variables pertains to the current configuration.
var ( var (
// currentCtxMu protects everything in this var block.
currentCtxMu sync.RWMutex
// currentCtx is the root context for the currently-running // currentCtx is the root context for the currently-running
// configuration, which can be accessed through this value. // configuration, which can be accessed through this value.
// If the Config contained in this value is not nil, then // If the Config contained in this value is not nil, then
// a config is currently active/running. // a config is currently active/running.
currentCtx Context currentCtx Context
currentCtxMu sync.RWMutex
// rawCfg is the current, generic-decoded configuration; // rawCfg is the current, generic-decoded configuration;
// we initialize it as a map with one field ("config") // we initialize it as a map with one field ("config")
@@ -986,6 +1021,10 @@ var (
// rawCfgIndex is the map of user-assigned ID to expanded // rawCfgIndex is the map of user-assigned ID to expanded
// path, for converting /id/ paths to /config/ paths. // path, for converting /id/ paths to /config/ paths.
rawCfgIndex map[string]string rawCfgIndex map[string]string
// rawCfgMu protects all the rawCfg fields and also
// essentially synchronizes config changes/reloads.
rawCfgMu sync.RWMutex
) )
// errSameConfig is returned if the new config is the same // errSameConfig is returned if the new config is the same
+19 -23
View File
@@ -52,9 +52,9 @@ func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconf
return nil, warnings, err return nil, warnings, err
} }
// lint check: see if input was properly formatted; sometimes messy files files parse // lint check: see if input was properly formatted; sometimes messy files parse
// successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry) // successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry)
if warning, different := formattingDifference(filename, body); different { if warning, different := FormattingDifference(filename, body); different {
warnings = append(warnings, warning) warnings = append(warnings, warning)
} }
@@ -63,10 +63,10 @@ func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconf
return result, warnings, err return result, warnings, err
} }
// formattingDifference returns a warning and true if the formatted version // FormattingDifference returns a warning and true if the formatted version
// is any different from the input; empty warning and false otherwise. // is any different from the input; empty warning and false otherwise.
// TODO: also perform this check on imported files // TODO: also perform this check on imported files
func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) { func FormattingDifference(filename string, body []byte) (caddyconfig.Warning, bool) {
// replace windows-style newlines to normalize comparison // replace windows-style newlines to normalize comparison
normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1) normalizedBody := bytes.Replace(body, []byte("\r\n"), []byte("\n"), -1)
@@ -88,34 +88,30 @@ func formattingDifference(filename string, body []byte) (caddyconfig.Warning, bo
return caddyconfig.Warning{ return caddyconfig.Warning{
File: filename, File: filename,
Line: line, Line: line,
Message: "Caddyfile input is not formatted; run the 'caddy fmt' command to fix inconsistencies", Message: "Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies",
}, true }, true
} }
// Unmarshaler is a type that can unmarshal // Unmarshaler is a type that can unmarshal Caddyfile tokens to
// Caddyfile tokens to set itself up for a // set itself up for a JSON encoding. The goal of an unmarshaler
// JSON encoding. The goal of an unmarshaler // is not to set itself up for actual use, but to set itself up for
// is not to set itself up for actual use, // being marshaled into JSON. Caddyfile-unmarshaled values will not
// but to set itself up for being marshaled // be used directly; they will be encoded as JSON and then used from
// into JSON. Caddyfile-unmarshaled values // that. Implementations _may_ be able to support multiple segments
// will not be used directly; they will be // (instances of their directive or batch of tokens); typically this
// encoded as JSON and then used from that. // means wrapping parsing logic in a loop: `for d.Next() { ... }`.
// Implementations must be able to support // More commonly, only a single segment is supported, so a simple
// multiple segments (instances of their // `d.Next()` at the start should be used to consume the module
// directive or batch of tokens); typically // identifier token (directive name, etc).
// this means wrapping all token logic in
// a loop: `for d.Next() { ... }`.
type Unmarshaler interface { type Unmarshaler interface {
UnmarshalCaddyfile(d *Dispenser) error UnmarshalCaddyfile(d *Dispenser) error
} }
// ServerType is a type that can evaluate a Caddyfile and set up a caddy config. // ServerType is a type that can evaluate a Caddyfile and set up a caddy config.
type ServerType interface { type ServerType interface {
// Setup takes the server blocks which // Setup takes the server blocks which contain tokens,
// contain tokens, as well as options // as well as options (e.g. CLI flags) and creates a
// (e.g. CLI flags) and creates a Caddy // Caddy config, along with any warnings or an error.
// config, along with any warnings or
// an error.
Setup([]ServerBlock, map[string]any) (*caddy.Config, []caddyconfig.Warning, error) Setup([]ServerBlock, map[string]any) (*caddy.Config, []caddyconfig.Warning, error)
} }
+30 -60
View File
@@ -101,12 +101,12 @@ func (d *Dispenser) nextOnSameLine() bool {
d.cursor++ d.cursor++
return true return true
} }
if d.cursor >= len(d.tokens) { if d.cursor >= len(d.tokens)-1 {
return false return false
} }
if d.cursor < len(d.tokens)-1 && curr := d.tokens[d.cursor]
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File && next := d.tokens[d.cursor+1]
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line { if !isNextOnNewLine(curr, next) {
d.cursor++ d.cursor++
return true return true
} }
@@ -122,12 +122,12 @@ func (d *Dispenser) NextLine() bool {
d.cursor++ d.cursor++
return true return true
} }
if d.cursor >= len(d.tokens) { if d.cursor >= len(d.tokens)-1 {
return false return false
} }
if d.cursor < len(d.tokens)-1 && curr := d.tokens[d.cursor]
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File || next := d.tokens[d.cursor+1]
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) { if isNextOnNewLine(curr, next) {
d.cursor++ d.cursor++
return true return true
} }
@@ -203,14 +203,17 @@ func (d *Dispenser) Val() string {
} }
// ValRaw gets the raw text of the current token (including quotes). // ValRaw gets the raw text of the current token (including quotes).
// If the token was a heredoc, then the delimiter is not included,
// because that is not relevant to any unmarshaling logic at this time.
// If there is no token loaded, it returns empty string. // If there is no token loaded, it returns empty string.
func (d *Dispenser) ValRaw() string { func (d *Dispenser) ValRaw() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) { if d.cursor < 0 || d.cursor >= len(d.tokens) {
return "" return ""
} }
quote := d.tokens[d.cursor].wasQuoted quote := d.tokens[d.cursor].wasQuoted
if quote > 0 { if quote > 0 && quote != '<' {
return string(quote) + d.tokens[d.cursor].Text + string(quote) // string literal // string literal
return string(quote) + d.tokens[d.cursor].Text + string(quote)
} }
return d.tokens[d.cursor].Text return d.tokens[d.cursor].Text
} }
@@ -388,22 +391,22 @@ func (d *Dispenser) Reset() {
// an argument. // an argument.
func (d *Dispenser) ArgErr() error { func (d *Dispenser) ArgErr() error {
if d.Val() == "{" { if d.Val() == "{" {
return d.Err("Unexpected token '{', expecting argument") return d.Err("unexpected token '{', expecting argument")
} }
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val()) return d.Errf("wrong argument count or unexpected line ending after '%s'", d.Val())
} }
// SyntaxErr creates a generic syntax error which explains what was // SyntaxErr creates a generic syntax error which explains what was
// found and what was expected. // found and what was expected.
func (d *Dispenser) SyntaxErr(expected string) error { func (d *Dispenser) SyntaxErr(expected string) error {
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected) msg := fmt.Sprintf("syntax error: unexpected token '%s', expecting '%s', at %s:%d import chain: ['%s']", d.Val(), expected, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
return errors.New(msg) return errors.New(msg)
} }
// EOFErr returns an error indicating that the dispenser reached // EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token. // the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error { func (d *Dispenser) EOFErr() error {
return d.Errf("Unexpected EOF") return d.Errf("unexpected EOF")
} }
// Err generates a custom parse-time error with a message of msg. // Err generates a custom parse-time error with a message of msg.
@@ -418,7 +421,10 @@ func (d *Dispenser) Errf(format string, args ...any) error {
// WrapErr takes an existing error and adds the Caddyfile file and line number. // WrapErr takes an existing error and adds the Caddyfile file and line number.
func (d *Dispenser) WrapErr(err error) error { func (d *Dispenser) WrapErr(err error) error {
return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err) if len(d.Token().imports) > 0 {
return fmt.Errorf("%w, at %s:%d import chain ['%s']", err, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
}
return fmt.Errorf("%w, at %s:%d", err, d.File(), d.Line())
} }
// Delete deletes the current token and returns the updated slice // Delete deletes the current token and returns the updated slice
@@ -438,14 +444,14 @@ func (d *Dispenser) Delete() []Token {
return d.tokens return d.tokens
} }
// numLineBreaks counts how many line breaks are in the token // DeleteN is the same as Delete, but can delete many tokens at once.
// value given by the token index tknIdx. It returns 0 if the // If there aren't N tokens available to delete, none are deleted.
// token does not exist or there are no line breaks. func (d *Dispenser) DeleteN(amount int) []Token {
func (d *Dispenser) numLineBreaks(tknIdx int) int { if amount > 0 && d.cursor >= (amount-1) && d.cursor <= len(d.tokens)-1 {
if tknIdx < 0 || tknIdx >= len(d.tokens) { d.tokens = append(d.tokens[:d.cursor-(amount-1)], d.tokens[d.cursor+1:]...)
return 0 d.cursor -= amount
} }
return strings.Count(d.tokens[tknIdx].Text, "\n") return d.tokens
} }
// isNewLine determines whether the current token is on a different // isNewLine determines whether the current token is on a different
@@ -461,25 +467,7 @@ func (d *Dispenser) isNewLine() bool {
prev := d.tokens[d.cursor-1] prev := d.tokens[d.cursor-1]
curr := d.tokens[d.cursor] curr := d.tokens[d.cursor]
return isNextOnNewLine(prev, curr)
// If the previous token is from a different file,
// we can assume it's from a different line
if prev.File != curr.File {
return true
}
// The previous token may contain line breaks if
// it was quoted and spanned multiple lines. e.g:
//
// dir "foo
// bar
// baz"
prevLineBreaks := d.numLineBreaks(d.cursor - 1)
// If the previous token (incl line breaks) ends
// on a line earlier than the current token,
// then the current token is on a new line
return prev.Line+prevLineBreaks < curr.Line
} }
// isNextOnNewLine determines whether the current token is on a different // isNextOnNewLine determines whether the current token is on a different
@@ -495,23 +483,5 @@ func (d *Dispenser) isNextOnNewLine() bool {
curr := d.tokens[d.cursor] curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1] next := d.tokens[d.cursor+1]
return isNextOnNewLine(curr, next)
// If the next token is from a different file,
// we can assume it's from a different line
if curr.File != next.File {
return true
}
// The current token may contain line breaks if
// it was quoted and spanned multiple lines. e.g:
//
// dir "foo
// bar
// baz"
currLineBreaks := d.numLineBreaks(d.cursor)
// If the current token (incl line breaks) ends
// on a line earlier than the next token,
// then the next token is on a new line
return curr.Line+currLineBreaks < next.Line
} }
+1 -1
View File
@@ -305,7 +305,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err) t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
} }
var ErrBarIsFull = errors.New("bar is full") ErrBarIsFull := errors.New("bar is full")
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull) bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
if !errors.Is(bookingError, ErrBarIsFull) { if !errors.Is(bookingError, ErrBarIsFull) {
t.Errorf("Errf(): should be able to unwrap the error chain") t.Errorf("Errf(): should be able to unwrap the error chain")
+80
View File
@@ -18,6 +18,8 @@ import (
"bytes" "bytes"
"io" "io"
"unicode" "unicode"
"golang.org/x/exp/slices"
) )
// Format formats the input Caddyfile to a standard, nice-looking // Format formats the input Caddyfile to a standard, nice-looking
@@ -31,6 +33,14 @@ func Format(input []byte) []byte {
out := new(bytes.Buffer) out := new(bytes.Buffer)
rdr := bytes.NewReader(input) rdr := bytes.NewReader(input)
type heredocState int
const (
heredocClosed heredocState = 0
heredocOpening heredocState = 1
heredocOpened heredocState = 2
)
var ( var (
last rune // the last character that was written to the result last rune // the last character that was written to the result
@@ -47,6 +57,11 @@ func Format(input []byte) []byte {
quoted bool // whether we're in a quoted segment quoted bool // whether we're in a quoted segment
escaped bool // whether current char is escaped escaped bool // whether current char is escaped
heredoc heredocState // whether we're in a heredoc
heredocEscaped bool // whether heredoc is escaped
heredocMarker []rune
heredocClosingMarker []rune
nesting int // indentation level nesting int // indentation level
) )
@@ -75,6 +90,62 @@ func Format(input []byte) []byte {
panic(err) panic(err)
} }
// detect whether we have the start of a heredoc
if !quoted && !(heredoc != heredocClosed || heredocEscaped) &&
space && last == '<' && ch == '<' {
write(ch)
heredoc = heredocOpening
space = false
continue
}
if heredoc == heredocOpening {
if ch == '\n' {
if len(heredocMarker) > 0 && heredocMarkerRegexp.MatchString(string(heredocMarker)) {
heredoc = heredocOpened
} else {
heredocMarker = nil
heredoc = heredocClosed
nextLine()
continue
}
write(ch)
continue
}
if unicode.IsSpace(ch) {
// a space means it's just a regular token and not a heredoc
heredocMarker = nil
heredoc = heredocClosed
} else {
heredocMarker = append(heredocMarker, ch)
write(ch)
continue
}
}
// if we're in a heredoc, all characters are read&write as-is
if heredoc == heredocOpened {
heredocClosingMarker = append(heredocClosingMarker, ch)
if len(heredocClosingMarker) > len(heredocMarker)+1 { // We assert that the heredocClosingMarker is followed by a unicode.Space
heredocClosingMarker = heredocClosingMarker[1:]
}
// check if we're done
if unicode.IsSpace(ch) && slices.Equal(heredocClosingMarker[:len(heredocClosingMarker)-1], heredocMarker) {
heredocMarker = nil
heredocClosingMarker = nil
heredoc = heredocClosed
} else {
write(ch)
if ch == '\n' {
heredocClosingMarker = heredocClosingMarker[:0]
}
continue
}
}
if last == '<' && space {
space = false
}
if comment { if comment {
if ch == '\n' { if ch == '\n' {
comment = false comment = false
@@ -98,6 +169,9 @@ func Format(input []byte) []byte {
} }
if escaped { if escaped {
if ch == '<' {
heredocEscaped = true
}
write(ch) write(ch)
escaped = false escaped = false
continue continue
@@ -117,6 +191,7 @@ func Format(input []byte) []byte {
if unicode.IsSpace(ch) { if unicode.IsSpace(ch) {
space = true space = true
heredocEscaped = false
if ch == '\n' { if ch == '\n' {
newLines++ newLines++
} }
@@ -205,6 +280,11 @@ func Format(input []byte) []byte {
write('{') write('{')
openBraceWritten = true openBraceWritten = true
} }
if spacePrior && ch == '<' {
space = true
}
write(ch) write(ch)
beginningOfLine = false beginningOfLine = false
+70
View File
@@ -362,6 +362,76 @@ block {
block { block {
} }
`,
},
{
description: "keep heredoc as-is",
input: `block {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
}
`,
expect: `block {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
}
`,
},
{
description: "Mixing heredoc with regular part",
input: `block {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
respond "More than one space will be eaten" 200
}
block2 {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
respond "More than one space will be eaten" 200
}
`,
expect: `block {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
respond "More than one space will be eaten" 200
}
block2 {
heredoc <<HEREDOC
Here's more than one space Here's more than one space
HEREDOC
respond "More than one space will be eaten" 200
}
`,
},
{
description: "Heredoc as regular token",
input: `block {
heredoc <<HEREDOC "More than one space will be eaten"
}
`,
expect: `block {
heredoc <<HEREDOC "More than one space will be eaten"
}
`,
},
{
description: "Escape heredoc",
input: `block {
heredoc \<<HEREDOC
respond "More than one space will be eaten" 200
}
`,
expect: `block {
heredoc \<<HEREDOC
respond "More than one space will be eaten" 200
}
`, `,
}, },
} { } {
+160
View File
@@ -0,0 +1,160 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"regexp"
"strconv"
"strings"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
)
// parseVariadic determines if the token is a variadic placeholder,
// and if so, determines the index range (start/end) of args to use.
// Returns a boolean signaling whether a variadic placeholder was found,
// and the start and end indices.
func parseVariadic(token Token, argCount int) (bool, int, int) {
if !strings.HasPrefix(token.Text, "{args[") {
return false, 0, 0
}
if !strings.HasSuffix(token.Text, "]}") {
return false, 0, 0
}
argRange := strings.TrimSuffix(strings.TrimPrefix(token.Text, "{args["), "]}")
if argRange == "" {
caddy.Log().Named("caddyfile").Warn(
"Placeholder "+token.Text+" cannot have an empty index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
start, end, found := strings.Cut(argRange, ":")
// If no ":" delimiter is found, this is not a variadic.
// The replacer will pick this up.
if !found {
return false, 0, 0
}
// A valid token may contain several placeholders, and
// they may be separated by ":". It's not variadic.
// https://github.com/caddyserver/caddy/issues/5716
if strings.Contains(start, "}") || strings.Contains(end, "{") {
return false, 0, 0
}
var (
startIndex = 0
endIndex = argCount
err error
)
if start != "" {
startIndex, err = strconv.Atoi(start)
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" has an invalid start index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
}
if end != "" {
endIndex, err = strconv.Atoi(end)
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" has an invalid end index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
}
// bound check
if startIndex < 0 || startIndex > endIndex || endIndex > argCount {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" indices are out of bounds, only "+strconv.Itoa(argCount)+" argument(s) exist",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
return false, 0, 0
}
return true, startIndex, endIndex
}
// makeArgsReplacer prepares a Replacer which can replace
// non-variadic args placeholders in imported tokens.
func makeArgsReplacer(args []string) *caddy.Replacer {
repl := caddy.NewEmptyReplacer()
repl.Map(func(key string) (any, bool) {
// TODO: Remove the deprecated {args.*} placeholder
// support at some point in the future
if matches := argsRegexpIndexDeprecated.FindStringSubmatch(key); len(matches) > 0 {
// What's matched may be a substring of the key
if matches[0] != key {
return nil, false
}
value, err := strconv.Atoi(matches[1])
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args." + matches[1] + "} has an invalid index")
return nil, false
}
if value >= len(args) {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args." + matches[1] + "} index is out of bounds, only " + strconv.Itoa(len(args)) + " argument(s) exist")
return nil, false
}
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args." + matches[1] + "} deprecated, use {args[" + matches[1] + "]} instead")
return args[value], true
}
// Handle args[*] form
if matches := argsRegexpIndex.FindStringSubmatch(key); len(matches) > 0 {
// What's matched may be a substring of the key
if matches[0] != key {
return nil, false
}
if strings.Contains(matches[1], ":") {
caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder {args[" + matches[1] + "]} must be a token on its own")
return nil, false
}
value, err := strconv.Atoi(matches[1])
if err != nil {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args[" + matches[1] + "]} has an invalid index")
return nil, false
}
if value >= len(args) {
caddy.Log().Named("caddyfile").Warn(
"Placeholder {args[" + matches[1] + "]} index is out of bounds, only " + strconv.Itoa(len(args)) + " argument(s) exist")
return nil, false
}
return args[value], true
}
// Not an args placeholder, ignore
return nil, false
})
return repl
}
var (
argsRegexpIndexDeprecated = regexp.MustCompile(`args\.(.+)`)
argsRegexpIndex = regexp.MustCompile(`args\[(.+)]`)
)
+3
View File
@@ -34,6 +34,7 @@ func (i *importGraph) addNode(name string) {
} }
i.nodes[name] = true i.nodes[name] = true
} }
func (i *importGraph) addNodes(names []string) { func (i *importGraph) addNodes(names []string) {
for _, name := range names { for _, name := range names {
i.addNode(name) i.addNode(name)
@@ -43,6 +44,7 @@ func (i *importGraph) addNodes(names []string) {
func (i *importGraph) removeNode(name string) { func (i *importGraph) removeNode(name string) {
delete(i.nodes, name) delete(i.nodes, name)
} }
func (i *importGraph) removeNodes(names []string) { func (i *importGraph) removeNodes(names []string) {
for _, name := range names { for _, name := range names {
i.removeNode(name) i.removeNode(name)
@@ -73,6 +75,7 @@ func (i *importGraph) addEdge(from, to string) error {
i.edges[from] = append(i.edges[from], to) i.edges[from] = append(i.edges[from], to)
return nil return nil
} }
func (i *importGraph) addEdges(from string, tos []string) error { func (i *importGraph) addEdges(from string, tos []string) error {
for _, to := range tos { for _, to := range tos {
err := i.addEdge(from, to) err := i.addEdge(from, to)
+219 -32
View File
@@ -17,7 +17,10 @@ package caddyfile
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"fmt"
"io" "io"
"regexp"
"strings"
"unicode" "unicode"
) )
@@ -35,15 +38,41 @@ type (
// Token represents a single parsable unit. // Token represents a single parsable unit.
Token struct { Token struct {
File string File string
Line int imports []string
Text string Line int
wasQuoted rune // enclosing quote character, if any Text string
inSnippet bool wasQuoted rune // enclosing quote character, if any
snippetName string heredocMarker string
snippetName string
} }
) )
// Tokenize takes bytes as input and lexes it into
// a list of tokens that can be parsed as a Caddyfile.
// Also takes a filename to fill the token's File as
// the source of the tokens, which is important to
// determine relative paths for `import` directives.
func Tokenize(input []byte, filename string) ([]Token, error) {
l := lexer{}
if err := l.load(bytes.NewReader(input)); err != nil {
return nil, err
}
var tokens []Token
for {
found, err := l.next()
if err != nil {
return nil, err
}
if !found {
break
}
l.token.File = filename
tokens = append(tokens, l.token)
}
return tokens, nil
}
// load prepares the lexer to scan an input for tokens. // load prepares the lexer to scan an input for tokens.
// It discards any leading byte order mark. // It discards any leading byte order mark.
func (l *lexer) load(input io.Reader) error { func (l *lexer) load(input io.Reader) error {
@@ -75,28 +104,107 @@ func (l *lexer) load(input io.Reader) error {
// may be escaped. The rest of the line is skipped // may be escaped. The rest of the line is skipped
// if a "#" character is read in. Returns true if // if a "#" character is read in. Returns true if
// a token was loaded; false otherwise. // a token was loaded; false otherwise.
func (l *lexer) next() bool { func (l *lexer) next() (bool, error) {
var val []rune var val []rune
var comment, quoted, btQuoted, escaped bool var comment, quoted, btQuoted, inHeredoc, heredocEscaped, escaped bool
var heredocMarker string
makeToken := func(quoted rune) bool { makeToken := func(quoted rune) bool {
l.token.Text = string(val) l.token.Text = string(val)
l.token.wasQuoted = quoted l.token.wasQuoted = quoted
l.token.heredocMarker = heredocMarker
return true return true
} }
for { for {
// Read a character in; if err then if we had
// read some characters, make a token. If we
// reached EOF, then no more tokens to read.
// If no EOF, then we had a problem.
ch, _, err := l.reader.ReadRune() ch, _, err := l.reader.ReadRune()
if err != nil { if err != nil {
if len(val) > 0 { if len(val) > 0 {
return makeToken(0) if inHeredoc {
return false, fmt.Errorf("incomplete heredoc <<%s on line #%d, expected ending marker %s", heredocMarker, l.line+l.skippedLines, heredocMarker)
}
return makeToken(0), nil
} }
if err == io.EOF { if err == io.EOF {
return false return false, nil
} }
panic(err) return false, err
} }
// detect whether we have the start of a heredoc
if !(quoted || btQuoted) && !(inHeredoc || heredocEscaped) &&
len(val) > 1 && string(val[:2]) == "<<" {
// a space means it's just a regular token and not a heredoc
if ch == ' ' {
return makeToken(0), nil
}
// skip CR, we only care about LF
if ch == '\r' {
continue
}
// after hitting a newline, we know that the heredoc marker
// is the characters after the two << and the newline.
// we reset the val because the heredoc is syntax we don't
// want to keep.
if ch == '\n' {
if len(val) == 2 {
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line)
}
// check if there's too many <
if string(val[:3]) == "<<<" {
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
}
heredocMarker = string(val[2:])
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
}
inHeredoc = true
l.skippedLines++
val = nil
continue
}
val = append(val, ch)
continue
}
// if we're in a heredoc, all characters are read as-is
if inHeredoc {
val = append(val, ch)
if ch == '\n' {
l.skippedLines++
}
// check if we're done, i.e. that the last few characters are the marker
if len(val) >= len(heredocMarker) && heredocMarker == string(val[len(val)-len(heredocMarker):]) {
// set the final value
val, err = l.finalizeHeredoc(val, heredocMarker)
if err != nil {
return false, err
}
// set the line counter, and make the token
l.line += l.skippedLines
l.skippedLines = 0
return makeToken('<'), nil
}
// stay in the heredoc until we find the ending marker
continue
}
// track whether we found an escape '\' for the next
// iteration to be contextually aware
if !escaped && !btQuoted && ch == '\\' { if !escaped && !btQuoted && ch == '\\' {
escaped = true escaped = true
continue continue
@@ -111,26 +219,29 @@ func (l *lexer) next() bool {
} }
escaped = false escaped = false
} else { } else {
if quoted && ch == '"' { if (quoted && ch == '"') || (btQuoted && ch == '`') {
return makeToken('"') return makeToken(ch), nil
}
if btQuoted && ch == '`' {
return makeToken('`')
} }
} }
// allow quoted text to wrap continue on multiple lines
if ch == '\n' { if ch == '\n' {
l.line += 1 + l.skippedLines l.line += 1 + l.skippedLines
l.skippedLines = 0 l.skippedLines = 0
} }
// collect this character as part of the quoted token
val = append(val, ch) val = append(val, ch)
continue continue
} }
if unicode.IsSpace(ch) { if unicode.IsSpace(ch) {
// ignore CR altogether, we only actually care about LF (\n)
if ch == '\r' { if ch == '\r' {
continue continue
} }
// end of the line
if ch == '\n' { if ch == '\n' {
// newlines can be escaped to chain arguments
// onto multiple lines; else, increment the line count
if escaped { if escaped {
l.skippedLines++ l.skippedLines++
escaped = false escaped = false
@@ -138,14 +249,18 @@ func (l *lexer) next() bool {
l.line += 1 + l.skippedLines l.line += 1 + l.skippedLines
l.skippedLines = 0 l.skippedLines = 0
} }
// comments (#) are single-line only
comment = false comment = false
} }
// any kind of space means we're at the end of this token
if len(val) > 0 { if len(val) > 0 {
return makeToken(0) return makeToken(0), nil
} }
continue continue
} }
// comments must be at the start of a token,
// in other words, preceded by space or newline
if ch == '#' && len(val) == 0 { if ch == '#' && len(val) == 0 {
comment = true comment = true
} }
@@ -166,7 +281,12 @@ func (l *lexer) next() bool {
} }
if escaped { if escaped {
val = append(val, '\\') // allow escaping the first < to skip the heredoc syntax
if ch == '<' {
heredocEscaped = true
} else {
val = append(val, '\\')
}
escaped = false escaped = false
} }
@@ -174,24 +294,91 @@ func (l *lexer) next() bool {
} }
} }
// Tokenize takes bytes as input and lexes it into // finalizeHeredoc takes the runes read as the heredoc text and the marker,
// a list of tokens that can be parsed as a Caddyfile. // and processes the text to strip leading whitespace, returning the final
// Also takes a filename to fill the token's File as // value without the leading whitespace.
// the source of the tokens, which is important to func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) {
// determine relative paths for `import` directives. stringVal := string(val)
func Tokenize(input []byte, filename string) ([]Token, error) {
l := lexer{} // find the last newline of the heredoc, which is where the contents end
if err := l.load(bytes.NewReader(input)); err != nil { lastNewline := strings.LastIndex(stringVal, "\n")
return nil, err
// collapse the content, then split into separate lines
lines := strings.Split(stringVal[:lastNewline+1], "\n")
// figure out how much whitespace we need to strip from the front of every line
// by getting the string that precedes the marker, on the last line
paddingToStrip := stringVal[lastNewline+1 : len(stringVal)-len(marker)]
// iterate over each line and strip the whitespace from the front
var out string
for lineNum, lineText := range lines[:len(lines)-1] {
if lineText == "" || lineText == "\r" {
out += "\n"
continue
}
// find an exact match for the padding
index := strings.Index(lineText, paddingToStrip)
// if the padding doesn't match exactly at the start then we can't safely strip
if index != 0 {
return nil, fmt.Errorf("mismatched leading whitespace in heredoc <<%s on line #%d [%s], expected whitespace [%s] to match the closing marker", marker, l.line+lineNum+1, lineText, paddingToStrip)
}
// strip, then append the line, with the newline, to the output.
// also removes all "\r" because Windows.
out += strings.ReplaceAll(lineText[len(paddingToStrip):]+"\n", "\r", "")
} }
var tokens []Token
for l.next() { // Remove the trailing newline from the loop
l.token.File = filename if len(out) > 0 && out[len(out)-1] == '\n' {
tokens = append(tokens, l.token) out = out[:len(out)-1]
} }
return tokens, nil
// return the final value
return []rune(out), nil
} }
func (t Token) Quoted() bool { func (t Token) Quoted() bool {
return t.wasQuoted > 0 return t.wasQuoted > 0
} }
// NumLineBreaks counts how many line breaks are in the token text.
func (t Token) NumLineBreaks() int {
lineBreaks := strings.Count(t.Text, "\n")
if t.wasQuoted == '<' {
// heredocs have an extra linebreak because the opening
// delimiter is on its own line and is not included in the
// token Text itself, and the trailing newline is removed.
lineBreaks += 2
}
return lineBreaks
}
var heredocMarkerRegexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
// isNextOnNewLine tests whether t2 is on a different line from t1
func isNextOnNewLine(t1, t2 Token) bool {
// If the second token is from a different file,
// we can assume it's from a different line
if t1.File != t2.File {
return true
}
// If the second token is from a different import chain,
// we can assume it's from a different line
if len(t1.imports) != len(t2.imports) {
return true
}
for i, im := range t1.imports {
if im != t2.imports[i] {
return true
}
}
// If the first token (incl line breaks) ends
// on a line earlier than the next token,
// then the second token is on a new line
return t1.Line+t1.NumLineBreaks() < t2.Line
}
+271 -10
View File
@@ -18,13 +18,13 @@ import (
"testing" "testing"
) )
type lexerTestCase struct {
input []byte
expected []Token
}
func TestLexer(t *testing.T) { func TestLexer(t *testing.T) {
testCases := []lexerTestCase{ testCases := []struct {
input []byte
expected []Token
expectErr bool
errorMessage string
}{
{ {
input: []byte(`host:123`), input: []byte(`host:123`),
expected: []Token{ expected: []Token{
@@ -249,12 +249,273 @@ func TestLexer(t *testing.T) {
{Line: 1, Text: `quotes`}, {Line: 1, Text: `quotes`},
}, },
}, },
{
input: []byte(`heredoc <<EOF
content
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "content"},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<VERY-LONG-MARKER
content
VERY-LONG-MARKER same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "content"},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
extra-newline
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "extra-newline\n"},
{Line: 4, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
EOF
HERE same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: ``},
{Line: 3, Text: `HERE`},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: ""},
{Line: 2, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<EOF
content
EOF same-line-arg
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: "content"},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`prev-line
heredoc <<EOF
multi
line
content
EOF same-line-arg
next-line
`),
expected: []Token{
{Line: 1, Text: `prev-line`},
{Line: 2, Text: `heredoc`},
{Line: 2, Text: "\tmulti\n\tline\n\tcontent"},
{Line: 6, Text: `same-line-arg`},
{Line: 7, Text: `next-line`},
},
},
{
input: []byte(`escaped-heredoc \<< >>`),
expected: []Token{
{Line: 1, Text: `escaped-heredoc`},
{Line: 1, Text: `<<`},
{Line: 1, Text: `>>`},
},
},
{
input: []byte(`not-a-heredoc <EOF
content
`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<EOF`},
{Line: 2, Text: `content`},
},
},
{
input: []byte(`not-a-heredoc <<<EOF content`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<<EOF`},
{Line: 1, Text: `content`},
},
},
{
input: []byte(`not-a-heredoc "<<" ">>"`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<`},
{Line: 1, Text: `>>`},
},
},
{
input: []byte(`not-a-heredoc << >>`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<`},
{Line: 1, Text: `>>`},
},
},
{
input: []byte(`not-a-heredoc <<HERE SAME LINE
content
HERE same-line-arg
`),
expected: []Token{
{Line: 1, Text: `not-a-heredoc`},
{Line: 1, Text: `<<HERE`},
{Line: 1, Text: `SAME`},
{Line: 1, Text: `LINE`},
{Line: 2, Text: `content`},
{Line: 3, Text: `HERE`},
{Line: 3, Text: `same-line-arg`},
},
},
{
input: []byte(`heredoc <<s
s
`),
expected: []Token{
{Line: 1, Text: `heredoc`},
{Line: 1, Text: ""},
},
},
{
input: []byte("\u000Aheredoc \u003C\u003C\u0073\u0073\u000A\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F\u000A\u0073\u0073\u000A\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F\u000A\u00BF\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F"),
expected: []Token{
{
Line: 2,
Text: "heredoc",
},
{
Line: 2,
Text: "\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
},
{
Line: 5,
Text: "\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
},
{
Line: 6,
Text: "\u00BF\u00BF\u0057\u0001\u0000\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u00FF\u003D\u001F",
},
},
},
{
input: []byte("not-a-heredoc <<\n"),
expectErr: true,
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string",
},
{
input: []byte(`heredoc <<<EOF
content
EOF same-line-arg
`),
expectErr: true,
errorMessage: "too many '<' for heredoc on line #1; only use two, for example <<END",
},
{
input: []byte(`heredoc <<EOF
content
`),
expectErr: true,
errorMessage: "incomplete heredoc <<EOF on line #3, expected ending marker EOF",
},
{
input: []byte(`heredoc <<EOF
content
EOF
`),
expectErr: true,
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #2 [\tcontent], expected whitespace [\t\t] to match the closing marker",
},
{
input: []byte(`heredoc <<EOF
content
EOF
`),
expectErr: true,
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #2 [ content], expected whitespace [\t\t] to match the closing marker",
},
{
input: []byte(`heredoc <<EOF
The next line is a blank line
The previous line is a blank line
EOF`),
expected: []Token{
{Line: 1, Text: "heredoc"},
{Line: 1, Text: "The next line is a blank line\n\nThe previous line is a blank line"},
},
},
{
input: []byte(`heredoc <<EOF
One tab indented heredoc with blank next line
One tab indented heredoc with blank previous line
EOF`),
expected: []Token{
{Line: 1, Text: "heredoc"},
{Line: 1, Text: "One tab indented heredoc with blank next line\n\nOne tab indented heredoc with blank previous line"},
},
},
{
input: []byte(`heredoc <<EOF
The next line is a blank line with one tab
The previous line is a blank line with one tab
EOF`),
expected: []Token{
{Line: 1, Text: "heredoc"},
{Line: 1, Text: "The next line is a blank line with one tab\n\t\nThe previous line is a blank line with one tab"},
},
},
{
input: []byte(`heredoc <<EOF
The next line is a blank line with one tab less than the correct indentation
The previous line is a blank line with one tab less than the correct indentation
EOF`),
expectErr: true,
errorMessage: "mismatched leading whitespace in heredoc <<EOF on line #3 [\t], expected whitespace [\t\t] to match the closing marker",
},
} }
for i, testCase := range testCases { for i, testCase := range testCases {
actual, err := Tokenize(testCase.input, "") actual, err := Tokenize(testCase.input, "")
if testCase.expectErr {
if err == nil {
t.Fatalf("expected error, got actual: %v", actual)
continue
}
if err.Error() != testCase.errorMessage {
t.Fatalf("expected error '%v', got: %v", testCase.errorMessage, err)
}
continue
}
if err != nil { if err != nil {
t.Errorf("%v", err) t.Fatalf("%v", err)
} }
lexerCompare(t, i, testCase.expected, actual) lexerCompare(t, i, testCase.expected, actual)
} }
@@ -262,17 +523,17 @@ func TestLexer(t *testing.T) {
func lexerCompare(t *testing.T, n int, expected, actual []Token) { func lexerCompare(t *testing.T, n int, expected, actual []Token) {
if len(expected) != len(actual) { if len(expected) != len(actual) {
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual)) t.Fatalf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
} }
for i := 0; i < len(actual) && i < len(expected); i++ { for i := 0; i < len(actual) && i < len(expected); i++ {
if actual[i].Line != expected[i].Line { if actual[i].Line != expected[i].Line {
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d", t.Fatalf("Test case %d token %d ('%s'): expected line %d but was line %d",
n, i, expected[i].Text, expected[i].Line, actual[i].Line) n, i, expected[i].Text, expected[i].Line, actual[i].Line)
break break
} }
if actual[i].Text != expected[i].Text { if actual[i].Text != expected[i].Text {
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'", t.Fatalf("Test case %d token %d: expected text '%s' but was '%s'",
n, i, expected[i].Text, actual[i].Text) n, i, expected[i].Text, actual[i].Text)
break break
} }
+165 -55
View File
@@ -20,11 +20,11 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
) )
// Parse parses the input just enough to group tokens, in // Parse parses the input just enough to group tokens, in
@@ -61,20 +61,12 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) {
// It returns all the tokens from the input, unstructured // It returns all the tokens from the input, unstructured
// and in order. It may mutate input as it expands env vars. // and in order. It may mutate input as it expands env vars.
func allTokens(filename string, input []byte) ([]Token, error) { func allTokens(filename string, input []byte) ([]Token, error) {
inputCopy, err := replaceEnvVars(input) return Tokenize(replaceEnvVars(input), filename)
if err != nil {
return nil, err
}
tokens, err := Tokenize(inputCopy, filename)
if err != nil {
return nil, err
}
return tokens, nil
} }
// replaceEnvVars replaces all occurrences of environment variables. // replaceEnvVars replaces all occurrences of environment variables.
// It mutates the underlying array and returns the updated slice. // It mutates the underlying array and returns the updated slice.
func replaceEnvVars(input []byte) ([]byte, error) { func replaceEnvVars(input []byte) []byte {
var offset int var offset int
for { for {
begin := bytes.Index(input[offset:], spanOpen) begin := bytes.Index(input[offset:], spanOpen)
@@ -115,7 +107,7 @@ func replaceEnvVars(input []byte) ([]byte, error) {
// continue at the end of the replacement // continue at the end of the replacement
offset = begin + len(envVarBytes) offset = begin + len(envVarBytes)
} }
return input, nil return input
} }
type parser struct { type parser struct {
@@ -157,7 +149,6 @@ func (p *parser) begin() error {
} }
err := p.addresses() err := p.addresses()
if err != nil { if err != nil {
return err return err
} }
@@ -168,6 +159,25 @@ func (p *parser) begin() error {
return nil return nil
} }
if ok, name := p.isNamedRoute(); ok {
// we just need a dummy leading token to ease parsing later
nameToken := p.Token()
nameToken.Text = name
// named routes only have one key, the route name
p.block.Keys = []Token{nameToken}
p.block.IsNamedRoute = true
// get all the tokens from the block, including the braces
tokens, err := p.blockTokens(true)
if err != nil {
return err
}
tokens = append([]Token{nameToken}, tokens...)
p.block.Segments = []Segment{tokens}
return nil
}
if ok, name := p.isSnippet(); ok { if ok, name := p.isSnippet(); ok {
if p.definedSnippets == nil { if p.definedSnippets == nil {
p.definedSnippets = map[string][]Token{} p.definedSnippets = map[string][]Token{}
@@ -176,16 +186,15 @@ func (p *parser) begin() error {
return p.Errf("redeclaration of previously declared snippet %s", name) return p.Errf("redeclaration of previously declared snippet %s", name)
} }
// consume all tokens til matched close brace // consume all tokens til matched close brace
tokens, err := p.snippetTokens() tokens, err := p.blockTokens(false)
if err != nil { if err != nil {
return err return err
} }
// Just as we need to track which file the token comes from, we need to // Just as we need to track which file the token comes from, we need to
// keep track of which snippets do the tokens come from. This is helpful // keep track of which snippet the token comes from. This is helpful
// in tracking import cycles across files/snippets by namespacing them. Without // in tracking import cycles across files/snippets by namespacing them.
// this we end up with false-positives in cycle-detection. // Without this, we end up with false-positives in cycle-detection.
for k, v := range tokens { for k, v := range tokens {
v.inSnippet = true
v.snippetName = name v.snippetName = name
tokens[k] = v tokens[k] = v
} }
@@ -202,11 +211,12 @@ func (p *parser) addresses() error {
var expectingAnother bool var expectingAnother bool
for { for {
tkn := p.Val() value := p.Val()
token := p.Token()
// special case: import directive replaces tokens during parse-time // special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() { if value == "import" && p.isNewLine() {
err := p.doImport() err := p.doImport(0)
if err != nil { if err != nil {
return err return err
} }
@@ -214,9 +224,9 @@ func (p *parser) addresses() error {
} }
// Open brace definitely indicates end of addresses // Open brace definitely indicates end of addresses
if tkn == "{" { if value == "{" {
if expectingAnother { if expectingAnother {
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn) return p.Errf("Expected another address but had '%s' - check for extra comma", value)
} }
// Mark this server block as being defined with braces. // Mark this server block as being defined with braces.
// This is used to provide a better error message when // This is used to provide a better error message when
@@ -228,15 +238,15 @@ func (p *parser) addresses() error {
} }
// Users commonly forget to place a space between the address and the '{' // Users commonly forget to place a space between the address and the '{'
if strings.HasSuffix(tkn, "{") { if strings.HasSuffix(value, "{") {
return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", tkn) return p.Errf("Site addresses cannot end with a curly brace: '%s' - put a space between the token and the brace", value)
} }
if tkn != "" { // empty token possible if user typed "" if value != "" { // empty token possible if user typed ""
// Trailing comma indicates another address will follow, which // Trailing comma indicates another address will follow, which
// may possibly be on the next line // may possibly be on the next line
if tkn[len(tkn)-1] == ',' { if value[len(value)-1] == ',' {
tkn = tkn[:len(tkn)-1] value = value[:len(value)-1]
expectingAnother = true expectingAnother = true
} else { } else {
expectingAnother = false // but we may still see another one on this line expectingAnother = false // but we may still see another one on this line
@@ -245,11 +255,12 @@ func (p *parser) addresses() error {
// If there's a comma here, it's probably because they didn't use a space // If there's a comma here, it's probably because they didn't use a space
// between their two domains, e.g. "foo.com,bar.com", which would not be // between their two domains, e.g. "foo.com,bar.com", which would not be
// parsed as two separate site addresses. // parsed as two separate site addresses.
if strings.Contains(tkn, ",") { if strings.Contains(value, ",") {
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", tkn) return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", value)
} }
p.block.Keys = append(p.block.Keys, tkn) token.Text = value
p.block.Keys = append(p.block.Keys, token)
} }
// Advance token and possibly break out of loop or return error // Advance token and possibly break out of loop or return error
@@ -306,7 +317,7 @@ func (p *parser) directives() error {
// special case: import directive replaces tokens during parse-time // special case: import directive replaces tokens during parse-time
if p.Val() == "import" { if p.Val() == "import" {
err := p.doImport() err := p.doImport(1)
if err != nil { if err != nil {
return err return err
} }
@@ -332,7 +343,7 @@ func (p *parser) directives() error {
// is on the token before where the import directive was. In // is on the token before where the import directive was. In
// other words, call Next() to access the first token that was // other words, call Next() to access the first token that was
// imported. // imported.
func (p *parser) doImport() error { func (p *parser) doImport(nesting int) error {
// syntax checks // syntax checks
if !p.NextArg() { if !p.NextArg() {
return p.ArgErr() return p.ArgErr()
@@ -345,11 +356,8 @@ func (p *parser) doImport() error {
// grab remaining args as placeholder replacements // grab remaining args as placeholder replacements
args := p.RemainingArgs() args := p.RemainingArgs()
// add args to the replacer // set up a replacer for non-variadic args replacement
repl := caddy.NewEmptyReplacer() repl := makeArgsReplacer(args)
for index, arg := range args {
repl.Set("args."+strconv.Itoa(index), arg)
}
// splice out the import directive and its arguments // splice out the import directive and its arguments
// (2 tokens, plus the length of args) // (2 tokens, plus the length of args)
@@ -397,6 +405,20 @@ func (p *parser) doImport() error {
} else { } else {
return p.Errf("File to import not found: %s", importPattern) return p.Errf("File to import not found: %s", importPattern)
} }
} else {
// See issue #5295 - should skip any files that start with a . when iterating over them.
sep := string(filepath.Separator)
segGlobPattern := strings.Split(globPattern, sep)
if strings.HasPrefix(segGlobPattern[len(segGlobPattern)-1], "*") {
var tmpMatches []string
for _, m := range matches {
seg := strings.Split(m, sep)
if !strings.HasPrefix(seg[len(seg)-1], ".") {
tmpMatches = append(tmpMatches, m)
}
}
matches = tmpMatches
}
} }
// collect all the imported tokens // collect all the imported tokens
@@ -411,7 +433,7 @@ func (p *parser) doImport() error {
} }
nodeName := p.File() nodeName := p.File()
if p.Token().inSnippet { if p.Token().snippetName != "" {
nodeName += fmt.Sprintf(":%s", p.Token().snippetName) nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
} }
p.importGraph.addNode(nodeName) p.importGraph.addNode(nodeName)
@@ -422,13 +444,69 @@ func (p *parser) doImport() error {
} }
// copy the tokens so we don't overwrite p.definedSnippets // copy the tokens so we don't overwrite p.definedSnippets
tokensCopy := make([]Token, len(importedTokens)) tokensCopy := make([]Token, 0, len(importedTokens))
copy(tokensCopy, importedTokens)
var (
maybeSnippet bool
maybeSnippetId bool
index int
)
// run the argument replacer on the tokens // run the argument replacer on the tokens
for index, token := range tokensCopy { // golang for range slice return a copy of value
token.Text = repl.ReplaceKnown(token.Text, "") // similarly, append also copy value
tokensCopy[index] = token for i, token := range importedTokens {
// update the token's imports to refer to import directive filename, line number and snippet name if there is one
if token.snippetName != "" {
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import %s)", p.File(), p.Line(), token.snippetName))
} else {
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import)", p.File(), p.Line()))
}
// naive way of determine snippets, as snippets definition can only follow name + block
// format, won't check for nesting correctness or any other error, that's what parser does.
if !maybeSnippet && nesting == 0 {
// first of the line
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
index = 0
} else {
index++
}
if index == 0 && len(token.Text) >= 3 && strings.HasPrefix(token.Text, "(") && strings.HasSuffix(token.Text, ")") {
maybeSnippetId = true
}
}
switch token.Text {
case "{":
nesting++
if index == 1 && maybeSnippetId && nesting == 1 {
maybeSnippet = true
maybeSnippetId = false
}
case "}":
nesting--
if nesting == 0 && maybeSnippet {
maybeSnippet = false
}
}
if maybeSnippet {
tokensCopy = append(tokensCopy, token)
continue
}
foundVariadic, startIndex, endIndex := parseVariadic(token, len(args))
if foundVariadic {
for _, arg := range args[startIndex:endIndex] {
token.Text = arg
tokensCopy = append(tokensCopy, token)
}
} else {
token.Text = repl.ReplaceKnown(token.Text, "")
tokensCopy = append(tokensCopy, token)
}
} }
// splice the imported tokens in the place of the import statement // splice the imported tokens in the place of the import statement
@@ -459,6 +537,12 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
return nil, p.Errf("Could not read imported file %s: %v", importFile, err) return nil, p.Errf("Could not read imported file %s: %v", importFile, err)
} }
// only warning in case of empty files
if len(input) == 0 || len(strings.TrimSpace(string(input))) == 0 {
caddy.Log().Warn("Import file is empty", zap.String("file", importFile))
return []Token{}, nil
}
importedTokens, err := allTokens(importFile, input) importedTokens, err := allTokens(importFile, input)
if err != nil { if err != nil {
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err) return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
@@ -484,7 +568,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
// are loaded into the current server block for later use // are loaded into the current server block for later use
// by directive setup functions. // by directive setup functions.
func (p *parser) directive() error { func (p *parser) directive() error {
// a segment is a list of tokens associated with this directive // a segment is a list of tokens associated with this directive
var segment Segment var segment Segment
@@ -497,6 +580,9 @@ func (p *parser) directive() error {
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 { if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected next token after '{' on same line") return p.Err("Unexpected next token after '{' on same line")
} }
if p.isNewLine() {
return p.Err("Unexpected '{' on a new line; did you mean to place the '{' on the previous line?")
}
} else if p.Val() == "{}" { } else if p.Val() == "{}" {
if p.isNextOnNewLine() && p.Token().wasQuoted == 0 { if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected '{}' at end of line") return p.Err("Unexpected '{}' at end of line")
@@ -509,7 +595,7 @@ func (p *parser) directive() error {
} else if p.Val() == "}" && p.nesting == 0 { } else if p.Val() == "}" && p.nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace") return p.Err("Unexpected '}' because no matching opening brace")
} else if p.Val() == "import" && p.isNewLine() { } else if p.Val() == "import" && p.isNewLine() {
if err := p.doImport(); err != nil { if err := p.doImport(1); err != nil {
return err return err
} }
p.cursor-- // cursor is advanced when we continue, so roll back one more p.cursor-- // cursor is advanced when we continue, so roll back one more
@@ -550,28 +636,43 @@ func (p *parser) closeCurlyBrace() error {
return nil return nil
} }
func (p *parser) isNamedRoute() (bool, string) {
keys := p.block.Keys
// A named route block is a single key with parens, prefixed with &.
if len(keys) == 1 && strings.HasPrefix(keys[0].Text, "&(") && strings.HasSuffix(keys[0].Text, ")") {
return true, strings.TrimSuffix(keys[0].Text[2:], ")")
}
return false, ""
}
func (p *parser) isSnippet() (bool, string) { func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies. // A snippet block is a single key with parens. Nothing else qualifies.
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") { if len(keys) == 1 && strings.HasPrefix(keys[0].Text, "(") && strings.HasSuffix(keys[0].Text, ")") {
return true, strings.TrimSuffix(keys[0][1:], ")") return true, strings.TrimSuffix(keys[0].Text[1:], ")")
} }
return false, "" return false, ""
} }
// read and store everything in a block for later replay. // read and store everything in a block for later replay.
func (p *parser) snippetTokens() ([]Token, error) { func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) {
// snippet must have curlies. // block must have curlies.
err := p.openCurlyBrace() err := p.openCurlyBrace()
if err != nil { if err != nil {
return nil, err return nil, err
} }
nesting := 1 // count our own nesting in snippets nesting := 1 // count our own nesting
tokens := []Token{} tokens := []Token{}
if retainCurlies {
tokens = append(tokens, p.Token())
}
for p.Next() { for p.Next() {
if p.Val() == "}" { if p.Val() == "}" {
nesting-- nesting--
if nesting == 0 { if nesting == 0 {
if retainCurlies {
tokens = append(tokens, p.Token())
}
break break
} }
} }
@@ -591,9 +692,18 @@ func (p *parser) snippetTokens() ([]Token, error) {
// head of the server block with tokens, which are // head of the server block with tokens, which are
// grouped by segments. // grouped by segments.
type ServerBlock struct { type ServerBlock struct {
HasBraces bool HasBraces bool
Keys []string Keys []Token
Segments []Segment Segments []Segment
IsNamedRoute bool
}
func (sb ServerBlock) GetKeysText() []string {
res := []string{}
for _, k := range sb.Keys {
res = append(res, k.Text)
}
return res
} }
// DispenseDirective returns a dispenser that contains // DispenseDirective returns a dispenser that contains
+164 -24
View File
@@ -21,11 +21,96 @@ import (
"testing" "testing"
) )
func TestParseVariadic(t *testing.T) {
args := make([]string, 10)
for i, tc := range []struct {
input string
result bool
}{
{
input: "",
result: false,
},
{
input: "{args[1",
result: false,
},
{
input: "1]}",
result: false,
},
{
input: "{args[:]}aaaaa",
result: false,
},
{
input: "aaaaa{args[:]}",
result: false,
},
{
input: "{args.}",
result: false,
},
{
input: "{args.1}",
result: false,
},
{
input: "{args[]}",
result: false,
},
{
input: "{args[:]}",
result: true,
},
{
input: "{args[:]}",
result: true,
},
{
input: "{args[0:]}",
result: true,
},
{
input: "{args[:0]}",
result: true,
},
{
input: "{args[-1:]}",
result: false,
},
{
input: "{args[:11]}",
result: false,
},
{
input: "{args[10:0]}",
result: false,
},
{
input: "{args[0:10]}",
result: true,
},
{
input: "{args[0]}:{args[1]}:{args[2]}",
result: false,
},
} {
token := Token{
File: "test",
Line: 1,
Text: tc.input,
}
if v, _, _ := parseVariadic(token, len(args)); v != tc.result {
t.Errorf("Test %d error expectation failed Expected: %t, got %t", i, tc.result, v)
}
}
}
func TestAllTokens(t *testing.T) { func TestAllTokens(t *testing.T) {
input := []byte("a b c\nd e") input := []byte("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"} expected := []string{"a", "b", "c", "d", "e"}
tokens, err := allTokens("TestAllTokens", input) tokens, err := allTokens("TestAllTokens", input)
if err != nil { if err != nil {
t.Fatalf("Expected no error, got %v", err) t.Fatalf("Expected no error, got %v", err)
} }
@@ -63,10 +148,11 @@ func TestParseOneAndImport(t *testing.T) {
"localhost", "localhost",
}, []int{1}}, }, []int{1}},
{`localhost:1234 {
`localhost:1234
dir1 foo bar`, false, []string{ dir1 foo bar`, false, []string{
"localhost:1234", "localhost:1234",
}, []int{3}, }, []int{3},
}, },
{`localhost { {`localhost {
@@ -187,6 +273,23 @@ func TestParseOneAndImport(t *testing.T) {
{`import testdata/not_found.txt`, true, []string{}, []int{}}, {`import testdata/not_found.txt`, true, []string{}, []int{}},
// empty file should just log a warning, and result in no tokens
{`import testdata/empty.txt`, false, []string{}, []int{}},
{`import testdata/only_white_space.txt`, false, []string{}, []int{}},
// import path/to/dir/* should skip any files that start with a . when iterating over them.
{`localhost
dir1 arg1
import testdata/glob/*`, false, []string{
"localhost",
}, []int{2, 3, 1}},
// import path/to/dir/.* should continue to read all dotfiles in a dir.
{`import testdata/glob/.*`, false, []string{
"host1",
}, []int{1, 2}},
{`""`, false, []string{}, []int{}}, {`""`, false, []string{}, []int{}},
{``, false, []string{}, []int{}}, {``, false, []string{}, []int{}},
@@ -194,6 +297,14 @@ func TestParseOneAndImport(t *testing.T) {
// Unexpected next token after '{' on same line // Unexpected next token after '{' on same line
{`localhost {`localhost
dir1 { a b }`, true, []string{"localhost"}, []int{}}, dir1 { a b }`, true, []string{"localhost"}, []int{}},
// Unexpected '{' on a new line
{`localhost
dir1
{
a b
}`, true, []string{"localhost"}, []int{}},
// Workaround with quotes // Workaround with quotes
{`localhost {`localhost
dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}}, dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
@@ -236,7 +347,7 @@ func TestParseOneAndImport(t *testing.T) {
i, len(test.keys), len(result.Keys)) i, len(test.keys), len(result.Keys))
continue continue
} }
for j, addr := range result.Keys { for j, addr := range result.GetKeysText() {
if addr != test.keys[j] { if addr != test.keys[j] {
t.Errorf("Test %d, key %d: Expected '%s', but was '%s'", t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
i, j, test.keys[j], addr) i, j, test.keys[j], addr)
@@ -268,8 +379,9 @@ func TestRecursiveImport(t *testing.T) {
} }
isExpected := func(got ServerBlock) bool { isExpected := func(got ServerBlock) bool {
if len(got.Keys) != 1 || got.Keys[0] != "localhost" { textKeys := got.GetKeysText()
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys) if len(textKeys) != 1 || textKeys[0] != "localhost" {
t.Errorf("got keys unexpected: expect localhost, got %v", textKeys)
return false return false
} }
if len(got.Segments) != 2 { if len(got.Segments) != 2 {
@@ -296,13 +408,13 @@ func TestRecursiveImport(t *testing.T) {
err = os.WriteFile(recursiveFile1, []byte( err = os.WriteFile(recursiveFile1, []byte(
`localhost `localhost
dir1 dir1
import recursive_import_test2`), 0644) import recursive_import_test2`), 0o644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.Remove(recursiveFile1) defer os.Remove(recursiveFile1)
err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0644) err = os.WriteFile(recursiveFile2, []byte("dir2 1"), 0o644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -330,7 +442,7 @@ func TestRecursiveImport(t *testing.T) {
err = os.WriteFile(recursiveFile1, []byte( err = os.WriteFile(recursiveFile1, []byte(
`localhost `localhost
dir1 dir1
import `+recursiveFile2), 0644) import `+recursiveFile2), 0o644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -363,8 +475,9 @@ func TestDirectiveImport(t *testing.T) {
} }
isExpected := func(got ServerBlock) bool { isExpected := func(got ServerBlock) bool {
if len(got.Keys) != 1 || got.Keys[0] != "localhost" { textKeys := got.GetKeysText()
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys) if len(textKeys) != 1 || textKeys[0] != "localhost" {
t.Errorf("got keys unexpected: expect localhost, got %v", textKeys)
return false return false
} }
if len(got.Segments) != 2 { if len(got.Segments) != 2 {
@@ -384,7 +497,7 @@ func TestDirectiveImport(t *testing.T) {
} }
err = os.WriteFile(directiveFile, []byte(`prop1 1 err = os.WriteFile(directiveFile, []byte(`prop1 1
prop2 2`), 0644) prop2 2`), 0o644)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -505,7 +618,7 @@ func TestParseAll(t *testing.T) {
i, len(test.keys[j]), j, len(block.Keys)) i, len(test.keys[j]), j, len(block.Keys))
continue continue
} }
for k, addr := range block.Keys { for k, addr := range block.GetKeysText() {
if addr != test.keys[j][k] { if addr != test.keys[j][k] {
t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'", t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
i, j, k, test.keys[j][k], addr) i, j, k, test.keys[j][k], addr)
@@ -604,16 +717,43 @@ func TestEnvironmentReplacement(t *testing.T) {
expect: "}{$", expect: "}{$",
}, },
} { } {
actual, err := replaceEnvVars([]byte(test.input)) actual := replaceEnvVars([]byte(test.input))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(actual, []byte(test.expect)) { if !bytes.Equal(actual, []byte(test.expect)) {
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual) t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
} }
} }
} }
func TestImportReplacementInJSONWithBrace(t *testing.T) {
for i, test := range []struct {
args []string
input string
expect string
}{
{
args: []string{"123"},
input: "{args[0]}",
expect: "123",
},
{
args: []string{"123"},
input: `{"key":"{args[0]}"}`,
expect: `{"key":"123"}`,
},
{
args: []string{"123", "123"},
input: `{"key":[{args[0]},{args[1]}]}`,
expect: `{"key":[123,123]}`,
},
} {
repl := makeArgsReplacer(test.args)
actual := repl.ReplaceKnown(test.input, "")
if actual != test.expect {
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
}
}
}
func TestSnippets(t *testing.T) { func TestSnippets(t *testing.T) {
p := testParser(` p := testParser(`
(common) { (common) {
@@ -631,7 +771,7 @@ func TestSnippets(t *testing.T) {
if len(blocks) != 1 { if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks)) t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
} }
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual { if actual, expected := blocks[0].GetKeysText()[0], "http://example.com"; expected != actual {
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual) t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
} }
if len(blocks[0].Segments) != 2 { if len(blocks[0].Segments) != 2 {
@@ -663,7 +803,7 @@ func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
fileName := writeStringToTempFileOrDie(t, ` fileName := writeStringToTempFileOrDie(t, `
http://example.com { http://example.com {
# This isn't an import directive, it's just an arg with value 'import' # This isn't an import directive, it's just an arg with value 'import'
basicauth / import password basic_auth / import password
} }
`) `)
// Parse the root file that imports the other one. // Parse the root file that imports the other one.
@@ -674,12 +814,12 @@ func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
} }
auth := blocks[0].Segments[0] auth := blocks[0].Segments[0]
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
if line != "basicauth / import password" { if line != "basic_auth / import password" {
// Previously, it would be changed to: // Previously, it would be changed to:
// basicauth / import /path/to/test/dir/password // basic_auth / import /path/to/test/dir/password
// referencing a file that (probably) doesn't exist and changing the // referencing a file that (probably) doesn't exist and changing the
// password! // password!
t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line) t.Errorf("Expected basic_auth tokens to be 'basic_auth / import password' but got %#q", line)
} }
} }
@@ -706,7 +846,7 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) {
if len(blocks) != 1 { if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks)) t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
} }
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual { if actual, expected := blocks[0].GetKeysText()[0], "http://example.com"; expected != actual {
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual) t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
} }
if len(blocks[0].Segments) != 1 { if len(blocks[0].Segments) != 1 {
View File
+4
View File
@@ -0,0 +1,4 @@
host1 {
dir1
dir2 arg1
}
+2
View File
@@ -0,0 +1,2 @@
dir2 arg1 arg2
dir3
+1 -1
View File
@@ -1 +1 @@
{args.0} {args[0]}
+1 -1
View File
@@ -1 +1 @@
{args.0} {args.1} {args[0]} {args[1]}
+7
View File
@@ -0,0 +1,7 @@
 
+22 -8
View File
@@ -24,10 +24,11 @@ import (
"strings" "strings"
"unicode" "unicode"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/certmagic"
) )
// mapAddressToServerBlocks returns a map of listener address to list of server // mapAddressToServerBlocks returns a map of listener address to list of server
@@ -77,7 +78,8 @@ import (
// multiple addresses to the same lists of server blocks (a many:many mapping). // multiple addresses to the same lists of server blocks (a many:many mapping).
// (Doing this is essentially a map-reduce technique.) // (Doing this is essentially a map-reduce technique.)
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock, func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
options map[string]any) (map[string][]serverBlock, error) { options map[string]any,
) (map[string][]serverBlock, error) {
sbmap := make(map[string][]serverBlock) sbmap := make(map[string][]serverBlock)
for i, sblock := range originalServerBlocks { for i, sblock := range originalServerBlocks {
@@ -86,15 +88,15 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
// will be served by them; this has the effect of treating each // will be served by them; this has the effect of treating each
// key of a server block as its own, but without having to repeat its // key of a server block as its own, but without having to repeat its
// contents in cases where multiple keys really can be served together // contents in cases where multiple keys really can be served together
addrToKeys := make(map[string][]string) addrToKeys := make(map[string][]caddyfile.Token)
for j, key := range sblock.block.Keys { for j, key := range sblock.block.Keys {
// a key can have multiple listener addresses if there are multiple // a key can have multiple listener addresses if there are multiple
// arguments to the 'bind' directive (although they will all have // arguments to the 'bind' directive (although they will all have
// the same port, since the port is defined by the key or is implicit // the same port, since the port is defined by the key or is implicit
// through automatic HTTPS) // through automatic HTTPS)
addrs, err := st.listenerAddrsForServerBlockKey(sblock, key, options) addrs, err := st.listenerAddrsForServerBlockKey(sblock, key.Text, options)
if err != nil { if err != nil {
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key, err) return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key.Text, err)
} }
// associate this key with each listener address it is served on // associate this key with each listener address it is served on
@@ -120,9 +122,9 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
// parse keys so that we only have to do it once // parse keys so that we only have to do it once
parsedKeys := make([]Address, 0, len(keys)) parsedKeys := make([]Address, 0, len(keys))
for _, key := range keys { for _, key := range keys {
addr, err := ParseAddress(key) addr, err := ParseAddress(key.Text)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing key '%s': %v", key, err) return nil, fmt.Errorf("parsing key '%s': %v", key.Text, err)
} }
parsedKeys = append(parsedKeys, addr.Normalize()) parsedKeys = append(parsedKeys, addr.Normalize())
} }
@@ -187,13 +189,25 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
// listenerAddrsForServerBlockKey essentially converts the Caddyfile // listenerAddrsForServerBlockKey essentially converts the Caddyfile
// site addresses to Caddy listener addresses for each server block. // site addresses to Caddy listener addresses for each server block.
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string, func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
options map[string]any) ([]string, error) { options map[string]any,
) ([]string, error) {
addr, err := ParseAddress(key) addr, err := ParseAddress(key)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing key: %v", err) return nil, fmt.Errorf("parsing key: %v", err)
} }
addr = addr.Normalize() addr = addr.Normalize()
switch addr.Scheme {
case "wss":
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
case "ws":
return nil, fmt.Errorf("the scheme ws:// is only supported in browsers; use http:// instead")
case "https", "http", "":
// Do nothing or handle the valid schemes
default:
return nil, fmt.Errorf("unsupported URL scheme %s://", addr.Scheme)
}
// figure out the HTTP and HTTPS ports; either // figure out the HTTP and HTTPS ports; either
// use defaults, or override with user config // use defaults, or override with user config
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort) httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
File diff suppressed because it is too large Load Diff
+140 -2
View File
@@ -1,6 +1,7 @@
package httpcaddyfile package httpcaddyfile
import ( import (
"strings"
"testing" "testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -51,12 +52,13 @@ func TestLogDirectiveSyntax(t *testing.T) {
}, },
{ {
input: `:8080 { input: `:8080 {
log invalid { log name-override {
output file foo.log output file foo.log
} }
} }
`, `,
expectError: true, output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
expectError: false,
}, },
} { } {
@@ -213,3 +215,139 @@ func TestRedirDirectiveSyntax(t *testing.T) {
} }
} }
} }
func TestImportErrorLine(t *testing.T) {
for i, tc := range []struct {
input string
errorFunc func(err error) bool
}{
{
input: `(t1) {
abort {args[:]}
}
:8080 {
import t1
import t1 true
}`,
errorFunc: func(err error) bool {
return err != nil && strings.Contains(err.Error(), "Caddyfile:6 (import t1)")
},
},
{
input: `(t1) {
abort {args[:]}
}
:8080 {
import t1 true
}`,
errorFunc: func(err error) bool {
return err != nil && strings.Contains(err.Error(), "Caddyfile:5 (import t1)")
},
},
{
input: `
import testdata/import_variadic_snippet.txt
:8080 {
import t1 true
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `
import testdata/import_variadic_with_import.txt
:8080 {
import t1 true
import t2 true
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if !tc.errorFunc(err) {
t.Errorf("Test %d error expectation failed, got %s", i, err)
continue
}
}
}
func TestNestedImport(t *testing.T) {
for i, tc := range []struct {
input string
errorFunc func(err error) bool
}{
{
input: `(t1) {
respond {args[0]} {args[1]}
}
(t2) {
import t1 {args[0]} 202
}
:8080 {
handle {
import t2 "foobar"
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `(t1) {
respond {args[:]}
}
(t2) {
import t1 {args[0]} {args[1]}
}
:8080 {
handle {
import t2 "foobar" 202
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `(t1) {
respond {args[0]} {args[1]}
}
(t2) {
import t1 {args[:]}
}
:8080 {
handle {
import t2 "foobar" 202
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if !tc.errorFunc(err) {
t.Errorf("Test %d error expectation failed, got %s", i, err)
continue
}
}
}
+25 -25
View File
@@ -41,6 +41,7 @@ var directiveOrder = []string{
"map", "map",
"vars", "vars",
"fs",
"root", "root",
"skip_log", "skip_log",
@@ -57,7 +58,8 @@ var directiveOrder = []string{
"try_files", "try_files",
// middleware handlers; some wrap responses // middleware handlers; some wrap responses
"basicauth", "basicauth", // TODO: deprecated, renamed to basic_auth
"basic_auth",
"forward_auth", "forward_auth",
"request_header", "request_header",
"encode", "encode",
@@ -65,6 +67,7 @@ var directiveOrder = []string{
"templates", "templates",
// special routing & dispatching directives // special routing & dispatching directives
"invoke",
"handle", "handle",
"handle_path", "handle_path",
"route", "route",
@@ -172,6 +175,7 @@ func (h Helper) Caddyfiles() []string {
for file := range files { for file := range files {
filesSlice = append(filesSlice, file) filesSlice = append(filesSlice, file)
} }
sort.Strings(filesSlice)
return filesSlice return filesSlice
} }
@@ -215,7 +219,8 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
// NewRoute returns config values relevant to creating a new HTTP route. // NewRoute returns config values relevant to creating a new HTTP route.
func (h Helper) NewRoute(matcherSet caddy.ModuleMap, func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
handler caddyhttp.MiddlewareHandler) []ConfigValue { handler caddyhttp.MiddlewareHandler,
) []ConfigValue {
mod, err := caddy.GetModule(caddy.GetModuleID(handler)) mod, err := caddy.GetModule(caddy.GetModuleID(handler))
if err != nil { if err != nil {
*h.warnings = append(*h.warnings, caddyconfig.Warning{ *h.warnings = append(*h.warnings, caddyconfig.Warning{
@@ -267,12 +272,6 @@ func (h Helper) GroupRoutes(vals []ConfigValue) {
} }
} }
// NewBindAddresses returns config values relevant to adding
// listener bind addresses to the config.
func (h Helper) NewBindAddresses(addrs []string) []ConfigValue {
return []ConfigValue{{Class: "bind", Value: addrs}}
}
// WithDispenser returns a new instance based on d. All others Helper // WithDispenser returns a new instance based on d. All others Helper
// fields are copied, so typically maps are shared with this new instance. // fields are copied, so typically maps are shared with this new instance.
func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper { func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper {
@@ -289,7 +288,7 @@ func ParseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
return nil, err return nil, err
} }
return buildSubroute(allResults, h.groupCounter) return buildSubroute(allResults, h.groupCounter, true)
} }
// parseSegmentAsConfig parses the segment such that its subdirectives // parseSegmentAsConfig parses the segment such that its subdirectives
@@ -427,26 +426,16 @@ func sortRoutes(routes []ConfigValue) {
jPathLen = len(jPM[0]) jPathLen = len(jPM[0])
} }
// some directives involve setting values which can overwrite sortByPath := func() bool {
// each other, so it makes most sense to reverse the order so
// that the lease specific matcher is first; everything else
// has most-specific matcher first
if iDir == "vars" {
// we can only confidently compare path lengths if both // we can only confidently compare path lengths if both
// directives have a single path to match (issue #5037) // directives have a single path to match (issue #5037)
if iPathLen > 0 && jPathLen > 0 { if iPathLen > 0 && jPathLen > 0 {
// sort least-specific (shortest) path first // if both paths are the same except for a trailing wildcard,
return iPathLen < jPathLen // sort by the shorter path first (which is more specific)
} if strings.TrimSuffix(iPM[0], "*") == strings.TrimSuffix(jPM[0], "*") {
return iPathLen < jPathLen
}
// if both directives don't have a single path to compare,
// sort whichever one has no matcher first; if both have
// no matcher, sort equally (stable sort preserves order)
return len(iRoute.MatcherSetsRaw) == 0 && len(jRoute.MatcherSetsRaw) > 0
} else {
// we can only confidently compare path lengths if both
// directives have a single path to match (issue #5037)
if iPathLen > 0 && jPathLen > 0 {
// sort most-specific (longest) path first // sort most-specific (longest) path first
return iPathLen > jPathLen return iPathLen > jPathLen
} }
@@ -455,7 +444,18 @@ func sortRoutes(routes []ConfigValue) {
// sort whichever one has a matcher first; if both have // sort whichever one has a matcher first; if both have
// a matcher, sort equally (stable sort preserves order) // a matcher, sort equally (stable sort preserves order)
return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0 return len(iRoute.MatcherSetsRaw) > 0 && len(jRoute.MatcherSetsRaw) == 0
}()
// some directives involve setting values which can overwrite
// each other, so it makes most sense to reverse the order so
// that the least-specific matcher is first, allowing the last
// matching one to win
if iDir == "vars" {
return !sortByPath
} }
// everything else is most-specific matcher first
return sortByPath
}) })
} }
+6 -3
View File
@@ -31,20 +31,23 @@ func TestHostsFromKeys(t *testing.T) {
[]Address{ []Address{
{Original: ":2015", Port: "2015"}, {Original: ":2015", Port: "2015"},
}, },
[]string{}, []string{}, []string{},
[]string{},
}, },
{ {
[]Address{ []Address{
{Original: ":443", Port: "443"}, {Original: ":443", Port: "443"},
}, },
[]string{}, []string{}, []string{},
[]string{},
}, },
{ {
[]Address{ []Address{
{Original: "foo", Host: "foo"}, {Original: "foo", Host: "foo"},
{Original: ":2015", Port: "2015"}, {Original: ":2015", Port: "2015"},
}, },
[]string{}, []string{"foo"}, []string{},
[]string{"foo"},
}, },
{ {
[]Address{ []Address{
+300 -146
View File
@@ -17,19 +17,21 @@ package httpcaddyfile
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"reflect" "reflect"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"go.uber.org/zap"
"golang.org/x/exp/slices"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"go.uber.org/zap"
) )
func init() { func init() {
@@ -48,12 +50,13 @@ type App struct {
} }
// ServerType can set up a config from an HTTP Caddyfile. // ServerType can set up a config from an HTTP Caddyfile.
type ServerType struct { type ServerType struct{}
}
// Setup makes a config from the tokens. // Setup makes a config from the tokens.
func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock, func (st ServerType) Setup(
options map[string]any) (*caddy.Config, []caddyconfig.Warning, error) { inputServerBlocks []caddyfile.ServerBlock,
options map[string]any,
) (*caddy.Config, []caddyconfig.Warning, error) {
var warnings []caddyconfig.Warning var warnings []caddyconfig.Warning
gc := counter{new(int)} gc := counter{new(int)}
state := make(map[string]any) state := make(map[string]any)
@@ -62,8 +65,11 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks)) originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks))
for _, sblock := range inputServerBlocks { for _, sblock := range inputServerBlocks {
for j, k := range sblock.Keys { for j, k := range sblock.Keys {
if j == 0 && strings.HasPrefix(k, "@") { if j == 0 && strings.HasPrefix(k.Text, "@") {
return nil, warnings, fmt.Errorf("cannot define a matcher outside of a site block: '%s'", k) return nil, warnings, fmt.Errorf("%s:%d: cannot define a matcher outside of a site block: '%s'", k.File, k.Line, k.Text)
}
if _, ok := registeredDirectives[k.Text]; ok {
return nil, warnings, fmt.Errorf("%s:%d: parsed '%s' as a site address, but it is a known directive; directives must appear in a site block", k.File, k.Line, k.Text)
} }
} }
originalServerBlocks = append(originalServerBlocks, serverBlock{ originalServerBlocks = append(originalServerBlocks, serverBlock{
@@ -79,41 +85,18 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
return nil, warnings, err return nil, warnings, err
} }
// replace shorthand placeholders (which are convenient // this will replace both static and user-defined placeholder shorthands
// when writing a Caddyfile) with their actual placeholder // with actual identifiers used by Caddy
// identifiers or variable names replacer := NewShorthandReplacer()
replacer := strings.NewReplacer(placeholderShorthands()...)
// these are placeholders that allow a user-defined final originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer)
// parameters, but we still want to provide a shorthand if err != nil {
// for those, so we use a regexp to replace return nil, warnings, err
regexpReplacements := []struct {
search *regexp.Regexp
replace string
}{
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
} }
for _, sb := range originalServerBlocks { for _, sb := range originalServerBlocks {
for _, segment := range sb.block.Segments { for i := range sb.block.Segments {
for i := 0; i < len(segment); i++ { replacer.ApplyToSegment(&sb.block.Segments[i])
// simple string replacements
segment[i].Text = replacer.Replace(segment[i].Text)
// complex regexp replacements
for _, r := range regexpReplacements {
segment[i].Text = r.search.ReplaceAllString(segment[i].Text, r.replace)
}
}
} }
if len(sb.block.Keys) == 0 { if len(sb.block.Keys) == 0 {
@@ -172,6 +155,18 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
result.directive = dir result.directive = dir
sb.pile[result.Class] = append(sb.pile[result.Class], result) sb.pile[result.Class] = append(sb.pile[result.Class], result)
} }
// specially handle named routes that were pulled out from
// the invoke directive, which could be nested anywhere within
// some subroutes in this directive; we add them to the pile
// for this server block
if state[namedRouteKey] != nil {
for name := range state[namedRouteKey].(map[string]struct{}) {
result := ConfigValue{Class: namedRouteKey, Value: name}
sb.pile[result.Class] = append(sb.pile[result.Class], result)
}
state[namedRouteKey] = nil
}
} }
} }
@@ -222,7 +217,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if ncl.name == caddy.DefaultLoggerName { if ncl.name == caddy.DefaultLoggerName {
hasDefaultLog = true hasDefaultLog = true
} }
if _, ok := options["debug"]; ok && ncl.log.Level == "" { if _, ok := options["debug"]; ok && ncl.log != nil && ncl.log.Level == "" {
ncl.log.Level = zap.DebugLevel.CapitalString() ncl.log.Level = zap.DebugLevel.CapitalString()
} }
customLogs = append(customLogs, ncl) customLogs = append(customLogs, ncl)
@@ -241,7 +236,9 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if _, ok := options["debug"]; ok { if _, ok := options["debug"]; ok {
customLogs = append(customLogs, namedCustomLog{ customLogs = append(customLogs, namedCustomLog{
name: caddy.DefaultLoggerName, name: caddy.DefaultLoggerName,
log: &caddy.CustomLog{Level: zap.DebugLevel.CapitalString()}, log: &caddy.CustomLog{
BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()},
},
}) })
} }
} }
@@ -277,6 +274,12 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) { if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) {
cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings) cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings)
} }
if filesystems, ok := options["filesystem"].(caddy.Module); ok {
cfg.AppsRaw["caddy.filesystems"] = caddyconfig.JSON(
filesystems,
&warnings)
}
if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok { if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr, cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
"module", "module",
@@ -286,13 +289,37 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil { if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil {
cfg.Admin = adminConfig cfg.Admin = adminConfig
} }
if pc, ok := options["persist_config"].(string); ok && pc == "off" {
if cfg.Admin == nil {
cfg.Admin = new(caddy.AdminConfig)
}
if cfg.Admin.Config == nil {
cfg.Admin.Config = new(caddy.ConfigSettings)
}
cfg.Admin.Config.Persist = new(bool)
}
if len(customLogs) > 0 { if len(customLogs) > 0 {
if cfg.Logging == nil { if cfg.Logging == nil {
cfg.Logging = &caddy.Logging{ cfg.Logging = &caddy.Logging{
Logs: make(map[string]*caddy.CustomLog), Logs: make(map[string]*caddy.CustomLog),
} }
} }
// Add the default log first if defined, so that it doesn't
// accidentally get re-created below due to the Exclude logic
for _, ncl := range customLogs { for _, ncl := range customLogs {
if ncl.name == caddy.DefaultLoggerName && ncl.log != nil {
cfg.Logging.Logs[caddy.DefaultLoggerName] = ncl.log
break
}
}
// Add the rest of the custom logs
for _, ncl := range customLogs {
if ncl.log == nil || ncl.name == caddy.DefaultLoggerName {
continue
}
if ncl.name != "" { if ncl.name != "" {
cfg.Logging.Logs[ncl.name] = ncl.log cfg.Logging.Logs[ncl.name] = ncl.log
} }
@@ -306,8 +333,16 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
} }
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...) defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
// avoid duplicates by sorting + compacting
sort.Strings(defaultLog.Exclude)
defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude)
} }
} }
// we may have not actually added anything, so remove if empty
if len(cfg.Logging.Logs) == 0 {
cfg.Logging = nil
}
} }
return cfg, warnings, nil return cfg, warnings, nil
@@ -390,6 +425,81 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
return serverBlocks[1:], nil return serverBlocks[1:], nil
} }
// extractNamedRoutes pulls out any named route server blocks
// so they don't get parsed as sites, and stores them in options
// for later.
func (ServerType) extractNamedRoutes(
serverBlocks []serverBlock,
options map[string]any,
warnings *[]caddyconfig.Warning,
replacer ShorthandReplacer,
) ([]serverBlock, error) {
namedRoutes := map[string]*caddyhttp.Route{}
gc := counter{new(int)}
state := make(map[string]any)
// copy the server blocks so we can
// splice out the named route ones
filtered := append([]serverBlock{}, serverBlocks...)
index := -1
for _, sb := range serverBlocks {
index++
if !sb.block.IsNamedRoute {
continue
}
// splice out this block, because we know it's not a real server
filtered = append(filtered[:index], filtered[index+1:]...)
index--
if len(sb.block.Segments) == 0 {
continue
}
wholeSegment := caddyfile.Segment{}
for i := range sb.block.Segments {
// replace user-defined placeholder shorthands in extracted named routes
replacer.ApplyToSegment(&sb.block.Segments[i])
// zip up all the segments since ParseSegmentAsSubroute
// was designed to take a directive+
wholeSegment = append(wholeSegment, sb.block.Segments[i]...)
}
h := Helper{
Dispenser: caddyfile.NewDispenser(wholeSegment),
options: options,
warnings: warnings,
matcherDefs: nil,
parentBlock: sb.block,
groupCounter: gc,
State: state,
}
handler, err := ParseSegmentAsSubroute(h)
if err != nil {
return nil, err
}
subroute := handler.(*caddyhttp.Subroute)
route := caddyhttp.Route{}
if len(subroute.Routes) == 1 && len(subroute.Routes[0].MatcherSetsRaw) == 0 {
// if there's only one route with no matcher, then we can simplify
route.HandlersRaw = append(route.HandlersRaw, subroute.Routes[0].HandlersRaw[0])
} else {
// otherwise we need the whole subroute
route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)}
}
namedRoutes[sb.block.GetKeysText()[0]] = &route
}
options["named_routes"] = namedRoutes
return filtered, nil
}
// serversFromPairings creates the servers for each pairing of addresses // serversFromPairings creates the servers for each pairing of addresses
// to server blocks. Each pairing is essentially a server definition. // to server blocks. Each pairing is essentially a server definition.
func (st *ServerType) serversFromPairings( func (st *ServerType) serversFromPairings(
@@ -400,6 +510,7 @@ func (st *ServerType) serversFromPairings(
) (map[string]*caddyhttp.Server, error) { ) (map[string]*caddyhttp.Server, error) {
servers := make(map[string]*caddyhttp.Server) servers := make(map[string]*caddyhttp.Server)
defaultSNI := tryString(options["default_sni"], warnings) defaultSNI := tryString(options["default_sni"], warnings)
fallbackSNI := tryString(options["fallback_sni"], warnings)
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort) httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok { if hp, ok := options["http_port"].(int); ok {
@@ -420,12 +531,12 @@ func (st *ServerType) serversFromPairings(
// address), otherwise their routes will improperly be added // address), otherwise their routes will improperly be added
// to the same server (see issue #4635) // to the same server (see issue #4635)
for j, sblock1 := range p.serverBlocks { for j, sblock1 := range p.serverBlocks {
for _, key := range sblock1.block.Keys { for _, key := range sblock1.block.GetKeysText() {
for k, sblock2 := range p.serverBlocks { for k, sblock2 := range p.serverBlocks {
if k == j { if k == j {
continue continue
} }
if sliceContains(sblock2.block.Keys, key) { if sliceContains(sblock2.block.GetKeysText(), key) {
return nil, fmt.Errorf("ambiguous site definition: %s", key) return nil, fmt.Errorf("ambiguous site definition: %s", key)
} }
} }
@@ -528,6 +639,24 @@ func (st *ServerType) serversFromPairings(
} }
} }
// add named routes to the server if 'invoke' was used inside of it
configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route)
for _, sblock := range p.serverBlocks {
if len(sblock.pile[namedRouteKey]) == 0 {
continue
}
for _, value := range sblock.pile[namedRouteKey] {
if srv.NamedRoutes == nil {
srv.NamedRoutes = map[string]*caddyhttp.Route{}
}
name := value.Value.(string)
if configuredNamedRoutes[name] == nil {
return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name)
}
srv.NamedRoutes[name] = configuredNamedRoutes[name]
}
}
// create a subroute for each site in the server block // create a subroute for each site in the server block
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock) matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
@@ -557,14 +686,21 @@ func (st *ServerType) serversFromPairings(
cp.DefaultSNI = defaultSNI cp.DefaultSNI = defaultSNI
break break
} }
if h == fallbackSNI {
hosts = append(hosts, "")
cp.FallbackSNI = fallbackSNI
break
}
} }
if len(hosts) > 0 { if len(hosts) > 0 {
slices.Sort(hosts) // for deterministic JSON output
cp.MatchersRaw = caddy.ModuleMap{ cp.MatchersRaw = caddy.ModuleMap{
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones "sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
} }
} else { } else {
cp.DefaultSNI = defaultSNI cp.DefaultSNI = defaultSNI
cp.FallbackSNI = fallbackSNI
} }
// only append this policy if it actually changes something // only append this policy if it actually changes something
@@ -590,10 +726,20 @@ func (st *ServerType) serversFromPairings(
} }
} }
// If TLS is specified as directive, it will also result in 1 or more connection policy being created
// Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
// specifying prefix "https://"
// Second part of the condition is to allow creating TLS conn policy even though `auto_https` has been disabled
// ensuring compatibility with behavior described in below link
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
(addr.Host != "" && srv.AutoHTTPS != nil && !sliceContains(srv.AutoHTTPS.Skip, addr.Host))
// we'll need to remember if the address qualifies for auto-HTTPS, so we // we'll need to remember if the address qualifies for auto-HTTPS, so we
// can add a TLS conn policy if necessary // can add a TLS conn policy if necessary
if addr.Scheme == "https" || if addr.Scheme == "https" ||
(addr.Scheme != "http" && addr.Host != "" && addr.Port != httpPort) { (addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
addressQualifiesForTLS = true addressQualifiesForTLS = true
} }
// predict whether auto-HTTPS will add the conn policy for us; if so, we // predict whether auto-HTTPS will add the conn policy for us; if so, we
@@ -618,7 +764,7 @@ func (st *ServerType) serversFromPairings(
// set up each handler directive, making sure to honor directive order // set up each handler directive, making sure to honor directive order
dirRoutes := sblock.pile["route"] dirRoutes := sblock.pile["route"]
siteSubroute, err := buildSubroute(dirRoutes, groupCounter) siteSubroute, err := buildSubroute(dirRoutes, groupCounter, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -631,10 +777,19 @@ func (st *ServerType) serversFromPairings(
if srv.Errors == nil { if srv.Errors == nil {
srv.Errors = new(caddyhttp.HTTPErrorConfig) srv.Errors = new(caddyhttp.HTTPErrorConfig)
} }
sort.SliceStable(errorSubrouteVals, func(i, j int) bool {
sri, srj := errorSubrouteVals[i].Value.(*caddyhttp.Subroute), errorSubrouteVals[j].Value.(*caddyhttp.Subroute)
if len(sri.Routes[0].MatcherSetsRaw) == 0 && len(srj.Routes[0].MatcherSetsRaw) != 0 {
return false
}
return true
})
errorsSubroute := &caddyhttp.Subroute{}
for _, val := range errorSubrouteVals { for _, val := range errorSubrouteVals {
sr := val.Value.(*caddyhttp.Subroute) sr := val.Value.(*caddyhttp.Subroute)
srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings) errorsSubroute.Routes = append(errorsSubroute.Routes, sr.Routes...)
} }
srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, errorsSubroute, matcherSetsEnc, p, warnings)
} }
// add log associations // add log associations
@@ -642,18 +797,31 @@ func (st *ServerType) serversFromPairings(
sblockLogHosts := sblock.hostsFromKeys(true) sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] { for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog) ncl := cval.Value.(namedCustomLog)
if sblock.hasHostCatchAllKey() { if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 {
// all requests for hosts not able to be listed should use // all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block // this log because it's a catch-all-hosts server block
srv.Logs.DefaultLoggerName = ncl.name srv.Logs.DefaultLoggerName = ncl.name
} else { } else if len(ncl.hostnames) > 0 {
// map each host to the user's desired logger name // if the logger overrides the hostnames, map that to the logger name
for _, h := range sblockLogHosts { for _, h := range ncl.hostnames {
if srv.Logs.LoggerNames == nil { if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string) srv.Logs.LoggerNames = make(map[string]string)
} }
srv.Logs.LoggerNames[h] = ncl.name srv.Logs.LoggerNames[h] = ncl.name
} }
} else {
// otherwise, map each host to the logger name
for _, h := range sblockLogHosts {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
// strip the port from the host, if any
host, _, err := net.SplitHostPort(h)
if err != nil {
host = h
}
srv.Logs.LoggerNames[host] = ncl.name
}
} }
} }
if srv.Logs != nil && len(sblock.pile["custom_log"]) == 0 { if srv.Logs != nil && len(sblock.pile["custom_log"]) == 0 {
@@ -669,6 +837,11 @@ func (st *ServerType) serversFromPairings(
} }
} }
// sort for deterministic JSON output
if srv.Logs != nil {
slices.Sort(srv.Logs.SkipHosts)
}
// a server cannot (natively) serve both HTTP and HTTPS at the // a server cannot (natively) serve both HTTP and HTTPS at the
// same time, so make sure the configuration isn't in conflict // same time, so make sure the configuration isn't in conflict
err := detectConflictingSchemes(srv, p.serverBlocks, options) err := detectConflictingSchemes(srv, p.serverBlocks, options)
@@ -690,8 +863,8 @@ func (st *ServerType) serversFromPairings(
// policy missing for any HTTPS-enabled hosts, if so, add it... maybe? // policy missing for any HTTPS-enabled hosts, if so, add it... maybe?
if addressQualifiesForTLS && if addressQualifiesForTLS &&
!hasCatchAllTLSConnPolicy && !hasCatchAllTLSConnPolicy &&
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "") { (len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI}) srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI})
} }
// tidy things up a bit // tidy things up a bit
@@ -706,7 +879,7 @@ func (st *ServerType) serversFromPairings(
err := applyServerOptions(servers, options, warnings) err := applyServerOptions(servers, options, warnings)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("applying global server options: %v", err)
} }
return servers, nil return servers, nil
@@ -900,8 +1073,8 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
subroute *caddyhttp.Subroute, subroute *caddyhttp.Subroute,
matcherSetsEnc []caddy.ModuleMap, matcherSetsEnc []caddy.ModuleMap,
p sbAddrAssociation, p sbAddrAssociation,
warnings *[]caddyconfig.Warning) caddyhttp.RouteList { warnings *[]caddyconfig.Warning,
) caddyhttp.RouteList {
// nothing to do if... there's nothing to do // nothing to do if... there's nothing to do
if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil { if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
return routeList return routeList
@@ -959,14 +1132,16 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
// buildSubroute turns the config values, which are expected to be routes // buildSubroute turns the config values, which are expected to be routes
// into a clean and orderly subroute that has all the routes within it. // into a clean and orderly subroute that has all the routes within it.
func buildSubroute(routes []ConfigValue, groupCounter counter) (*caddyhttp.Subroute, error) { func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool) (*caddyhttp.Subroute, error) {
for _, val := range routes { if needsSorting {
if !directiveIsOrdered(val.directive) { for _, val := range routes {
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here", val.directive) if !directiveIsOrdered(val.directive) {
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option", val.directive)
}
} }
}
sortRoutes(routes) sortRoutes(routes)
}
subroute := new(caddyhttp.Subroute) subroute := new(caddyhttp.Subroute)
@@ -1209,68 +1384,73 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
} }
func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error { func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error {
for d.Next() { d.Next() // advance to the first token
// this is the "name" for "named matchers"
definitionName := d.Val()
if _, ok := matchers[definitionName]; ok { // this is the "name" for "named matchers"
return fmt.Errorf("matcher is defined more than once: %s", definitionName) definitionName := d.Val()
if _, ok := matchers[definitionName]; ok {
return fmt.Errorf("matcher is defined more than once: %s", definitionName)
}
matchers[definitionName] = make(caddy.ModuleMap)
// given a matcher name and the tokens following it, parse
// the tokens as a matcher module and record it
makeMatcher := func(matcherName string, tokens []caddyfile.Token) error {
mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil {
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
} }
matchers[definitionName] = make(caddy.ModuleMap) unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok {
return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
}
err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens))
if err != nil {
return err
}
rm, ok := unm.(caddyhttp.RequestMatcher)
if !ok {
return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
return nil
}
// given a matcher name and the tokens following it, parse // if the next token is quoted, we can assume it's not a matcher name
// the tokens as a matcher module and record it // and that it's probably an 'expression' matcher
makeMatcher := func(matcherName string, tokens []caddyfile.Token) error { if d.NextArg() {
mod, err := caddy.GetModule("http.matchers." + matcherName) if d.Token().Quoted() {
if err != nil { // since it was missing the matcher name, we insert a token
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err) // in front of the expression token itself
} err := makeMatcher("expression", []caddyfile.Token{
unm, ok := mod.New().(caddyfile.Unmarshaler) {Text: "expression", File: d.File(), Line: d.Line()},
if !ok { d.Token(),
return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) })
}
err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens))
if err != nil { if err != nil {
return err return err
} }
rm, ok := unm.(caddyhttp.RequestMatcher)
if !ok {
return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
return nil return nil
} }
// if the next token is quoted, we can assume it's not a matcher name // if it wasn't quoted, then we need to rewind after calling
// and that it's probably an 'expression' matcher // d.NextArg() so the below properly grabs the matcher name
if d.NextArg() { d.Prev()
if d.Token().Quoted() { }
err := makeMatcher("expression", []caddyfile.Token{d.Token()})
if err != nil {
return err
}
continue
}
// if it wasn't quoted, then we need to rewind after calling // in case there are multiple instances of the same matcher, concatenate
// d.NextArg() so the below properly grabs the matcher name // their tokens (we expect that UnmarshalCaddyfile should be able to
d.Prev() // handle more than one segment); otherwise, we'd overwrite other
} // instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token)
// in case there are multiple instances of the same matcher, concatenate for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
// their tokens (we expect that UnmarshalCaddyfile should be able to matcherName := d.Val()
// handle more than one segment); otherwise, we'd overwrite other tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
// instances of the matcher in this set }
tokensByMatcherName := make(map[string][]caddyfile.Token) for matcherName, tokens := range tokensByMatcherName {
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { err := makeMatcher(matcherName, tokens)
matcherName := d.Val() if err != nil {
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...) return err
}
for matcherName, tokens := range tokensByMatcherName {
err := makeMatcher(matcherName, tokens)
if err != nil {
return err
}
} }
} }
return nil return nil
@@ -1288,36 +1468,6 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul
return msEncoded, nil return msEncoded, nil
} }
// placeholderShorthands returns a slice of old-new string pairs,
// where the left of the pair is a placeholder shorthand that may
// be used in the Caddyfile, and the right is the replacement.
func placeholderShorthands() []string {
return []string{
"{dir}", "{http.request.uri.path.dir}",
"{file}", "{http.request.uri.path.file}",
"{host}", "{http.request.host}",
"{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
"{method}", "{http.request.method}",
"{path}", "{http.request.uri.path}",
"{query}", "{http.request.uri.query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
"{uri}", "{http.request.uri}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
}
}
// WasReplacedPlaceholderShorthand checks if a token string was // WasReplacedPlaceholderShorthand checks if a token string was
// likely a replaced shorthand of the known Caddyfile placeholder // likely a replaced shorthand of the known Caddyfile placeholder
// replacement outputs. Useful to prevent some user-defined map // replacement outputs. Useful to prevent some user-defined map
@@ -1433,8 +1583,9 @@ func (c counter) nextGroup() string {
} }
type namedCustomLog struct { type namedCustomLog struct {
name string name string
log *caddy.CustomLog hostnames []string
log *caddy.CustomLog
} }
// sbAddrAssociation is a mapping from a list of // sbAddrAssociation is a mapping from a list of
@@ -1445,7 +1596,10 @@ type sbAddrAssociation struct {
serverBlocks []serverBlock serverBlocks []serverBlock
} }
const matcherPrefix = "@" const (
matcherPrefix = "@"
namedRouteKey = "named_route"
)
// Interface guard // Interface guard
var _ caddyfile.ServerType = (*ServerType)(nil) var _ caddyfile.ServerType = (*ServerType)(nil)
+206 -187
View File
@@ -17,12 +17,13 @@ package httpcaddyfile
import ( import (
"strconv" "strconv"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
) )
func init() { func init() {
@@ -33,6 +34,7 @@ func init() {
RegisterGlobalOption("grace_period", parseOptDuration) RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("shutdown_delay", parseOptDuration) RegisterGlobalOption("shutdown_delay", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString) RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("fallback_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder) RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("storage", parseOptStorage) RegisterGlobalOption("storage", parseOptStorage)
RegisterGlobalOption("storage_clean_interval", parseOptDuration) RegisterGlobalOption("storage_clean_interval", parseOptDuration)
@@ -54,110 +56,109 @@ func init() {
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions) RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
RegisterGlobalOption("log", parseLogOptions) RegisterGlobalOption("log", parseLogOptions)
RegisterGlobalOption("preferred_chains", parseOptPreferredChains) RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
RegisterGlobalOption("persist_config", parseOptPersistConfig)
} }
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil } func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
var httpPort int var httpPort int
for d.Next() { var httpPortStr string
var httpPortStr string if !d.AllArgs(&httpPortStr) {
if !d.AllArgs(&httpPortStr) { return 0, d.ArgErr()
return 0, d.ArgErr() }
} var err error
var err error httpPort, err = strconv.Atoi(httpPortStr)
httpPort, err = strconv.Atoi(httpPortStr) if err != nil {
if err != nil { return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err)
return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err)
}
} }
return httpPort, nil return httpPort, nil
} }
func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
var httpsPort int var httpsPort int
for d.Next() { var httpsPortStr string
var httpsPortStr string if !d.AllArgs(&httpsPortStr) {
if !d.AllArgs(&httpsPortStr) { return 0, d.ArgErr()
return 0, d.ArgErr() }
} var err error
var err error httpsPort, err = strconv.Atoi(httpsPortStr)
httpsPort, err = strconv.Atoi(httpsPortStr) if err != nil {
if err != nil { return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err)
return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err)
}
} }
return httpsPort, nil return httpsPort, nil
} }
func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
// get directive name
if !d.Next() {
return nil, d.ArgErr()
}
dirName := d.Val()
if _, ok := registeredDirectives[dirName]; !ok {
return nil, d.Errf("%s is not a registered directive", dirName)
}
// get positional token
if !d.Next() {
return nil, d.ArgErr()
}
pos := d.Val()
newOrder := directiveOrder newOrder := directiveOrder
for d.Next() { // if directive exists, first remove it
// get directive name for i, d := range newOrder {
if !d.Next() { if d == dirName {
return nil, d.ArgErr() newOrder = append(newOrder[:i], newOrder[i+1:]...)
} break
dirName := d.Val()
if _, ok := registeredDirectives[dirName]; !ok {
return nil, d.Errf("%s is not a registered directive", dirName)
} }
}
// get positional token // act on the positional
if !d.Next() { switch pos {
return nil, d.ArgErr() case "first":
} newOrder = append([]string{dirName}, newOrder...)
pos := d.Val()
// if directive exists, first remove it
for i, d := range newOrder {
if d == dirName {
newOrder = append(newOrder[:i], newOrder[i+1:]...)
break
}
}
// act on the positional
switch pos {
case "first":
newOrder = append([]string{dirName}, newOrder...)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case "last":
newOrder = append(newOrder, dirName)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case "before":
case "after":
default:
return nil, d.Errf("unknown positional '%s'", pos)
}
// get name of other directive
if !d.NextArg() {
return nil, d.ArgErr()
}
otherDir := d.Val()
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
directiveOrder = newOrder
return newOrder, nil
case "last":
newOrder = append(newOrder, dirName)
if d.NextArg() {
return nil, d.ArgErr()
}
directiveOrder = newOrder
return newOrder, nil
case "before":
case "after":
default:
return nil, d.Errf("unknown positional '%s'", pos)
}
// insert directive into proper position // get name of other directive
for i, d := range newOrder { if !d.NextArg() {
if d == otherDir { return nil, d.ArgErr()
if pos == "before" { }
newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...) otherDir := d.Val()
} else if pos == "after" { if d.NextArg() {
newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...) return nil, d.ArgErr()
} }
break
// insert directive into proper position
for i, d := range newOrder {
if d == otherDir {
if pos == "before" {
newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
} else if pos == "after" {
newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
} }
break
} }
} }
@@ -220,57 +221,58 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
eab := new(acme.EAB) eab := new(acme.EAB)
for d.Next() { d.Next() // consume option name
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
for nesting := d.Nesting(); d.NextBlock(nesting); { for d.NextBlock(0) {
switch d.Val() { switch d.Val() {
case "key_id": case "key_id":
if !d.NextArg() { if !d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
}
eab.KeyID = d.Val()
case "mac_key":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.MACKey = d.Val()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
} }
eab.KeyID = d.Val()
case "mac_key":
if !d.NextArg() {
return nil, d.ArgErr()
}
eab.MACKey = d.Val()
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
} }
} }
return eab, nil return eab, nil
} }
func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) { func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) {
d.Next() // consume option name
var issuers []certmagic.Issuer var issuers []certmagic.Issuer
if existing != nil { if existing != nil {
issuers = existing.([]certmagic.Issuer) issuers = existing.([]certmagic.Issuer)
} }
for d.Next() { // consume option name
if !d.Next() { // get issuer module name // get issuer module name
return nil, d.ArgErr() if !d.Next() {
} return nil, d.ArgErr()
modID := "tls.issuance." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
}
issuers = append(issuers, iss)
} }
modID := "tls.issuance." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
iss, ok := unm.(certmagic.Issuer)
if !ok {
return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
}
issuers = append(issuers, iss)
return issuers, nil return issuers, nil
} }
func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name d.Next() // consume option name
if !d.Next() { if !d.Next() {
return "", d.ArgErr() return "", d.ArgErr()
} }
@@ -282,7 +284,7 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
} }
func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name d.Next() // consume option name
val := d.RemainingArgs() val := d.RemainingArgs()
if len(val) == 0 { if len(val) == 0 {
return "", d.ArgErr() return "", d.ArgErr()
@@ -291,33 +293,33 @@ func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) {
} }
func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
adminCfg := new(caddy.AdminConfig) adminCfg := new(caddy.AdminConfig)
for d.Next() { if d.NextArg() {
if d.NextArg() { listenAddress := d.Val()
listenAddress := d.Val() if listenAddress == "off" {
if listenAddress == "off" { adminCfg.Disabled = true
adminCfg.Disabled = true if d.Next() { // Do not accept any remaining options including block
if d.Next() { // Do not accept any remaining options including block return nil, d.Err("No more option is allowed after turning off admin config")
return nil, d.Err("No more option is allowed after turning off admin config") }
} } else {
} else { adminCfg.Listen = listenAddress
adminCfg.Listen = listenAddress if d.NextArg() { // At most 1 arg is allowed
if d.NextArg() { // At most 1 arg is allowed return nil, d.ArgErr()
return nil, d.ArgErr()
}
} }
} }
for nesting := d.Nesting(); d.NextBlock(nesting); { }
switch d.Val() { for d.NextBlock(0) {
case "enforce_origin": switch d.Val() {
adminCfg.EnforceOrigin = true case "enforce_origin":
adminCfg.EnforceOrigin = true
case "origins": case "origins":
adminCfg.Origins = d.RemainingArgs() adminCfg.Origins = d.RemainingArgs()
default: default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val()) return nil, d.Errf("unrecognized parameter '%s'", d.Val())
}
} }
} }
if adminCfg.Listen == "" && !adminCfg.Disabled { if adminCfg.Listen == "" && !adminCfg.Disabled {
@@ -327,57 +329,59 @@ func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
} }
func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
if d.NextArg() {
return nil, d.ArgErr()
}
var ond *caddytls.OnDemandConfig var ond *caddytls.OnDemandConfig
for d.Next() {
if d.NextArg() {
return nil, d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "ask":
if !d.NextArg() {
return nil, d.ArgErr()
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
ond.Ask = d.Val()
case "interval": for nesting := d.Nesting(); d.NextBlock(nesting); {
if !d.NextArg() { switch d.Val() {
return nil, d.ArgErr() case "ask":
} if !d.NextArg() {
dur, err := caddy.ParseDuration(d.Val()) return nil, d.ArgErr()
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Interval = caddy.Duration(dur)
case "burst":
if !d.NextArg() {
return nil, d.ArgErr()
}
burst, err := strconv.Atoi(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Burst = burst
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
} }
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
perm := caddytls.PermissionByHTTP{Endpoint: d.Val()}
ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", "http", nil)
case "interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Interval = caddy.Duration(dur)
case "burst":
if !d.NextArg() {
return nil, d.ArgErr()
}
burst, err := strconv.Atoi(d.Val())
if err != nil {
return nil, err
}
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
if ond.RateLimit == nil {
ond.RateLimit = new(caddytls.RateLimit)
}
ond.RateLimit.Burst = burst
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
} }
} }
if ond == nil { if ond == nil {
@@ -386,8 +390,23 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
return ond, nil return ond, nil
} }
func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
if !d.Next() {
return "", d.ArgErr()
}
val := d.Val()
if d.Next() {
return "", d.ArgErr()
}
if val != "off" {
return "", d.Errf("persist_config must be 'off'")
}
return val, nil
}
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume parameter name d.Next() // consume option name
if !d.Next() { if !d.Next() {
return "", d.ArgErr() return "", d.ArgErr()
} }
+127 -116
View File
@@ -15,6 +15,7 @@
package httpcaddyfile package httpcaddyfile
import ( import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddypki"
@@ -26,134 +27,145 @@ func init() {
// parsePKIApp parses the global log option. Syntax: // parsePKIApp parses the global log option. Syntax:
// //
// pki { // pki {
// ca [<id>] { // ca [<id>] {
// name <name> // name <name>
// root_cn <name> // root_cn <name>
// intermediate_cn <name> // intermediate_cn <name>
// root { // intermediate_lifetime <duration>
// cert <path> // root {
// key <path> // cert <path>
// format <format> // key <path>
// } // format <format>
// intermediate { // }
// cert <path> // intermediate {
// key <path> // cert <path>
// format <format> // key <path>
// } // format <format>
// } // }
// } // }
// }
// //
// When the CA ID is unspecified, 'local' is assumed. // When the CA ID is unspecified, 'local' is assumed.
func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) { func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) {
pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)} d.Next() // consume app name
for d.Next() { pki := &caddypki.PKI{
for nesting := d.Nesting(); d.NextBlock(nesting); { CAs: make(map[string]*caddypki.CA),
switch d.Val() { }
case "ca": for d.NextBlock(0) {
pkiCa := new(caddypki.CA) switch d.Val() {
case "ca":
pkiCa := new(caddypki.CA)
if d.NextArg() {
pkiCa.ID = d.Val()
if d.NextArg() { if d.NextArg() {
pkiCa.ID = d.Val() return nil, d.ArgErr()
if d.NextArg() { }
}
if pkiCa.ID == "" {
pkiCa.ID = caddypki.DefaultCAID
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "name":
if !d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
} pkiCa.Name = d.Val()
if pkiCa.ID == "" {
pkiCa.ID = caddypki.DefaultCAID
}
for nesting := d.Nesting(); d.NextBlock(nesting); { case "root_cn":
switch d.Val() { if !d.NextArg() {
case "name": return nil, d.ArgErr()
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Name = d.Val()
case "root_cn":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.RootCommonName = d.Val()
case "intermediate_cn":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.IntermediateCommonName = d.Val()
case "root":
if pkiCa.Root == nil {
pkiCa.Root = new(caddypki.KeyPair)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "cert":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.Certificate = d.Val()
case "key":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.PrivateKey = d.Val()
case "format":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.Format = d.Val()
default:
return nil, d.Errf("unrecognized pki ca root option '%s'", d.Val())
}
}
case "intermediate":
if pkiCa.Intermediate == nil {
pkiCa.Intermediate = new(caddypki.KeyPair)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "cert":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.Certificate = d.Val()
case "key":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.PrivateKey = d.Val()
case "format":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.Format = d.Val()
default:
return nil, d.Errf("unrecognized pki ca intermediate option '%s'", d.Val())
}
}
default:
return nil, d.Errf("unrecognized pki ca option '%s'", d.Val())
} }
pkiCa.RootCommonName = d.Val()
case "intermediate_cn":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.IntermediateCommonName = d.Val()
case "intermediate_lifetime":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, err
}
pkiCa.IntermediateLifetime = caddy.Duration(dur)
case "root":
if pkiCa.Root == nil {
pkiCa.Root = new(caddypki.KeyPair)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "cert":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.Certificate = d.Val()
case "key":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.PrivateKey = d.Val()
case "format":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Root.Format = d.Val()
default:
return nil, d.Errf("unrecognized pki ca root option '%s'", d.Val())
}
}
case "intermediate":
if pkiCa.Intermediate == nil {
pkiCa.Intermediate = new(caddypki.KeyPair)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "cert":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.Certificate = d.Val()
case "key":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.PrivateKey = d.Val()
case "format":
if !d.NextArg() {
return nil, d.ArgErr()
}
pkiCa.Intermediate.Format = d.Val()
default:
return nil, d.Errf("unrecognized pki ca intermediate option '%s'", d.Val())
}
}
default:
return nil, d.Errf("unrecognized pki ca option '%s'", d.Val())
} }
pki.CAs[pkiCa.ID] = pkiCa
default:
return nil, d.Errf("unrecognized pki option '%s'", d.Val())
} }
pki.CAs[pkiCa.ID] = pkiCa
default:
return nil, d.Errf("unrecognized pki option '%s'", d.Val())
} }
} }
return pki, nil return pki, nil
} }
@@ -162,7 +174,6 @@ func (st ServerType) buildPKIApp(
options map[string]any, options map[string]any,
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddypki.PKI, []caddyconfig.Warning, error) { ) (*caddypki.PKI, []caddyconfig.Warning, error) {
skipInstallTrust := false skipInstallTrust := false
if _, ok := options["skip_install_trust"]; ok { if _, ok := options["skip_install_trust"]; ok {
skipInstallTrust = true skipInstallTrust = true
+259 -170
View File
@@ -18,11 +18,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/dustin/go-humanize"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dustin/go-humanize"
) )
// serverOptions collects server config overrides parsed from Caddyfile global options // serverOptions collects server config overrides parsed from Caddyfile global options
@@ -33,6 +34,7 @@ type serverOptions struct {
ListenerAddress string ListenerAddress string
// These will all map 1:1 to the caddyhttp.Server struct // These will all map 1:1 to the caddyhttp.Server struct
Name string
ListenerWrappersRaw []json.RawMessage ListenerWrappersRaw []json.RawMessage
ReadTimeout caddy.Duration ReadTimeout caddy.Duration
ReadHeaderTimeout caddy.Duration ReadHeaderTimeout caddy.Duration
@@ -40,188 +42,246 @@ type serverOptions struct {
IdleTimeout caddy.Duration IdleTimeout caddy.Duration
KeepAliveInterval caddy.Duration KeepAliveInterval caddy.Duration
MaxHeaderBytes int MaxHeaderBytes int
EnableFullDuplex bool
Protocols []string Protocols []string
StrictSNIHost *bool StrictSNIHost *bool
TrustedProxiesRaw json.RawMessage
TrustedProxiesStrict int
ClientIPHeaders []string
ShouldLogCredentials bool ShouldLogCredentials bool
Metrics *caddyhttp.Metrics Metrics *caddyhttp.Metrics
} }
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
d.Next() // consume option name
serverOpts := serverOptions{} serverOpts := serverOptions{}
for d.Next() { if d.NextArg() {
serverOpts.ListenerAddress = d.Val()
if d.NextArg() { if d.NextArg() {
serverOpts.ListenerAddress = d.Val() return nil, d.ArgErr()
}
}
for d.NextBlock(0) {
switch d.Val() {
case "name":
if serverOpts.ListenerAddress == "" {
return nil, d.Errf("cannot set a name for a server without a listener address")
}
if !d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.Name = d.Val()
case "listener_wrappers":
for nesting := d.Nesting(); d.NextBlock(nesting); {
modID := "caddy.listeners." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
if !ok {
return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
}
jsonListenerWrapper := caddyconfig.JSONModuleObject(
listenerWrapper,
"wrapper",
listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
nil,
)
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
}
case "timeouts":
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "read_body":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_body timeout duration: %v", err)
}
serverOpts.ReadTimeout = caddy.Duration(dur)
case "read_header":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_header timeout duration: %v", err)
}
serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
case "write":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing write timeout duration: %v", err)
}
serverOpts.WriteTimeout = caddy.Duration(dur)
case "idle":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing idle timeout duration: %v", err)
}
serverOpts.IdleTimeout = caddy.Duration(dur)
default:
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
}
}
case "keepalive_interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing keepalive interval duration: %v", err)
}
serverOpts.KeepAliveInterval = caddy.Duration(dur)
case "max_header_size":
var sizeStr string
if !d.AllArgs(&sizeStr) {
return nil, d.ArgErr()
}
size, err := humanize.ParseBytes(sizeStr)
if err != nil {
return nil, d.Errf("parsing max_header_size: %v", err)
}
serverOpts.MaxHeaderBytes = int(size)
case "enable_full_duplex":
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
} serverOpts.EnableFullDuplex = true
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "listener_wrappers":
for nesting := d.Nesting(); d.NextBlock(nesting); {
modID := "caddy.listeners." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
listenerWrapper, ok := unm.(caddy.ListenerWrapper)
if !ok {
return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
}
jsonListenerWrapper := caddyconfig.JSONModuleObject(
listenerWrapper,
"wrapper",
listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
nil,
)
serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
}
case "timeouts": case "log_credentials":
for nesting := d.Nesting(); d.NextBlock(nesting); { if d.NextArg() {
switch d.Val() { return nil, d.ArgErr()
case "read_body":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_body timeout duration: %v", err)
}
serverOpts.ReadTimeout = caddy.Duration(dur)
case "read_header":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing read_header timeout duration: %v", err)
}
serverOpts.ReadHeaderTimeout = caddy.Duration(dur)
case "write":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing write timeout duration: %v", err)
}
serverOpts.WriteTimeout = caddy.Duration(dur)
case "idle":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing idle timeout duration: %v", err)
}
serverOpts.IdleTimeout = caddy.Duration(dur)
default:
return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
}
}
case "keepalive_interval":
if !d.NextArg() {
return nil, d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return nil, d.Errf("parsing keepalive interval duration: %v", err)
}
serverOpts.KeepAliveInterval = caddy.Duration(dur)
case "max_header_size":
var sizeStr string
if !d.AllArgs(&sizeStr) {
return nil, d.ArgErr()
}
size, err := humanize.ParseBytes(sizeStr)
if err != nil {
return nil, d.Errf("parsing max_header_size: %v", err)
}
serverOpts.MaxHeaderBytes = int(size)
case "log_credentials":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.ShouldLogCredentials = true
case "protocols":
protos := d.RemainingArgs()
for _, proto := range protos {
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
}
if sliceContains(serverOpts.Protocols, proto) {
return nil, d.Errf("protocol %s specified more than once", proto)
}
serverOpts.Protocols = append(serverOpts.Protocols, proto)
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
case "strict_sni_host":
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
case "metrics":
if d.NextArg() {
return nil, d.ArgErr()
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
serverOpts.Metrics = new(caddyhttp.Metrics)
// TODO: DEPRECATED. (August 2022)
case "protocol":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol sub-option will be removed soon")
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "allow_h2c":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: allow_h2c will be removed soon; use protocols option instead")
if d.NextArg() {
return nil, d.ArgErr()
}
if sliceContains(serverOpts.Protocols, "h2c") {
return nil, d.Errf("protocol h2c already specified")
}
serverOpts.Protocols = append(serverOpts.Protocols, "h2c")
case "strict_sni_host":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol > strict_sni_host in this position will be removed soon; move up to the servers block instead")
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
default:
return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
}
}
default:
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
} }
serverOpts.ShouldLogCredentials = true
case "protocols":
protos := d.RemainingArgs()
for _, proto := range protos {
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
}
if sliceContains(serverOpts.Protocols, proto) {
return nil, d.Errf("protocol %s specified more than once", proto)
}
serverOpts.Protocols = append(serverOpts.Protocols, proto)
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
case "strict_sni_host":
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
case "trusted_proxies":
if !d.NextArg() {
return nil, d.Err("trusted_proxies expects an IP range source module name as its first argument")
}
modID := "http.ip_sources." + d.Val()
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return nil, err
}
source, ok := unm.(caddyhttp.IPRangeSource)
if !ok {
return nil, fmt.Errorf("module %s (%T) is not an IP range source", modID, unm)
}
jsonSource := caddyconfig.JSONModuleObject(
source,
"source",
source.(caddy.Module).CaddyModule().ID.Name(),
nil,
)
serverOpts.TrustedProxiesRaw = jsonSource
case "trusted_proxies_strict":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.TrustedProxiesStrict = 1
case "client_ip_headers":
headers := d.RemainingArgs()
for _, header := range headers {
if sliceContains(serverOpts.ClientIPHeaders, header) {
return nil, d.Errf("client IP header %s specified more than once", header)
}
serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
case "metrics":
if d.NextArg() {
return nil, d.ArgErr()
}
if nesting := d.Nesting(); d.NextBlock(nesting) {
return nil, d.ArgErr()
}
serverOpts.Metrics = new(caddyhttp.Metrics)
// TODO: DEPRECATED. (August 2022)
case "protocol":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol sub-option will be removed soon")
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "allow_h2c":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: allow_h2c will be removed soon; use protocols option instead")
if d.NextArg() {
return nil, d.ArgErr()
}
if sliceContains(serverOpts.Protocols, "h2c") {
return nil, d.Errf("protocol h2c already specified")
}
serverOpts.Protocols = append(serverOpts.Protocols, "h2c")
case "strict_sni_host":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol > strict_sni_host in this position will be removed soon; move up to the servers block instead")
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal
default:
return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
}
}
default:
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
} }
} }
return serverOpts, nil return serverOpts, nil
@@ -238,7 +298,22 @@ func applyServerOptions(
return nil return nil
} }
for _, server := range servers { // check for duplicate names, which would clobber the config
existingNames := map[string]bool{}
for _, opts := range serverOpts {
if opts.Name == "" {
continue
}
if existingNames[opts.Name] {
return fmt.Errorf("cannot use duplicate server name '%s'", opts.Name)
}
existingNames[opts.Name] = true
}
// collect the server name overrides
nameReplacements := map[string]string{}
for key, server := range servers {
// find the options that apply to this server // find the options that apply to this server
opts := func() *serverOptions { opts := func() *serverOptions {
for _, entry := range serverOpts { for _, entry := range serverOpts {
@@ -267,8 +342,12 @@ func applyServerOptions(
server.IdleTimeout = opts.IdleTimeout server.IdleTimeout = opts.IdleTimeout
server.KeepAliveInterval = opts.KeepAliveInterval server.KeepAliveInterval = opts.KeepAliveInterval
server.MaxHeaderBytes = opts.MaxHeaderBytes server.MaxHeaderBytes = opts.MaxHeaderBytes
server.EnableFullDuplex = opts.EnableFullDuplex
server.Protocols = opts.Protocols server.Protocols = opts.Protocols
server.StrictSNIHost = opts.StrictSNIHost server.StrictSNIHost = opts.StrictSNIHost
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
server.ClientIPHeaders = opts.ClientIPHeaders
server.TrustedProxiesStrict = opts.TrustedProxiesStrict
server.Metrics = opts.Metrics server.Metrics = opts.Metrics
if opts.ShouldLogCredentials { if opts.ShouldLogCredentials {
if server.Logs == nil { if server.Logs == nil {
@@ -276,6 +355,16 @@ func applyServerOptions(
} }
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
} }
if opts.Name != "" {
nameReplacements[key] = opts.Name
}
}
// rename the servers if marked to do so
for old, new := range nameReplacements {
servers[new] = servers[old]
delete(servers, old)
} }
return nil return nil
+93
View File
@@ -0,0 +1,93 @@
package httpcaddyfile
import (
"regexp"
"strings"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
type ComplexShorthandReplacer struct {
search *regexp.Regexp
replace string
}
type ShorthandReplacer struct {
complex []ComplexShorthandReplacer
simple *strings.Replacer
}
func NewShorthandReplacer() ShorthandReplacer {
// replace shorthand placeholders (which are convenient
// when writing a Caddyfile) with their actual placeholder
// identifiers or variable names
replacer := strings.NewReplacer(placeholderShorthands()...)
// these are placeholders that allow a user-defined final
// parameters, but we still want to provide a shorthand
// for those, so we use a regexp to replace
regexpReplacements := []ComplexShorthandReplacer{
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
}
return ShorthandReplacer{
complex: regexpReplacements,
simple: replacer,
}
}
// placeholderShorthands returns a slice of old-new string pairs,
// where the left of the pair is a placeholder shorthand that may
// be used in the Caddyfile, and the right is the replacement.
func placeholderShorthands() []string {
return []string{
"{dir}", "{http.request.uri.path.dir}",
"{file}", "{http.request.uri.path.file}",
"{host}", "{http.request.host}",
"{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
"{method}", "{http.request.method}",
"{path}", "{http.request.uri.path}",
"{query}", "{http.request.uri.query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
"{uri}", "{http.request.uri}",
"{uuid}", "{http.request.uuid}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
"{tls_client_serial}", "{http.request.tls.client.serial}",
"{tls_client_subject}", "{http.request.tls.client.subject}",
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
"{client_ip}", "{http.vars.client_ip}",
}
}
// ApplyToSegment replaces shorthand placeholder to its full placeholder, understandable by Caddy.
func (s ShorthandReplacer) ApplyToSegment(segment *caddyfile.Segment) {
if segment != nil {
for i := 0; i < len(*segment); i++ {
// simple string replacements
(*segment)[i].Text = s.simple.Replace((*segment)[i].Text)
// complex regexp replacements
for _, r := range s.complex {
(*segment)[i].Text = r.search.ReplaceAllString((*segment)[i].Text, r.replace)
}
}
}
}
@@ -0,0 +1,9 @@
(t2) {
respond 200 {
body {args[:]}
}
}
:8082 {
import t2 false
}
@@ -0,0 +1,9 @@
(t1) {
respond 200 {
body {args[:]}
}
}
:8081 {
import t1 false
}
@@ -0,0 +1,15 @@
(t1) {
respond 200 {
body {args[:]}
}
}
:8081 {
import t1 false
}
import import_variadic.txt
:8083 {
import t2 true
}
+33 -22
View File
@@ -23,12 +23,13 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
) )
func (st ServerType) buildTLSApp( func (st ServerType) buildTLSApp(
@@ -36,7 +37,6 @@ func (st ServerType) buildTLSApp(
options map[string]any, options map[string]any,
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddytls.TLS, []caddyconfig.Warning, error) { ) (*caddytls.TLS, []caddyconfig.Warning, error) {
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)} tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
var certLoaders []caddytls.CertificateLoader var certLoaders []caddytls.CertificateLoader
@@ -118,6 +118,11 @@ func (st ServerType) buildTLSApp(
ap.OnDemand = true ap.OnDemand = true
} }
// reuse private keys tls
if _, ok := sblock.pile["tls.reuse_private_keys"]; ok {
ap.ReusePrivateKeys = true
}
if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok { if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok {
ap.KeyType = keyTypeVals[0].Value.(string) ap.KeyType = keyTypeVals[0].Value.(string)
} }
@@ -206,8 +211,8 @@ func (st ServerType) buildTLSApp(
} }
// associate our new automation policy with this server block's hosts // associate our new automation policy with this server block's hosts
ap.Subjects = sblock.hostsFromKeysNotHTTP(httpPort) ap.SubjectsRaw = sblock.hostsFromKeysNotHTTP(httpPort)
sort.Strings(ap.Subjects) // solely for deterministic test results sort.Strings(ap.SubjectsRaw) // solely for deterministic test results
// if a combination of public and internal names were given // if a combination of public and internal names were given
// for this same server block and no issuer was specified, we // for this same server block and no issuer was specified, we
@@ -217,7 +222,11 @@ func (st ServerType) buildTLSApp(
var ap2 *caddytls.AutomationPolicy var ap2 *caddytls.AutomationPolicy
if len(ap.Issuers) == 0 { if len(ap.Issuers) == 0 {
var internal, external []string var internal, external []string
for _, s := range ap.Subjects { for _, s := range ap.SubjectsRaw {
// do not create Issuers for Tailscale domains; they will be given a Manager instead
if strings.HasSuffix(strings.ToLower(s), ".ts.net") {
continue
}
if !certmagic.SubjectQualifiesForCert(s) { if !certmagic.SubjectQualifiesForCert(s) {
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s) return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
} }
@@ -235,10 +244,10 @@ func (st ServerType) buildTLSApp(
} }
} }
if len(external) > 0 && len(internal) > 0 { if len(external) > 0 && len(internal) > 0 {
ap.Subjects = external ap.SubjectsRaw = external
apCopy := *ap apCopy := *ap
ap2 = &apCopy ap2 = &apCopy
ap2.Subjects = internal ap2.SubjectsRaw = internal
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)} ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
} }
} }
@@ -339,14 +348,14 @@ func (st ServerType) buildTLSApp(
for h := range httpsHostsSharedWithHostlessKey { for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h) al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) { if !certmagic.SubjectQualifiesForPublicCert(h) {
internalAP.Subjects = append(internalAP.Subjects, h) internalAP.SubjectsRaw = append(internalAP.SubjectsRaw, h)
} }
} }
} }
if len(al) > 0 { if len(al) > 0 {
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings) tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
} }
if len(internalAP.Subjects) > 0 { if len(internalAP.SubjectsRaw) > 0 {
if tlsApp.Automation == nil { if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig) tlsApp.Automation = new(caddytls.AutomationConfig)
} }
@@ -412,7 +421,7 @@ func (st ServerType) buildTLSApp(
// for convenience) // for convenience)
automationHostSet := make(map[string]struct{}) automationHostSet := make(map[string]struct{})
for _, ap := range tlsApp.Automation.Policies { for _, ap := range tlsApp.Automation.Policies {
for _, s := range ap.Subjects { for _, s := range ap.SubjectsRaw {
if _, ok := automationHostSet[s]; ok { if _, ok := automationHostSet[s]; ok {
return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s) return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
} }
@@ -533,7 +542,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
if automationPolicyIsSubset(aps[j], aps[i]) { if automationPolicyIsSubset(aps[j], aps[i]) {
return false return false
} }
return len(aps[i].Subjects) > len(aps[j].Subjects) return len(aps[i].SubjectsRaw) > len(aps[j].SubjectsRaw)
}) })
emptyAPCount := 0 emptyAPCount := 0
@@ -541,7 +550,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
// compute the number of empty policies (disregarding subjects) - see #4128 // compute the number of empty policies (disregarding subjects) - see #4128
emptyAP := new(caddytls.AutomationPolicy) emptyAP := new(caddytls.AutomationPolicy)
for i := 0; i < len(aps); i++ { for i := 0; i < len(aps); i++ {
emptyAP.Subjects = aps[i].Subjects emptyAP.SubjectsRaw = aps[i].SubjectsRaw
if reflect.DeepEqual(aps[i], emptyAP) { if reflect.DeepEqual(aps[i], emptyAP) {
emptyAPCount++ emptyAPCount++
if !automationPolicyHasAllPublicNames(aps[i]) { if !automationPolicyHasAllPublicNames(aps[i]) {
@@ -578,12 +587,14 @@ outer:
// eaten up by the one with subjects; and if both have subjects, we // eaten up by the one with subjects; and if both have subjects, we
// need to combine their lists // need to combine their lists
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) && if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) && bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
aps[i].MustStaple == aps[j].MustStaple && aps[i].MustStaple == aps[j].MustStaple &&
aps[i].KeyType == aps[j].KeyType && aps[i].KeyType == aps[j].KeyType &&
aps[i].OnDemand == aps[j].OnDemand && aps[i].OnDemand == aps[j].OnDemand &&
aps[i].ReusePrivateKeys == aps[j].ReusePrivateKeys &&
aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio { aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio {
if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 { if len(aps[i].SubjectsRaw) > 0 && len(aps[j].SubjectsRaw) == 0 {
// later policy (at j) has no subjects ("catch-all"), so we can // later policy (at j) has no subjects ("catch-all"), so we can
// remove the identical-but-more-specific policy that comes first // remove the identical-but-more-specific policy that comes first
// AS LONG AS it is not shadowed by another policy before it; e.g. // AS LONG AS it is not shadowed by another policy before it; e.g.
@@ -598,9 +609,9 @@ outer:
} }
} else { } else {
// avoid repeated subjects // avoid repeated subjects
for _, subj := range aps[j].Subjects { for _, subj := range aps[j].SubjectsRaw {
if !sliceContains(aps[i].Subjects, subj) { if !sliceContains(aps[i].SubjectsRaw, subj) {
aps[i].Subjects = append(aps[i].Subjects, subj) aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
} }
} }
aps = append(aps[:j], aps[j+1:]...) aps = append(aps[:j], aps[j+1:]...)
@@ -616,15 +627,15 @@ outer:
// automationPolicyIsSubset returns true if a's subjects are a subset // automationPolicyIsSubset returns true if a's subjects are a subset
// of b's subjects. // of b's subjects.
func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool { func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
if len(b.Subjects) == 0 { if len(b.SubjectsRaw) == 0 {
return true return true
} }
if len(a.Subjects) == 0 { if len(a.SubjectsRaw) == 0 {
return false return false
} }
for _, aSubj := range a.Subjects { for _, aSubj := range a.SubjectsRaw {
var inSuperset bool var inSuperset bool
for _, bSubj := range b.Subjects { for _, bSubj := range b.SubjectsRaw {
if certmagic.MatchWildcard(aSubj, bSubj) { if certmagic.MatchWildcard(aSubj, bSubj) {
inSuperset = true inSuperset = true
break break
@@ -662,7 +673,7 @@ func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) b
} }
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool { func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
for _, subj := range ap.Subjects { for _, subj := range ap.SubjectsRaw {
if !subjectQualifiesForPublicCert(ap, subj) { if !subjectQualifiesForPublicCert(ap, subj) {
return false return false
} }
+2 -2
View File
@@ -47,8 +47,8 @@ func TestAutomationPolicyIsSubset(t *testing.T) {
expect: false, expect: false,
}, },
} { } {
apA := &caddytls.AutomationPolicy{Subjects: test.a} apA := &caddytls.AutomationPolicy{SubjectsRaw: test.a}
apB := &caddytls.AutomationPolicy{Subjects: test.b} apB := &caddytls.AutomationPolicy{SubjectsRaw: test.b}
if actual := automationPolicyIsSubset(apA, apB); actual != test.expect { if actual := automationPolicyIsSubset(apA, apB); actual != test.expect {
t.Errorf("Test %d: Expected %t but got %t (A: %v B: %v)", i, test.expect, actual, test.a, test.b) t.Errorf("Test %d: Expected %t but got %t (A: %v B: %v)", i, test.expect, actual, test.a, test.b)
} }
+22 -5
View File
@@ -30,8 +30,14 @@ func init() {
caddy.RegisterModule(HTTPLoader{}) caddy.RegisterModule(HTTPLoader{})
} }
// HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config // HTTPLoader can load Caddy configs over HTTP(S).
// based on the Content-Type header of the HTTP response. //
// If the response is not a JSON config, a config adapter must be specified
// either in the loader config (`adapter`), or in the Content-Type HTTP header
// returned in the HTTP response from the server. The Content-Type header is
// read just like the admin API's `/load` endpoint. Uf you don't have control
// over the HTTP server (but can still trust its response), you can override
// the Content-Type header by setting the `adapter` property in this config.
type HTTPLoader struct { type HTTPLoader struct {
// The method for the request. Default: GET // The method for the request. Default: GET
Method string `json:"method,omitempty"` Method string `json:"method,omitempty"`
@@ -45,6 +51,11 @@ type HTTPLoader struct {
// Maximum time allowed for a complete connection and request. // Maximum time allowed for a complete connection and request.
Timeout caddy.Duration `json:"timeout,omitempty"` Timeout caddy.Duration `json:"timeout,omitempty"`
// The name of the config adapter to use, if any. Only needed
// if the HTTP response is not a JSON config and if the server's
// Content-Type header is missing or incorrect.
Adapter string `json:"adapter,omitempty"`
TLS *struct { TLS *struct {
// Present this instance's managed remote identity credentials to the server. // Present this instance's managed remote identity credentials to the server.
UseServerIdentity bool `json:"use_server_identity,omitempty"` UseServerIdentity bool `json:"use_server_identity,omitempty"`
@@ -108,7 +119,12 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
return nil, err return nil, err
} }
result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body) // adapt the config based on either manually-configured adapter or server's response header
ct := resp.Header.Get("Content-Type")
if hl.Adapter != "" {
ct = "text/" + hl.Adapter
}
result, warnings, err := adaptByContentType(ct, body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -124,6 +140,7 @@ func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response
if err != nil { if err != nil {
return nil, fmt.Errorf("problem calling http loader url: %v", err) return nil, fmt.Errorf("problem calling http loader url: %v", err)
} else if resp.StatusCode < 200 || resp.StatusCode > 499 { } else if resp.StatusCode < 200 || resp.StatusCode > 499 {
resp.Body.Close()
return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode) return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
} }
return resp, nil return resp, nil
@@ -134,16 +151,16 @@ func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http
var err error var err error
const maxAttempts = 10 const maxAttempts = 10
// attempt up to 10 times
for i := 0; i < maxAttempts; i++ { for i := 0; i < maxAttempts; i++ {
resp, err = attemptHttpCall(client, request) resp, err = attemptHttpCall(client, request)
if err != nil && i < maxAttempts-1 { if err != nil && i < maxAttempts-1 {
// wait 500ms before reattempting, or until context is done
select { select {
case <-time.After(time.Millisecond * 500): case <-time.After(time.Millisecond * 500):
case <-ctx.Done(): case <-ctx.Done():
return resp, ctx.Err() return resp, ctx.Err()
} }
} else {
break
} }
} }
+32 -30
View File
@@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -22,9 +23,10 @@ import (
"time" "time"
"github.com/aryann/difflib" "github.com/aryann/difflib"
"github.com/caddyserver/caddy/v2/caddyconfig"
caddycmd "github.com/caddyserver/caddy/v2/cmd" caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2/caddyconfig"
// plug in Caddy modules here // plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard" _ "github.com/caddyserver/caddy/v2/modules/standard"
) )
@@ -58,12 +60,11 @@ var (
type Tester struct { type Tester struct {
Client *http.Client Client *http.Client
configLoaded bool configLoaded bool
t *testing.T t testing.TB
} }
// NewTester will create a new testing client with an attached cookie jar // NewTester will create a new testing client with an attached cookie jar
func NewTester(t *testing.T) *Tester { func NewTester(t testing.TB) *Tester {
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
if err != nil { if err != nil {
t.Fatalf("failed to create cookiejar: %s", err) t.Fatalf("failed to create cookiejar: %s", err)
@@ -94,7 +95,6 @@ func timeElapsed(start time.Time, name string) {
// InitServer this will configure the server with a configurion of a specific // InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type. // type. The configType must be either "json" or the adapter type.
func (tc *Tester) InitServer(rawConfig string, configType string) { func (tc *Tester) InitServer(rawConfig string, configType string) {
if err := tc.initServer(rawConfig, configType); err != nil { if err := tc.initServer(rawConfig, configType); err != nil {
tc.t.Logf("failed to load config: %s", err) tc.t.Logf("failed to load config: %s", err)
tc.t.Fail() tc.t.Fail()
@@ -108,13 +108,12 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
// InitServer this will configure the server with a configurion of a specific // InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type. // type. The configType must be either "json" or the adapter type.
func (tc *Tester) initServer(rawConfig string, configType string) error { func (tc *Tester) initServer(rawConfig string, configType string) error {
if testing.Short() { if testing.Short() {
tc.t.SkipNow() tc.t.SkipNow()
return nil return nil
} }
err := validateTestPrerequisites() err := validateTestPrerequisites(tc.t)
if err != nil { if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err) tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil return nil
@@ -122,7 +121,6 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
tc.t.Cleanup(func() { tc.t.Cleanup(func() {
if tc.t.Failed() && tc.configLoaded { if tc.t.Failed() && tc.configLoaded {
res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
if err != nil { if err != nil {
tc.t.Log("unable to read the current config") tc.t.Log("unable to read the current config")
@@ -218,33 +216,49 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
if reflect.DeepEqual(expected, fetchConfig(client)) { if reflect.DeepEqual(expected, fetchConfig(client)) {
return nil return nil
} }
time.Sleep(10 * time.Millisecond) time.Sleep(1 * time.Second)
} }
tc.t.Errorf("POSTed configuration isn't active") tc.t.Errorf("POSTed configuration isn't active")
return errors.New("EnsureConfigRunning: POSTed configuration isn't active") return errors.New("EnsureConfigRunning: POSTed configuration isn't active")
} }
const initConfig = `{
admin localhost:2999
}
`
// validateTestPrerequisites ensures the certificates are available in the // validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running. // designated path and Caddy sub-process is running.
func validateTestPrerequisites() error { func validateTestPrerequisites(t testing.TB) error {
// check certificates are found // check certificates are found
for _, certName := range Default.Certifcates { for _, certName := range Default.Certifcates {
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) { if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("caddy integration test certificates (%s) not found", certName) return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
} }
} }
if isCaddyAdminRunning() != nil { if isCaddyAdminRunning() != nil {
// setup the init config file, and set the cleanup afterwards
f, err := os.CreateTemp("", "")
if err != nil {
return err
}
t.Cleanup(func() {
os.Remove(f.Name())
})
if _, err := f.WriteString(initConfig); err != nil {
return err
}
// start inprocess caddy server // start inprocess caddy server
os.Args = []string{"caddy", "run", "--config", "./test.init.config", "--adapter", "caddyfile"} os.Args = []string{"caddy", "run", "--config", f.Name(), "--adapter", "caddyfile"}
go func() { go func() {
caddycmd.Main() caddycmd.Main()
}() }()
// wait for caddy to start serving the initial config // wait for caddy to start serving the initial config
for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
time.Sleep(10 * time.Millisecond) time.Sleep(1 * time.Second)
} }
} }
@@ -267,7 +281,6 @@ func isCaddyAdminRunning() error {
} }
func getIntegrationDir() string { func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1) _, filename, _, ok := runtime.Caller(1)
if !ok { if !ok {
panic("unable to determine the current file path") panic("unable to determine the current file path")
@@ -287,7 +300,6 @@ func prependCaddyFilePath(rawConfig string) string {
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally // CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
func CreateTestingTransport() *http.Transport { func CreateTestingTransport() *http.Transport {
dialer := net.Dialer{ dialer := net.Dialer{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second, KeepAlive: 5 * time.Second,
@@ -315,7 +327,6 @@ func CreateTestingTransport() *http.Transport {
// AssertLoadError will load a config and expect an error // AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
tc := NewTester(t) tc := NewTester(t)
err := tc.initServer(rawConfig, configType) err := tc.initServer(rawConfig, configType)
@@ -326,7 +337,6 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
// AssertRedirect makes a request and asserts the redirection happens // AssertRedirect makes a request and asserts the redirection happens
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response { func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error { redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
} }
@@ -363,8 +373,7 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
} }
// CompareAdapt adapts a config and then compares it against an expected result // CompareAdapt adapts a config and then compares it against an expected result
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool { func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool {
cfgAdapter := caddyconfig.GetAdapter(adapterName) cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil { if cfgAdapter == nil {
t.Logf("unrecognized config adapter '%s'", adapterName) t.Logf("unrecognized config adapter '%s'", adapterName)
@@ -423,7 +432,7 @@ func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string,
} }
// AssertAdapt adapts a config and then tests it against an expected result // AssertAdapt adapts a config and then tests it against an expected result
func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) { func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) {
ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse) ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse)
if !ok { if !ok {
t.Fail() t.Fail()
@@ -432,7 +441,7 @@ func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedRes
// Generic request functions // Generic request functions
func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) { func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) {
requestContentType := "" requestContentType := ""
for _, requestHeader := range requestHeaders { for _, requestHeader := range requestHeaders {
arr := strings.SplitAfterN(requestHeader, ":", 2) arr := strings.SplitAfterN(requestHeader, ":", 2)
@@ -452,14 +461,13 @@ func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions // AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response { func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.Client.Do(req) resp, err := tc.Client.Do(req)
if err != nil { if err != nil {
tc.t.Fatalf("failed to call server %s", err) tc.t.Fatalf("failed to call server %s", err)
} }
if expectedStatusCode != resp.StatusCode { if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode) tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
} }
return resp return resp
@@ -467,7 +475,6 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
// AssertResponse request a URI and assert the status code and the body contains a string // AssertResponse request a URI and assert the status code and the body contains a string
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode) resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close() defer resp.Body.Close()
@@ -489,7 +496,6 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
// AssertGetResponse GET a URI and expect a statusCode and body text // AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil) req, err := http.NewRequest("GET", requestURI, nil)
if err != nil { if err != nil {
tc.t.Fatalf("unable to create request %s", err) tc.t.Fatalf("unable to create request %s", err)
@@ -500,7 +506,6 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
// AssertDeleteResponse request a URI and expect a statusCode and body text // AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil) req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil { if err != nil {
tc.t.Fatalf("unable to create request %s", err) tc.t.Fatalf("unable to create request %s", err)
@@ -511,7 +516,6 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
// AssertPostResponseBody POST to a URI and assert the response code and body // AssertPostResponseBody POST to a URI and assert the response code and body
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("POST", requestURI, requestBody) req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil { if err != nil {
tc.t.Errorf("failed to create request %s", err) tc.t.Errorf("failed to create request %s", err)
@@ -525,7 +529,6 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
// AssertPutResponseBody PUT to a URI and assert the response code and body // AssertPutResponseBody PUT to a URI and assert the response code and body
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PUT", requestURI, requestBody) req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil { if err != nil {
tc.t.Errorf("failed to create request %s", err) tc.t.Errorf("failed to create request %s", err)
@@ -539,7 +542,6 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
// AssertPatchResponseBody PATCH to a URI and assert the response code and body // AssertPatchResponseBody PATCH to a URI and assert the response code and body
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("PATCH", requestURI, requestBody) req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil { if err != nil {
tc.t.Errorf("failed to create request %s", err) tc.t.Errorf("failed to create request %s", err)
+206
View File
@@ -0,0 +1,206 @@
package integration
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"net"
"net/http"
"strings"
"testing"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
smallstepacme "github.com/smallstep/certificates/acme"
"go.uber.org/zap"
)
const acmeChallengePort = 9081
// Test the basic functionality of Caddy's ACME server
func TestACMEServerWithDefaults(t *testing.T) {
ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
}
acme.localhost {
acme_server
}
`, "caddyfile")
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
},
}
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating account key: %v", err)
}
account := acme.Account{
Contact: []string{"mailto:you@example.com"},
TermsOfServiceAgreed: true,
PrivateKey: accountPrivateKey,
}
account, err = client.NewAccount(ctx, account)
if err != nil {
t.Errorf("new account: %v", err)
return
}
// Every certificate needs a key.
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating certificate key: %v", err)
return
}
certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"})
if err != nil {
t.Errorf("obtaining certificate: %v", err)
return
}
// ACME servers should usually give you the entire certificate chain
// in PEM format, and sometimes even alternate chains! It's up to you
// which one(s) to store and use, but whatever you do, be sure to
// store the certificate and key somewhere safe and secure, i.e. don't
// lose them!
for _, cert := range certs {
t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM)
}
}
func TestACMEServerWithMismatchedChallenges(t *testing.T) {
ctx := context.Background()
logger := caddy.Log().Named("acmez")
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
}
acme.localhost {
acme_server {
challenges tls-alpn-01
}
}
`, "caddyfile")
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
},
}
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating account key: %v", err)
}
account := acme.Account{
Contact: []string{"mailto:you@example.com"},
TermsOfServiceAgreed: true,
PrivateKey: accountPrivateKey,
}
account, err = client.NewAccount(ctx, account)
if err != nil {
t.Errorf("new account: %v", err)
return
}
// Every certificate needs a key.
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating certificate key: %v", err)
return
}
certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"})
if len(certs) > 0 {
t.Errorf("expected '0' certificates, but received '%d'", len(certs))
}
if err == nil {
t.Error("expected errors, but received none")
}
const expectedErrMsg = "no solvers available for remaining challenges (configured=[http-01] offered=[tls-alpn-01] remaining=[tls-alpn-01])"
if !strings.Contains(err.Error(), expectedErrMsg) {
t.Errorf(`received error message does not match expectation: expected="%s" received="%s"`, expectedErrMsg, err.Error())
}
}
// naiveHTTPSolver is a no-op acmez.Solver for example purposes only.
type naiveHTTPSolver struct {
srv *http.Server
logger *zap.Logger
}
func (s *naiveHTTPSolver) Present(ctx context.Context, challenge acme.Challenge) error {
smallstepacme.InsecurePortHTTP01 = acmeChallengePort
s.srv = &http.Server{
Addr: fmt.Sprintf(":%d", acmeChallengePort),
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
s.logger.Info("received request on challenge server", zap.String("path", r.URL.Path))
if r.Method == "GET" && r.URL.Path == challenge.HTTP01ResourcePath() && strings.EqualFold(host, challenge.Identifier.Value) {
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(challenge.KeyAuthorization))
r.Close = true
s.logger.Info("served key authentication",
zap.String("identifier", challenge.Identifier.Value),
zap.String("challenge", "http-01"),
zap.String("remote", r.RemoteAddr),
)
}
}),
}
l, err := net.Listen("tcp", fmt.Sprintf(":%d", acmeChallengePort))
if err != nil {
return err
}
s.logger.Info("present challenge", zap.Any("challenge", challenge))
go s.srv.Serve(l)
return nil
}
func (s naiveHTTPSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error {
smallstepacme.InsecurePortHTTP01 = 0
s.logger.Info("cleanup", zap.Any("challenge", challenge))
if s.srv != nil {
s.srv.Close()
}
return nil
}
+209
View File
@@ -0,0 +1,209 @@
package integration
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"strings"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
)
func TestACMEServerDirectory(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
pki {
ca local {
name "Caddy Local Authority"
}
}
}
acme.localhost:9443 {
acme_server
}
`, "caddyfile")
tester.AssertGetResponse(
"https://acme.localhost:9443/acme/local/directory",
200,
`{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"}
`)
}
func TestACMEServerAllowPolicy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
pki {
ca local {
name "Caddy Local Authority"
}
}
}
acme.localhost {
acme_server {
challenges http-01
allow {
domains localhost
}
}
}
`, "caddyfile")
ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
},
}
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating account key: %v", err)
}
account := acme.Account{
Contact: []string{"mailto:you@example.com"},
TermsOfServiceAgreed: true,
PrivateKey: accountPrivateKey,
}
account, err = client.NewAccount(ctx, account)
if err != nil {
t.Errorf("new account: %v", err)
return
}
// Every certificate needs a key.
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating certificate key: %v", err)
return
}
{
certs, err := client.ObtainCertificate(
ctx,
account,
certPrivateKey,
[]string{"localhost"},
)
if err != nil {
t.Errorf("obtaining certificate for allowed domain: %v", err)
return
}
// ACME servers should usually give you the entire certificate chain
// in PEM format, and sometimes even alternate chains! It's up to you
// which one(s) to store and use, but whatever you do, be sure to
// store the certificate and key somewhere safe and secure, i.e. don't
// lose them!
for _, cert := range certs {
t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM)
}
}
{
_, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
if err == nil {
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
t.Logf("unexpected error: %v", err)
}
}
}
func TestACMEServerDenyPolicy(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
pki {
ca local {
name "Caddy Local Authority"
}
}
}
acme.localhost {
acme_server {
deny {
domains deny.localhost
}
}
}
`, "caddyfile")
ctx := context.Background()
logger, err := zap.NewDevelopment()
if err != nil {
t.Error(err)
return
}
client := acmez.Client{
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: logger,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
},
}
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating account key: %v", err)
}
account := acme.Account{
Contact: []string{"mailto:you@example.com"},
TermsOfServiceAgreed: true,
PrivateKey: accountPrivateKey,
}
account, err = client.NewAccount(ctx, account)
if err != nil {
t.Errorf("new account: %v", err)
return
}
// Every certificate needs a key.
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Errorf("generating certificate key: %v", err)
return
}
{
_, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"deny.localhost"})
if err == nil {
t.Errorf("obtaining certificate for 'deny.localhost' domain")
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
t.Logf("unexpected error: %v", err)
}
}
}
@@ -0,0 +1,65 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
challenges dns-01
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"challenges": [
"dns-01"
],
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}
@@ -0,0 +1,62 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
challenges
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}
@@ -0,0 +1,108 @@
{
pki {
ca internal {
name "Internal"
root_cn "Internal Root Cert"
intermediate_cn "Internal Intermediate Cert"
}
ca internal-long-lived {
name "Long-lived"
root_cn "Internal Root Cert 2"
intermediate_cn "Internal Intermediate Cert 2"
}
}
}
acme-internal.example.com {
acme_server {
ca internal
}
}
acme-long-lived.example.com {
acme_server {
ca internal-long-lived
lifetime 7d
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme-long-lived.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal-long-lived",
"handler": "acme_server",
"lifetime": 604800000000000
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"acme-internal.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "internal",
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"internal": {
"name": "Internal",
"root_common_name": "Internal Root Cert",
"intermediate_common_name": "Internal Intermediate Cert"
},
"internal-long-lived": {
"name": "Long-lived",
"root_common_name": "Internal Root Cert 2",
"intermediate_common_name": "Internal Intermediate Cert 2"
}
}
}
}
}
@@ -0,0 +1,66 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
challenges dns-01 http-01
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"challenges": [
"dns-01",
"http-01"
],
"handler": "acme_server"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}
@@ -0,0 +1,37 @@
:8443 {
tls internal {
on_demand
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8443"
],
"tls_connection_policies": [
{}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"module": "internal"
}
],
"on_demand": true
}
]
}
}
}
}
@@ -11,6 +11,7 @@ encode gzip zstd {
header Content-Type application/xhtml+xml* header Content-Type application/xhtml+xml*
header Content-Type application/atom+xml* header Content-Type application/atom+xml*
header Content-Type application/rss+xml* header Content-Type application/rss+xml*
header Content-Type application/wasm*
header Content-Type image/svg+xml* header Content-Type image/svg+xml*
} }
} }
@@ -47,6 +48,7 @@ encode {
"application/xhtml+xml*", "application/xhtml+xml*",
"application/atom+xml*", "application/atom+xml*",
"application/rss+xml*", "application/rss+xml*",
"application/wasm*",
"image/svg+xml*" "image/svg+xml*"
] ]
}, },
@@ -0,0 +1,245 @@
foo.localhost {
root * /srv
error /private* "Unauthorized" 410
error /fivehundred* "Internal Server Error" 500
handle_errors 5xx {
respond "Error In range [500 .. 599]"
}
handle_errors 410 {
respond "404 or 410 error"
}
}
bar.localhost {
root * /srv
error /private* "Unauthorized" 410
error /fivehundred* "Internal Server Error" 500
handle_errors 5xx {
respond "Error In range [500 .. 599] from second site"
}
handle_errors 410 {
respond "404 or 410 error from second site"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"foo.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Internal Server Error",
"handler": "error",
"status_code": 500
}
],
"match": [
{
"path": [
"/fivehundred*"
]
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"bar.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Internal Server Error",
"handler": "error",
"status_code": 500
}
],
"match": [
{
"path": [
"/fivehundred*"
]
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"foo.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "404 or 410 error",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} in [410]"
}
]
},
{
"handle": [
{
"body": "Error In range [500 .. 599]",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"bar.localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "404 or 410 error from second site",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} in [410]"
}
]
},
{
"handle": [
{
"body": "Error In range [500 .. 599] from second site",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,120 @@
{
http_port 3010
}
localhost:3010 {
root * /srv
error /private* "Unauthorized" 410
error /hidden* "Not found" 404
handle_errors 4xx {
respond "Error in the [400 .. 499] range"
}
}
----------
{
"apps": {
"http": {
"http_port": 3010,
"servers": {
"srv0": {
"listen": [
":3010"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
},
{
"handle": [
{
"error": "Not found",
"handler": "error",
"status_code": 404
}
],
"match": [
{
"path": [
"/hidden*"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Error in the [400 .. 499] range",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,153 @@
{
http_port 2099
}
localhost:2099 {
root * /srv
error /private* "Unauthorized" 410
error /threehundred* "Moved Permanently" 301
error /internalerr* "Internal Server Error" 500
handle_errors 500 3xx {
respond "Error code is equal to 500 or in the [300..399] range"
}
handle_errors 4xx {
respond "Error in the [400 .. 499] range"
}
}
----------
{
"apps": {
"http": {
"http_port": 2099,
"servers": {
"srv0": {
"listen": [
":2099"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Moved Permanently",
"handler": "error",
"status_code": 301
}
],
"match": [
{
"path": [
"/threehundred*"
]
}
]
},
{
"handle": [
{
"error": "Internal Server Error",
"handler": "error",
"status_code": 500
}
],
"match": [
{
"path": [
"/internalerr*"
]
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Error in the [400 .. 499] range",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
}
]
},
{
"handle": [
{
"body": "Error code is equal to 500 or in the [300..399] range",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 300 \u0026\u0026 {http.error.status_code} \u003c= 399 || {http.error.status_code} in [500]"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,120 @@
{
http_port 3010
}
localhost:3010 {
root * /srv
error /private* "Unauthorized" 410
error /hidden* "Not found" 404
handle_errors 404 410 {
respond "404 or 410 error"
}
}
----------
{
"apps": {
"http": {
"http_port": 3010,
"servers": {
"srv0": {
"listen": [
":3010"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
},
{
"handle": [
{
"error": "Not found",
"handler": "error",
"status_code": 404
}
],
"match": [
{
"path": [
"/hidden*"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "404 or 410 error",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} in [404, 410]"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -0,0 +1,148 @@
{
http_port 2099
}
localhost:2099 {
root * /srv
error /private* "Unauthorized" 410
error /hidden* "Not found" 404
error /internalerr* "Internal Server Error" 500
handle_errors {
respond "Fallback route: code outside the [400..499] range"
}
handle_errors 4xx {
respond "Error in the [400 .. 499] range"
}
}
----------
{
"apps": {
"http": {
"http_port": 2099,
"servers": {
"srv0": {
"listen": [
":2099"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "vars",
"root": "/srv"
}
]
},
{
"handle": [
{
"error": "Internal Server Error",
"handler": "error",
"status_code": 500
}
],
"match": [
{
"path": [
"/internalerr*"
]
}
]
},
{
"handle": [
{
"error": "Unauthorized",
"handler": "error",
"status_code": 410
}
],
"match": [
{
"path": [
"/private*"
]
}
]
},
{
"handle": [
{
"error": "Not found",
"handler": "error",
"status_code": 404
}
],
"match": [
{
"path": [
"/hidden*"
]
}
]
}
]
}
],
"terminal": true
}
],
"errors": {
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Error in the [400 .. 499] range",
"handler": "static_response"
}
],
"match": [
{
"expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
}
]
},
{
"handle": [
{
"body": "Fallback route: code outside the [400..499] range",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
}
@@ -69,11 +69,14 @@
} }
], ],
"on_demand": { "on_demand": {
"permission": {
"endpoint": "https://example.com",
"module": "http"
},
"rate_limit": { "rate_limit": {
"interval": 30000000000, "interval": 30000000000,
"burst": 20 "burst": 20
}, }
"ask": "https://example.com"
} }
}, },
"disable_ocsp_stapling": true "disable_ocsp_stapling": true
@@ -78,11 +78,14 @@
} }
], ],
"on_demand": { "on_demand": {
"permission": {
"endpoint": "https://example.com",
"module": "http"
},
"rate_limit": { "rate_limit": {
"interval": 30000000000, "interval": 30000000000,
"burst": 20 "burst": 20
}, }
"ask": "https://example.com"
}, },
"ocsp_interval": 172800000000000, "ocsp_interval": 172800000000000,
"renew_interval": 86400000000000, "renew_interval": 86400000000000,
@@ -71,11 +71,14 @@
} }
], ],
"on_demand": { "on_demand": {
"permission": {
"endpoint": "https://example.com",
"module": "http"
},
"rate_limit": { "rate_limit": {
"interval": 30000000000, "interval": 30000000000,
"burst": 20 "burst": 20
}, }
"ask": "https://example.com"
} }
} }
} }
@@ -0,0 +1,36 @@
{
http_port 8080
persist_config off
admin {
origins localhost:2019 [::1]:2019 127.0.0.1:2019 192.168.10.128
}
}
:80
----------
{
"admin": {
"listen": "localhost:2019",
"origins": [
"localhost:2019",
"[::1]:2019",
"127.0.0.1:2019",
"192.168.10.128"
],
"config": {
"persist": false
}
},
"apps": {
"http": {
"http_port": 8080,
"servers": {
"srv0": {
"listen": [
":80"
]
}
}
}
}
}
@@ -0,0 +1,25 @@
{
persist_config off
}
:8881 {
}
----------
{
"admin": {
"config": {
"persist": false
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
]
}
}
}
}
}
@@ -11,9 +11,13 @@
idle 30s idle 30s
} }
max_header_size 100MB max_header_size 100MB
enable_full_duplex
log_credentials log_credentials
protocols h1 h2 h2c h3 protocols h1 h2 h2c h3
strict_sni_host strict_sni_host
trusted_proxies static private_ranges
client_ip_headers Custom-Real-Client-IP X-Forwarded-For
client_ip_headers A-Third-One
} }
} }
@@ -42,6 +46,7 @@ foo.com {
"write_timeout": 30000000000, "write_timeout": 30000000000,
"idle_timeout": 30000000000, "idle_timeout": 30000000000,
"max_header_bytes": 100000000, "max_header_bytes": 100000000,
"enable_full_duplex": true,
"routes": [ "routes": [
{ {
"match": [ "match": [
@@ -55,6 +60,22 @@ foo.com {
} }
], ],
"strict_sni_host": true, "strict_sni_host": true,
"trusted_proxies": {
"ranges": [
"192.168.0.0/16",
"172.16.0.0/12",
"10.0.0.0/8",
"127.0.0.1/8",
"fd00::/8",
"::1"
],
"source": "static"
},
"client_ip_headers": [
"Custom-Real-Client-IP",
"X-Forwarded-For",
"A-Third-One"
],
"logs": { "logs": {
"should_log_credentials": true "should_log_credentials": true
}, },
@@ -0,0 +1,78 @@
:8881 {
route {
handle /foo/* {
respond "Foo"
}
handle {
respond "Bar"
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Foo",
"handler": "static_response"
}
]
}
]
}
],
"match": [
{
"path": [
"/foo/*"
]
}
]
},
{
"group": "group2",
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Bar",
"handler": "static_response"
}
]
}
]
}
]
}
]
}
]
}
]
}
}
}
}
}
@@ -17,6 +17,8 @@
+Link "Foo" +Link "Foo"
+Link "Bar" +Link "Bar"
} }
header >Set Defer
header >Replace Deferred Replacement
} }
---------- ----------
{ {
@@ -136,6 +138,31 @@
] ]
} }
} }
},
{
"handler": "headers",
"response": {
"deferred": true,
"set": {
"Set": [
"Defer"
]
}
}
},
{
"handler": "headers",
"response": {
"deferred": true,
"replace": {
"Replace": [
{
"replace": "Replacement",
"search_regexp": "Deferred"
}
]
}
}
} }
] ]
} }
@@ -0,0 +1,50 @@
example.com {
respond <<EOF
<html>
<head><title>Foo</title>
<body>Foo</body>
</html>
EOF 200
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "\u003chtml\u003e\n \u003chead\u003e\u003ctitle\u003eFoo\u003c/title\u003e\n \u003cbody\u003eFoo\u003c/body\u003e\n\u003c/html\u003e",
"handler": "static_response",
"status_code": 200
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}

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