Compare commits

..

1064 Commits

Author SHA1 Message Date
Matthew Holt c827a71d5d Version 0.8.2 2016-02-25 10:26:42 -07:00
Matthew Holt 2ecc837020 templates: .Truncate can truncate from end of string if length is negative 2016-02-24 20:32:26 -07:00
Matthew Holt c37ad7f677 Only write error message/page if body not already written (fixes #567)
Based on work started in, and replaces, #614
2016-02-24 19:50:46 -07:00
Matthew Holt 737c7c4372 fastcgi: Only perform extra copy if necessary; added tests 2016-02-24 16:42:01 -07:00
Matt Holt 367397dbd6 Merge pull request #623 from xlab/f/docflags
markdown: Implement .DocFlags and tests
2016-02-24 14:45:22 -07:00
Matt Holt a2dbfdc10e Merge pull request #636 from humboldtux/fastcgi-cl
fastcgi: Explicitly set Content-Length (fixes #626)
2016-02-24 14:39:15 -07:00
Benoit Benedetti ef5f9c771d FastCGI: Explicitly set Content-Length #626 2016-02-24 22:11:07 +01:00
Matthew Holt 05957b4965 gzip: Implement http.Hijacker (fixes #635) 2016-02-24 12:23:50 -07:00
Matt Holt 72fcdec8d8 Merge pull request #627 from ax-nathan/master
rewrite: Allow status to be 2xx or 4xx.
2016-02-24 11:01:54 -07:00
Nathan Probst f4bb43781c Remove unneeded Regexp from tests. 2016-02-24 10:28:06 -07:00
Matt Holt 197297b0d7 Merge pull request #628 from elcore/master
tls: Add SHA384 ciphers
2016-02-23 15:14:24 -07:00
elcore a541eb7899 Adding new cipher suites 2016-02-23 21:23:13 +00:00
Nathan Probst 2ea6c95ac4 Allow rewrite status codes to be 2xx and 4xx. 2016-02-22 15:30:55 -07:00
Maxim Kupriianov c7674e2060 Implement .DocFlags directive and tests. It holds all the boolean-typed front matter values. 2016-02-22 13:53:47 +03:00
Matt Holt c12847e5ba Merge pull request #620 from humboldtux/recorder
middleware: Export ResponseRecorder and add a couple getter methods
2016-02-20 15:17:45 -07:00
Benoit Benedetti bec130a563 Recorder: Exporting ResponseRecorder #614 2016-02-20 23:10:04 +01:00
Matthew Holt 09b7ce6c93 Try to get Go 1.6 on appveyor 2016-02-19 18:07:48 -07:00
Matt Holt b860be01bb Merge pull request #615 from 1lann/master
errors: Set missing Content-Type for plaintext error messages
2016-02-19 14:52:43 -07:00
Matthew Holt f7b5187bf3 server: Add "Referer" to log entry when host not found 2016-02-19 13:34:54 -07:00
Matthew Holt 09a7af8cae https: Wait as long as possible to create ACME client at startup (fixes #617) 2016-02-19 10:33:01 -07:00
Jason Chu 5f2670fdde Fix missing Content-Type for certain errors
And corrected an error in a copy and pasted comment
2016-02-20 00:42:17 +08:00
Matthew Holt ecf913e58d Update change log 2016-02-18 20:57:38 -07:00
Matthew Holt d05f89294e https: Minor refactoring and some new tests 2016-02-18 20:33:15 -07:00
Matthew Holt 1ef7f3c4b1 Remove path scoping for middleware slice
It was implemented for almost a year but we'll probably never use it, especially since we'll match more than the path in the future.
2016-02-17 18:11:03 -07:00
Matthew Holt f25ae8230f Move to Go 1.6 and set CGO_ENABLED=0 in tests 2016-02-17 16:08:25 -07:00
Matthew Holt 1cfd960f3c Bug fixes and other improvements to TLS functions
Now attempt to staple OCSP even for certs that don't have an existing staple (issue #605). "tls off" short-circuits tls setup function. Now we call getEmail() when setting up an acme.Client that does renewals, rather than making a new account with empty email address. Check certificate expiry every 12 hours, and OCSP every hour.
2016-02-15 23:39:04 -07:00
Matt Holt 2dba44327a Merge pull request #600 from jacobhands/betterlogging
Rotate process log
2016-02-14 23:32:50 -07:00
Matthew Holt cae9f7de9c gofmt -s; fix misspellings and lint; Go 1.5.3 in Travis CI 2016-02-14 00:10:57 -07:00
Matthew Holt a11e14aca8 Fix HTTPS config for empty/no Caddyfile
This fixes a regression introduced in recent commits that enabled TLS on the default ":2015" config. This fix is possible because On-Demand TLS is no longer implicit; it must be explicitly enabled by the user by setting a maximum number of certificates to issue.
2016-02-12 13:04:24 -07:00
Jacob Hands dc63e50172 Use rotating log files 2016-02-12 08:30:47 -06:00
Matthew Holt 04c7c442c5 https: Only create ACMEClient if it's actually going to be used
Otherwise it tries to create an account and stuff at first start, even without a Caddyfile or when serving localhost.
2016-02-11 16:20:59 -07:00
Matthew Holt 7bd2adf0dc Fix edge case related to reloaded configs and ACME challenge
If Caddy is running but not listening on port 80, reloading Caddy with a new Caddyfile that needs to obtain a TLS cert from the CA would fail, because it was just assumed that, if reloading, port 80 as already in use. That is not always the case, so we scan the servers to see if one of them is listening on port 80, and we configure the ACME client accordingly. Kind of a hack... but it works.
2016-02-11 15:37:51 -07:00
Matthew Holt 1fe39e4633 Additional mitigation for on-demand TLS
After 10 certificates are issued, no new certificate requests are allowed for 10 minutes after a successful issuance.
2016-02-11 14:27:57 -07:00
Matthew Holt 216a617249 tls: Some bug fixes, basic rate limiting, max_certs setting 2016-02-11 13:48:52 -07:00
Matthew Holt d25a3e95e4 Merge branch 'master' into getcertificate 2016-02-11 00:06:23 -07:00
Matthew Holt 11103bd8d6 Major refactor of all HTTPS/TLS/ACME code
Biggest change is no longer using standard library's tls.Config.getCertificate function to get a certificate during TLS handshake. Implemented our own cache which can be changed dynamically at runtime, even during TLS handshakes. As such, restarts are no longer required after certificate renewals or OCSP updates.

We also allow loading multiple certificates and keys per host, even by specifying a directory (tls got a new 'load' command for that).

Renamed the letsencrypt package to https in a gradual effort to become more generic; and https is more fitting for what the package does now.

There are still some known bugs, e.g. reloading where a new certificate is required but port 80 isn't currently listening, will cause the challenge to fail. There's still plenty of cleanup to do and tests to write. It is especially confusing right now how we enable "on-demand" TLS during setup and keep track of that. But this change should basically work so far.
2016-02-11 00:06:05 -07:00
Matt Holt f1ba7fa343 Merge pull request #467 from eiszfuchs/feature/proxy-socket
proxy: Support unix sockets
2016-02-10 11:52:57 -07:00
eiszfuchs 7091a2090b created http.Transport and tests for unix sockets 2016-02-10 19:45:31 +01:00
Matt Holt 57ffe5a619 Merge pull request #591 from Barberrrry/master
fastcgi: New function DialWithDialer to create FCGIClient with custom Dialer.
2016-02-10 09:06:29 -07:00
Vadim Petrov b1208d3fdf New function DialWithDialer to create FCGIClient with custom Dialer. 2016-02-10 18:03:43 +03:00
Matt Holt b089d14b67 Merge pull request #548 from captncraig/register
Making directives externally registerable
2016-02-05 10:10:10 -07:00
Craig Peterson e72fc20c78 making directives externally registerable 2016-02-05 10:03:20 -07:00
Matt Holt 5b7e0361dd Merge pull request #573 from miekg/markdown-directive
templates: Add .Markdown directive
2016-02-04 13:59:34 -07:00
Miek Gieben 86f36bdb61 Add .Markdown directive
This allows any template to use:
{{.Markdown "filename"}} which will convert the markdown contents
of filename to HTML and then include the HTML in the template.
2016-02-04 20:54:19 +00:00
Matt Holt 3278106421 Merge pull request #572 from miekg/markdown-def-lists
markdown: enable definition lists
2016-02-04 10:20:17 -07:00
Matt Holt f9b8e31ad7 Merge pull request #571 from incon/log-remote-host
When the requested host is not found, log the remote host.
2016-02-04 07:42:05 -07:00
Miek Gieben fbdfc979ec Markdown: enable definition lists 2016-02-04 11:21:44 +00:00
David Darrell 2acaf2fa6f Move logic to split the port to only happen when the host is not found. 2016-02-04 16:17:10 +08:00
David Darrell f4fcfa8793 When the requested host is not found log the remote host. 2016-02-04 12:46:24 +08:00
Matt Holt 79db939259 Merge pull request #551 from denquixote/acmehandlehost
letsencrypt: properly retrieve hostname from request.
2016-02-01 09:23:03 -07:00
Matt Holt f9b6ede92b Merge pull request #543 from DenBeke/master
fastcgi: IPv6 when parsing r.RemoteAddr
2016-02-01 08:26:21 -07:00
Abiola Ibrahim 184abe3bc8 Merge pull request #561 from DenBeke/fix-vet
basicauth: fixed 'go vet' printing function value
2016-02-01 11:31:04 +01:00
MathiasB fde9bbeb32 basicauth: fixed 'go vet' printing function value 2016-02-01 11:17:16 +01:00
MathiasB c59fd1c76e Defined test function in TestBuildEnv 2016-02-01 09:39:13 +01:00
Matthew Holt c5983e305f Merge branch 'master' of github.com:mholt/caddy 2016-01-30 21:05:18 -07:00
Den Quixote 8d057c8614 letsencrypt: properly retrieve hostname from request. 2016-01-30 02:20:34 +01:00
MathiasB ac197f1694 FastCGI: some simple tests for buildEnv
More tests are needed for the other environmental variables.
These tests were specifically made for testing of IP addresses.
2016-01-29 11:46:15 +01:00
MathiasB d8be787f39 FastCGI: IPv6 when parsing r.RemoteAddr 2016-01-28 15:26:33 +01:00
Matt Holt a8c8b48390 Merge pull request #542 from cudevmaxwell/master
fastcgi: Parse address from fastcgi directive, pass results to Dial()
2016-01-28 00:21:06 -07:00
Kevin Bowrin 4d4ea94465 Parse address from fastcgi directive, and pass results to fcgiclient Dial().
This allows scheme prefixes "tcp://" and "fastcgi://" in configuration.

Fixes #540
2016-01-28 00:19:00 -05:00
Matt Holt aeaf58b16a Merge pull request #541 from jungle-boogie/patch-5
wrap lines to 80
2016-01-27 12:34:38 -07:00
jungle-boogie 73ed286309 wrap lines to 80
also update copyright year.
2016-01-27 11:28:49 -08:00
Matthew Holt 9e900b0a08 godoc 2016-01-25 20:45:23 -07:00
Matthew Holt f1b2637d44 letsencrypt: Enable activation on empty hosts; fix email bug 2016-01-25 20:21:08 -07:00
Matthew Holt 178c4d11d9 Merge branch 'master' into getcertificate 2016-01-25 13:47:13 -07:00
Matthew Holt 7613ae3bf0 Change to Windows line endings for poor Notepad 2016-01-25 13:46:07 -07:00
Matthew Holt ad664e5bba browse: Render names with multiple consecutive spaces correctly 2016-01-25 13:43:35 -07:00
Matthew Holt cf06abd691 Log restarts 2016-01-25 13:41:51 -07:00
Matthew Holt a6abec8210 letsencrypt: Update to match upstream refactor
Challenge names now have their own type and constants
2016-01-25 08:47:28 -07:00
Matthew Holt 82b049229b proxy: Add basic proxying test and InsecureSkipVerify transport test 2016-01-23 10:14:13 -07:00
Matt Holt fae612d53b Merge pull request #529 from FiloSottile/filippo/insecure
proxy: add a insecure_skip_verify option - closes #320
2016-01-23 09:13:30 -07:00
Filippo Valsorda bae4ac9764 proxy: add a insecure_skip_verify option - closes #320 2016-01-23 03:26:31 +00:00
Matt Holt 6e340cb1d6 Merge pull request #514 from upsuper/fix-user-key-perm
letsencrypt: Fix perm of user key
2016-01-16 00:46:45 -07:00
Xidorn Quan 0d8d0ba5a0 letsencrypt: Fix perm of user key 2016-01-16 13:12:03 +11:00
Matt Holt 8655ea671b Merge pull request #510 from alehano/master
gzip: Add .svg to default ext list
2016-01-15 15:33:15 -07:00
Matthew Holt 0d8526b7d9 gzip: Allow empty extension (#509) 2016-01-15 11:59:05 -07:00
Alexey Khalyapin c9e0517e5e Add .svg to default ext list in gzip middleware 2016-01-14 11:52:03 +03:00
Matthew Holt e74558eaea browse: Fix for files and dirs with ':' in name 2016-01-13 13:17:58 -07:00
Matthew Holt b0ccab7b4a tls: Fix failing test 2016-01-13 09:24:03 -07:00
Matthew Holt 47079c3d24 PoC: on-demand TLS
Implements "on-demand TLS" as I call it, which means obtaining TLS certificates on-the-fly during TLS handshakes if a certificate for the requested hostname is not already available. Only the first request for a new hostname will experience higher latency; subsequent requests will get the new certificates right out of memory.

Code still needs lots of cleanup but the feature is basically working.
2016-01-13 00:32:46 -07:00
Matthew Holt b4cab78bec Starting transition to Go 1.6 (http2 compatibility)
I've built this on Go 1.6 beta 1 and made some changes to be more compatible. Namely, I removed the use of the /x/net/http2 package and let net/http enable h2 by default; updated the way h2 is disabled (if the user requires it); moved TLS_FALLBACK_SCSV to the front of the cipher suites list (all values not accepted by http2 must go after those allowed by it); removed the NextProto default of http/1.1; set the http.Server.TLSConfig value to the TLS config used by the listener (we left it nil before, but this prevents automatic enabling of h2).

It is very likely there is more to do, but at least already Caddy uses HTTP/2 when built with Go 1.6.
2016-01-13 00:29:22 -07:00
Matthew Holt 3c96718027 Version 0.8.1 2016-01-12 10:01:57 -07:00
Matthew Holt 4b6e0e9369 tls: Fix failing test 2016-01-12 08:55:01 -07:00
Matthew Holt 2bcbdd6a17 Merge branch 'getcertificate' 2016-01-12 08:52:52 -07:00
Matthew Holt 8f2196c047 tls: No arguments to directive and no block is an error 2016-01-12 08:52:43 -07:00
Matthew Holt c7d4d051cb letsencrypt: Ensure no prompt if user is not there
Also only set custom address if alternate port is specified (rather than using a blank address; just cleaner this way)
2016-01-12 08:52:08 -07:00
Matt Holt e283af4d9b Merge pull request #502 from abiosoft/gzip-fix
gzip: Fix for wrong content-type when templates is used.
2016-01-12 08:32:09 -07:00
Abiola Ibrahim 12cd2d528c Gzip: Fix for wrong content-type when templates is used. 2016-01-12 15:06:08 +01:00
Matthew Holt 8a6c778c8d Log if host is not configured for server 2016-01-11 09:07:14 -07:00
Matthew Holt 77eae62d9f letsencrypt: Don't prompt if user is not there
This change fixes the scenario where you reload the config and it tries to obtain a cert from the ACME server, but no email address is found or terms have not been agreed to in-process. This is unfortunate but it should not stop the server from reloading, so we assume empty email address in this case.
2016-01-10 23:40:55 -07:00
Matthew Holt 97c8c9582a Updated changelog 2016-01-10 21:52:56 -07:00
Matthew Holt ed0c0db6a3 If scheme and port defy convention, it is an error
This prevents serving HTTPS over port 80 or HTTP over 443. It's confusing and we don't allow it.
2016-01-10 20:51:50 -07:00
Matthew Holt 202849055c tls: Extra requirements to set port to 443
It is unexpected to serve localhost on port 443 or any server on 443 if TLS is disabled, even if the port is blank. Also don't warn about how to force TLS on the HTTP port.
2016-01-10 20:47:43 -07:00
Matthew Holt 060ab92d29 Reorder a few things
The docs link to this structure and all its methods related to the browse template; keeping them together makes it possible to link to the whole block of code that is relevant.
2016-01-10 09:47:26 -07:00
Matthew Holt 0830c728fe Remove pidfile when program exits (closes #495) 2016-01-09 21:48:07 -07:00
Matthew Holt dab679df86 import: Fix multiple imports (closes #480) 2016-01-09 10:52:21 -07:00
Matthew Holt 9453224639 Merge branch 'letsencryptfix' 2016-01-08 23:59:15 -07:00
Matthew Holt fd1765973a letsencrypt: Tests for handler that proxies challenge requests
Doesn't test the SkipInsecureVerify proxy setting, but that can be done at another time.
2016-01-08 16:44:54 -07:00
Matthew Holt 0efe39a705 Switch back to LE production endpoint 2016-01-08 14:13:21 -07:00
Matthew Holt a3f3bc67e1 Merge branch 'browse-tpl' 2016-01-08 14:02:55 -07:00
Matthew Holt 8b93bfe751 letsencrypt: More tests! \o/ 2016-01-08 13:49:06 -07:00
Matthew Holt 897b6c5b0e letsencrypt: More tests, other minor improvements 2016-01-08 12:32:47 -07:00
Matthew Holt fc928e0b3b letsencrypt: Couple minor refactors/fixes 2016-01-08 11:55:31 -07:00
Matthew Holt 93b301372b rewrite: Fix a lint suggestion 2016-01-06 22:23:18 -07:00
Matthew Holt ce4981d046 Merge branch 'master' into letsencryptfix
# Conflicts:
#	caddy/letsencrypt/letsencrypt.go
#	caddy/letsencrypt/letsencrypt_test.go
2016-01-06 22:21:15 -07:00
Matthew Holt 62b210b544 browse: Minor changes to improve icon positioning in template
Also make sure column header cells don't wrap
2016-01-06 22:18:17 -07:00
Matt Holt 5f6a0a4c0b Merge pull request #479 from abiosoft/rewrite-patch
rewrite: not_has, not_match, multiple to, and status codes
2016-01-06 22:08:16 -07:00
Matt Holt cae9880800 Merge pull request #483 from benschumacher/master
letsencrypt: Make plaintext redirect hosts honor the Bind settings
2016-01-06 22:06:53 -07:00
Matthew Holt 6d49392602 Improve tests, fix a few lint warnings 2016-01-06 16:04:33 -07:00
Matthew Holt 4593982065 letsencrypt: Major refactor of Activate(), fixes #474 and closes #397
Makes restarts cleaner and improves configuration usability related to the tls directive
2016-01-06 16:04:08 -07:00
Matthew Holt 94100a7ba6 Fix failing tests 2016-01-04 17:34:10 -07:00
Matthew Holt e9c2e50684 Merge branch 'master' into letsencryptfix
# Conflicts:
#	caddy/letsencrypt/letsencrypt.go
2016-01-03 23:56:22 -07:00
Ben Schumacher 82b0c0b9eb Make Let's Encrypt module honor the Bind settings 2016-01-03 23:56:14 -07:00
Matthew Holt 55601d3ec2 letsencrypt: Fix OCSP stapling and restarts with new LE-capable hosts
Before, Caddy couldn't support graceful (zero-downtime) restarts when the reloaded Caddyfile had a host in it that was elligible for a LE certificate because the port was already in use. This commit makes it possible to do zero-downtime reloads and issue certificates for new hosts that need it. Supports only http-01 challenge at this time.

OCSP stapling is improved in that it updates before the expiration time when the validity window has shifted forward. See 30c949085c. Before it only used to update when the status changed.

This commit also sets the user agent for Let's Encrypt requests with a string containing "Caddy".
2016-01-03 17:05:10 -07:00
Matthew Holt 829a0f34d0 Preserve and clean up original host input in Caddyfile-JSON conversions 2016-01-03 16:46:26 -07:00
Matthew Holt bb80f99190 tls: Allow opening block without specifying cert+key args 2016-01-03 16:44:30 -07:00
Matthew Holt 946ff5e87b Parser separate scheme/port, refactor config loading
By separating scheme and port at the parser, we are able to set the port appropriately and also keep the semantics of the scheme being specified by the user later on. The parser also stores an address' original input. Also, the config refactor makes it possible to partially load a config - valuable for determining which ones will need Let's Encrypt integration turned on during a restart.
2016-01-03 16:41:29 -07:00
Abiola Ibrahim 0a04fa40f4 Oops. status code check should be after all validations. 2016-01-02 08:08:55 +01:00
Abiola Ibrahim 48d7f1ead2 Refactor. Stop useless rewrite if status code is set. 2016-01-01 07:05:30 +01:00
Abiola Ibrahim be2f5c4b38 Support for 4xx status codes. 2015-12-31 23:19:11 +01:00
Abiola Ibrahim 281007c482 Merge remote-tracking branch 'upstream/master' into rewrite-patch 2015-12-31 20:34:25 +01:00
Matthew Holt b6326d402d Fix for case-insensitive header replacements (#476) 2015-12-31 12:31:30 -07:00
Matthew Holt e2a3ec4c3d Replacer supports case-insensitive header placeholders (fixes #476) 2015-12-31 12:12:16 -07:00
Abiola Ibrahim 3468986260 Support multiple values for to in simple rule. 2015-12-31 20:11:31 +01:00
Abiola Ibrahim 55f69fd742 Add not_has and not_match conditions. 2015-12-31 20:10:42 +01:00
Matthew Holt 1af7865e6c Move SVG up to top and make layout mobile-friendly 2015-12-31 11:18:43 -07:00
Matt Holt 4636ca1051 Merge pull request #471 from abiosoft/rewrite-improvements
rewrite: Support for rewrite match group.
2015-12-31 11:07:14 -07:00
Matthew Holt 94e3e7e5eb browse: New default template 2015-12-31 00:23:10 -07:00
Abiola Ibrahim 3c086fb2e6 Support for rewrite match group. 2015-12-30 21:47:37 +01:00
Abiola Ibrahim 55aa492dc1 Merge pull request #470 from abiosoft/replacer-patch
Replacer patch
2015-12-30 21:06:55 +01:00
Abiola Ibrahim 7dadcd5834 Add ability to set custom values. 2015-12-30 20:42:03 +01:00
Abiola Ibrahim 73327e784d Merge branch 'master' into replacer-patch 2015-12-30 20:26:11 +01:00
Abiola Ibrahim bb23f68a43 Merge pull request #463 from abiosoft/rewrite-improvements
Rewrite improvements
2015-12-30 19:42:13 +01:00
Matt Holt 6a27968f73 Merge pull request #385 from radim/master
Support glob character in import
2015-12-29 16:21:26 -07:00
Radim Marek 1e7ec3397b Import allows only one expression 2015-12-29 23:32:59 +01:00
Abiola Ibrahim 168723a026 Added escaped versions of uri, query and path. 2015-12-24 09:00:10 +01:00
Abiola Ibrahim 92bd914418 Fix vet errors. 2015-12-23 13:23:43 +01:00
Abiola Ibrahim 9110dc4745 Refactor. Tests and tests data. 2015-12-23 12:11:11 +01:00
Abiola Ibrahim 1ed786f836 Cleanups and panic prevention in tests. 2015-12-23 09:36:00 +01:00
Abiola Ibrahim 4d5bc9fa6c Backward compatibility ensured. 2015-12-23 09:02:52 +01:00
Abiola Ibrahim 98d8c0f81b Added new rewrite features. 2015-12-22 23:19:22 +01:00
Matt Holt 32b8857eea Merge pull request #461 from abiosoft/markdown-sitegen-panic
markdown: Fix panic on sitegen for request dependent template values.
2015-12-22 12:03:20 -07:00
Abiola Ibrahim 9e163a655d Use proper struct constructors instead. 2015-12-22 14:43:48 +01:00
Abiola Ibrahim 4d867e848b Markdown: Fix panic on sitegen for request dependent template values. 2015-12-22 13:32:27 +01:00
Abiola Ibrahim c748ef944b Merge pull request #449 from abiosoft/master
Gzip: Fix missing gzip encoding headers.
2015-12-21 20:54:00 +01:00
Abiola Ibrahim 55d22f4ead Merge remote-tracking branch 'upstream/master' 2015-12-21 20:49:01 +01:00
Abiola Ibrahim 3f787a20e3 Merge pull request #438 from captncraig/patch-1
Gzip: Append to Vary header instead of replacing.
2015-12-21 20:48:03 +01:00
Matt Holt 6276be4e90 Merge pull request #457 from abiosoft/fcgi-hanging-bug
fastcgi: Close client connections when done.
2015-12-21 10:46:00 -07:00
Abiola Ibrahim f639d3cd68 FastCGI: Close client connections when done. 2015-12-21 11:57:20 +01:00
Abiola Ibrahim 43020533f7 Merge remote-tracking branch 'upstream/master' 2015-12-19 21:58:24 +01:00
Matt Holt a5836aebfa Merge pull request #412 from pavlik/master
tls: Remove ECDHE-RSA-3DES-EDE-CBC-SHA and RSA-3DES-EDE-CBC-SHA from the default TLS config
2015-12-19 11:57:45 -07:00
Pavel Pavlenko 3dd4c0eb6a Fix TestTLSParseBasic 2015-12-19 14:37:38 +03:00
Pavel Pavlenko 1e27b5be89 Remove ECDHE-RSA-3DES-EDE-CBC-SHA and RSA-3DES-EDE-CBC-SHA from the default TLS config 2015-12-19 14:30:25 +03:00
Abiola Ibrahim a946d65fe6 Oops. Tests. 2015-12-18 21:25:06 +01:00
Abiola Ibrahim f04ff063ed Gzip: Fix missing gzip encoding headers. 2015-12-18 20:58:23 +01:00
Matt Holt 35ec61cc88 Merge pull request #441 from jungle-boogie/patch-1
Feek --> Feel
2015-12-17 21:50:04 -07:00
jungle-boogie 8f23c430ae Feek --> Feel
unless you meant Freak
2015-12-17 13:12:22 -08:00
Matthew Holt 5eadea6615 Slack -> Gitter 2015-12-17 08:37:16 -07:00
Craig Peterson 34d3cd7c92 Gzip: Append to Vary header instead of replacing. 2015-12-15 08:56:44 -07:00
Matt Holt f11cd4d9dd Merge pull request #429 from abiosoft/php-error-log
fastcgi: separate standard and error output responses.
2015-12-14 10:14:52 -07:00
Matt Holt a8c9d47805 Merge pull request #404 from abiosoft/master
gzip: support for min_length.
2015-12-14 10:11:22 -07:00
Abiola Ibrahim 3966936bd6 Remove trailing new line for cleaner log output.
Return correct status code.
2015-12-13 12:58:21 +01:00
Abiola Ibrahim b7fd1f4e9e FastCGI: separate standard and error output responses. 2015-12-12 14:04:48 +01:00
Matthew Holt b0397df719 Save obtained certs even if there were failures 2015-12-11 15:42:22 -07:00
Radim Marek eb48885d4d Updated comments 2015-12-11 22:02:31 +01:00
Radim Marek afbda595f6 import glob tests 2015-12-11 21:52:35 +01:00
Abiola Ibrahim b65ddbc750 Merge remote-tracking branch 'upstream/master' 2015-12-10 02:37:19 +01:00
Matt Holt aba0ae358e Merge pull request #415 from abiosoft/appveyor-fix
Fix race condition in test for AppVeyor
2015-12-09 15:50:51 -07:00
Abiola Ibrahim 2e295b51b3 Use channel instead for a synchronous interval. 2015-12-09 20:18:18 +01:00
Abiola Ibrahim 59dbea768c Fix race condition on AppVeyor. Increase timeout a bit. 2015-12-09 19:22:20 +01:00
Abiola Ibrahim a44d59f1e5 Use ioutil.Discard instead for unneeded bytes. 2015-12-09 18:44:25 +01:00
Pavel Pavlenko e4ff77ed07 fix tls_test.go 2015-12-09 11:27:59 +03:00
Pavel Pavlenko b6c4178f0a Remove ECDHE-RSA-3DES-EDE-CBC-SHA and RSA-3DES-EDE-CBC-SHA from the default TLS config 2015-12-09 11:10:55 +03:00
Abiola Ibrahim 23631cfaca Fix deleted Content-Length header bug. 2015-12-08 12:01:24 +01:00
Abiola Ibrahim 8631f33940 remove minor ugly parenthesis 2015-12-07 23:27:57 +01:00
Abiola Ibrahim ab5087e215 Gzip: support for min_length. 2015-12-07 23:17:05 +01:00
Radim Marek d56a9a1c5d Correct position of the newly imported tokens 2015-12-06 23:50:26 +01:00
Matt Holt 41bdd77545 Merge pull request #381 from ReadmeCritic/master
Update README URLs based on HTTP redirects
2015-12-06 10:23:32 -07:00
Matt Holt b7827a342a Merge pull request #387 from Moter8/patch-1
Latest Go 1.5 version
2015-12-05 09:38:15 -07:00
Carsten Hagemann e17a18365d Latest Go 1.5 version 2015-12-05 13:05:51 +01:00
Radim Marek d1216f409d Handle no matches 2015-12-04 23:22:03 +01:00
Radim Marek 12f594779c Added support magic characters in import pattern
Import now allows to use the star wildcard, question mark and square
brackets as used by filepath.Glob
2015-12-04 23:04:12 +01:00
ReadmeCritic b3f5e4d4ad Update README URLs based on HTTP redirects 2015-12-04 07:20:56 -08:00
Matthew Holt 5b93799a62 Version 0.8.0 2015-12-03 19:52:15 -07:00
Matthew Holt fd14f257df markdown: Add (currently failing) test for empty body 2015-12-02 17:09:43 -07:00
Matt Holt d044e497f6 Merge pull request #377 from abiosoft/master
markdown: Fix "metadata not closed" bug. More tests.
2015-12-02 17:04:16 -07:00
Abiola Ibrahim 6f4835f91a Markdown: Fix "metadata not closed" bug. More tests. 2015-12-03 00:41:12 +01:00
Matthew Holt 9002db2ae0 letsencrypt: Remote duplicate hosts from certificate request
Domain names must be unique in cert bundle request or really bad things happen (like, um, a panic)
2015-12-02 12:40:57 -07:00
Matthew Holt 19c6bbf6a2 Update changelist (env vars) 2015-12-02 11:42:50 -07:00
Matt Holt ef2ca1da3d Merge pull request #357 from tw4452852/my_md
markdown: fix json front matter parse issue when body content is long
2015-11-27 00:07:19 -07:00
Tw fbc18c5b85 markdown: fix json format parse issue
We can't use json meta parser's remaining buffered data as the markdown body
because it may not contain the entire original content.
Now we adopt the way like toml and yaml parser's way to extract the meta content
at first.

Also when spilting the meta data and content body, additional io.Copy is
unnecessary.

Fix issue #355

Signed-off-by: Tw <tw19881113@gmail.com>
2015-11-25 08:32:14 +08:00
Matt Holt d93fe53e84 Merge pull request #362 from jungle-boogie/patch-3
wrap lines to 80 characters
2015-11-24 13:42:21 -07:00
Matt Holt 0a40970dea Merge pull request #361 from jungle-boogie/patch-2
more copy edits
2015-11-24 13:34:39 -07:00
jungle-boogie 6478eee338 removed stray ) and spaces. 2015-11-24 11:16:05 -08:00
jungle-boogie a60c739797 wrap lines to 80 characters
also linked to bug reporting article.
2015-11-24 11:09:35 -08:00
jungle-boogie 19ca7d812e more copy edits
add links to inspired by projects and updated language with golang.
2015-11-24 11:04:15 -08:00
Matt Holt bc37cf0d1c Merge pull request #360 from jungle-boogie/patch-1
copy edit to wrap around 80 chars
2015-11-24 11:24:24 -07:00
jungle-boogie 78b95deb55 copy edit to wrap around 80 chars
not all lines wrap at 80 characters but now most do.
2015-11-24 10:17:34 -08:00
Matt Holt b787569820 Merge pull request #359 from abiosoft/master
rewrite: Use middleware.Replacer for simple rule
2015-11-24 10:17:02 -07:00
Abiola Ibrahim 016344bae7 Rewrite: Use middleware.Replacer for simple rule. 2015-11-24 12:20:39 +01:00
Matt Holt 0b51369932 Merge pull request #356 from Luit/master
proxy: Two small websocket fixes
2015-11-21 13:37:59 -07:00
Luit van Drongelen 1fb66d534a Close proxy's backend request earlier when re-connecting for websocket 2015-11-21 20:03:46 +01:00
Luit van Drongelen f0b1edaf8c Fix proxy for websocket with altered Host header 2015-11-21 20:00:44 +01:00
Matt Holt 4dbb4274d9 Merge pull request #354 from pyed/patch-3
browse: allow consecutive spaces in the header
2015-11-20 23:48:56 -07:00
pyed 9886e89e42 allow consecutive spaces in the header 2015-11-21 05:49:27 +03:00
Matt Holt 3e402e0692 Merge pull request #349 from carlisia/cc-lint-fixes
Add a few super minor lint fixes
2015-11-19 08:17:41 -07:00
Carlisia Campos 0a1721d5b2 Add a few super minor lint fixes
Other lint warnings left behind are ones due to external package and
due to maintainer's preferences.
2015-11-19 06:56:28 -08:00
Matthew Holt 4d907d57fa Whoops, emergency bug fix
Made a faulty assumption that virualhosts could share acme proxy handlers; turns out they can't without fumbling up the middleware configuration (middleware chains overlap and cross over into other virtualhosts)!
2015-11-18 18:41:01 -07:00
Matthew Holt 24352e799a Remove SimpleHTTP and bump version to 0.8 beta 4! 2015-11-18 17:40:35 -07:00
Matthew Holt e17d43b58a Default host now empty string; default port now depends on host
Hosts which are eligible for automatic HTTPS have default port "https" but other hosts (wildcards, loopback, etc.) have the default port 2015. The default host of empty string should be more IPv6-compatible.
2015-11-18 10:05:13 -07:00
Matthew Holt 580b50ea20 letsencrypt: Support for http-01, awkwardly straddling that and SimpleHTTP for now 2015-11-17 18:11:19 -07:00
Matthew Holt 659df6967e letsencrypt: Don't assume default port of 443 2015-11-17 16:17:43 -07:00
Matthew Holt b9244cdf2e templates: Another context fix when host header is missing port 2015-11-17 14:35:18 -07:00
Matthew Holt a2ba00bdc8 Update docs n things 2015-11-17 10:19:03 -07:00
Matthew Holt 1d47e590e5 proxy: Make headers when upstream is created; avoid potential nil ptr deref 2015-11-17 10:18:13 -07:00
Matt Holt 280ba9db85 Merge pull request #345 from tw4452852/my_proxy
proxy: make http header block scoped
2015-11-17 08:20:29 -07:00
Matt Holt 7f98a6cccf Merge pull request #347 from tw4452852/my_health
proxy: make tests workable when offline
2015-11-17 08:14:50 -07:00
Tw a5b117fcdf proxy: make tests workable when offline
Instead of accessing the google website, we setup a local server
for test, then tests will work fine even we are offline.

Fix issue #346

Signed-off-by: Tw <tw19881113@gmail.com>
2015-11-17 15:18:02 +08:00
Tw f56d2090b6 proxy: make http header block scoped
Each proxy block should could specify its own http header
instead of sharing a global one.

Fix issue #341

Signed-off-by: Tw <tw19881113@gmail.com>
2015-11-17 14:07:32 +08:00
Matt Holt 37e3cf684d Merge pull request #343 from abiosoft/master
proxy: 'except' property to ignore subpaths
2015-11-16 18:06:58 -07:00
Abiola Ibrahim 7949388da8 Proxy: Allow ignored subpaths. 2015-11-16 17:22:06 +01:00
Matthew Holt dd119e04b1 Fix go vet 2015-11-15 11:06:50 -07:00
Matthew Holt f7cfe79905 websocket: Simple buildEnv test, and fix for addresses without port 2015-11-15 11:05:26 -07:00
Matthew Holt 3dc5e0e181 Added a few little tests 2015-11-15 10:55:15 -07:00
Matthew Holt 1ca34c4ecf Couple fixes for env var replacements and tests 2015-11-15 08:01:24 -07:00
Matt Holt 837ee9f042 Merge pull request #319 from mbanzon/issue-304
parser: Allow use of environment variables in tokens
2015-11-15 07:17:42 -07:00
Michael Banzon d448c919e8 Changed implementation of issue #304 fix
It no longer uses regular expressions.
It supports both the Unix `{$ENV_VAR}` _and_ the Windows `{%ENV_VAR%}`
syntax.
Added test for both Unix and Windows env. syntax.
2015-11-15 11:16:37 +01:00
Matthew Holt 7d5b6b96ea Make signal trapping optional
Go programs using the caddy package may not want the it to capture all the signals...
2015-11-14 21:59:43 -07:00
Matthew Holt 7b064535bf Changed SIGINT and added support for HUP, QUIT, and TERM 2015-11-14 20:56:34 -07:00
Matthew Holt b42334eb91 Several improvements and bug fixes related to graceful reloads
Added a -grace flag to customize graceful shutdown period, fixed bugs related to closing file descriptors (and dup'ed fds), improved healthcheck signaling to parent, fixed a race condition with the graceful listener, etc. These improvements mainly provide better support for frequent reloading or unusual use cases of Start and Stop after a Restart (POSIX systems). This forum thread was valuable help in debugging: https://forum.golangbridge.org/t/bind-address-already-in-use-even-after-listener-closed/1510?u=matt
2015-11-14 18:00:25 -07:00
Matthew Holt 94c746c44f letsencrypt: Return an error if making site folder fails 2015-11-14 18:00:25 -07:00
Matthew Holt 7d46a7d5f4 Much refactor; many fix; so wow
Fixed pidfile writing problem where a pidfile would be written even if child failed, also cleaned up restarts a bit and fixed a few bugs, it's more robust now in case of failures and with logging.
2015-11-14 18:00:25 -07:00
Matthew Holt 9e2cef38f6 Write pidfile only if server starts successfully
Whether the original parent process or a child process as part of a restart, the pidfile will not be written/changed until that process has started successfully. It is written every time caddy.Start() succeeds (may be reundant, but that's probably okay).
2015-11-14 18:00:24 -07:00
Michael Banzon e166ebf68b Added test for environment replacement.
Added test for the fix of issue #304
2015-11-14 18:54:29 +01:00
Matt Holt 33b1d4c55d Merge pull request #340 from cubicdaiya/setcpu-test-fix
fixed test failure on CPU 1 core machine.
2015-11-13 23:11:35 -07:00
Matt Holt ae2e0900c1 Merge pull request #339 from cubicdaiya/vary-accept-encoding
gzip: added Vary: Accept-Encoding to response header.
2015-11-13 23:10:59 -07:00
Tatsuhiko Kubo 91ac2c58fa fixed test failure.
When CPU is 1 core, expected value (int(0.5 * float32(maxCPU))) is zero.
But runtime.GOMAXPROCS(-1) returns always 1.
2015-11-14 11:38:26 +09:00
Tatsuhiko Kubo 69662d4d7d gzip: added Vary: Accept-Encoding to response header.
When the downstream is cache server or CDN, it is important.
2015-11-14 06:11:37 +09:00
Matt Holt fc6afe2a8b Merge pull request #333 from mholt/firststartup
startup: Only run commands at first startup
2015-11-10 23:03:17 -07:00
Matthew Holt 51d2ff4e47 markdown: Default extensions .md, .markdown, and .mdown 2015-11-10 22:39:27 -07:00
Matthew Holt d46967d1e2 core: Fixed minor restart bug
Actually, restart on posix systems failed entirely if caddy was executed without the path included; also fixed a related bug where a variable was declared but never assigned.
2015-11-10 21:26:22 -07:00
Matthew Holt 4d78013646 Clean up flags 2015-11-10 19:50:40 -07:00
Matthew Holt 5cced604e4 startup: Only run commands at first startup
We had to hack some special support into the server and caddy packages for this. There are some middlewares which should only execute commands when the original parent process first starts up. For example, someone using the startup directive to start a backend service would not expect the command to be executed every time the config was reloaded or changed - only once when they first started the original caddy process.

This commit adds FirstStartup to the virtualhost config
2015-11-10 19:46:18 -07:00
Matt Holt a39ed2823e Merge pull request #332 from coolaj86/coolaj86-alphabetize
Alphabetize command line options, add -pidfile
2015-11-10 19:02:39 -07:00
AJ ONeal 4bed399ca4 Alphabetize command line options, vars, and checks
As per https://github.com/mholt/caddy/issues/331
2015-11-10 17:52:29 -08:00
AJ ONeal 93c330c4ce add --pidfile string option
As per https://github.com/mholt/caddy/issues/317
2015-11-10 17:44:00 -08:00
Matthew Holt 76ec785e87 ext: Fix panic when URL path is empty 2015-11-10 16:04:02 -07:00
Matthew Holt e9b9432da5 "-conf stdin" required to pipe in Caddyfile
Some programs (Node.js, supervisor, etc.) open a stdin pipe by default and don't use it, causing Caddy to block. It is their error, but we have to try to accommodate unfortunately. To fix this more universally, parent must explicitly set -conf to "stdin" to read from pipe.
2015-11-10 15:06:47 -07:00
Matthew Holt c31e86db02 caddyfile: Change JSON format to use arrays, not objects
Since a directive can appear on multiple lines, the object syntax wasn't working well. This also fixes several other serialization bugs.
2015-11-10 11:49:01 -07:00
Matthew Holt 13557eb5ef core: Fix bug that caused parent process to block indefinitely
The error channel used when starting all the servers must be buffered so that, even if there are no errors at startup, the returns that insert into the error channel will not be blocked, since after startup, nobody is reading that channel anymore.
2015-11-09 11:52:43 -07:00
Matthew Holt 02213402e8 Unexport internal types; improved markdown summaries 2015-11-09 07:45:37 -07:00
Matt Holt e1f23a1eb7 Merge pull request #326 from PatelNDipen/master
Wrote tests for browse.go and redir.go
2015-11-08 09:37:04 -07:00
Dipen Patel 485af2c6ba removed comment from browse test 2015-11-08 08:40:18 -05:00
Matthew Holt 171fd34b3c markdown: Make base path optional, always generate links
The base path being optional in the Caddyfile is convenient when you just want the whole site to be markdown-enabled. The other change is to always generate links... this is because an index page for markdown files may not be statically generated, but it should still show links. Commit 09341fc was a regression, and this fixes it.
2015-11-07 20:24:17 -07:00
Dipen Patel 1017142d9b Made style adjustments to browse and redir tests 2015-11-07 22:17:26 -05:00
Matthew Holt be9f644425 -host and -port flags affect shorthand caddyfile 2015-11-07 20:03:02 -07:00
Matthew Holt 8628a50b7d Forgot this one 2015-11-07 17:25:44 -07:00
Matthew Holt 161db70c15 Latest Go 1.4 version 2015-11-07 17:23:16 -07:00
Matthew Holt e7b8be31cf This calls for a new beta version 2015-11-07 17:22:41 -07:00
Matthew Holt e56f7affc9 Show message when activating HTTPS
It can take a few seconds...
2015-11-07 17:12:21 -07:00
Dipen Patel 2b1cc77f4b Wrote tests for browse.go and redir.go 2015-11-07 18:03:21 -05:00
Matthew Holt 18e9aa4d57 Pipe and -conf flag together are an error (#315) 2015-11-06 13:22:37 -07:00
Matt Holt cf5aa1bed1 Merge pull request #323 from tpng/patch-1
use log instead of fmt to obey the -log flag
2015-11-06 08:38:51 -07:00
Benny Ng c35b201685 use log instead of fmt to obey the -log flag 2015-11-06 23:09:40 +08:00
Matt Holt a1481bc29e Merge pull request #306 from mholt/bug/websocket-races
fixed data races in websockets
2015-11-05 17:19:35 -07:00
Matthew Holt d34e92ee70 checkDirectives must've slipped through the cracks in big merge
Also we're not messing with log flags anymore during parsing. Timestamps could come in handy, plus concurrent logging is going on now.
2015-11-05 17:14:47 -07:00
Matthew Holt bcea5182c6 Don't truncate process log; more consistent output 2015-11-05 17:01:08 -07:00
Matthew Holt 2fb4810cdb Fixed racy error reporting at server startup
Previously, if a listener fails to bind (for example), there was a race in caddy.go between unblocking the startup waitgroup and returning the error and putting it into errChan. Now, an error is returned directly into errChan and the closing of the startup waitgroup is defered until after that return takes place.
2015-11-05 15:21:13 -07:00
Matthew Holt 411dd7dff5 New -log flag to direct log output to desired place
Log file can also be stdout or stderr. Log output is disabled by default now, which makes it more feasible to add more log statements to trace program flow in debugging situations.
2015-11-05 14:07:52 -07:00
Matt Holt 96f04cdc38 Merge pull request #321 from coolaj86/patch-1
add helpful suggestion to identify problem
2015-11-05 13:46:58 -07:00
AJ ONeal b963c7c9ac add helpful suggestion to identify problem 2015-11-05 12:39:17 -08:00
Matthew Holt fc7f7dffa8 Prevent panic in case of hung loading sequence (fixes #315)
This is known to happen if another program spawns Caddy with a stdin pipe but does not close its write end of Caddy's stdin.
2015-11-05 13:31:24 -07:00
Matt Holt 47c5b6c9c4 Merge pull request #305 from buddhamagnet/refactor/lint-vet
Lint pacification
2015-11-05 10:25:24 -07:00
Michael Banzon 8774c90709 Removed the Windows part. It wasn't working properly.
For issue #304.
2015-11-05 18:12:06 +01:00
Michael Banzon 01465932e7 Environment variables Windows style
Added the Windows style ({%%}) for environment variables in Caddyfiles
(for issue #304).
2015-11-05 18:01:44 +01:00
Michael Banzon 72c0527b7d Added support for env vars in Caddyfile
This is work in progress for #304
2015-11-05 08:26:10 +01:00
buddhamagnet e23af5e99a final corrections 2015-11-05 00:44:35 +00:00
buddhamagnet 57f1d3c205 pass golint
pass all tests

respond to maintainer comments

reinstate assignment of t

correct typo

correct typo

pass linter some more
2015-11-05 00:40:35 +00:00
Matt Holt 7a159ad934 Merge pull request #313 from mholt/fix-tls-defaults-le
Fix regression: Ensure TLS defaults are added by LE handlers.
2015-11-04 16:21:09 -07:00
xenolf 6fdc83faeb Fix regression: Ensure TLS defaults are added by LE handlers. 2015-11-04 23:53:03 +01:00
Matthew Holt d36685acdd letsencrypt: Fix bug if different emails used; beta 2 2015-11-04 12:19:43 -07:00
Matthew Holt 051d2a68c0 Fixed behavior with empty Caddyfile
If the -host flag is used, we might still have to set up Let's Encrypt, so this change is necessary.
2015-11-04 09:26:11 -07:00
Austin 34c369155c trim possible line terminators from message 2015-11-03 13:37:32 -08:00
Matthew Holt 7f7a6abafd Revised README 2015-11-03 12:39:25 -07:00
Matthew Holt 5e1573dd84 Better error handling at startup and fixed some bugs
Fixed bug where manually specifying port 443 disabled TLS (whoops); otherHostHasScheme was the culprit, since it would return true even if it was the same config that had that scheme.

Also, an error at startup (if not a restart) is now fatal, rather than keeping a half-alive zombie server.
2015-11-03 12:01:54 -07:00
Matthew Holt e8006acf80 Fix -port, -host, and -root flags when Caddyfile is missing 2015-11-03 08:10:16 -07:00
Matt Holt 295d21f37d Merge pull request #308 from mholt/letsencrypt
Let's Encrypt
2015-11-02 21:06:31 -07:00
Matthew Holt 866427491c Forgot something 2015-11-02 21:02:35 -07:00
Matthew Holt 9905f48c8e Update changelog and readme 2015-11-02 20:56:13 -07:00
Matthew Holt 0970c058f7 tls: Repair from messy merge 2015-11-02 20:54:38 -07:00
Matthew Holt ad057ab873 Merge branch 'master' into letsencrypt
Conflicts:
	caddy/parse/parse.go
	caddy/parse/parsing.go
	config/config.go
	config/setup/controller.go
	main.go
	server/server.go
2015-11-02 20:26:55 -07:00
Matthew Holt 09341fca12 markdown: Don't generate static site or links unless sitegen is enabled 2015-11-02 20:15:42 -07:00
Matthew Holt c3e6463676 A few comments, slight tweaks 2015-11-02 19:27:42 -07:00
Matthew Holt d18cf12f14 letsencrypt: Fixed renewals
By chaining in a middleware handler and using newly exposed hooks from the acme package, we're able to proxy ACME requests on port 443 to the ACME client listening on a different port.
2015-11-02 19:27:23 -07:00
Austin abc7c6a148 fixed data races in websockets 2015-11-02 14:19:38 -08:00
Matthew Holt b143bbdbaa letsencrypt: Better logic for handling issuance failures
This fixes a bug with the -agree flag
2015-11-02 14:09:35 -07:00
Matthew Holt be0fb0053d letsencrypt: Re-prompt user if obtaining certs fails due to updated SA 2015-11-02 11:06:42 -07:00
Matthew Holt 2712dcd1f5 tls: If port unspecified and user provides cert+key, use 443 2015-11-01 19:01:46 -07:00
xenolf cac58eaab9 Update to latest lego changes 2015-11-02 01:41:02 +01:00
Matthew Holt 9a4e26a518 letsencrypt: Don't store KeyFile as field in user; staying consistent 2015-11-01 10:58:58 -07:00
Matthew Holt a729be295a letsencrypt: Activate during config load just after tls directive
Before, we were activating Let's Encrypt after all the directives were executed. This means their setup functions had access to potentially erroneous information about the server's TLS setup, since the letsencrypt package makes changes to the port, etc. Now, we execute all directives up to and including tls, then activate letsencrypt, then finish with the rest of the directives. It's a bit ugly, but I do think it is more correct. It also fixes some bugs, for example: a host that only has a catch-all redirect.
2015-11-01 09:46:23 -07:00
Matt Holt b6078eded1 Merge pull request #301 from abiosoft/master
Windows Build: Remove PATH from output.
2015-10-31 16:50:31 -06:00
Abiola Ibrahim ea642f6e1d Remove PATH from build output 2015-10-31 23:46:55 +01:00
Matthew Holt 4d71620cb0 core (Windows): Retry every 100ms for 2s if listener fails to bind
In testing, I've found that Windows doesn't release the socket right away even though the listener is closed, so calling caddy.Start() right after caddy.Stop() can fail. This change has server.ListenAndServe() try up to 20 times every 100ms to bind the listener, and only return an error if it doesn't succeed after 2 seconds. This might be kind of nifty for Unix, too, but there hasn't been a need for it yet.
2015-10-31 13:22:23 -06:00
Matthew Holt e4028b23c7 letsencrypt: Email prompt includes link to SA 2015-10-31 13:15:47 -06:00
Matt Holt 96c7c2768c Merge pull request #300 from PatelNDipen/master
startup/shutdown: test file

Also modified NewTestController to include a value for OncePerServerBlock
2015-10-31 10:39:41 -06:00
Dipen Patel 78d857a374 debugged startupshutdown.go 2015-10-31 12:33:50 -04:00
Dipen Patel 19148eba44 wrote startupshutdown tests and added OncePerServerBlock value in the NewTestController function of the controller.go file 2015-10-31 10:48:25 -04:00
Abiola Ibrahim 6a32076271 Merge pull request #299 from abiosoft/master
Fix for issues #297 and #298
2015-10-31 07:53:17 +01:00
Abiola Ibrahim ef617f9ce4 Merge pull request #295 from guilhermebr/master
Add option to change delims in templates
2015-10-31 07:51:22 +01:00
Matthew Holt 3843cea959 letsencrypt: Allow (but warn about) empty emails 2015-10-30 23:44:00 -06:00
Abiola Ibrahim dd1c49bde9 Fix for issues #297 and #298 2015-10-31 02:24:37 +01:00
Matthew Holt e99b3af0a5 letsencrypt: Numerous bug fixes 2015-10-30 15:55:59 -06:00
Matthew Holt 88c646c86c core: Start() blocks until servers finish starting
Also improved/clarified some docs
2015-10-30 00:19:43 -06:00
Matthew Holt 64cded8246 letsencrypt: Don't maintain assets of sites we don't maintain 2015-10-29 17:24:11 -06:00
Matthew Holt e3be524447 core: Fix for graceful reload after first reload signal
The file path of the originally-loaded Caddyfile must be piped to the forked process; previously it was using stdin after the first fork, which wouldn't load the newest Caddyfile from disk, which is the point of SIGUSR1.
2015-10-29 17:23:20 -06:00
Guilherme Rezende a62a7f7cf1 Add new optional block tests to setup/templates_test.go 2015-10-29 20:38:15 -02:00
Guilherme Rezende 9d456bba9b Add argument in new optional block in templates midd to set delimiters 2015-10-29 20:33:01 -02:00
Matthew Holt 89ad7593bd Merge branch 'caddyfile' into letsencrypt 2015-10-29 15:41:34 -06:00
Matthew Holt d227bec0ff Move common function into existing file 2015-10-29 10:34:47 -06:00
Matt Holt a3f0fff734 Merge pull request #296 from Makpoc/last-modified
markdown, templates: Add Last-Modified header
2015-10-29 10:31:38 -06:00
Matthew Holt efeeece735 caddyfile: http and https hosts should render in URL format 2015-10-29 10:13:30 -06:00
Matthew Holt 234783548f markdown: Enable tables, fenced code, and strikethrough (closes #294) 2015-10-29 09:59:32 -06:00
makpoc 5a29107f3b Add Last-Modified header when serving markdown and templates 2015-10-29 11:06:35 +02:00
Matthew Holt 976f5182e1 caddyfile: Better string and number handling 2015-10-29 00:22:56 -06:00
Matthew Holt 30c949085c letsencrypt: Stubbed out OCSP staple updates
OCSP status is checked at a regular interval, and if the OCSP status changes for any of the certificates, the change callback is executed (restarts the server, updating the OCSP staple).
2015-10-28 23:43:26 -06:00
Matthew Holt 6762df415c Clean up leaking goroutines and safer Start()/Stop() 2015-10-28 22:54:27 -06:00
Matthew Holt 1818b1ea62 letsencrypt: Better error handling, prompt user for SA 2015-10-28 18:12:07 -06:00
xenolf b67543f81c Track the latest lego OCSP changes 2015-10-28 16:35:19 +01:00
Matt Holt 94ff7dc6fb Merge pull request #287 from Makpoc/parsewincmd
Fix windows command parsing
2015-10-27 23:50:21 -06:00
Matthew Holt cc229aefae templates: Parse host successfully when port is implicit (fixes #292) 2015-10-27 23:20:05 -06:00
Matt Holt 7d91cfb512 Merge pull request #290 from mholt/le-graceful
Graceful restarts/reloads, refactoring
2015-10-27 14:17:40 -06:00
Matthew Holt 8548641dc1 letsencrypt: Check for errors 2015-10-27 13:02:47 -06:00
Matthew Holt c46898592f Merge branch 'letsencrypt' into le-graceful
Conflicts:
	caddy/letsencrypt/letsencrypt.go
	caddy/letsencrypt/renew.go
2015-10-27 12:59:55 -06:00
Matthew Holt 362ead2760 Minor test improvements 2015-10-27 12:53:31 -06:00
Matthew Holt a6ea1e6b55 letsencrypt: -ca flag to customize CA server 2015-10-27 12:52:58 -06:00
Matthew Holt 0f19df8a81 Keep tests deterministic 2015-10-27 00:43:24 -06:00
Matthew Holt ee5c842c7d Code to convert between JSON and Caddyfile
This will be used by the API so clients have an easier time manipulating the configuration
2015-10-27 00:07:22 -06:00
Matthew Holt c487b702a2 Little cleanup 2015-10-27 00:05:22 -06:00
Matthew Holt bb6613d0ae core: Fix SIGUSR1 so it actually reloads config 2015-10-26 17:57:32 -06:00
Matthew Holt 821c0fab09 core: Refactoring POSIX-only code for build tags 2015-10-26 16:49:05 -06:00
Matthew Holt 5b1962303d core: More refactoring, code cleanup, docs 2015-10-26 14:55:03 -06:00
Matthew Holt 41c4484222 core: SIGUSR1 to reload config; some code cleanup 2015-10-26 14:28:50 -06:00
Matthew Holt 4ebff9a130 core: Major refactor for graceful restarts; numerous fixes
Merged config and app packages into one called caddy. Abstracted away caddy startup functionality making it easier to embed Caddy in any Go application and use it as a library. Graceful restart (should) now ensure child starts properly. Now piping a gob bundle to child process so that the child can match up inherited listeners to server address. Much cleanup still to do.
2015-10-26 13:34:31 -06:00
Matthew Holt 6936658019 letsencrypt: Work with latest lego changes 2015-10-25 19:30:29 -06:00
Matthew Holt b5b31e398c letsencrypt: Graceful restarts
Lots of refinement still needed and runs only on POSIX systems. Windows will not get true graceful restarts (for now), but we will opt for very, very quick forceful restarts. Also, server configs are no longer put into a map; it is critical that they stay ordered so that they can be matched with their sockets in the child process after forking.

This implementation of graceful restarts is probably not perfect, but it is a good start. Lots of details to attend to now.
2015-10-25 18:45:55 -06:00
xenolf f9f1aafe0c Update to lego update. DevMode no longer exists. 2015-10-26 00:53:36 +01:00
Makpoc d1b667fbce Two quotes next to each other result in one escaped quote; Add Split Example, add/refactor tests for every platform. 2015-10-24 15:33:04 +03:00
xenolf 91465d8e6f Support for OCSP Stapling. Fixes #280 2015-10-24 04:36:54 +02:00
xenolf f8ad050dda Update for latest lego changes (cert bundling) 2015-10-24 04:35:55 +02:00
makpoc 0d004ccbab Attempt to fix windows command parsing + add more tests 2015-10-23 20:21:05 +03:00
xenolf 2e5eb63850 Function name changed in lego 2015-10-23 16:29:05 +02:00
Matthew Holt f24ecee603 letsencrypt: Basic renewal failover and better error handling 2015-10-21 21:28:33 -06:00
Matt Holt c5635f21a3 Merge pull request #283 from mholt/le-simplerenew
letsencrypt: Simplify timing mechanism for checking renewals
2015-10-21 17:08:57 -06:00
Matthew Holt 605f1942ef Merge branch 'letsencrypt' into le-simplerenew
Conflicts:
	config/letsencrypt/letsencrypt.go
2015-10-21 16:35:32 -06:00
Matthew Holt fec491fb12 Removed another test that is Windows-specific
We're not trying to test the shlex library; just our wrapper function
2015-10-21 14:15:42 -06:00
Matthew Holt 794d271152 Remove extra tests that were Linux-specific
These tests with the backslash seem to assert that shlex (our Unix shell parsing library) is working properly, not our wrapper function (that parses commands for both Windows and non-Windows). These tests break on Windows so I have removed them.
2015-10-21 14:11:30 -06:00
Matthew Holt 29362e45bc Parse Windows commands differently than Unix commands
Stinkin' backslashes
2015-10-21 14:03:33 -06:00
Matthew Holt a16beb98de letsencrypt: Revoke certificate 2015-10-21 00:09:45 -06:00
Matthew Holt 38885e4301 Simplify timing mechanism for checking renewals 2015-10-20 20:16:01 -06:00
Matt Holt 136119f8ac Merge pull request #282 from Makpoc/fileserver_tests
core: fileServer tests
2015-10-20 18:18:11 -06:00
Makpoc e3ec7394ab fix go vet error 2015-10-21 02:18:33 +03:00
Makpoc ddd69d19c0 Add tests for fileserver.go 2015-10-21 02:08:36 +03:00
Makpoc 8ecc366582 Check and return the correct error if Stat method fails (see golang issue #12991) 2015-10-21 01:25:38 +03:00
Matt Holt 4db54f8ddc Merge pull request #281 from mem/master
Add TestNewDefault to config tests
2015-10-20 10:36:59 -06:00
Marcelo Magallon 8f9f6caa4e Update config_test.go 2015-10-20 10:31:21 -06:00
Marcelo E. Magallon 7e41f6ed62 Add TestNewDefault to config tests
Very simple test to make sure that NewDefault is populating the correct
fields with the correct values.
2015-10-20 05:13:00 -06:00
Matt Holt 159eb68a11 Merge pull request #279 from pcasaretto/coverage-up
Test app.SetCPU, config.makeOnces, config.makeStorages
2015-10-19 20:55:35 -06:00
Paulo L F Casaretto 815231b1e0 Test app.SetCPU function 2015-10-20 00:33:36 -02:00
Paulo L F Casaretto 0feb0d9244 Test validDirective function 2015-10-20 00:33:19 -02:00
Paulo L F Casaretto 1db138ed55 Test makeStorages function 2015-10-19 23:23:18 -02:00
Paulo L F Casaretto 4c0d4dd780 Test makeOnces function 2015-10-19 23:19:12 -02:00
xenolf c626774da2 First, raw renewal implementation. Pretty basic :D 2015-10-20 02:44:00 +02:00
Matthew Holt acf43857a3 Fix ServerBlockStorage so it actually stores stuff 2015-10-19 07:41:58 -06:00
Matthew Holt e2f6c51fb0 core: Controller has field to persist server state
Also added ServerBlockHostIndex
2015-10-19 07:41:58 -06:00
Matthew Holt c4a7378466 core: Disable TLS for sites where http is explicitly defined (fix) 2015-10-19 07:41:58 -06:00
Matthew Holt a17e9b6b02 Add ServerBlockIndex and ServerBlockHosts to Controller
This way, Setup functions have access to the list of hosts that share the server block, and also, if needed for some reason, the index of the server block in the input
2015-10-19 07:41:58 -06:00
Matthew Holt f978967e5e OncePerServerBlock may now return an error 2015-10-19 07:41:58 -06:00
Matthew Holt fc413e2403 First use of OncePerServerBlock in a Setup function
startup and shutdown commands should only be executed once per appearance in the Caddyfile (naturally meaning once per server block).

Notice that we support multiple occurrences of startup and shutdown in the same server block by building the callback array incrementally as we parse the Caddyfile, then we append all the callbacks all at once. Quite literally, the OncePerServerBlock function executes only once per server block!
2015-10-19 07:41:58 -06:00
Matthew Holt 38719765bf Don't share sync.Once with all directives
If each server block had only one sync.Once then all directives would refer to it and only the first directive would be able to use it! So this commit changes it to a map of sync.Once instances, keyed by directive. So by creating a new map for every server block, each directive in that block can get its own sync.Once which is exactly what is needed. They won't step on each other this way.
2015-10-19 07:41:58 -06:00
Matthew Holt f3596f734d Epic revert of 0ac8bf5 and adding OncePerServerBlock
Turns out having each server block share a single server.Config during initialization when the Setup functions are being called was a bad idea. Sure, startup and shutdown functions were only executed once, but they had no idea what their hostname or port was. So here we revert to the old way of doing things where Setup may be called multiple times per server block (once per host associated with the block, to be precise), but the Setup functions now know their host and port since the config belongs to exactly one virtualHost. To have something happen just once per server block, use OncePerServerBlock, a new function available on each Controller.
2015-10-19 07:41:58 -06:00
Matt Holt 1d15fe069a Merge pull request #278 from Makpoc/context_tests
Cover the rest of the (not one-liner) functions in context
2015-10-19 07:08:34 -06:00
makpoc 72a5579d83 Cover the rest of the (not one-liner) functions in context 2015-10-19 13:51:49 +03:00
Matthew Holt cd0b47d068 letsencrypt: Don't auto-configure loopback hosts or 'tls off'
User can specify 'tls off" in Caddyfile to force-disable automatic HTTPS configuration
2015-10-18 22:50:42 -06:00
Matthew Holt 4c93ab8c68 Merge branch 'configfix' into letsencrypt 2015-10-18 19:48:57 -06:00
Matthew Holt c0ebe31560 Fix ServerBlockStorage so it actually stores stuff 2015-10-18 19:27:51 -06:00
Matthew Holt cc1ff93250 letsencrypt: Fix Windows tests 2015-10-18 12:12:33 -06:00
Matthew Holt 42ac2d2dde letsencrypt: More tests, tests for user.go & slight refactoring 2015-10-18 12:09:06 -06:00
Matthew Holt d764111886 letsencrypt: Storage tests 2015-10-18 10:39:28 -06:00
Matthew Holt 8cd6b8aa99 letsencrypt: Tests for load/save RSA keys and redirPlaintextHost 2015-10-17 23:35:59 -06:00
Matthew Holt da8a4fafcc letsencrypt: Use existing certs & keys if already in storage 2015-10-17 22:55:50 -06:00
Matthew Holt 9f9de389d5 lego provides PEM-encoded certificate bytes for us 2015-10-17 21:18:46 -06:00
Matthew Holt 7568b0e215 Compatibility with latest lego commits (dev mode enabled) 2015-10-17 21:00:48 -06:00
Matthew Holt a75663501d Little more refactoring in letsencrypt 2015-10-17 20:51:46 -06:00
Matthew Holt 96ae288c4b More refactoring; cleaning up code, preparing for tests 2015-10-17 20:44:33 -06:00
Matthew Holt a3a826572f Refactor letsencrypt code into its own package 2015-10-17 20:17:24 -06:00
Matthew Holt fe7ad8ee05 core: Controller has field to persist server state
Also added ServerBlockHostIndex
2015-10-17 14:11:32 -06:00
Matt Holt 3614a093e3 Merge pull request #275 from mem/master
Add tests for websocket configuration
2015-10-17 13:14:43 -06:00
Marcelo E. Magallon 6325bcf5b2 Add tests for websocket configuration
Command arguments:

	websocket /api5 "cmd arg1 arg2 arg3"

Optional block:

	websocket /api6 cat {
		respawn
	}

Invalid option in optional block:

	websocket /api7 cat {
		invalid
	}
2015-10-17 12:09:55 -06:00
Matthew Holt 307c2ffe3c Remove obsolete test 2015-10-17 11:19:56 -06:00
Matthew Holt 06913ab74f Oops (pass a pointer) 2015-10-17 11:15:43 -06:00
Matthew Holt 506630200b Redirect HTTP requests to HTTPS by default 2015-10-17 09:36:25 -06:00
Matthew Holt df194d567f Don't forget to set port to "https" and indicate TLS enabled 2015-10-17 09:06:05 -06:00
Matthew Holt 9727603250 Try to use most recent user email if not provided
Also more comments and starting to clean up code
2015-10-17 00:01:32 -06:00
Matthew Holt a0c8428f8c Can issue and use SSL certs and serve sites
Code is a huge mess; much cleanup to follow.
2015-10-16 23:30:00 -06:00
Matthew Holt dd91812b11 Merge branch 'configfix' into letsencrypt 2015-10-16 11:47:32 -06:00
Matthew Holt 10619f06b4 core: Disable TLS for sites where http is explicitly defined (fix) 2015-10-16 11:47:13 -06:00
Matthew Holt 0a1e472fc2 Merge branch 'configfix' into letsencrypt
Conflicts:
	config/config.go
2015-10-16 11:40:44 -06:00
Matthew Holt 4e92c71259 LE flags, modified tis directive, moved LE stuff to own file 2015-10-16 11:38:56 -06:00
Matthew Holt 2236780190 Add ServerBlockIndex and ServerBlockHosts to Controller
This way, Setup functions have access to the list of hosts that share the server block, and also, if needed for some reason, the index of the server block in the input
2015-10-15 23:34:54 -06:00
Matt Holt 3faffdce2d Merge pull request #274 from Makpoc/context_tests
Add context.go tests
2015-10-15 15:29:23 -06:00
Makpoc d6242e9cac Apply review comments - change the used domain, remove obsolete function, remove commented tests 2015-10-15 23:09:02 +03:00
Matthew Holt 691204ceed OncePerServerBlock may now return an error 2015-10-15 11:38:17 -06:00
makpoc bd4d9c6fe2 add tests for context.Header,IP,URL,Host,Port,Method,PathMatches 2015-10-15 19:46:23 +03:00
makpoc 3440f5cfbe add tests for context.Cookie() and context.IP() 2015-10-15 18:26:13 +03:00
Matthew Holt a518049fa2 Merge branch 'master' into configfix 2015-10-15 00:13:40 -06:00
Matthew Holt 35e309cf87 First use of OncePerServerBlock in a Setup function
startup and shutdown commands should only be executed once per appearance in the Caddyfile (naturally meaning once per server block).

Notice that we support multiple occurrences of startup and shutdown in the same server block by building the callback array incrementally as we parse the Caddyfile, then we append all the callbacks all at once. Quite literally, the OncePerServerBlock function executes only once per server block!
2015-10-15 00:11:26 -06:00
Matthew Holt e0fdddc73f Don't share sync.Once with all directives
If each server block had only one sync.Once then all directives would refer to it and only the first directive would be able to use it! So this commit changes it to a map of sync.Once instances, keyed by directive. So by creating a new map for every server block, each directive in that block can get its own sync.Once which is exactly what is needed. They won't step on each other this way.
2015-10-15 00:07:26 -06:00
Matthew Holt 0c07f7adcc Epic revert of 0ac8bf5 and adding OncePerServerBlock
Turns out having each server block share a single server.Config during initialization when the Setup functions are being called was a bad idea. Sure, startup and shutdown functions were only executed once, but they had no idea what their hostname or port was. So here we revert to the old way of doing things where Setup may be called multiple times per server block (once per host associated with the block, to be precise), but the Setup functions now know their host and port since the config belongs to exactly one virtualHost. To have something happen just once per server block, use OncePerServerBlock, a new function available on each Controller.
2015-10-14 23:45:28 -06:00
Austin Cherry a48ed9a246 Merge pull request #273 from mem/master
Simplify websocket ticker shutdown code
2015-10-14 18:29:00 -07:00
Marcelo E. Magallon d4a14af14d Simplify websocket ticker shutdown code
"A receive from a closed channel returns the zero value immediately"

Close the tickerChan in the calling function, this causes "case <-c" to
unblock immediately, ending the goroutine and stopping the ticker.
2015-10-14 18:48:43 -06:00
Makpoc f7e3ed13f9 TestInclude 2 should fail. Update test data and fix error checking 2015-10-15 02:21:02 +03:00
Makpoc 71c4962ff6 tests for context.Include 2015-10-15 02:09:37 +03:00
Matthew Holt b713a7796e Change c:\go to c:\gopath to avoid conflicts 2015-10-14 13:03:30 -06:00
Matt Holt 65e812d3a9 Merge pull request #270 from Makpoc/master
Add tests for command splitting and fix root tests on Windows
2015-10-14 10:13:53 -06:00
Matt Holt 5c3085fe51 Merge pull request #271 from zmb3/windows_failures
Fix test failures on Windows.
2015-10-14 10:13:18 -06:00
makpoc 6af26e2306 Use null byte in filename to simulate 'unable to access' on both windows and linux 2015-10-14 09:35:50 +03:00
Matt Holt a914565f51 Merge pull request #269 from mholt/chore/gorilla-websocket
websocket: Refactored to use gorilla instead of golang/x
2015-10-13 23:43:41 -06:00
Austin 24893bf740 removed panics, cleaned up leaking ticker routine 2015-10-13 19:07:54 -07:00
Zac Bergquist 26cbea9e12 Re-enable test
I had commented out this check just to make sure the rest of the test cases were succeeding and forgot to add it back in.
2015-10-13 20:23:05 -04:00
Zac Bergquist f7fcd7447a Fix test failure on non-Windows OS.
NewTestController now sets the site root to '.' to accomodate Windows.  This introduced a failure on Linux because we join "." and an absolute path in /tmp/ and end up looking for the temp file in the wrong place.  This change puts the temp file under the current working directory, which should resolve the issue.
2015-10-13 20:16:43 -04:00
Zac Bergquist 16bd63fc26 Removed my debug prints 2015-10-13 20:04:34 -04:00
Zac Bergquist e158cda057 Fix test failures on Windows.
Most of the Windows test failures are due to the path separator not being "/".  The general approach I took here was to keep paths in "URL form" (ie using "/" separators) as much as possible, and only convert to native paths when we attempt to open a file.  This will allow the most consistency between different host OS.  For example, data structures that store paths still store them with "/" delimiters.  Functions that accepted paths as input and return them as outputs still use "/".

There are still a few test failures that need to be sorted out.

- config/setup/TestRoot (I hear this has already been fixed by someone else)
- middleware/basicauth/TestBrowseTemplate and middleware/templates/Test (a line endings issue that I'm still working through)
2015-10-13 19:49:53 -04:00
Matthew Holt 7121e2c770 Change c:\go to c:\gopath to avoid conflicts 2015-10-13 16:13:13 -06:00
Makpoc f122b3bbdf Fix failing test (windows) - simulate an error by executing stat on a filename with zero-byte in it. Fix cleanup of created files after the tests. 2015-10-13 23:35:24 +03:00
Matt Holt 6717edcb87 Add AppVeyor badge
Distinguish between windows and linux builds
2015-10-13 22:41:10 +03:00
Matt Holt dee2e8e67d Add AppVeyor badge
Distinguish between windows and linux builds
2015-10-13 09:52:47 -06:00
makpoc 4544dabd56 Add tests for command splitting 2015-10-13 14:39:18 +03:00
Austin 222781abca websocket refactored to use gorilla 2015-10-12 19:59:11 -07:00
Matthew Holt 55a098cae8 Add AppVeyor manifest 2015-10-12 19:11:02 -06:00
Matt Holt 837c17c396 Merge pull request #267 from zmb3/reportcard
Ran gofmt -s, fixed some golint warnings, refactored some large functions

Minor quality improvements (closes #253)
2015-10-11 16:00:58 -06:00
Zac Bergquist f9bc74626d Address various lint and gocyclo warnings. Fixes #253 2015-10-11 16:28:02 -04:00
Matt Holt 17c91152e0 Merge pull request #266 from Makpoc/master
Add tests for root.go
2015-10-10 20:00:33 -06:00
Matthew Holt d414ef0d0f browse: Fix tests that fail only in CI environment
... I think. Submitting as PR to double-check. This change changes file mod times on the testdata to ensure they are not all the same so that the sort is predictable!
2015-10-10 19:53:11 -06:00
Makpoc e66aa25fce Fail the test if the configuration fails. 2015-10-10 04:15:25 +03:00
Matt Holt 75d82e8666 Merge pull request #265 from Karthic-Hackintosh/master
browse: Better test coverage and fix #264
2015-10-09 12:38:05 -06:00
makpoc af42d2a54a Add tests for root.go 2015-10-09 18:20:28 +03:00
Karthic Rao f5cd4f17f8 Exhaustive test coverage to test the usage of sort,order and limit parameter for the browse middleware 2015-10-09 11:28:11 +05:30
Matthew Holt 02c7770b57 Update change list 2015-10-08 11:30:46 -06:00
Matt Holt 0f049856a4 Merge pull request #262 from pyed/patch-4
browse: allow consecutive spaces in filenames
2015-10-07 10:41:19 -06:00
pyed bd14171b88 allow consecutive spaces for browse 2015-10-07 19:16:49 +03:00
Matthew Holt e6ba930e65 Merge branch 'master' of github.com:mholt/caddy 2015-10-01 09:58:17 -07:00
Matthew Holt 61a6b9511a Commenting on the need for additional redirect tests 2015-10-01 09:58:07 -07:00
Matt Holt 87efc67f48 Merge pull request #259 from abiosoft/master
New core middleware, MIME.
2015-10-01 09:56:56 -07:00
Abiola Ibrahim 9e2da6ec48 New core middleware, MIME. 2015-09-30 18:37:10 +01:00
Matthew Holt 3f9f675c43 redir: Include scheme in redirect rules
And added tests for status code and scheme
2015-09-30 08:38:31 -06:00
Matthew Holt 698399e61f Move controller_test.go into controller.go
Turns out the stuff in the test file needs to be exported so external add-ons can use them
2015-09-28 21:16:40 -06:00
Matthew Holt ec676fa15e Version bump: 0.7.6 2015-09-28 14:57:00 -06:00
Matthew Holt 122e3a9430 rewrite: Make internal header field name a const 2015-09-28 14:54:48 -06:00
Matt Holt 79a7f8a460 Merge pull request #247 from DenBeke/master
fastcgi: Stripping PATH_INFO from SCRIPT_NAME
2015-09-28 14:33:27 -06:00
Mathias Beke bb85a84561 Merge remote-tracking branch 'upstream/master'
Conflicts:
	middleware/fastcgi/fastcgi.go
2015-09-28 22:11:05 +02:00
Matthew Holt be6fc35326 fastcgi: Fix REQUEST_URI if rewrite directive changes URL 2015-09-27 18:48:28 -06:00
Matthew Holt 79de2a5de2 Stubbed out basic code to obtain Let's Encrypt cert 2015-09-26 18:16:14 -06:00
Matt Holt ca1f1362cc Merge pull request #257 from tarfu/patch-1
core: change to new http2 repo
2015-09-25 06:22:16 -06:00
Tobias Breitwieser 0ca0d552eb change to official http2 repo
The golang.org/x/net/http2 is now the official http2 repo.
It is advised to change the imports to it.
2015-09-25 14:10:03 +02:00
Mathias Beke 8baead6107 Merge remote-tracking branch 'upstream/master' 2015-09-25 11:54:15 +02:00
Matthew Holt 4f5a29d6d1 errors: New 'visible' mode to write stack trace to response
Also updated change list and added/improved tests
2015-09-24 16:21:28 -06:00
Matthew Holt da7562367c errors: Restore http status text in test 2015-09-24 14:01:08 -06:00
Matthew Holt 6001c94f30 errors: Fix test 2015-09-24 13:46:54 -06:00
Matt Holt 104a5998cb Merge pull request #251 from abiosoft/master
rewrite: Use middleware.Replacer
2015-09-23 14:22:54 -06:00
Matthew Holt 6cbd3ab096 proxy: 64-bit word alignment for 32-bit systems (fixes #252) 2015-09-22 16:47:39 -06:00
Abiola Ibrahim 7f9fa5730b Rewrite: Use only fragment, remove frag. 2015-09-20 18:13:53 +01:00
Matthew Holt bdccc51437 More consistent error messages 2015-09-20 10:55:16 -06:00
Abiola Ibrahim 0e039a1868 Rewrite: Use middleware.Replacer.
Bug fix for regexps starting with '/'.
2015-09-20 08:49:55 +01:00
Matthew Holt 10ab037833 Moved fileServer and browse.IndexPages into middleware package 2015-09-19 20:35:48 -06:00
Matt Holt 540a651fdf Merge pull request #250 from hacdias/master
browse: User defined variables (for templates)
2015-09-19 20:11:44 -06:00
Matthew Holt ee893325c4 Update change list 2015-09-19 11:24:44 -06:00
Henrique Dias 8120e57850 add user defined variables into browse template 2015-09-18 08:52:12 +01:00
Henrique Dias 043e000459 Merge pull request #1 from mholt/master
Update
2015-09-18 08:44:47 +01:00
Matt Holt 66fb8f031b Merge pull request #248 from hacdias/master
browse: Option to ignore indexes
2015-09-17 19:01:12 -06:00
Matthew Holt 9e2bef146e middleware: Added StripHTML to Context type 2015-09-17 16:23:30 -06:00
Henrique Dias 4c642e9d3c browse IgnoreIndexes option 2015-09-17 20:37:49 +01:00
Henrique Dias 30b19190dc add ignoreIndexes option to browse 2015-09-17 20:33:39 +01:00
Matthew Holt 840bc505f6 This is a pretty cool change 2015-09-16 21:31:58 -06:00
Matthew Holt 8c843ceefd middleware: Add StripExt to Context type for stripping extensions from paths 2015-09-16 21:31:58 -06:00
Mathias Beke aa5a595762 middleware/fastcgi: Stripping PATH_INFO from SCRIPT_NAME 2015-09-16 20:25:40 +02:00
Matt Holt 9dfb940d80 Merge pull request #245 from LK4D4/update_go
Use latest Go minor versions for testing
2015-09-14 11:49:59 -06:00
Alexander Morozov 1dbfeb7ecd Use latest minors Go for testing
Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-14 10:43:46 -07:00
Matt Holt f4054b6954 Merge pull request #241 from LK4D4/fix_race_test
markdown: Use less strict condition to avoid problems with concurrency
2015-09-11 15:58:13 -06:00
Alexander Morozov faaef83954 Use less strict condition to avoid problems with concurrency
In latest go versions TestWatcher fails pretty often, because it is
"more concurrent" now. Reproducible with go master:
while go test github.com/mholt/caddy/middleware/markdown; do :; done

Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-11 10:25:13 -07:00
Matt Holt 287543a0e6 Merge pull request #238 from LK4D4/fix_vet
Fix all vet warnings and add vet check to CI
2015-09-11 10:21:22 -06:00
Abiola Ibrahim 7545755b00 Merge pull request #240 from LK4D4/fix_map_race
markdown: fix race in accessing map
2015-09-11 17:00:24 +01:00
Abiola Ibrahim 26dc212f4c Merge pull request #239 from LK4D4/race_test
Fix race in test
2015-09-11 16:58:19 +01:00
Alexander Morozov a5128da67a markdown: fix race in accessing map
Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-11 08:34:52 -07:00
Alexander Morozov 37eedf5cdc Fix race in test
Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-11 08:34:08 -07:00
Alexander Morozov f2e680430a Add vet check to CI
Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-10 20:53:09 -07:00
Alexander Morozov 740a6a7ad5 Use func from stdlib to clone *tls.Config for calming vet
Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-10 20:52:49 -07:00
Alexander Morozov 1236e492a9 Fix format verbs for funcs
Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-10 19:59:19 -07:00
Alexander Morozov 80db177f5a Fix vet warnings about unkeyed fields
Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-10 19:57:23 -07:00
Alexander Morozov 3d1cac313c Use %v instead of %p to calm vet
Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-10 19:52:23 -07:00
Matt Holt dc4a5ae1fd Merge pull request #237 from LK4D4/fix_lock
markdown: Use markdown.Config as pointer everywhere
2015-09-10 18:23:26 -06:00
Alexander Morozov da7b9a6bbc Use markdown.Config as pointer everywhere
* As value mutex was copied and therefore synchronization worked wrong
* It's pretty big structure with reference types, so copying create unnecessary
  pressure on GC

Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-10 15:12:50 -07:00
Matt Holt 55de037035 Merge pull request #236 from LK4D4/fix_format_in_test
basicauth: Fix format call in tests
2015-09-10 15:16:13 -06:00
Alexander Morozov c468b114e4 Fix format call in tests
Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-10 14:04:33 -07:00
Matt Holt d96bd5269a Merge pull request #235 from Karthic-Hackintosh/master
middleware: Complete test coverage for replacer
2015-09-10 08:27:23 -06:00
Karthic Rao ed4148f20e Complete test coverage for replacer for Go 2015-09-10 10:28:13 +05:30
Matt Holt f8e2cc8008 Merge pull request #234 from humboldtux/browse
core: Configuration as command line arg #222
2015-09-08 14:44:28 -06:00
Benoit Benedetti 5d32af8a6b Fix typo in loadConfigs comment 2015-09-08 22:38:30 +02:00
Benoit Benedetti ed10863494 Configuration as command line arg #222 2015-09-08 22:27:05 +02:00
Matthew Holt 4e1717db4c basicauth: htpasswd path now relative to site root 2015-09-05 16:04:30 -06:00
Matt Holt 159b68aab4 Merge pull request #228 from tgulacsi/htpasswd
basicauth: Add htpasswd support
2015-09-05 14:56:23 -06:00
Matt Holt d76cf6d337 Merge pull request #230 from evermax/master
log, errors: Use localtime for the log roller timestamp
2015-09-04 11:27:23 -06:00
Maxime 69950e57f0 Use localtime for the log roller timestamp 2015-09-04 19:18:01 +02:00
Matt Holt d44ab3dbab Merge pull request #229 from LK4D4/fix_format
Fix formatting directives in tests
2015-09-04 10:07:30 -06:00
Alexander Morozov b199825c3b Fix formatting directives in tests
Signed-off-by: Alexander Morozov <lk4d4@docker.com>
2015-09-04 08:34:58 -07:00
Matthew Holt 94becb89f6 Add Go 1.5 to the Travis CI manifest 2015-09-02 18:28:03 -06:00
Matt Holt 1f4231e1f0 Merge pull request #216 from evermax/master
log, errors: Added lumberjack library for log rolling
2015-09-02 17:44:10 -06:00
Maxime bdcbd11d65 Merge branch 'master' of https://github.com/mholt/caddy 2015-09-02 15:16:06 +02:00
Maxime 008160998a Added LogRoller parser and entity.
The errors and logs can now have log rolling if provided by the user.
The current customisable parameter of it are:
The maximal size of the file before rolling.
The maximal age/time of the file before rolling.
The number of backups to keep.
2015-09-02 15:13:31 +02:00
Matt Holt 1cafb1eea5 Merge pull request #226 from Karthic-Hackintosh/master
browse: Initial test for json response in browse.go
2015-08-31 21:34:54 -06:00
Tamás Gulácsi 392f1d70eb Add htpasswd support for basic auth
If the password arg starts with htpasswd=, then the rest is treated as
the file name of the htpasswd file, and used for md5 and sha1 hashes.
2015-08-30 20:08:42 +02:00
Matthew Holt d79d2611ca Mention setcap in readme so it's more prominent 2015-08-30 10:55:30 -06:00
karthic rao e3cea042d6 Left over comments removed
Redundant comments in the code removed
2015-08-30 19:00:35 +05:30
Karthic Rao 679668e3c0 removed redundant comment lines 2015-08-30 18:57:20 +05:30
Karthic Rao 730269743f Json response initial test for browse.go 2015-08-29 08:04:01 +05:30
Matt Holt bfc61824b9 Merge pull request #225 from Karthic-Hackintosh/master
middleware: Initial test for replacer
2015-08-27 15:01:45 -06:00
Karthic Rao 444f9e40d5 initial test for replacer 2015-08-27 23:36:32 +05:30
Matt Holt c5006321a7 Merge pull request #224 from pyed/patch-2
Fixing comment
2015-08-27 10:13:38 -06:00
pyed b9d3e7721e Fixing my comment
the old comment might throw the source-reader off, my bad.
2015-08-27 18:01:46 +03:00
Matt Holt 49a229835a Merge pull request #220 from pyed/json
browse: Optional JSON output
2015-08-27 08:48:44 -06:00
Abdulelah Alfuntukh 414b47d653 adds json option for the browse middleware 2015-08-24 23:37:11 +03:00
Abiola Ibrahim b62d087bb6 Merge pull request #218 from Karthic-Hackintosh/master
Complete test coverage for recorder.go of middleware package
2015-08-14 07:23:27 +01:00
Karthic Rao 4704625e3a Complete test coverage for middleware/recorder.go 2015-08-14 09:59:22 +05:30
Karthic Rao 53c4797606 Initial setup of test for recorder.go of middleware package 2015-08-11 22:02:13 +05:30
Matthew Holt 60b6c0c03d Add link to joshix/caddy in readme 2015-08-10 16:33:36 -06:00
Matt Holt 5f3ef9c0da Merge pull request #215 from Karthic-Hackintosh/master
Initial test for middleware/middleware.go
2015-08-10 07:26:56 -06:00
Maxime bb5a322ce2 Added lumberjack library for log rolling 2015-08-08 16:13:10 +02:00
Karthic Rao bb072faeee Initial test for middleware/middleware.go 2015-08-08 00:56:59 +05:30
Matthew Holt a2be7b4548 Bumped version to 0.7.5 2015-08-05 13:26:12 -06:00
Matthew Holt 7796ff0f69 core: Disable TLS for http sites (again)
Fixes bug introduced in 0ac8bf5 - Also note that setup functions no longer have access to server port. Will need to fix later.
2015-08-05 11:33:51 -06:00
Matt Holt afd6b7ea27 Merge pull request #210 from evermax/master
log, errors: Support system log (except Windows)

Specify log file of 'syslog' to output to system log
2015-08-05 10:25:49 -06:00
Matthew Holt b62de4c521 New template action NowDate to get the time.Time of Now() 2015-08-05 08:27:13 -06:00
Matthew Holt 2e8a74ecff markdown: Prefix log messages, and slight refactor
Also change sha1 to md5 for the directory scans; slightly faster.
2015-08-05 08:15:52 -06:00
Matthew Holt e94e90b046 Add new docker link, DO link, and enhanced comment 2015-08-05 07:37:51 -06:00
Matthew Holt 7173764d6d markdown: Render lists as part of summary (upstream bug workaround)
See github.com/russross/blackfriday/issues/189
2015-08-05 07:37:51 -06:00
Matthew Holt e4643f048a core: Bind all listeners to wildcard host by default (closes #208)
This behavior can still be overridden by bind directive
2015-08-05 07:37:51 -06:00
Matt Holt 236c8c4eaf Merge pull request #214 from abiosoft/master
markdown: generate static sites after links
2015-08-05 07:24:56 -06:00
Abiola Ibrahim 3b910645e7 Markdown: generate static sites after links. 2015-08-05 09:55:04 +01:00
Matt Holt 9669363504 Merge pull request #213 from abiosoft/master
markdown: Watch for file changes for links. Removed sitegen requirement for link index.
2015-08-04 18:06:37 -06:00
Abiola Ibrahim b5d79bdccc Markdown: Removed unused constant. 2015-08-05 01:01:57 +01:00
Abiola Ibrahim 1d3d705aae Markdown: Rename 'development' to 'dev' in config. 2015-08-05 00:49:42 +01:00
Abiola Ibrahim 2ab466599d Markdown: Modify development mode to generate links on page requests. 2015-08-05 00:41:04 +01:00
Abiola Ibrahim 851026d3fa Markdown: Watch for file changes. Removed sitegen dependency for links. 2015-08-04 23:35:09 +01:00
Matthew Holt 32da2ed706 Updated change log 2015-08-03 17:51:33 -06:00
Matthew Holt d8c50264cc Fix parser when address is empty token
Bug detected by go-fuzz (more fuzzing is required; need larger corpus)
2015-08-03 17:31:10 -06:00
Matthew Holt 8d81ae88da fastcgi: Add HTTPS env variable on HTTPS connections 2015-08-03 17:16:24 -06:00
Maxime f1f1eb040a Changed the facility of syslog to be LOCAL0 2015-08-03 12:00:17 +02:00
Matthew Holt 36fa6e857b markdown: Sitegen only occurs if directory exists
We do this by returning an error and stopping link generation (which scans the markdown path for files)
2015-08-01 16:09:10 -06:00
Matthew Holt b401267aa4 License is a text file 2015-08-01 16:07:59 -06:00
Matthew Holt e63b83c8c4 markdown: report errors during initial sitegen 2015-08-01 13:08:55 -06:00
Matthew Holt 0ac8bf58ea core: Run startup/shutdown functions only once
Even if defined for multiple hosts. Startup or shutdown callbacks registered by any directive (startup, shutdown, markdown, git, log, etc.) will only run as many times as it appears in the Caddyfile, not repeated for each host that shares that server block. Fixing this involved refactoring three packages (yeesh) and we need to restore some tests that are no longer valid (that used to verify splitting a multiServerBlock into multiple serverBlocks).
2015-08-01 13:08:31 -06:00
Maxime 7fb1c7e91d Change access log level to INFO 2015-07-30 22:01:15 +02:00
Maxime 60690c78ae Merge branch 'master' of https://github.com/mholt/caddy 2015-07-30 20:32:15 +02:00
Maxime 4b92808bbf Add syslog support for log and errors directives 2015-07-30 20:29:41 +02:00
Matthew Holt 73397a0973 New version 2015-07-30 10:29:28 -06:00
Matthew Holt 35e25be1a4 browse: Size displayed in IEC format
Powers of 2, or base 1024, rather than powers of 10 or base 1000. The powers of 2 are technically more accurate.
2015-07-29 19:15:38 -06:00
Matthew Holt f7129b219e Fix markdown tests that I broke 2015-07-29 17:47:33 -06:00
Matthew Holt e1153f8797 Update change list... 2015-07-29 17:43:00 -06:00
Matthew Holt ff28bc8b0a markdown: Change .Url -> .URL, increase summary length
Also, summary truncated at nearest space instead of middle of word, and code spans become part of summary.
2015-07-29 17:43:00 -06:00
Matthew Holt 0b01489f7d templates: Date -> Now, add Replace and Truncate 2015-07-29 17:43:00 -06:00
Matt Holt 7a69770026 Merge pull request #207 from evermax/master
markdown: Added test on static files generation
2015-07-29 16:58:14 -06:00
Maxime ec51e14451 Merge branch 'master' of https://github.com/mholt/caddy 2015-07-29 23:43:26 +02:00
Matt Holt 86e9749d6c Merge pull request #204 from abiosoft/master
markdown: Add .Links action and flattened metadata structure

.Links available only for generated sites and variables no longer in a [variables] category in metadata (flat structure).
2015-07-29 11:12:02 -06:00
Abiola Ibrahim aa89f30f2a renamed sorter to byDate. 2015-07-29 18:06:53 +01:00
Maxime da794a866e Change date of the files in test to serve the static file and not generate a new one 2015-07-29 18:00:08 +02:00
Maxime 705cd16dee Fixed path issue: was absolute path but should be relative path 2015-07-29 17:12:32 +02:00
Maxime 0168a627a4 Added test on markdown static files generation 2015-07-29 16:21:35 +02:00
Abiola Ibrahim 7b29568eb1 Code cleanups.
Fix more race conditions.
2015-07-29 12:21:34 +01:00
Matthew Holt a585379bbb proxy: Parse error if property is unknown 2015-07-28 13:50:40 -06:00
Abiola Ibrahim e240cd5ba2 Metadata variables flattened.
Fix race condition on parsers.
Added page links generator.
2015-07-28 05:21:09 +01:00
Matt Holt e043bbdd62 Merge pull request #203 from Karthic-Hackintosh/master
Test setup func for fastcgi.go
2015-07-27 09:01:18 -06:00
Karthic Rao 094436c23a Complete Test set For config/setup/fastcgi.go 2015-07-27 20:19:20 +05:30
Matt Holt c6b2600c62 Merge pull request #202 from evermax/master
markdown: Change metadata variables type to map[string]string

Instead of map[string]interface{}
2015-07-26 12:03:14 -06:00
Maxime d1eb2ea9e2 Changed metadata variables type:
from map[string]interface{} to map[string]string
2015-07-26 18:32:34 +02:00
Matthew Holt 453d3eb567 markdown: Fix when md file has front matter but empty body 2015-07-25 15:47:33 -06:00
Matthew Holt 53b7b131cb Update change list, and note in comment 2015-07-25 15:18:10 -06:00
Matthew Holt bec9b9a3f7 redir: Redirect tables
Open a redir block to bulk-specify a bunch of redirects that share a status code
2015-07-25 15:18:10 -06:00
Matt Holt bf47951f3a Merge pull request #196 from evermax/master
markdown, browse: Integrated Context struct for templating
2015-07-25 15:15:51 -06:00
Maxime 604c8abb59 Remove debug line, add file name as default title 2015-07-25 22:39:13 +02:00
Maxime ef4a4b0ab8 Removed attribute not needed. 2015-07-24 22:17:14 +02:00
Maxime 24bdb433c9 Changed .Var to .Doc in Markdown templates
Put the title into the .Doc variables as well.
Changed the test template file to use new names.
2015-07-24 22:14:05 +02:00
Matthew Holt 6006de5795 Update change list 2015-07-24 10:27:19 -06:00
Matthew Holt a578c43810 browse: Add txt files to list of default files 2015-07-24 10:27:19 -06:00
Matthew Holt 74b758034e redir: Allows replacements; defaults to exact match redirects
This is a breaking change for those who expect catch-all redirects to preserve path; use {uri} variable explicitly now
2015-07-24 10:27:19 -06:00
Matthew Holt 04571ff393 NewReplacer takes third argument for empty value string 2015-07-24 10:27:19 -06:00
Matt Holt 7adff28aa9 Merge pull request #198 from pyed/sort
browse: Save sorting preferences as cookies
2015-07-23 11:06:56 -06:00
pyed 1589129ea1 Save sorting preferences as cookies
To be used across directories
2015-07-23 13:25:03 +03:00
Maxime 97dcc79a7f Remove undesired committed debug logs 2015-07-23 11:53:15 +02:00
Maxime bc2feece4b Moved test files to testdata folder.
Changed the tests accordingly.
2015-07-23 09:35:46 +02:00
Matt Holt 1f17895b12 Merge pull request #197 from Karthic-Hackintosh/master
initial test for config/setup/fastcgi.go
2015-07-22 23:25:04 -06:00
Karthic Rao 665f24d85f initial test for config/setup/fastcgi.go 2015-07-23 10:27:42 +05:30
Maxime 2df30d186e Added a test on markdown for the default template 2015-07-21 17:45:32 +02:00
Maxime 6451e10d3e Add context to markdown template
Created a struct containing middleware.Context, Title, Markdown and the
variables from the user to use to render the template.
The title now can be accessed via {{.Title}}.
The variables can now be accessed via {{.Var.myVariableName}}.
2015-07-21 07:58:34 +02:00
Matt Holt d222a9e9f2 Merge pull request #192 from netmml/patch-2
specify go versions to use via travis
2015-07-19 23:13:49 -06:00
Matthew Holt 00997db5ae markdown: Fix large markdown files that got truncated 2015-07-18 12:57:16 -06:00
Maxime 2d5320c454 Added test for the browse directive
Created sample files for the test
2015-07-18 11:37:05 +02:00
Maxime 2fa6e278d2 Merge branch 'master' of https://github.com/mholt/caddy 2015-07-18 09:58:27 +02:00
netmml ef381fbb54 Update .travis.yml 2015-07-18 03:37:22 -04:00
Matthew Holt a74b20f278 .zip for Win/Mac, .tar.gz for Linux/BSD 2015-07-17 17:26:33 -06:00
Maxime f536bc94b2 Added the Context to the browse directive
Moved the Context type to middleware and exported it.
Users can use .Include and others in browse directive templating
Created test for the templates directive.
2015-07-17 20:07:24 +02:00
Matthew Holt 95140f948f version bump 0.7.3 2015-07-15 09:52:47 -06:00
Matthew Holt afc540f6b7 Updated changes 2015-07-15 09:36:34 -06:00
Matt Holt fcf2622c26 Merge pull request #187 from evermax/master
redir: Preserve query string on catch-all redirect (fixes #180)
2015-07-13 09:42:19 -06:00
Maxime d9ebc5398a Changes regarding review
Use path.Join and then check if the request had a slash at the end to
place it again.
2015-07-12 21:22:15 +02:00
Maxime eea68c34ad Changes regarding comment.
Used http status code instead of a hardcoded value.
Used url.Parse instead of url.ParseRequestURI, so that you can parse
both absolute and relative URL.
2015-07-12 16:43:35 +02:00
Maxime 8a2d0890a2 Changes regarding issue 180
The get parameters are now forwarded when redirected.
Added some tests to validate this behavior.
2015-07-12 16:01:32 +02:00
Matthew Holt 1a82943db2 core: Simplify Server initializer 2015-07-11 12:00:21 -06:00
Matt Holt 3c36edec1f Merge pull request #185 from Karthic-Hackintosh/master
Complete test coverage for config/setup/markdown.go
2015-07-11 10:39:52 -06:00
Matt Holt 0f913e89db Merge pull request #183 from peterhellberg/extensions_tests
ext: Initial tests for the extensions middleware
2015-07-11 10:38:42 -06:00
Karthic Rao 2b7ec1b023 Complete test coverage for config/setup/markdown.go 2015-07-11 00:44:31 +05:30
Peter Hellberg 33fa29fda0 extensions: Initial tests 2015-07-10 12:05:06 -06:00
Matthew Holt d3c229375c Fixed import command, added tests 2015-07-07 22:38:48 -06:00
Matthew Holt c82d7c2dd2 templates: Better error handling for missing files 2015-07-06 23:37:27 -06:00
Matt Holt 553d76dab3 Merge pull request #179 from abiosoft/master
gzip: Const for wildcard extension (and added test)
2015-07-05 23:46:30 -06:00
Abiola Ibrahim d4f0ac2303 Merge remote-tracking branch 'upstream/master' 2015-07-06 06:43:02 +01:00
Abiola Ibrahim 4588812d24 Gzip: Fix wildcard extension bug. 2015-07-06 06:36:48 +01:00
Matthew Holt 9467dbdd40 Fix errors tests 2015-07-05 23:23:35 -06:00
Matthew Holt 68c416e414 gzip: Fix to allow wildcard extension 2015-07-05 23:23:21 -06:00
Matthew Holt 71c4fdbc85 errors: Prepend timestamp to log entry 2015-07-05 23:20:37 -06:00
Matt Holt 7dbe42286d Merge pull request #174 from mholt/gzipfix
gzip: Make it gzip again

Removes mimes filter in favor of extensions filter (might be temporary)
2015-07-05 23:19:51 -06:00
Matthew Holt b5579ca910 gzip: Remove mimes 2015-07-03 18:13:30 -06:00
Matt Holt cd2b255ae3 Merge pull request #177 from Karthic-Hackintosh/master
preliminary test case for config/setup/markdown.go
2015-07-03 13:29:53 -06:00
Karthic Rao 93f29a7598 preliminary test case for config/setup/markdown.go 2015-07-03 10:03:22 +05:30
Matthew Holt 32ef35b952 gzip: Fix tests 2015-07-01 19:05:31 -06:00
Matthew Holt abf22909f1 gzip: Make it gzip again 2015-07-01 18:56:30 -06:00
Matthew Holt a86e16ddde That's a wrap; version 0.7.2 2015-07-01 12:09:31 -06:00
Matt Holt 263fa064cd Merge pull request #169 from abiosoft/master
git: Remove from core (available as add-on)
2015-07-01 11:44:49 -06:00
Matthew Holt 915172e9ef templates: Close files after done including them 2015-07-01 11:36:37 -06:00
Matthew Holt 4d066b7e30 ext: Only append extension if request is not for directory (fixes #173) 2015-07-01 11:35:52 -06:00
Abiola Ibrahim a7f0705bcf Merge remote-tracking branch 'upstream/master' 2015-07-01 14:01:45 +01:00
Matthew Holt 16f18bfeff Update change list 2015-06-30 18:21:18 -06:00
Matthew Holt 7a42e60bcb templates: Support for nested include files
i.e. included files are also parsed as templates
2015-06-30 18:21:18 -06:00
Abiola Ibrahim aecdecbdf8 Merge remote-tracking branch 'upstream/master' 2015-06-30 17:31:40 +01:00
Matt Holt a790db134d Merge pull request #171 from bigtiger/patch-1
Massage some verbiage
2015-06-30 10:24:59 -06:00
Matt Holt 75b77d508d Merge pull request #172 from abiosoft/fastcgi-fix
fastcgi: Fix for Issue 141: index not found, 502 Bad Gateway.
2015-06-30 10:23:49 -06:00
Abiola Ibrahim 4240817a3a Fix for Issue 141: index not found, 502 Bad Gateway. 2015-06-30 11:54:50 +01:00
Jim Remsik 24307e85b7 Massage some verbiage
The grammar felt off on the sentence I changed. You can take or leave this PR, I won't be offended if you dislike my suggestion.
2015-06-29 16:52:40 -07:00
Abiola Ibrahim b030d0cf79 Merge remote-tracking branch 'upstream/master' 2015-06-29 09:16:59 +01:00
Matt Holt b0d3a8e1b5 Merge pull request #170 from Karthic-Hackintosh/master
Complete test coverage for config/setup/templates.go
2015-06-28 10:00:33 -06:00
Karthic Rao 62456c1bed Complete test coverage for config/setup/templates.go 2015-06-28 07:03:42 +05:30
Abiola Ibrahim 3f1f6720ee Decouple git middleware from caddy core. Now available as an add-on at https://github.com/abiosoft/caddy-git. 2015-06-28 00:31:52 +01:00
Matthew Holt ab0cbf3e12 Updated change list 2015-06-26 16:32:49 -06:00
Matt Holt c12257a1a7 Merge pull request #168 from Karthic-Hackintosh/master
Initial Test case for config/setup/templates.go
2015-06-26 16:23:39 -06:00
Karthic Rao e039577d66 export of variables undone 2015-06-27 03:49:44 +05:30
Karthic Rao 9995466a18 Initial Test case for config/setup/templates.go 2015-06-27 02:27:02 +05:30
Matthew Holt f424f450f2 Fix typo 2015-06-25 08:55:02 -06:00
Matt Holt 677f67db48 Merge pull request #166 from mholt/graceful-resolve
core: Handle address lookup and bind errors more gracefully (fixes #136 and #164)
2015-06-25 08:18:41 -06:00
Matthew Holt 47d1f5eecf Removed tests that are not cross-platform compatible 2015-06-23 22:13:29 -06:00
Matthew Holt d8391d6fbd core: Handle address lookup and bind errors more gracefully (fixes #136 and #164)
Addresses which fail to resolve are handled more gracefully in the two most common cases: the hostname doesn't resolve or the port is unknown (like "http" on a system that doesn't support that port name). If the hostname doesn't resolve, the host is served on the listener at host 0.0.0.0. If the port is unknown, we attempt to rewrite it as a number manually and try again.
2015-06-23 22:01:37 -06:00
Matt Holt 640cd059ce Merge pull request #165 from Karthic-Hackintosh/master
Test for webSocketParse function in config/setup
2015-06-23 20:04:07 -06:00
Karthic Rao a78cea7d8a test for webSocketParse function in config/setup 2015-06-24 07:05:53 +05:30
Matt Holt 7044cbbd67 Merge pull request #161 from Karthic-Hackintosh/master
Modularizing config/setup/Websocket.go and test file for the same
2015-06-23 10:06:05 -06:00
Karthic Rao 292c15cd48 Modularizing config/setup/Websocket.go and for the same 2015-06-23 14:40:27 +05:30
Matt Holt 06a7f1d3da Merge pull request #160 from guilhermebr/master
errors: change missing errors file from error to a warning during parsing
2015-06-22 20:38:23 -06:00
Guilherme Rezende efbf01b49d change from error to a warning in errors setup 2015-06-22 23:19:48 -03:00
Matthew Holt 4b349805db browse: better-looking sort order arrows 2015-06-22 15:04:22 -06:00
Matt Holt 47096e112a Merge pull request #156 from pyed/sort
browse: Ability to sort
2015-06-22 12:40:58 -06:00
pyed 68add78230 Implement sorting functionality for "Browse" 2015-06-21 18:04:47 +03:00
Matt Holt ebae65b6af Merge pull request #157 from abiosoft/master
setup: export functions and variables for external packages.
2015-06-20 10:14:15 -06:00
Abiola Ibrahim 460c0c8a42 setup: export functions and variables for external packages. 2015-06-20 14:59:33 +01:00
Matt Holt 528d1b03f1 Merge pull request #155 from Karthic-Hackintosh/master
Tests for config/setup/internal.go
2015-06-19 12:27:43 -06:00
Karthic Rao 9d33d9d6b0 typo corrected 2015-06-19 23:54:34 +05:30
Karthic Rao d9729b4a2e test for config/setup/internal.go 2015-06-19 23:34:54 +05:30
Matt Holt 1db6c244bb Merge pull request #153 from Karthic-Hackintosh/master
Tests for config/setup/log.go
2015-06-19 11:35:01 -06:00
Karthic Rao 13c5d25a2e Typo corrected 2015-06-19 22:30:48 +05:30
Karthic Rao c166261513 more test cases for the test struct 2015-06-19 19:37:48 +05:30
Karthic Rao 84998a4d19 Tests for config/setup/log.go 2015-06-19 19:15:57 +05:30
Matt Holt 6b27d4ce11 Merge pull request #131 from slav123/master
fastcgi: fix #127
2015-06-17 17:26:52 -06:00
Slawomir Jasinski f11e136068 Update fcgiclient.go 2015-06-18 09:12:35 +10:00
Matt Holt 707ea554ac Merge pull request #149 from Karthic-Hackintosh/master
ext: test for function extParse
2015-06-17 07:01:26 -06:00
Karthic Rao 65f7190030 more cases added to test struct in extParse test 2015-06-17 14:07:26 +05:30
Karthic Rao a5a5c06716 test for function extParse written for config/setup/ext_go 2015-06-17 12:48:52 +05:30
Karthic Rao 9c832893af Merge branch 'master' of https://github.com/mholt/caddy 2015-06-17 12:46:34 +05:30
Karthic Rao 4e15901df1 solving merge conflicts 2015-06-17 12:46:12 +05:30
Karthic Rao 9a32d08e9f test for function exParse under config/setup/ext.go 2015-06-17 11:59:24 +05:30
Matthew Holt c811d416a7 log: Customizable default error function 2015-06-15 10:17:09 -06:00
Matt Holt 92391bfdf9 Merge pull request #135 from simonjefford/ensure_correct_log_status
log: ensure the correct status is always logged
2015-06-15 09:25:29 -06:00
Simon Jefford 6c1f2af53a log: ensure the correct status is always logged
in the case of error (>=400) then no response may have been sent
2015-06-14 21:00:27 +01:00
Matt Holt 7875f98b71 Merge pull request #138 from Karthic-Hackintosh/master
Preliminary test case for setup/config/ext.go
2015-06-13 09:01:47 -06:00
karthic rao 076fc4d72c Update
Update with missing assertion for the Next middleware being properly set , this makes sure that Ext middleware carries the requests to the further middlewares .
2015-06-13 20:25:23 +05:30
karthic rao d7db1b9576 Added Assertions
Added assertions to check for the extensions in the order specified
2015-06-13 15:56:29 +05:30
karthic rao 2a166f088d Update ext_test.go 2015-06-13 10:05:42 +05:30
Karthic Rao 2175c68319 Preliminary test case for setup/config/ext.go 2015-06-13 09:54:54 +05:30
Matthew Holt 3418770fe1 Latest change list 2015-06-12 08:30:53 -06:00
Matt Holt d7051e986f Merge pull request #134 from tw4452852/hijack
middleware: let middleware.responseRecorder be a http.Hijacker
2015-06-12 08:26:49 -06:00
Matthew Holt f36d9bfa2a Add dist/release build script and licenses 2015-06-11 23:17:11 -06:00
Tw e79a88856a let middleware.responseRecorder be a http.Hijacker
Signed-off-by: Tw <tw19881113@gmail.com>
2015-06-12 11:42:28 +08:00
Slawomir Jasinski db2368cd0b Removed parentheses #127 2015-06-12 11:33:55 +10:00
Matt Holt 0f9d26829c Merge pull request #130 from abiosoft/master
Gzip: Accept MIME types.
2015-06-11 19:27:02 -06:00
slav123 29404e34d9 code cleanup 2015-06-11 13:17:56 +10:00
Slawomir Jasinski 14b64fef43 fix #127
fixes issue with Status header coming from php-fpm 5.5 different then regular "HTTP/1.1 200 OK".
If server returns  Status code - "200" will be handled properly instead "throwing runtime error: index out of range"
2015-06-11 09:59:30 +10:00
Abiola Ibrahim e0f10c2b03 Gzip: Accept MIME types. 2015-06-10 22:02:08 +01:00
Matt Holt 01aca02edc Merge pull request #129 from tw4452852/rename_internal
internal: rename middleware's package name from `internal` to `inner`
2015-06-10 08:04:27 -06:00
Matt Holt 5cdfa0aaaf Merge pull request #128 from coolaj86/meta-redirects
redirect: add ability to do meta redirects
2015-06-10 08:03:59 -06:00
Tw 90921a9deb rename middleware's name from internal to inner
The internal package has the special meaning in go
(see https://golang.org/s/go14internal).
So rename it to `inner`.

Signed-off-by: Tw <tw19881113@gmail.com>
2015-06-10 15:48:41 +08:00
AJ ONeal d6a7dfc1a5 add ability to do meta redirects
Proper Location redirects are disadvantageous in some situations.
For example, you may want a developer to know that a resource is available
via https, but you don't want an insecure call to the API to succeed.
2015-06-10 05:48:40 +00:00
Matthew Holt 00093a2052 core: Fix to allow empty (wildcard) host 2015-06-09 23:07:32 -06:00
Matt Holt 3a795de828 Merge pull request #117 from zmb3/errorfmt
Update error strings (start with lowercase letters)
2015-06-09 08:24:08 -06:00
Matt Holt cde60ed6b2 Merge pull request #121 from tw4452852/test_errors
errors: Add unit test for errors middleware
2015-06-09 08:06:09 -06:00
Tw b717e6f2d8 Add unit test for errors middleware
Signed-off-by: Tw <tw19881113@gmail.com>
2015-06-09 15:24:53 +08:00
Zac Bergquist 3aff1677cc Fix failing test that's looking for a specific error message. 2015-06-08 20:29:48 -04:00
Zac Bergquist 9e97d79c81 Ensure that proper names are capitalized in error strings. 2015-06-08 17:35:16 -04:00
Matt Holt 47415937dd Merge pull request #118 from mholt/jumanjiman-docker
Add link to Docker container jumanjiman/caddy
2015-06-08 08:24:20 -06:00
Matthew Holt aed649475e Add link to Docker container jumanjiman/caddy 2015-06-07 20:22:16 -06:00
Zac Bergquist 41e1f1ffa5 Update error strings (start with lowercase letters) 2015-06-07 20:49:17 -04:00
Matt Holt 20b6e971c0 Merge pull request #116 from abiosoft/master
Git: fix for data races.
2015-06-07 14:32:20 -06:00
Abiola Ibrahim c42e60a3d2 Git: fix for data races. 2015-06-07 20:39:24 +01:00
Matt Holt 995a2ea618 Merge pull request #112 from abiosoft/master
Gzip: Added compression level, extension and path filters.
2015-06-06 18:37:59 -06:00
Abiola Ibrahim 13db60d382 rename gzip test function to TestGzipHandler 2015-06-07 01:27:36 +01:00
Abiola Ibrahim c9233d7446 Gzip: Added compression level, extension and path filters. 2015-06-07 01:21:54 +01:00
Matt Holt 6080c4fab1 Merge pull request #105 from abiosoft/master
FastCGI: allow "unix:" prefix for unix sockets.
2015-06-04 16:09:08 -06:00
Abiola Ibrahim 820b2af43a FastCGI: allow "unix:" prefix for unix sockets. 2015-06-04 23:03:58 +01:00
Matt Holt 1cb0053720 Merge pull request #102 from brk0v/proxy-without
proxy: Add "without" option
2015-06-04 08:24:48 -06:00
Viacheslav Biriukov 822a615c6c rollback tests 2015-06-04 14:02:52 +00:00
Viacheslav Biriukov 593557659c fix tests and change naming 2015-06-04 13:57:39 +00:00
Matt Holt c6e2b9ccc0 Merge pull request #99 from xenolf/github-deploy
git: Webhook middleware for GitHub events
2015-06-03 19:30:20 -06:00
xenolf b4780a41d3 Added webhook functionality to the git middleware.
The webhook providers reside behind a small interface which determines if
a provider should run. If a provider should run it delegates
responsibility of the request to the provider.
ghdeploy initial commit

Added webhook functionality to the git middleware.
The webhook providers reside behind a small interface which determines if a provider should run. If a provider should run it delegates responsibility of the request to the provider.

Add tests

Remove old implementation

Fix inconsistency with git interval pulling.

Remove '\n' from logging statements and put the initial pull into a startup function
2015-06-04 03:24:16 +02:00
Viacheslav Biriukov 4790dacbf7 add without to proxy middleware 2015-06-03 18:06:24 +00:00
Matthew Holt 4852f0580b Bump to 0.7.1 2015-06-02 12:19:08 -06:00
Matt Holt 9d7639a9d3 Merge pull request #98 from mholt/clientauth
tls: Client authentication
2015-06-02 12:05:05 -06:00
Matt Holt 6c52368124 tls: Fix format string 2015-06-02 07:42:38 -06:00
Matthew Holt c78eb50eb8 tls: Client authentication 2015-06-01 23:22:11 -06:00
Matthew Holt 9ce0e8e17c proxy: Added tests for reverse websocket proxy 2015-06-01 22:39:53 -06:00
Matthew Holt 32825e8a79 basicauth: Patch timing vulnerability 2015-06-01 20:33:07 -06:00
Matthew Holt cb8691a381 Add abiosoft's docker container to the list 2015-06-01 20:33:07 -06:00
Matt Holt 5ddcd60332 Merge pull request #96 from acmacalister/feature/ws-proxy
add supported for ws in reverse proxy
2015-06-01 20:31:41 -06:00
Austin 68cd4bdeab check server response instead of client 2015-06-01 19:29:32 -07:00
Austin ccd3e55b32 changes as noted in PR 2015-06-01 10:23:57 -07:00
Austin 56ec7b9887 websocket directive, upgrade comparison 2015-05-30 11:34:54 -07:00
Austin 2d6ff40649 add supported for ws in reverse proxy 2015-05-29 19:21:50 -07:00
Matt Holt 9bdd9bdecc Merge pull request #94 from acmacalister/feature/custom-policies
added custom policy support
2015-05-29 12:33:08 -06:00
Austin dd946f8ab5 moved init to policy.go 2015-05-28 18:16:23 -07:00
Austin 593aec9ab1 changes per comment 2015-05-28 16:53:54 -07:00
Austin 6b173b5170 added custom policy support 2015-05-28 15:56:11 -07:00
Matt Holt 130301e32e Merge pull request #92 from abiosoft/master
Git: code refactor. replace Sleep with Ticker
2015-05-28 08:37:13 -06:00
Abiola Ibrahim 2013838bfd Git: mock time functions in tests. 2015-05-28 10:20:26 +01:00
Abiola Ibrahim 879558b9ee Git: code refactor. replace Sleep with Ticker 2015-05-26 20:20:57 +01:00
Matt Holt ee059c0910 Merge pull request #90 from abiosoft/master
Git: More tests. Code refactor.
2015-05-25 22:15:56 -06:00
Abiola Ibrahim 6c6e0e3f73 Git: More tests. Code refactor. 2015-05-26 04:44:47 +01:00
Matthew Holt b1c8b48e6e core: Version 0.7.0 2015-05-25 15:48:25 -06:00
Matthew Holt 6f05794bb8 git: Fixed unusual but potent race condition 2015-05-25 15:39:04 -06:00
Matt Holt 346135fed3 Merge pull request #89 from guilhermebr/master
removed tls cache option
2015-05-25 15:00:36 -06:00
Guilherme Rezende 69939108e1 removed tls cache option 2015-05-25 14:42:09 -03:00
Matthew Holt 674f454e70 t.Fatal -> t.Fatalf 2015-05-25 08:27:54 -06:00
Abiola Ibrahim a881838836 Merge pull request #88 from zmb3/lintwarnings
Fix lint warnings
2015-05-25 06:28:25 +01:00
Zac Bergquist e4b50aa814 Fix more lint warnings 2015-05-24 22:52:34 -04:00
Zac Bergquist fd8490c689 Fix lint warnings for middleware/websockets 2015-05-24 21:04:03 -04:00
Zac Bergquist d0a51048d7 Fix lint warnings in middleware/rewrite 2015-05-24 21:00:54 -04:00
Zac Bergquist 506f131428 Fix lint warnings for middleware/proxy 2015-05-24 20:58:17 -04:00
Matthew Holt e42c6bf0bb Updated readme 2015-05-23 14:29:59 -06:00
Abiola Ibrahim 535f956682 Merge pull request #86 from abiosoft/master
FastCGI: support for unix sockets.
2015-05-23 03:46:47 +01:00
Abiola Ibrahim 4e94b85ec2 FastCGI: support for unix sockets. 2015-05-23 03:39:23 +01:00
Abiola Ibrahim 6b3b04ffb7 Merge pull request #85 from jpoehls/master
Tweaked ulimit warning message.
2015-05-23 02:37:45 +01:00
Joshua Poehls 36bc3a453f Tweaked ulimit warning message. 2015-05-22 20:24:48 -05:00
Matt Holt 99c069df15 Merge pull request #84 from jpoehls/master
Updated ulimit warning message to include the recommended min value.
2015-05-22 18:50:12 -06:00
Joshua Poehls 04fd7ce9e1 Updated ulimit warning message to include the recommended min value. 2015-05-22 19:34:00 -05:00
Matt Holt cc958947e5 Merge pull request #83 from abiosoft/master
Git: Minor fixes. Refactor. Added tests.
2015-05-22 17:13:29 -06:00
Abiola Ibrahim f44cd5d740 Git: Minor fixes. Refactor. Added tests. 2015-05-22 20:50:04 +01:00
Matthew Holt bba2d63de8 Updated contribution guidelines 2015-05-22 11:08:24 -06:00
Matthew Holt 3420bd6e06 Another test case for parser 2015-05-21 23:50:37 -06:00
Matthew Holt b10d846019 More parser tests 2015-05-21 23:36:17 -06:00
Matthew Holt 11ddb5c6ca Tests for the parser 2015-05-21 18:46:42 -06:00
Matthew Holt b37fed4cc8 Beginning some tests for the parser 2015-05-21 16:31:01 -06:00
Matthew Holt 5397eef234 Added tests for allTokens 2015-05-21 15:42:49 -06:00
Matthew Holt d311345aa5 Fix for running ulimit check 2015-05-21 11:21:08 -06:00
Matthew Holt d6df615588 tls: Mainstream compatibility improvements, better security rating 2015-05-21 10:37:39 -06:00
Matthew Holt ce6e30c09e Lil' bit of godoc 2015-05-21 00:40:58 -06:00
Matthew Holt ee754b4a47 Bug fixes 2015-05-21 00:40:05 -06:00
Matthew Holt 5f72b7438a Created app package, and better TLS compatibility with HTTP/2 2015-05-21 00:06:53 -06:00
Matthew Holt ea9607302a Whoops 2015-05-20 20:50:19 -06:00
Matthew Holt 26bb17337e Warn if ulimit is too low when serving production sites 2015-05-20 20:46:27 -06:00
Matthew Holt 5e8491cf7f Allow IPv6 address without port (fixes #80) 2015-05-20 20:15:39 -06:00
Matthew Holt e42c6ab520 Notice displayed if non-localhost hosts resolve to loopback 2015-05-20 20:06:30 -06:00
Matt Holt 9c039474b3 Merge pull request #78 from guilhermebr/master
tls: Optional block for ciphers, protocols and cache options
2015-05-18 22:57:38 -06:00
Guilherme Rezende b378316103 replace c.ArgErr with c.Err in tls when is the case 2015-05-18 23:27:35 -03:00
Guilherme Rezende a94c7dd788 change tls ssl3 protocol key and tests 2015-05-18 18:15:41 -03:00
Guilherme Rezende 823a7eac03 Added tls option block including: ciphers, protocols and cache options
Signed-off-by: Guilherme Rezende <guilhermebr@gmail.com>
2015-05-18 16:38:21 -03:00
Matt Holt cf2808ae45 Merge pull request #75 from abiosoft/master
Rewrite: Support for Regular Expressions.
2015-05-16 18:44:20 -07:00
Abiola Ibrahim c382c885e4 use middleware.Path for base path comparison 2015-05-16 16:57:57 +01:00
Abiola Ibrahim 7ae9e3a262 Rewrite: Added new variables file, dir, fragment. 2015-05-16 16:30:15 +01:00
Abiola Ibrahim 74d162f377 Rewrite: Support for Regular Expressions. 2015-05-16 13:03:48 +01:00
Abiola Ibrahim ad7b453f03 Rewrite: modified syntax. 2015-05-15 18:47:26 +01:00
Abiola Ibrahim b2afc30d12 Rewrite: added regexp. awaiting documentation and tests. 2015-05-15 02:43:29 +01:00
Matt Holt 1076daa8c9 Merge pull request #73 from abiosoft/master
Fix for Issue 72: Markdown: 500 for YAML metadata
2015-05-12 18:04:03 -06:00
Abiola Ibrahim 8394d72f48 Fix for Issue 72: Markdown: 500 for YAML metadata 2015-05-13 00:44:35 +01:00
Abiola Ibrahim 018fd21741 Merge pull request #71 from abiosoft/master
browse: return forbidden (403) only when it is a permission error.
2015-05-10 17:59:55 +01:00
Abiola Ibrahim a1312465b5 browse: return forbidden (403) only when it is a permission error. 2015-05-10 17:58:44 +01:00
Matt Holt e2273ea676 Merge pull request #69 from peterhellberg/headers_test
headers: Initial test for Headers and change of Rule.Url to Rule.Path
2015-05-10 07:49:11 -06:00
Matt Holt c6eaf0db36 Merge pull request #62 from jordic/testing_basicauth
basicauth: Should write an unauthorized code to response. (Actually only returns it)
2015-05-10 07:47:16 -06:00
Michael Schöbel a0eca49795 Merge pull request #70 from mschoebel/internal_middleware_tests
Internal middleware: added tests
2015-05-10 14:57:16 +02:00
Michael Schoebel e0173ec4c7 internal middleware: added tests 2015-05-10 14:41:48 +02:00
jordi collell 99fa4581aa basicauth: patch for overlapping rules 2015-05-10 08:20:58 +02:00
Peter Hellberg 37b1a81fc7 headers: Corrected copy paste (BasicAuth->Headers) 2015-05-10 07:44:43 +02:00
Matthew Holt 4272536518 markdown: sitegen keyword and run generation at startup
Also fixed bug for markdown files that don't contain front matter
2015-05-09 21:12:52 -06:00
Peter Hellberg 0d5a8a7383 headers: Test for Headers and headersParse funcs 2015-05-09 23:10:18 +02:00
Peter Hellberg df6efe5d88 headers: Replaced usage of Url to Path in setup 2015-05-09 21:57:55 +02:00
Peter Hellberg d9dc9326f2 headers: Initial test for Headers 2015-05-09 21:47:02 +02:00
Peter Hellberg b5fff09b54 headers: Changed Rule.Url to Rule.Path
Updated ServeHTTP comment to indicate that it is 
setting headers and not adding them to existing values.
2015-05-09 21:45:28 +02:00
Matt Holt a96c4d707b Merge pull request #67 from peterhellberg/redirect_test
redirect: initial test for Redirect
2015-05-09 12:21:59 -06:00
Peter Hellberg 0f9df18dfb Check if the Next handler was called unexpectedly
Removed body check and replaced urlPrinter with a inlined Next handler
2015-05-09 20:07:29 +02:00
Matthew Holt 8ea98f8cce markdown: Fix panic: assignment to entry in nil map
Ensures metadata.Variables is made
2015-05-09 11:49:28 -06:00
Matt Holt f2f7e6825f Merge pull request #68 from abiosoft/master
Markdown: support for templates and metadata
2015-05-09 09:13:40 -06:00
Abiola Ibrahim 2f5e2f39cb markdown: remove identifier from Json metadata 2015-05-09 11:49:54 +01:00
jordi collell 4c11854927 added header match and a new failing test 2015-05-09 08:11:02 +02:00
Abiola Ibrahim 6ce83aad2b markdown: Refactor fixes 2015-05-09 00:54:39 +01:00
Matt Holt 2743f97892 Merge pull request #66 from stanislavromanov/master
Close #64 (Make it possible to remove headers)
2015-05-08 17:26:36 -06:00
Abiola Ibrahim 978aef2ae7 Merge remote-tracking branch 'upstream/master' 2015-05-08 23:51:09 +01:00
Abiola Ibrahim 48a12c605a markdown: Added template support. 2015-05-08 23:45:31 +01:00
Peter Hellberg 95b4e61a07 redirect: initial test for Redirect 2015-05-09 00:36:58 +02:00
Matthew Holt 2501691ea4 lexer: Fixed backslashes in quoted strings (closes #65) 2015-05-08 10:32:57 -06:00
stanislavromanov a5a90fe6fc close #64 2015-05-08 17:47:37 +02:00
Abiola Ibrahim 0fccd3707d markdown: documentation done. awaiting test 2015-05-08 16:20:07 +01:00
jordi collell 253c069b26 if basic auth fails should write unauthorized to response 2015-05-08 09:41:17 +02:00
Abiola Ibrahim 2c7de8f328 Merge remote-tracking branch 'upstream/master' 2015-05-08 06:52:14 +01:00
jordi collell 64d203491c Some failing tests 2015-05-08 07:41:48 +02:00
Matthew Holt b2ee6638e4 Tests for rewrite middleware 2015-05-07 14:55:34 -06:00
Matt Holt 557410ffd7 Merge pull request #58 from mschoebel/internal_middleware
Adding "internal" middleware
2015-05-07 13:28:09 -06:00
Matthew Holt 40105094e7 Some tests and utilities for setup functions 2015-05-07 13:11:03 -06:00
Matthew Holt 0dba8d406b NextBlock() doesn't enter an empty block 2015-05-07 13:10:00 -06:00
Matthew Holt 2ce5102473 Add -version flag 2015-05-07 13:09:40 -06:00
Michael Schoebel e3d64169ed Adapted internal middleware
- Detect too many internal redirects - return 500 in this case
2015-05-07 20:48:29 +02:00
Michael Schoebel a5b565e193 Adapted internal middleware
- redirect internally regardless of proxy status code
- support multiple internal redirects
2015-05-07 20:20:45 +02:00
Abiola Ibrahim 7443fd0973 Merge remote-tracking branch 'upstream/master' 2015-05-07 14:02:40 +01:00
Abiola Ibrahim ba613a1567 markdown: template integration done. awaiting documentation and test 2015-05-07 13:45:27 +01:00
Abiola Ibrahim 0bfdb50ade markdown: working version of template integration. Awaiting static site generation and tests. 2015-05-07 00:19:02 +01:00
Matthew Holt a5f20829cb Added link to Slack channel 2015-05-06 16:39:58 -06:00
Matthew Holt abe3e5f597 Move flag.Parse() to main() in case other packages use flags 2015-05-06 14:57:32 -06:00
Matthew Holt e12428efb9 Add build status 2015-05-06 14:49:24 -06:00
Michael Schoebel 0650dd7171 New internal middleware 2015-05-06 22:44:37 +02:00
Matthew Holt 7c844909b9 fastcgi: Add support for OPTIONS requests 2015-05-06 11:14:02 -06:00
Matthew Holt 898896f9e0 Fix for stdin on Windows 2015-05-06 09:16:10 -06:00
Matthew Holt 63ccc626f9 Fix parsing bug for one-line Caddyfiles 2015-05-06 08:58:15 -06:00
Abiola Ibrahim 434ec7b6ea Merge remote-tracking branch 'upstream/master' 2015-05-06 09:06:14 +01:00
Matthew Holt b2549c317c Minor flag help text change; go fmt 2015-05-05 23:19:14 -06:00
Matthew Holt 20c01883c3 Adding Travis CI manifest 2015-05-05 22:54:13 -06:00
Matthew Holt 340a53fb80 Disabling fcgiclient tests until they can be rewritten 2015-05-05 22:53:14 -06:00
Abiola Ibrahim 25847a6192 markdown: added template codes. awaiting integration and tests 2015-05-06 03:37:29 +01:00
Abiola Ibrahim 69eb5cdd8e Merge pull request #56 from abiosoft/master
server: read -conf flag before reading stdin config
2015-05-06 03:34:25 +01:00
Abiola Ibrahim 70d6caf95b server: read -conf flag before reading stdin config file 2015-05-06 03:31:25 +01:00
Matthew Holt 47717fee88 Expanded index file support to other middlewares (fixes #27) 2015-05-05 15:50:42 -06:00
Matthew Holt a9064a7871 templates: Changed .RemoteAddr to .IP and stripped port 2015-05-05 15:49:22 -06:00
Matthew Holt 21c26f48d0 Ensure a default root is always set in the configs 2015-05-05 15:48:40 -06:00
Matthew Holt 857b4f90d9 errors: Log includes file and line number of panics 2015-05-05 15:48:10 -06:00
Matt Holt 97e702b963 Merge pull request #51 from ChannelMeter/tls/disable-http
tls: suggestion - disable tls if port is http?
2015-05-05 13:35:49 -06:00
Nimi Wariboko Jr accb3e616d Add warning message when tls is disabled when its used on http 2015-05-05 12:30:24 -07:00
Matthew Holt 33786408f0 Startup/shutdown commands now have stdin 2015-05-05 11:20:57 -06:00
Matthew Holt 9a78857b31 Startup/shutdown commands run in background with & 2015-05-05 11:08:45 -06:00
Matt Holt 1e730a74a0 Merge pull request #50 from ChannelMeter/core/bind_address
core: Add the option to specify what address to bind on in Caddyfile
2015-05-05 00:05:42 -06:00
Nimi Wariboko Jr 46f7930787 Rename bindaddr to just bind 2015-05-04 22:58:08 -07:00
Nimi Wariboko Jr 68793ffe13 Disable tls if the port is http 2015-05-04 22:26:28 -07:00
Matt Holt 4637f14b7f Merge pull request #48 from ChannelMeter/proxy/refactor
proxy: Refactor so that other upstream backends can be implemented
2015-05-04 23:20:44 -06:00
Matt Holt a3b853dd47 Merge pull request #49 from guilhermebr/master
adding crypto/tls sessioncache
2015-05-04 23:15:13 -06:00
Nimi Wariboko Jr d3aedbeb9a core: add bindaddr directive, allowing you to specify what address to listen on 2015-05-04 21:38:49 -07:00
Guilherme Rezende da6a097dcc adding crypto/tls sessioncache 2015-05-05 00:25:29 -03:00
Nimi Wariboko Jr 0ed5b364c6 Refactor proxy middleware so that 1.) From() is exposed 2.) Other upstreams can be implemented/plugged in 2015-05-04 19:58:18 -07:00
Matthew Holt 2dbd14b6dc Consistent app name/version info; pipe config data through stdin 2015-05-04 16:23:16 -06:00
Matthew Holt 085f6e9560 Keepin' the comments true 2015-05-04 13:42:39 -06:00
Matthew Holt 20118bdfd2 Clearing out the old stuff 2015-05-04 13:40:07 -06:00
Matthew Holt 088f41b334 Began adding tests 2015-05-04 12:04:14 -06:00
Matthew Holt e4fdf171c7 More refactoring - nearly complete 2015-05-04 11:49:49 -06:00
Matthew Holt 6029973bdc Major refactoring of middleware and parser in progress 2015-05-04 11:04:37 -06:00
Matthew Holt 995edf0566 Bringing in latest from master; refactoring under way 2015-05-04 11:02:46 -06:00
Matt Holt 5f32f9b1c8 Merge pull request #40 from ChannelMeter/proxy-middleware
Proxy Middleware: Add support for multiple backends, load balancing & healthchecks
2015-05-03 15:58:50 -06:00
Nimi Wariboko Jr 264e5b7911 Use the provided Replacer tools in order to proxy string interpolation. 2015-05-03 13:33:08 -07:00
Nimi Wariboko Jr a28d5585f5 Export Replacer type 2015-05-03 12:43:50 -07:00
Nimi Wariboko Jr 082ae70d1d Allow responseRecorder to be nil 2015-05-03 12:38:06 -07:00
Nimi Wariboko Jr 2aa958e058 Update {remote} replacer to use X-Forwarded-For if its provided 2015-05-03 12:37:00 -07:00
Matt Holt 290cf82936 Merge pull request #43 from dgryski/vet-fixes
config: format string fixes from vet
2015-05-03 11:47:47 -06:00
Damian Gryski a872ff2d77 config: format string fixes from vet 2015-05-03 19:43:04 +02:00
Matt Holt 4ed9387801 Merge pull request #41 from abiosoft/master
fastcgi: allow more request types.
2015-05-03 08:08:21 -06:00
Abiola Ibrahim 225d5977ff fastcgi: allow more request types. 2015-05-03 12:12:18 +01:00
Nimi Wariboko Jr 4a4b80450a Upgrade proxy middleware. Add support for: multiple backends, load balancing, health checks, and pluggable backends 2015-05-02 22:45:01 -07:00
Matthew Holt 747d59b895 Replace Open with Stat 2015-05-02 11:57:53 -06:00
Matthew Holt ca95b561dc gzip: Fix Content-Length header for proxies requests (closes #38) 2015-05-02 09:20:39 -06:00
Matt Holt 9df9ad975d Merge pull request #37 from abiosoft/master
git: post pull command. retries after pull failure.
2015-05-01 23:03:43 -06:00
Abiola Ibrahim 9cd1587cf7 git: post pull command. retries after pull failure. 2015-05-02 04:20:01 +01:00
Matthew Holt 782ba32457 Only a warning if site root doesn't exist 2015-05-01 16:23:28 -06:00
Matthew Holt d11819721d core: Error if root directory is not found 2015-05-01 13:35:57 -06:00
Matthew Holt 7ee3653342 core: Kill whole process if any server fail to start 2015-05-01 13:35:57 -06:00
Matt Holt 9e3852f21c Merge pull request #34 from abiosoft/master
fastcgi: user defined environment variables
2015-05-01 13:33:22 -06:00
Abiola Ibrahim 447d0ce0e2 fastcgi: user defined environment variables 2015-05-01 19:55:47 +01:00
Matthew Holt 49bb3f1387 git: Service routine, customizable logger, no more HTTP handler 2015-05-01 12:19:30 -06:00
Matt Holt 53a89c953a Merge pull request #33 from daviskoh/improvement/specify-go-version-dependency
Specify Go Version in README
2015-05-01 11:45:30 -06:00
Davis Koh 75d713cdea add go version to Running from Source section in README.md 2015-05-01 12:35:03 -04:00
Matt Holt 32c104c660 Merge pull request #32 from abiosoft/master
Implementation of Git middleware
2015-05-01 10:23:13 -06:00
Abiola Ibrahim 0d2ed0784f Modified repository path to be relative to root path. 2015-05-01 17:18:58 +01:00
Abiola Ibrahim 479c611420 Implementation of Git middleware
Defaults path to site root.
2015-05-01 16:41:34 +01:00
Matthew Holt d0556d6236 Add link to Docker container 2015-05-01 08:55:34 -06:00
Matthew Holt 37e3fe5f1f core: Fix dyslexic/backward handling of 403/404 errors 2015-04-30 11:58:38 -06:00
Matthew Holt 9dfbbbcda4 errors: Pointer to handler prevents nil pointer errors in handling (fixes #15)
If we do not use a pointer here, the startup function that opens the log file stores the log file in a copy of the handler, not the same instance of the handler, causing panics during requests, which is bad, especially when the response is gzipped: the next recover() is beyond the gzip handler, so the browser downloads a gz file instead.
2015-04-30 10:16:49 -06:00
Matthew Holt b1e1caba29 core: Graceful error handling during heavy load; proper error responses 2015-04-30 10:14:58 -06:00
Matthew Holt b51e8bc191 fastcgi: Fix for handling errors that come from responder 2015-04-30 07:50:07 -06:00
Matthew Holt 27722463a7 -host flag to set default host 2015-04-29 22:30:12 -06:00
Matthew Holt 3bc4e84ed3 Default host is now 0.0.0.0 (wildcard)
Doesn't break using localhost to access the site
2015-04-29 22:30:03 -06:00
Matt Holt 60beddf6c8 Merge pull request #18 from mholt/portflag
-port flag to allow overriding default port
2015-04-29 09:27:30 -06:00
Matthew Holt d00bb87f17 -port flag to override default port
Default port used if none is specified in config
2015-04-28 22:13:00 -06:00
Matt Holt 0f332bd9fb Merge pull request #14 from abiosoft/master
Fix for Issue 13: Trouble running in Docker containers (or binding to 0.0.0.0)
2015-04-28 20:25:15 -06:00
Abiola Ibrahim e04e06d6e2 Fix for Issue 13: Trouble running in Docker containers (or binding to 0.0.0.0) 2015-04-28 22:31:42 +01:00
Thomas Hansen 17fa5a9334 adding support for fastcgi index files in subdirectories 2015-04-28 13:15:14 -06:00
Matthew Holt 9b74901b40 errors: Fix file paths for error pages & empty log filenames 2015-04-28 13:05:01 -06:00
Matthew Holt 78e6d7db95 Clarified "no dependencies"; info about 3rd party libraries 2015-04-28 12:07:37 -06:00
Matthew Holt 2cf06bc3ee Ignore automated build script and stuff 2015-04-27 23:20:58 -06:00
Matthew Holt 264820e3e8 Fixed config file leak, but new todo item 2015-04-27 22:27:34 -06:00
Matthew Holt 979041a072 More canonical godoc shield, I suppose 2015-04-27 17:19:54 -06:00
Matthew Holt ff344535ba Added some godoc 2015-04-27 10:56:57 -06:00
Matthew Holt 1df35eb687 Fixed link/email in instructions 2015-04-27 10:45:47 -06:00
Matthew Holt cba96c9b35 Merge branch 'master' of github.com:mholt/caddy 2015-04-27 10:42:17 -06:00
Matthew Holt d7f0133f5f Add notes for contributors 2015-04-27 10:41:55 -06:00
Matt Holt e5d064d513 Create LICENSE.md 2015-04-27 10:41:32 -06:00
Matt Holt c33a49fc5e Merge pull request #2 from thomas4019/master
gzip middleware now strips encoding header
2015-04-27 09:54:54 -06:00
Matthew Holt a837bb6681 The README sorely needed an update 2015-04-27 09:53:08 -06:00
Thomas Hansen dbef6c73bc Merge branch 'master' of https://github.com/mholt/caddy 2015-04-27 09:39:40 -06:00
Matthew Holt fa2403c1d3 websockets: quick version fix 2015-04-27 07:35:39 -06:00
Matthew Holt c1916c0fb5 Server header in response
Version number purposefully excluded (for now?)
2015-04-26 23:09:26 -06:00
Matthew Holt dba4dcb4a5 gzip strips Accept-Encoding header after using it 2015-04-26 22:53:47 -06:00
Thomas Hansen 9d26a9268b added comment about encoding header 2015-04-26 22:15:43 -06:00
Thomas Hansen 1b17072a89 gzip middleware now strips encoding header 2015-04-26 22:01:20 -06:00
Matthew Holt 7d46108c12 With just a destination, default redir code is now 301 2015-04-26 20:20:32 -06:00
Matt Holt cd53ec9bcc Merge pull request #1 from thomas4019/master
adding support for php including clean urls and wordpress permalinks
2015-04-25 23:41:57 -06:00
Thomas Hansen 1ac32a5256 generalizing fastcgi parameters, and improving headers passed. 2015-04-25 21:56:14 -06:00
Thomas Hansen 9e12c45d82 Merge branch 'master' of https://github.com/mholt/caddy 2015-04-25 19:06:39 -06:00
Matthew Holt 24d9d23743 Default port is 2015 2015-04-25 14:28:56 -06:00
Matthew Holt ce74333348 Markdown requires a base path (for now) 2015-04-25 12:26:04 -06:00
Matthew Holt 46f5325c15 More accurate initialization output 2015-04-24 20:09:31 -06:00
Matthew Holt aa89b95075 Replaced cpu directive with command line flag 2015-04-24 20:08:14 -06:00
Matthew Holt 27fc1672d4 Basic auth middleware 2015-04-23 14:57:07 -06:00
Matthew Holt e6c5482b7c Slightly more helpful parse error message 2015-04-23 14:39:21 -06:00
Matthew Holt 95dce5cdfc Latency now available with recorder and replacer 2015-04-23 13:35:56 -06:00
Matthew Holt 51139a5f56 log: Fix so user can specify custom log format 2015-04-23 13:35:21 -06:00
Matthew Holt dd3ff0fcb5 Shows site addresses at start; new -quiet flag 2015-04-23 13:28:05 -06:00
Matthew Holt d088194585 Default port is now 80 2015-04-22 13:22:03 -06:00
Matthew Holt 5f187738e6 Better parse support for files with only an address line 2015-04-22 13:21:51 -06:00
Matthew Holt c10d2e0d45 Make the thing compile 2015-04-21 22:30:47 -06:00
Matthew Holt 1a8f753303 Meh. 2015-04-21 21:41:58 -06:00
Matthew Holt 23f7f5ebba Minor UI tweaks to directory listings 2015-04-21 21:36:43 -06:00
Matthew Holt bdd145b0de Better error handling when executing templates 2015-04-21 21:36:30 -06:00
Matthew Holt 0cbaed2443 A few helpful comments 2015-04-21 16:00:16 -06:00
Matthew Holt 99c0cbdf29 Fixed a typo 2015-04-21 12:12:58 -06:00
Thomas Hansen 96985fb3fd adding support for php including clean urls and wordpress permalinks 2015-04-20 17:40:54 -06:00
Matthew Holt 981ca72ee6 Enforce canonical URLs 2015-04-18 13:24:54 -06:00
Matthew Holt 6a32de4b47 Use text/template because html shouldn't be escaped for this 2015-04-18 12:28:22 -06:00
Matthew Holt f5d0ed5b1c More template love 2015-04-18 11:31:59 -06:00
Matthew Holt 55801b48ec More template functions 2015-04-18 11:08:41 -06:00
Matthew Holt 3ec870cb56 Templates middleware with "include" functionality 2015-04-18 09:57:51 -06:00
Matthew Holt cd0421ceb8 Package extension -> extensions 2015-04-18 09:55:02 -06:00
Matthew Holt 9a27beb79c Quick bug fix for empty Caddyfile 2015-04-18 09:53:43 -06:00
Matthew Holt e6532b6d85 Multiple addresses may be specified per server block 2015-04-15 23:24:39 -06:00
Matthew Holt 7d96cfa424 Turn off log timestamp for parse errors (easier to read) 2015-04-15 23:17:56 -06:00
Matthew Holt c7af6725ca Removed Host() and Port() functions from Controller
I don't think they'll be necessary; can get same info from request Host header
2015-04-15 23:17:28 -06:00
Matthew Holt feec7c5b40 Virtual hosts and SNI support 2015-04-15 14:11:32 -06:00
Matthew Holt 07964a6112 Fixed bug in parser; implicit server block with middleware directives 2015-04-14 13:26:35 -06:00
Matthew Holt a93db40138 Improvements to the redirect middleware 2015-04-12 18:13:58 -06:00
Matthew Holt b7c8afab2f Respond with 404 if requesting server's config file 2015-04-12 17:44:02 -06:00
Matthew Holt 6ca475def8 Redirect now does exact path matching like rewrite middleware 2015-04-12 17:40:59 -06:00
Matthew Holt d8e7adcdb4 Refactored proxy middleware 2015-04-11 17:24:47 -06:00
Matthew Holt 113b175db7 Refactored fastcgi middleware 2015-04-11 17:15:17 -06:00
Matthew Holt 40bf7c5285 Refactored redirect middleware 2015-04-11 17:06:09 -06:00
Matthew Holt abeb337f45 Refactored rewrite middleware 2015-04-11 16:58:34 -06:00
Matthew Holt d0a0216602 Added flag to disable http/2 support (still enabled by default) 2015-04-09 10:08:22 -06:00
Matthew Holt 2a0cfb608d Bug fix for default error handling with gzip 2015-04-08 23:24:59 -06:00
Matthew Holt d33256f1dc Refactor: Middleware chain uses Handler instead of HandlerFunc 2015-04-02 23:30:54 -06:00
Matthew Holt db2cd9e941 Renamed extensionless to extension, Extensionless to Ext 2015-04-02 21:59:45 -06:00
Matthew Holt 3e6f5de92f Renamed redirect -> redir 2015-03-31 23:57:09 -06:00
Matthew Holt 9f793dad28 Proxy destination may include scheme 2015-03-31 23:53:39 -06:00
Matthew Holt f2f5d4984d Markdown defaults to .md 2015-03-31 23:41:16 -06:00
Matthew Holt 29fec4742e Detailed godoc; better error handling convention 2015-03-29 22:01:42 -06:00
Matthew Holt 0a9a19305c Made catch-all redirects possible 2015-03-29 21:48:53 -06:00
Matthew Holt 4e9c432c14 Controller/Dispenser refactoring, typo fixes 2015-03-29 19:56:19 -06:00
Matthew Holt 6bf36d922c Refactored proxy middleware to return errors 2015-03-28 16:56:56 -06:00
Matthew Holt 076d4e0ec5 Refactored web socket middleware to return errors 2015-03-28 16:56:33 -06:00
Matthew Holt b87e6ccb76 Refactored markdown middleware to return errors 2015-03-28 16:55:40 -06:00
Matthew Holt 22707edcbf Refactored fastcgi middleware to return errors 2015-03-28 16:52:43 -06:00
Matthew Holt 7578298b3f Rewrote access log middleware 2015-03-28 16:50:42 -06:00
Matthew Holt d2892fc799 New error handler middleware 2015-03-28 16:50:06 -06:00
Matthew Holt 21b2e5a059 Refactored file server to return errors 2015-03-28 16:49:42 -06:00
Matthew Holt 878ae7ea89 Refactored rewrite middleware to return errors 2015-03-28 16:49:18 -06:00
Matthew Holt c657948824 Refactored redir middleware to return errors 2015-03-28 16:49:06 -06:00
Matthew Holt a39e71ca26 Refactored headers middleware to return errors 2015-03-28 16:48:43 -06:00
Matthew Holt 8f4e7f7fdc Refactored gzip middleware to return errors 2015-03-28 16:47:41 -06:00
Matthew Holt a674450198 Refactored ext middleware to return errors 2015-03-28 16:47:28 -06:00
Matthew Holt 843f6e83a9 Refactored browse middleware to return errors 2015-03-28 16:46:54 -06:00
Matthew Holt 058ff94828 Better middleware godoc, fixed ordering too 2015-03-28 16:45:12 -06:00
Matthew Holt 9378f38371 Major refactoring for better error handling 2015-03-28 16:37:37 -06:00
Matthew Holt 2dc39feabd Tweak to parser and main's error handling 2015-03-28 16:24:00 -06:00
Matthew Holt 09aad777f4 Proper host/port splitting; also log file perms 2015-03-26 23:39:36 -06:00
Matthew Holt da72a5fbcd Controller can register functions to run at shutdown 2015-03-26 23:22:48 -06:00
Matthew Holt 1146a9b90b Recover from panic during requests 2015-03-26 22:52:27 -06:00
Matthew Holt 2fbfafc408 New startup and shutdown directives 2015-03-26 09:52:03 -06:00
Matthew Holt b5dc1dde8b Updated README 2015-03-25 22:48:28 -06:00
Matthew Holt 63b39c78ee Better default template; other fixes 2015-03-24 23:05:42 -06:00
Matthew Holt 13d9bcc0c7 Clean URL middleware handles URLs ending with / 2015-03-24 21:56:22 -06:00
Matthew Holt ba0d63d722 Adapted std lib file server and gutted it 2015-03-24 21:55:51 -06:00
Matthew Holt 9672850d11 Bug fixes and improvements for browse middleware 2015-03-24 21:54:33 -06:00
Matthew Holt 00e43197fd Started browse middleware to list directory contents 2015-03-24 20:12:48 -06:00
Matthew Holt 284ab11c7f Little bit of cleanup 2015-03-21 15:18:50 -06:00
Matthew Holt e62b222372 Couple more controller tests 2015-03-21 15:11:31 -06:00
Matthew Holt a0e93009f0 Controller tests 2015-03-21 14:50:28 -06:00
Matthew Holt 5d4726446d Finished dispenser tests 2015-03-21 14:36:32 -06:00
Matthew Holt 010ac23e8a More tests! 2015-03-21 11:18:37 -06:00
Matthew Holt cdfc67db01 Some godoc 2015-03-21 11:04:08 -06:00
Matthew Holt 6d869ef55b Support multiple occurrences of markdown directive 2015-03-21 10:59:29 -06:00
Matthew Holt 2fa6129c3a Started dispenser tests 2015-03-20 18:22:22 -06:00
Matthew Holt bb6a921d1e Tests for location context parsing 2015-03-20 18:13:13 -06:00
Matthew Holt 9aaf81328f There's a std lib function for that 2015-03-20 18:12:53 -06:00
Matthew Holt 35225fe2d3 Docs and comments, la la 2015-03-20 18:11:54 -06:00
Matthew Holt 01266ece6b Minor style nit-pick 2015-03-20 00:03:41 -06:00
Matthew Holt 1b7415a81b Markdown handles titles a little better 2015-03-20 00:01:39 -06:00
Matthew Holt abdadf1ee1 Improvements to websocket middleware 2015-03-19 23:52:56 -06:00
Matthew Holt d7ae9fb4a2 Added markdown middleware 2015-03-16 11:45:51 -06:00
Matthew Holt fb78592425 Experimental HTTP/2 support 2015-03-16 11:44:54 -06:00
Matthew Holt af56c5033c New method to get remaining arguments on a line 2015-03-16 11:23:17 -06:00
Matthew Holt 3858e31942 A couple pesky env variables to deal with in websockets 2015-03-03 18:39:38 -07:00
Matthew Holt 37f0a37ed2 Filled out more web socket stuff 2015-03-03 17:36:18 -07:00
Matthew Holt 411f3256cc Minor changes to readme 2015-03-03 11:14:36 -07:00
Matthew Holt 811c6a986f Added WebSocket middleware 2015-03-03 09:49:45 -07:00
Matthew Holt 974acbf38c Partial support for location contexts in config files 2015-03-03 09:49:01 -07:00
Matthew Holt 634b8b707f Slight refactoring/renaming 2015-02-07 22:17:15 -07:00
Matthew Holt 0e43271cc9 Basic proxy feature works 2015-02-02 23:41:35 -07:00
Matthew Holt 5ae1790e52 Moved controller into its own file; other minor cleanups 2015-01-31 10:15:17 -07:00
Matthew Holt 16997d85eb Made 'extensionless' middleware more modular/useful 2015-01-30 11:09:36 -07:00
Matthew Holt 62d7d61381 Refactored the dispenser/controller 2015-01-30 10:00:41 -07:00
Matthew Holt ae2a2d5b00 Godoc for middleware packages and server package 2015-01-29 23:52:18 -07:00
Matthew Holt bcdf04d00e Inlined a fixed version of the fastcgi_client dependency 2015-01-29 23:48:35 -07:00
Matthew Holt ba88be0fe9 Allow nil middleware to be returned
In case a middleware actually just wants some code to execute at startup... will expand on that idea later.
2015-01-29 23:09:14 -07:00
Matthew Holt 8471c2d9d8 Updated docs; renamed a couple utility files 2015-01-29 22:52:21 -07:00
Matthew Holt dcc67863dc Experimenting to make middleware more independent 2015-01-29 22:46:09 -07:00
Matthew Holt ac7f50b4cd Updated doc comment 2015-01-29 22:14:31 -07:00
Matthew Holt 612d77eaab Moved Path type around 2015-01-29 22:08:40 -07:00
Matthew Holt 04996b2850 Exported NewReplacer and NewRecorder 2015-01-29 22:06:53 -07:00
Matthew Holt 261beb046e Moved rewrite middleware into its own package 2015-01-29 22:06:19 -07:00
Matthew Holt b8c43e55db Moved redirect middleware into its own package 2015-01-29 22:05:54 -07:00
Matthew Holt 13cf980879 Moved proxy middleware into its own package 2015-01-29 22:05:36 -07:00
Matthew Holt e6063fb26b Moved logging middleware into its own package 2015-01-29 22:05:21 -07:00
Matthew Holt 1e4baa53f0 Moved headers middleware into its own package
Further trying out spreading out the code outside of the nested functions
2015-01-29 22:05:05 -07:00
Matthew Holt 80ef5d761c Moved gzip middleware into its own package
Trying a different format where the middleware is a type that satisfies http.Handler
2015-01-29 22:04:18 -07:00
Matthew Holt affd470820 Moved fastcgi middleware into its own package 2015-01-29 22:03:14 -07:00
Matthew Holt 89783ac0c2 Moved extensionless middleware into its own package 2015-01-29 22:02:58 -07:00
Matthew Holt fe62afd3d9 Beginning to move middleware into their own packages 2015-01-29 22:02:17 -07:00
Matthew Holt dca59d0eda Stubbed out really basic proxy middleware 2015-01-29 17:18:14 -07:00
Matt Holt ec5f94adc8 Added feature list 2015-01-21 19:16:31 -07:00
Matthew Holt a38a2a0e4f Created basic fastcgi middleware layer 2015-01-21 17:51:47 -07:00
Matthew Holt fe1978c6f5 New 'cpu' directive; now uses all cores by default (if needed) 2015-01-21 14:10:52 -07:00
Matthew Holt 509db0b08f Wrote basic tests for parser 2015-01-21 13:19:55 -07:00
Matthew Holt eae024027f Parser fixes, and now using base filename 2015-01-21 13:19:25 -07:00
Matthew Holt decfda2705 Made parsing easier in middleware 2015-01-21 12:09:49 -07:00
Matthew Holt 318781512b Wrote lexer tests 2015-01-21 12:09:01 -07:00
Matthew Holt 286d558c54 Moved most docs to wiki 2015-01-19 17:13:00 -07:00
Matthew Holt 822c231f1c Renamed {time} placeholder to {when} 2015-01-19 17:12:38 -07:00
Matthew Holt 24fc2ae59e Major refactoring; more modular middleware 2015-01-18 23:11:21 -07:00
Matthew Holt 7b3d005662 Started adding tests 2015-01-13 17:25:55 -07:00
Matthew Holt 04162aaa79 Added flag to specify config file location 2015-01-13 16:22:16 -07:00
Matt Holt db1adcac97 Filled out README a little more 2015-01-13 15:45:08 -07:00
Matthew Holt 1e78262fc5 Created basic README 2015-01-13 13:24:43 -07:00
Matthew Holt 4497a16fb0 Early prototype; initial commit 2015-01-13 12:43:45 -07:00
693 changed files with 23233 additions and 100274 deletions
-5
View File
@@ -1,5 +0,0 @@
[*]
end_of_line = lf
[caddytest/integration/caddyfile_adapt/*.caddyfiletest]
indent_style = tab
-1
View File
@@ -1 +0,0 @@
*.go text eol=lf
-197
View File
@@ -1,197 +0,0 @@
Contributing to Caddy
=====================
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.
## Common Tasks
- [Contributing code](#contributing-code)
- [Writing a Caddy module](#writing-a-caddy-module)
- [Asking or answering questions for help using Caddy](#getting-help-using-caddy)
- [Reporting a bug](#reporting-bugs)
- [Suggesting an enhancement or a new feature](#suggesting-features)
- [Improving documentation](#improving-documentation)
Other menu items:
- [Values](#values)
- [Coordinated Disclosure](#coordinated-disclosure)
- [Thank You](#thank-you)
### Contributing code
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, first submit or comment in an issue to discuss your contribution, then open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy).
We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions&mdash;even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergeable.
Here are some of the expectations we have of contributors:
- **Open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that any changes are in-line with the project's goals and the best interests of its users. We can also discuss the best possible implementation. If there's already an issue about it, comment on the existing issue to claim it. A lot of valuable time can be saved by discussing a proposal first.
- **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we want to avoid.](https://twitter.com/iamdevloper/status/397664295875805184)
- **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.** 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 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`.
- **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.
- **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!
- **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
Contributing to Go projects on GitHub is fun and easy. After you have proposed your change in an issue, we recommend the following workflow:
1. [Fork this repo](https://github.com/caddyserver/caddy). This makes a copy of the code you can write to.
2. If you don't already have this repo (caddyserver/caddy.git) repo on your computer, clone it down: `git clone https://github.com/caddyserver/caddy.git`
3. Tell git that it can push the caddyserver/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/<your-username>/caddy.git`
4. Make your changes in the caddyserver/caddy.git repo on your computer.
5. Push your changes to your fork: `git push myfork`
6. [Create a pull request](https://github.com/caddyserver/caddy/pull/new/master) to merge your changes into caddyserver/caddy @ master. (Click "compare across forks" and change the head fork.)
This workflow is nice because you don't have to change import paths. You can get fancier by using different branches if you want.
### Writing a Caddy module
Caddy can do more with modules! Anyone can write one. Caddy modules are Go libraries that get compiled into Caddy, extending its feature set. They can add directives to the Caddyfile, add new configuration adapters, and even implement new server types (e.g. HTTP, DNS).
[Learn how to write a module here](https://caddyserver.com/docs/extending-caddy). You should also share and discuss your module idea [on the forums](https://caddy.community) to have people test it out. We don't use the Caddy issue tracker for third-party modules.
### Getting help using Caddy
If you have a question about using Caddy, [ask on our forum](https://caddy.community)! There will be more people there who can help you than just the Caddy developers who follow our issue tracker. Issues are not the place for usage questions.
Many people on the forums could benefit from your experience and expertise, too. Once you've been helped, consider giving back by answering other people's questions and participating in other discussions.
### Reporting bugs
Like every software, Caddy has its flaws. If you find one, [search the issues](https://github.com/caddyserver/caddy/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/caddyserver/caddy/issues/new) and describe the bug, and somebody will look into it! (This repository is only for Caddy and its standard modules.)
**You can help us fix bugs!** Speed up the patch by identifying the bug in the code. This can sometimes be done by adding `fmt.Println()` statements (or similar) in relevant code paths to narrow down where the problem may be. It's a good way to [introduce yourself to the Go language](https://tour.golang.org), too.
We may reply with an issue template. Please follow the template so we have all the needed information. Unredacted&mdash;yes, actual values matter. We need to be able to repeat the bug using your instructions. Please simplify the issue as much as possible. If you don't, we might close your report. The burden is on you to make it easily reproducible and to convince us that it is actually a bug in Caddy. This is easiest to do when you write clear, concise instructions so we can reproduce the behavior (even if it seems obvious). The more detailed and specific you are, the faster we will be able to help you!
We suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're getting free support when we fix your issues. If we helped you, please consider helping someone else!
#### Bug reporting expectations
Maintainers---or more generally, developers---need three things to act on bugs:
1. To agree or be convinced that it's a bug (reporter's responsibility).
- A bug is unintentional, undesired, or surprising behavior which violates documentation or relevant spec. It might be either a mistake in the documentation or a bug in the code.
- This project usually does not work around bugs in other software, systems, and dependencies; instead, we recommend that those bugs are fixed at their source. This sometimes means we close issues or reject PRs that attempt to fix, workaround, or hide bugs in other projects.
2. To be able to understand what is happening (mostly reporter's responsibility).
- If the reporter can provide satisfactory instructions such that a developer can reproduce the bug, the developer will likely be able to understand the bug, write a test case, and implement a fix. This is the least amount of work for everyone and path to the fastest resolution.
- Otherwise, the burden is on the reporter to test possible solutions. This is less preferable because it loosens the feedback loop, slows down debugging efforts, obscures the true nature of the problem from the developers, and is unlikely to result in new test cases.
3. A solution, or ideas toward a solution (mostly maintainer's responsibility).
- Sometimes the best solution is a documentation change.
- Usually the developers have the best domain knowledge for inventing a solution, but reporters may have ideas or preferences for how they would like the software to work.
- Security, correctness, and project goals/vision all take priority over a user's preferences.
- It's simply good business to yield a solution that satisfies the users, and it's even better business to leave them impressed.
Thus, at the very least, the reporter is expected to:
1. Convince the reader that it's a bug in Caddy (if it's not obvious).
2. Reduce the problem down to the minimum specific steps required to reproduce it.
The maintainer is usually able to do the rest; but of course the reporter may invest additional effort to speed up the process.
### Suggesting features
First, [search to see if your feature has already been requested](https://github.com/caddyserver/caddy/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and, without clarification, will have to be closed.
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good modules](#writing-a-caddy-module), which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core. Additionally, some features are bad ideas altogether (for either obvious or non-obvious reasons) which may be rejected. We'll try to explain why we reject a feature, but sometimes the best we can do is, "It's not a good fit for the project."
### Improving documentation
Caddy's documentation is available at [https://caddyserver.com/docs](https://caddyserver.com/docs) and its source is in the [website repo](https://github.com/caddyserver/website). If you would like to make a fix to the docs, please submit an issue there describing the change to make.
Note that third-party module documentation is not hosted by the Caddy website, other than basic usage examples. They are managed by the individual module authors, and you will have to contact them to change their documentation.
Our documentation is scoped to the Caddy project only: it is not for describing how other software or systems work, even if they relate to Caddy or web servers. That kind of content [can be found in our community wiki](https://caddy.community/c/wiki/13), however.
## Collaborator Instructions
Collaborators have push rights to the repository. We grant this permission after one or more successful, high-quality PRs are merged! We thank them for their help. The expectations we have of collaborators are:
- **Help review pull requests.** Be meticulous, but also kind. We love our contributors, but we critique the contribution to make it better. Multiple, thorough reviews make for the best contributions! Here are some questions to consider:
- Can the change be made more elegant?
- Is this a maintenance burden?
- What assumptions does the code make?
- Is it well-tested?
- Is the change a good fit for the project?
- Does it actually fix the problem or is it creating a special case instead?
- Does the change incur any new dependencies? (Avoid these!)
- **Answer issues.** If every collaborator helped out with issues, we could count the number of open issues on two hands. This means getting involved in the discussion, investigating the code, and yes, debugging it. It's fun. Really! :smile: Please, please help with open issues. Granted, some issues need to be done before others. And of course some are larger than others: you don't have to do it all yourself. Work with other collaborators as a team!
- **Do not merge pull requests until they have been approved by one or two other collaborators.** If a project owner approves the PR, it can be merged (as long as the conversation has finished too).
- **Prefer squashed commits over a messy merge.** If there are many little commits, please [squash the commits](https://stackoverflow.com/a/11732910/1048862) so we don't clutter the commit history.
- **Don't accept new dependencies lightly.** Dependencies can make the world crash and burn, but they are sometimes necessary. Choose carefully. Extremely small dependencies (a few lines of code) can be inlined. The rest may not be needed. For those that are, Caddy uses [go modules](https://github.com/golang/go/wiki/Modules). All external dependencies must be installed as modules, and _Caddy must not export any types defined by those dependencies_. Check this diligently!
- **Be extra careful in some areas of the code.** There are some critical areas in the Caddy code base that we review extra meticulously: the `caddyhttp` and `caddytls` packages especially.
- **Make sure tests test the actual thing.** Double-check that the tests fail without the change, and pass with it. It's important that they assert what they're purported to assert.
- **Recommended reading**
- [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) for an idea of what we look for in good, clean Go code
- [Linus Torvalds describes a good commit message](https://gist.github.com/matthewhudson/1475276)
- [Best Practices for Maintainers](https://opensource.guide/best-practices/)
- [Shrinking Code Review](https://alexgaynor.net/2015/dec/29/shrinking-code-review/)
## Values (WIP)
- A person is always more important than code. People don't like being handled "efficiently". But we can still process issues and pull requests efficiently while being kind, patient, and considerate.
- The ends justify the means, if the means are good. A good tree won't produce bad fruit. But if we cut corners or are hasty in our process, the end result will not be good.
## Security Policy
If you think you've found a security vulnerability, please refer to our [Security Policy](https://github.com/caddyserver/caddy/security/policy) document.
## Thank you
Thanks for your help! Caddy would not be what it is today without your contributions.
-12
View File
@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: [mholt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
-59
View File
@@ -1,59 +0,0 @@
# Security Policy
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
## Supported Versions
| Version | Supported |
| -------- | ----------|
| 2.latest | ✔️ |
| 1.x | :x: |
| < 1.x | :x: |
## Acceptable Scope
A security report must demonstrate a security bug in the source code from this repository.
Some security problems are the result of interplay between different components of the Web, rather than a vulnerability in the web server itself. Please only report vulnerabilities in the web server itself, as we cannot coerce the rest of the Web to be fixed (for example, we do not consider IP spoofing, BGP hijacks, or missing/misconfigured HTTP headers a vulnerability in the Caddy web server).
Vulnerabilities caused by misconfigurations are out of scope. Yes, it is entirely possible to craft and use a configuration that is unsafe, just like with every other web server; we recommend against doing that.
We do not accept reports if the steps imply or require a compromised system or third-party software, as we cannot control those. We expect that users secure their own systems and keep all their software patched. For example, if untrusted users are able to upload/write/host arbitrary files in the web root directory, it is NOT a security bug in Caddy if those files get served to clients; however, it _would_ be a valid report if a bug in Caddy's source code unintentionally gave unauthorized users the ability to upload unsafe files or delete files without relying on an unpatched system or piece of software.
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 (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
We get a lot of difficult reports that turn out to be invalid. Clear, obvious reports tend to be the most credible (but are also rare).
First please ensure your report falls within the accepted scope of security bugs (above).
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
- Most minimal possible config (without redactions!)
- Command(s)
- Precise HTTP requests (`curl -v` and its output please)
- Full log output (please enable debug mode)
- Specific minimal steps to reproduce the issue from scratch
- A working patch
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.
It will speed things up if you suggest a working patch, such as a code diff, and explain why and how it works. Reports that are not actionable, do not contain enough information, are too pushy/demanding, or are not able to convince us that it is a viable and practical attack on the web server itself may be deferred to a later time or possibly ignored, depending on available resources. Priority will be given to credible, responsible reports that are constructive, specific, and actionable. (We get a lot of invalid reports.) Thank you for understanding.
When you are ready, please email Matt Holt (the author) directly: matt at dyanim dot com.
Please don't encrypt the email body. It only makes the process more complicated.
Please also understand that due to our nature as an open source project, we do not have a budget to award security bounties. We can only thank you.
If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give us the name to use and/or your GitHub username. If you don't provide this we can't credit you.
Thanks for responsibly helping Caddy&mdash;and thousands of websites&mdash;be more secure!
-7
View File
@@ -1,7 +0,0 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
-220
View File
@@ -1,220 +0,0 @@
# Used as inspiration: https://github.com/mvdan/github-actions-golang
name: Tests
on:
push:
branches:
- master
- 2.*
pull_request:
branches:
- master
- 2.*
env:
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
jobs:
test:
strategy:
# Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false
matrix:
os:
- linux
- mac
- windows
go:
- '1.24'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.24'
GO_SEMVER: '~1.24.1'
# 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
# SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
- os: linux
OS_LABEL: ubuntu-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0
- os: mac
OS_LABEL: macos-14
CADDY_BIN_PATH: ./cmd/caddy/caddy
SUCCESS: 0
- os: windows
OS_LABEL: windows-latest
CADDY_BIN_PATH: ./cmd/caddy/caddy.exe
SUCCESS: 'True'
runs-on: ${{ matrix.OS_LABEL }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
# These tools would be useful if we later decide to reinvestigate
# publishing test/coverage reports to some tool for easier consumption
# - name: Install test and coverage analysis tools
# run: |
# go get github.com/axw/gocov/gocov
# go get github.com/AlekSi/gocov-xml
# go get -u github.com/jstemmer/go-junit-report
# echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Print Go version and environment
id: vars
shell: bash
run: |
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
printf "\n\nGo environment:\n\n"
go env
printf "\n\nSystem environment:\n\n"
env
printf "Git version: $(git version)\n\n"
# Calculate the short SHA1 hash of the git commit
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Get dependencies
run: |
go get -v -t -d ./...
# mkdir test-results
- name: Build Caddy
working-directory: ./cmd/caddy
env:
CGO_ENABLED: 0
run: |
go build -tags nobadger,nomysql,nopgx -trimpath -ldflags="-w -s" -v
- name: Smoke test Caddy
working-directory: ./cmd/caddy
run: |
./caddy start
./caddy stop
- name: Publish Build Artifact
uses: actions/upload-artifact@v4
with:
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
path: ${{ matrix.CADDY_BIN_PATH }}
compression-level: 0
# Commented bits below were useful to allow the job to continue
# even if the tests fail, so we can publish the report separately
# For info about set-output, see https://stackoverflow.com/questions/57850553/github-actions-check-steps-status
- name: Run tests
# id: step_test
# continue-on-error: true
run: |
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
go test -tags nobadger,nomysql,nopgx -v -coverprofile="cover-profile.out" -short -race ./...
# echo "status=$?" >> $GITHUB_OUTPUT
# Relevant step if we reinvestigate publishing test/coverage reports
# - name: Prepare coverage reports
# run: |
# mkdir coverage
# gocov convert cover-profile.out > coverage/coverage.json
# # Because Windows doesn't work with input redirection like *nix, but output redirection works.
# (cat ./coverage/coverage.json | gocov-xml) > coverage/coverage.xml
# To return the correct result even though we set 'continue-on-error: true'
# - name: Coerce correct build result
# if: matrix.os != 'windows' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }}
# run: |
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
# exit 1
s390x-test:
name: test (s390x on IBM Z)
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Tests
run: |
set +e
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
# short sha is enough?
short_sha=$(git rev-parse --short HEAD)
# To shorten the following lines
ssh_opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
ssh_host="$CI_USER@ci-s390x.caddyserver.com"
# The environment is fresh, so there's no point in keeping accepting and adding the key.
rsync -arz -e "ssh $ssh_opts" --progress --delete --exclude '.git' . "$ssh_host":/var/tmp/"$short_sha"
ssh $ssh_opts -t "$ssh_host" bash <<EOF
cd /var/tmp/$short_sha
go version
go env
printf "\n\n"
retries=3
exit_code=0
while ((retries > 0)); do
CGO_ENABLED=0 go test -p 1 -tags nobadger,nomysql,nopgx -v ./...
exit_code=$?
if ((exit_code == 0)); then
break
fi
echo "\n\nTest failed: \$exit_code, retrying..."
((retries--))
done
echo "Remote exit code: \$exit_code"
exit \$exit_code
EOF
test_result=$?
# There's no need leaving the files around
ssh $ssh_opts "$ssh_host" "rm -rf /var/tmp/'$short_sha'"
echo "Test exit code: $test_result"
exit $test_result
env:
SSH_KEY: ${{ secrets.S390X_SSH_KEY }}
CI_USER: ${{ secrets.CI_USER }}
goreleaser-check:
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: check
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: "~1.24"
check-latest: true
- name: Install xcaddy
run: |
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy version
- uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: build --single-target --snapshot
env:
TAG: ${{ github.head_ref || github.ref_name }}
-73
View File
@@ -1,73 +0,0 @@
name: Cross-Build
on:
push:
branches:
- master
- 2.*
pull_request:
branches:
- master
- 2.*
env:
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
jobs:
build:
strategy:
fail-fast: false
matrix:
goos:
- 'aix'
- 'linux'
- 'solaris'
- 'illumos'
- 'dragonfly'
- 'freebsd'
- 'openbsd'
- 'windows'
- 'darwin'
- 'netbsd'
go:
- '1.24'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.24'
GO_SEMVER: '~1.24.1'
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
- name: Print Go version and environment
id: vars
run: |
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
printf "\n\nGo environment:\n\n"
go env
printf "\n\nSystem environment:\n\n"
env
- name: Run Build
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
shell: bash
continue-on-error: true
working-directory: ./cmd/caddy
run: |
GOOS=$GOOS GOARCH=$GOARCH go build -tags=nobadger,nomysql,nopgx -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
-71
View File
@@ -1,71 +0,0 @@
name: Lint
on:
push:
branches:
- master
- 2.*
pull_request:
branches:
- master
- 2.*
permissions:
contents: read
env:
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
jobs:
# From https://github.com/golangci/golangci-lint-action
golangci:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint
strategy:
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:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '~1.24'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
args: --timeout 10m
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
govulncheck:
runs-on: ubuntu-latest
steps:
- name: govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-input: '~1.24.1'
check-latest: true
-182
View File
@@ -1,182 +0,0 @@
name: Release
on:
push:
tags:
- 'v*.*.*'
env:
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
jobs:
release:
name: Release
strategy:
matrix:
os:
- ubuntu-latest
go:
- '1.24'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- go: '1.24'
GO_SEMVER: '~1.24.1'
runs-on: ${{ matrix.os }}
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
permissions:
id-token: write
# https://docs.github.com/en/rest/overview/permissions-required-for-github-apps#permission-on-contents
# "Releases" is part of `contents`, so it needs the `write`
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
# Force fetch upstream tags -- because 65 minutes
# 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/
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
# git fetch --prune --unshallow
# which doesn't overwrite that tag because that would be destructive.
# Credit to @francislavoie for the investigation.
# https://github.com/actions/checkout/issues/290#issuecomment-680260080
- name: Force fetch upstream tags
run: git fetch --tags --force
# https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
- name: Print Go version and environment
id: vars
run: |
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
printf "\n\nGo environment:\n\n"
go env
printf "\n\nSystem environment:\n\n"
env
echo "version_tag=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
# Add "pip install" CLI tools to PATH
echo ~/.local/bin >> $GITHUB_PATH
# Parse semver
TAG=${GITHUB_REF/refs\/tags\//}
SEMVER_RE='[^0-9]*\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([0-9A-Za-z\.-]*\)'
TAG_MAJOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\1#"`
TAG_MINOR=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\2#"`
TAG_PATCH=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\3#"`
TAG_SPECIAL=`echo ${TAG#v} | sed -e "s#$SEMVER_RE#\4#"`
echo "tag_major=${TAG_MAJOR}" >> $GITHUB_OUTPUT
echo "tag_minor=${TAG_MINOR}" >> $GITHUB_OUTPUT
echo "tag_patch=${TAG_PATCH}" >> $GITHUB_OUTPUT
echo "tag_special=${TAG_SPECIAL}" >> $GITHUB_OUTPUT
# Cloudsmith CLI tooling for pushing releases
# See https://help.cloudsmith.io/docs/cli
- name: Install Cloudsmith CLI
run: pip install --upgrade cloudsmith-cli
- name: Validate commits and tag signatures
run: |
# Import Matt Holt's key
curl 'https://github.com/mholt.gpg' | gpg --import
echo "Verifying the tag: ${{ steps.vars.outputs.version_tag }}"
# tags are only accepted if signed by Matt's key
git verify-tag "${{ steps.vars.outputs.version_tag }}" || exit 1
- name: Install Cosign
uses: sigstore/cosign-installer@main
- name: Cosign version
run: cosign version
- name: Install Syft
uses: anchore/sbom-action/download-syft@main
- name: Syft version
run: syft version
- name: Install xcaddy
run: |
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy version
# GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean --timeout 60m
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.vars.outputs.version_tag }}
COSIGN_EXPERIMENTAL: 1
# Only publish on non-special tags (e.g. non-beta)
# We will continue to push to Gemfury for the foreseeable future, although
# Cloudsmith is probably better, to not break things for existing users of Gemfury.
# See https://gemfury.com/caddy/deb:caddy
- name: Publish .deb to Gemfury
if: ${{ steps.vars.outputs.tag_special == '' }}
env:
GEMFURY_PUSH_TOKEN: ${{ secrets.GEMFURY_PUSH_TOKEN }}
run: |
for filename in dist/*.deb; do
# armv6 and armv7 are both "armhf" so we can skip the duplicate
if [[ "$filename" == *"armv6"* ]]; then
echo "Skipping $filename"
continue
fi
curl -F package=@"$filename" https://${GEMFURY_PUSH_TOKEN}:@push.fury.io/caddy/
done
# Publish only special tags (unstable/beta/rc) to the "testing" repo
# See https://cloudsmith.io/~caddy/repos/testing/
- name: Publish .deb to Cloudsmith (special tags)
if: ${{ steps.vars.outputs.tag_special != '' }}
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
run: |
for filename in dist/*.deb; do
# armv6 and armv7 are both "armhf" so we can skip the duplicate
if [[ "$filename" == *"armv6"* ]]; then
echo "Skipping $filename"
continue
fi
echo "Pushing $filename to 'testing'"
cloudsmith push deb caddy/testing/any-distro/any-version $filename
done
# Publish stable tags to Cloudsmith to both repos, "stable" and "testing"
# See https://cloudsmith.io/~caddy/repos/stable/
- name: Publish .deb to Cloudsmith (stable tags)
if: ${{ steps.vars.outputs.tag_special == '' }}
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
run: |
for filename in dist/*.deb; do
# armv6 and armv7 are both "armhf" so we can skip the duplicate
if [[ "$filename" == *"armv6"* ]]; then
echo "Skipping $filename"
continue
fi
echo "Pushing $filename to 'stable'"
cloudsmith push deb caddy/stable/any-distro/any-version $filename
echo "Pushing $filename to 'testing'"
cloudsmith push deb caddy/testing/any-distro/any-version $filename
done
-35
View File
@@ -1,35 +0,0 @@
name: Release Published
# Event payload: https://developer.github.com/webhooks/event-payloads/#release
on:
release:
types: [published]
jobs:
release:
name: Release Published
strategy:
matrix:
os:
- ubuntu-latest
runs-on: ${{ matrix.os }}
steps:
# See https://github.com/peter-evans/repository-dispatch
- name: Trigger event on caddyserver/dist
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/dist
event-type: release-tagged
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
- name: Trigger event on caddyserver/caddy-docker
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/caddy-docker
event-type: release-tagged
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
+12 -27
View File
@@ -1,31 +1,16 @@
_gitignore/
*.log
Caddyfile
Caddyfile.*
!caddyfile/
!caddyfile.go
# artifacts from pprof tooling
*.prof
*.test
# build artifacts and helpers
cmd/caddy/caddy
cmd/caddy/caddy.exe
cmd/caddy/tmp/*.exe
cmd/caddy/.env
# mac specific
.DS_Store
Thumbs.db
_gitignore/
Vagrantfile
.vagrant/
# go modules
vendor
dist/builds/
dist/release/
# goreleaser artifacts
dist
caddy-build
caddy-dist
error.log
access.log
# IDE files
.idea/
.vscode/
/*.conf
Caddyfile
og_static/
-182
View File
@@ -1,182 +0,0 @@
linters-settings:
errcheck:
exclude-functions:
- fmt.*
- (go.uber.org/zap/zapcore.ObjectEncoder).AddObject
- (go.uber.org/zap/zapcore.ObjectEncoder).AddArray
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:
disable-all: true
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errcheck
- errname
- exhaustive
- gci
- gofmt
- goimports
- gofumpt
- gosec
- gosimple
- govet
- ineffassign
- importas
- misspell
- prealloc
- promlinter
- sloglint
- sqlclosecheck
- staticcheck
- tenv
- testableexamples
- testifylint
- tparallel
- typecheck
- unconvert
- unused
- wastedassign
- whitespace
- zerologlint
# these are implicitly disabled:
# - containedctx
# - contextcheck
# - cyclop
# - depguard
# - errchkjson
# - errorlint
# - exhaustruct
# - execinquery
# - exhaustruct
# - forbidigo
# - forcetypeassert
# - funlen
# - ginkgolinter
# - gocheckcompilerdirectives
# - gochecknoglobals
# - gochecknoinits
# - gochecksumtype
# - gocognit
# - goconst
# - gocritic
# - gocyclo
# - godot
# - godox
# - goerr113
# - goheader
# - gomnd
# - gomoddirectives
# - gomodguard
# - goprintffuncname
# - gosmopolitan
# - grouper
# - inamedparam
# - interfacebloat
# - ireturn
# - lll
# - loggercheck
# - maintidx
# - makezero
# - mirror
# - musttag
# - nakedret
# - nestif
# - nilerr
# - nilnil
# - nlreturn
# - noctx
# - nolintlint
# - nonamedreturns
# - nosprintfhostport
# - paralleltest
# - perfsprint
# - predeclared
# - protogetter
# - reassign
# - revive
# - rowserrcheck
# - stylecheck
# - tagalign
# - tagliatelle
# - testpackage
# - thelper
# - unparam
# - usestdlibvars
# - varnamelen
# - wrapcheck
# - wsl
run:
# default concurrency is a available CPU number.
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
timeout: 5m
issues-exit-code: 1
tests: false
# output configuration options
output:
formats:
- format: 'colored-line-number'
print-issued-lines: true
print-linter-name: true
issues:
exclude-rules:
- text: 'G115' # TODO: Either we should fix the issues or nuke the linter if it's bad
linters:
- gosec
# we aren't calling unknown URL
- text: 'G107' # G107: Url provided to HTTP request as taint input
linters:
- gosec
# 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
linters:
- gosec
# we're shelling out to known commands, not relying on user-defined input.
- text: 'G204' # G204: Audit use of command execution
linters:
- gosec
# the choice of weakrand is deliberate, hence the named import "weakrand"
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
text: 'G404' # G404: Insecure random number source (rand)
linters:
- gosec
- path: modules/caddyhttp/reverseproxy/streaming.go
text: 'G404' # G404: Insecure random number source (rand)
linters:
- gosec
- path: modules/logging/filters.go
linters:
- dupl
- path: modules/caddyhttp/matchers.go
linters:
- dupl
- path: modules/caddyhttp/vars.go
linters:
- dupl
- path: _test\.go
linters:
- errcheck
-214
View File
@@ -1,214 +0,0 @@
version: 2
before:
hooks:
# The build is done in this particular way to build Caddy in a designated directory named in .gitignore.
# 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
# subsequently causes gorleaser to refuse running.
- rm -rf caddy-build caddy-dist vendor
# vendor Caddy deps
- go mod vendor
- mkdir -p caddy-build
- cp cmd/caddy/main.go caddy-build/main.go
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
# prepare syso files for windows embedding
- /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
- /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build'
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
# 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.
- /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
- mkdir -p caddy-dist/man
- go mod download
- go run cmd/caddy/main.go manpage --directory ./caddy-dist/man
- gzip -r ./caddy-dist/man/
- /bin/sh -c 'go run cmd/caddy/main.go completion bash > ./caddy-dist/scripts/bash-completion'
builds:
- env:
- CGO_ENABLED=0
- GO111MODULE=on
dir: ./caddy-build
binary: caddy
goos:
- darwin
- linux
- windows
- freebsd
goarch:
- amd64
- arm
- arm64
- s390x
- ppc64le
- riscv64
goarm:
- "5"
- "6"
- "7"
ignore:
- goos: darwin
goarch: arm
- goos: darwin
goarch: ppc64le
- goos: darwin
goarch: s390x
- goos: darwin
goarch: riscv64
- goos: windows
goarch: ppc64le
- goos: windows
goarch: s390x
- goos: windows
goarch: riscv64
- goos: freebsd
goarch: ppc64le
- goos: freebsd
goarch: s390x
- goos: freebsd
goarch: riscv64
- goos: freebsd
goarch: arm
goarm: "5"
flags:
- -trimpath
- -mod=readonly
ldflags:
- -s -w
tags:
- nobadger
- nomysql
- nopgx
signs:
- cmd: cosign
signature: "${artifact}.sig"
certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
args: ["sign-blob", "--yes", "--output-signature=${signature}", "--output-certificate", "${certificate}", "${artifact}"]
artifacts: all
sboms:
- artifacts: binary
documents:
- >-
{{ .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
args: ["$artifact", "--file", "${document}", "--output", "cyclonedx-json"]
archives:
- id: default
format_overrides:
- goos: windows
formats: zip
name_template: >-
{{ .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:
algorithm: sha512
nfpms:
- id: default
package_name: caddy
vendor: Dyanim
homepage: https://caddyserver.com
maintainer: Matthew Holt <mholt@users.noreply.github.com>
description: |
Caddy - Powerful, enterprise-ready, open source web server with automatic HTTPS written in Go
license: Apache 2.0
formats:
- deb
# - rpm
bindir: /usr/bin
contents:
- src: ./caddy-dist/init/caddy.service
dst: /lib/systemd/system/caddy.service
- src: ./caddy-dist/init/caddy-api.service
dst: /lib/systemd/system/caddy-api.service
- src: ./caddy-dist/welcome/index.html
dst: /usr/share/caddy/index.html
- src: ./caddy-dist/scripts/bash-completion
dst: /etc/bash_completion.d/caddy
- src: ./caddy-dist/config/Caddyfile
dst: /etc/caddy/Caddyfile
type: config
- src: ./caddy-dist/man/*
dst: /usr/share/man/man8/
scripts:
postinstall: ./caddy-dist/scripts/postinstall.sh
preremove: ./caddy-dist/scripts/preremove.sh
postremove: ./caddy-dist/scripts/postremove.sh
provides:
- httpd
release:
github:
owner: caddyserver
name: caddy
draft: true
prerelease: auto
changelog:
sort: asc
filters:
exclude:
- '^chore:'
- '^ci:'
- '^docs?:'
- '^readme:'
- '^tests?:'
- '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package
+16
View File
@@ -0,0 +1,16 @@
language: go
go:
- 1.6
- tip
env:
- CGO_ENABLED=0
install:
- go get -t ./...
- go get golang.org/x/tools/cmd/vet
script:
- go vet ./...
- go test ./...
-10
View File
@@ -1,10 +0,0 @@
# This is the official list of Caddy Authors for copyright purposes.
# Authors may be either individual people or legal entities.
#
# Not all individual contributors are authors. For the full list of
# contributors, refer to the project's page on GitHub or the repo's
# commit history.
Matthew Holt <Matthew.Holt@gmail.com>
Light Code Labs <sales@lightcodelabs.com>
Ardan Labs <info@ardanlabs.com>
+46
View File
@@ -0,0 +1,46 @@
## Contributing to Caddy
**[Join our dev chat on Gitter](https://gitter.im/mholt/caddy)** to chat with
other Caddy developers! (Dev chat only; try our
[support room](https://gitter.im/caddyserver/support) for help or
[general](https://gitter.im/caddyserver/general) for anything else.)
This project gladly accepts contributions and we encourage interested users to
get involved!
#### For small tweaks, bug fixes, and tests
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time.
Bug fixes should be under test to assert correct behavior. Thank you for
helping out in simple ways!
#### Ideas, questions, bug reports
Feel free to [open an issue](https://github.com/mholt/caddy/issues) with your
ideas, questions, and bug reports, if one does not already exist for it. Bug
reports should state expected behavior and contain clear instructions for
isolating and reproducing the problem.
See [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
#### New features
Before submitting a pull request, please open an issue first to discuss it and
claim it. This prevents overlapping efforts and keeps the project in-line with
its goals. If you prefer to discuss the feature privately, you can reach other
developers on Gitter or you may email me directly. (My email address is below.)
And don't forget to write tests for new features!
#### Vulnerabilities
If you've found a vulnerability that is serious, please email me: Matthew dot
Holt at Gmail. If it's not a big deal, a pull request will probably be faster.
## Thank you
Thanks for your help! Caddy would not be what it is today without your contributions.
+2 -3
View File
@@ -1,4 +1,3 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -179,7 +178,7 @@
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
@@ -187,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+123 -152
View File
@@ -1,201 +1,172 @@
<p align="center">
<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>
<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>
<hr>
<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">
<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>
<br>
<a href="https://x.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/twitter/follow/caddyserver" alt="@caddyserver on Twitter"></a>
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
<br>
<a href="https://sourcegraph.com/github.com/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
<a href="https://cloudsmith.io/~caddy/repos/"><img src="https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith" alt="Cloudsmith"></a>
</p>
<p align="center">
<a href="https://github.com/caddyserver/caddy/releases">Releases</a> ·
<a href="https://caddyserver.com/docs/">Documentation</a> ·
<a href="https://caddy.community">Get Help</a>
</p>
[![Caddy](https://caddyserver.com/resources/images/caddy-boxed.png)](https://caddyserver.com)
[![Dev Chat](https://img.shields.io/badge/dev%20chat-gitter-ff69b4.svg?style=flat-square&label=dev+chat&color=ff69b4)](https://gitter.im/mholt/caddy)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/mholt/caddy)
[![Linux Build Status](https://img.shields.io/travis/mholt/caddy.svg?style=flat-square&label=linux+build)](https://travis-ci.org/mholt/caddy)
[![Windows Build Status](https://img.shields.io/appveyor/ci/mholt/caddy.svg?style=flat-square&label=windows+build)](https://ci.appveyor.com/project/mholt/caddy)
Caddy is a lightweight, general-purpose web server for Windows, Mac, Linux, BSD
and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android).
It is a capable alternative to other popular and easy to use web servers.
([@caddyserver](https://twitter.com/caddyserver) on Twitter)
The most notable features are HTTP/2, [Let's Encrypt](https://letsencrypt.org)
support, Virtual Hosts, TLS + SNI, and easy configuration with a
[Caddyfile](https://caddyserver.com/docs/caddyfile). In development, you usually
put one Caddyfile with each site. In production, Caddy serves HTTPS by default
and manages all cryptographic assets for you.
[Download](https://github.com/mholt/caddy/releases) ·
[User Guide](https://caddyserver.com/docs)
### Menu
- [Features](#features)
- [Install](#install)
- [Build from source](#build-from-source)
- [For development](#for-development)
- [With version information and/or plugins](#with-version-information-andor-plugins)
- [Quick start](#quick-start)
- [Overview](#overview)
- [Full documentation](#full-documentation)
- [Getting help](#getting-help)
- [About](#about)
<p align="center">
<b>Powered by</b>
<br>
<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>
- [Getting Caddy](#getting-caddy)
- [Quick Start](#quick-start)
- [Running from Source](#running-from-source)
- [Contributing](#contributing)
- [About the Project](#about-the-project)
## [Features](https://caddyserver.com/features)
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
- **Dynamic configuration** with the [JSON API](https://caddyserver.com/docs/api)
- [**Config adapters**](https://caddyserver.com/docs/config-adapters) if you don't like JSON
- **Automatic HTTPS** by default
- [ZeroSSL](https://zerossl.com) and [Let's Encrypt](https://letsencrypt.org) for public names
- Fully-managed local CA for internal names & IPs
- Can coordinate with other Caddy instances in a cluster
- Multi-issuer fallback
- Encrypted ClientHello (ECH) support
- **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
- **Scales to hundreds of thousands of sites** as proven in production
- **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
- **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers
- Actually **fun to use**
- So much more to [discover](https://caddyserver.com/features)
## Install
## Getting Caddy
The simplest, cross-platform way to get started is to download Caddy from [GitHub Releases](https://github.com/caddyserver/caddy/releases) and place the executable file in your PATH.
Caddy binaries have no dependencies and are available for nearly every platform.
See [our online documentation](https://caddyserver.com/docs/install) for other install instructions.
[Latest release](https://github.com/mholt/caddy/releases/latest)
## Build from source
Requirements:
- [Go 1.24.0 or newer](https://golang.org/dl/)
## Quick Start
### For development
The website has [full documentation](https://caddyserver.com/docs) but this will
get you started in about 30 seconds:
_**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
$ git clone "https://github.com/caddyserver/caddy.git"
$ cd caddy/cmd/caddy/
$ go build
```
When you run Caddy, it may try to bind to low ports unless otherwise specified in your config. If your OS requires elevated privileges for this, you will need to give your new binary permission to do so. On Linux, this can be done easily with: `sudo setcap cap_net_bind_service=+ep ./caddy`
If you prefer to use `go run` which only creates temporary binaries, you can still do this with the included `setcap.sh` like so:
```bash
$ go run -exec ./setcap.sh main.go
```
If you don't want to type your password for `setcap`, use `sudo visudo` to edit your sudoers file and allow your user account to run that command without a password, for example:
Place a file named "Caddyfile" with your site. Paste this into it and save:
```
username ALL=(ALL:ALL) NOPASSWD: /usr/sbin/setcap
localhost
gzip
browse
ext .html
websocket /echo cat
log ../access.log
header /api Access-Control-Allow-Origin *
```
replacing `username` with your actual username. Please be careful and only do this if you know what you are doing! We are only qualified to document how to use Caddy, not Go tooling or your computer, and we are providing these instructions for convenience only; please learn how to use your own computer at your own risk and make any needful adjustments.
Run `caddy` from that directory, and it will automatically use that Caddyfile to
configure itself.
### With version information and/or plugins
That simple file enables compression, allows directory browsing (for folders
without an index file), serves clean URLs, hosts a WebSocket echo server at
/echo, logs requests to access.log, and adds the coveted
`Access-Control-Allow-Origin: *` header for all responses from some API.
Using [our builder tool, `xcaddy`](https://github.com/caddyserver/xcaddy)...
Wow! Caddy can do a lot with just a few lines.
#### Defining multiple sites
You can run multiple sites from the same Caddyfile, too:
```
$ xcaddy build
site1.com {
# ...
}
site2.com, sub.site2.com {
# ...
}
```
...the following steps are automated:
Note that all these sites will automatically be served over HTTPS using Let's
Encrypt as the CA. Caddy will manage the certificates (including renewals) for
you. You don't even have to think about it.
1. Create a new folder: `mkdir caddy`
2. Change into it: `cd caddy`
3. Copy [Caddy's main.go](https://github.com/caddyserver/caddy/blob/master/cmd/caddy/main.go) into the empty folder. Add imports for any custom plugins you want to add.
4. Initialize a Go module: `go mod init caddy`
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag, commit, or branch name.
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
7. Compile: `go build -tags=nobadger,nomysql,nopgx`
For more documentation, please view [the website](https://caddyserver.com/docs).
You may also be interested in the [developer guide]
(https://github.com/mholt/caddy/wiki) on this project's GitHub wiki.
## Quick start
## Running from Source
The [Caddy website](https://caddyserver.com/docs/) has documentation that includes tutorials, quick-start guides, reference, and more.
Note: You will need **[Go 1.6](https://golang.org/dl/)** or newer.
**We recommend that all users -- regardless of experience level -- do our [Getting Started](https://caddyserver.com/docs/getting-started) guide to become familiar with using Caddy.**
1. `$ go get github.com/mholt/caddy`
2. `cd` into your website's directory
3. Run `caddy` (assumes `$GOPATH/bin` is in your `$PATH`)
If you've only got a minute, [the website has several quick-start tutorials](https://caddyserver.com/docs/quick-starts) to choose from! However, after finishing a quick-start tutorial, please read more documentation to understand how the software works. 🙂
If you're tinkering, you can also use `go run main.go`.
By default, Caddy serves the current directory at
[localhost:2015](http://localhost:2015). You can place a Caddyfile to configure
Caddy for serving your site.
Caddy accepts some flags from the command line. Run `caddy -h` to view the help
for flags. You can also pipe a Caddyfile into the caddy command.
**Running as root:** We advise against this; use setcap instead, like so:
`setcap cap_net_bind_service=+ep ./caddy` This will allow you to listen on
ports < 1024 like 80 and 443.
#### Docker Container
Caddy is available as a Docker container from any of these sources:
- [abiosoft/caddy](https://hub.docker.com/r/abiosoft/caddy/)
- [darron/caddy](https://hub.docker.com/r/darron/caddy/)
- [joshix/caddy](https://hub.docker.com/r/joshix/caddy/)
- [jumanjiman/caddy](https://hub.docker.com/r/jumanjiman/caddy/)
- [zenithar/nano-caddy](https://hub.docker.com/r/zenithar/nano-caddy/)
#### 3rd-party dependencies
Although Caddy's binaries are completely static, Caddy relies on some excellent
libraries. [Godoc.org](https://godoc.org/github.com/mholt/caddy) shows the
packages that each Caddy package imports.
## Overview
## Contributing
Caddy is most often used as an HTTPS server, but it is suitable for any long-running Go program. First and foremost, it is a platform to run Go applications. Caddy "apps" are just Go programs that are implemented as Caddy modules. Two apps -- `tls` and `http` -- ship standard with Caddy.
**[Join our dev chat on Gitter](https://gitter.im/mholt/caddy)** to chat with
other Caddy developers! (Dev chat only; try our
[support room](https://gitter.im/caddyserver/support) for help or
[general](https://gitter.im/caddyserver/general) for anything else.)
Caddy apps instantly benefit from [automated documentation](https://caddyserver.com/docs/json/), graceful on-line [config changes via API](https://caddyserver.com/docs/api), and unification with other Caddy apps.
This project would not be what it is without your help. Please see the
[contributing guidelines](https://github.com/mholt/caddy/blob/master/CONTRIBUTING.md)
if you haven't already.
Although [JSON](https://caddyserver.com/docs/json/) is Caddy's native config language, Caddy can accept input from [config adapters](https://caddyserver.com/docs/config-adapters) which can essentially convert any config format of your choice into JSON: Caddyfile, JSON 5, YAML, TOML, NGINX config, and more.
Thanks for making Caddy -- and the Web -- better!
The primary way to configure Caddy is through [its API](https://caddyserver.com/docs/api), but if you prefer config files, the [command-line interface](https://caddyserver.com/docs/command-line) supports those too.
Caddy exposes an unprecedented level of control compared to any web server in existence. In Caddy, you are usually setting the actual values of the initialized types in memory that power everything from your HTTP handlers and TLS handshakes to your storage medium. Caddy is also ridiculously extensible, with a powerful plugin system that makes vast improvements over other web servers.
To wield the power of this design, you need to know how the config document is structured. Please see [our documentation site](https://caddyserver.com/docs/) for details about [Caddy's config structure](https://caddyserver.com/docs/json/).
Nearly all of Caddy's configuration is contained in a single config document, rather than being scattered across CLI flags and env variables and a configuration file as with other web servers. This makes managing your server config more straightforward and reduces hidden variables/factors.
## Full documentation
Our website has complete documentation:
**https://caddyserver.com/docs/**
The docs are also open source. You can contribute to them here: https://github.com/caddyserver/website
Special thanks to
[![DigitalOcean](http://i.imgur.com/sfGr0eY.png)](https://www.digitalocean.com)
for hosting the Caddy project.
## Getting help
- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com) before help is needed.
## About the project
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers!
- Individuals can exchange help for free on our community forum at https://caddy.community. Remember that people give help out of their spare time and good will. The best way to get help is to give it first!
Please use our [issue tracker](https://github.com/caddyserver/caddy/issues) only for bug reports and feature requests, i.e. actionable development items (support questions will usually be referred to the forums).
Caddy was born out of the need for a "batteries-included" web server that runs
anywhere and doesn't have to take its configuration with it. Caddy took
inspiration from [spark](https://github.com/rif/spark),
[nginx](https://github.com/nginx/nginx), lighttpd,
[Websocketd](https://github.com/joewalnes/websocketd)
and [Vagrant](https://www.vagrantup.com/),
which provides a pleasant mixture of features from each of them.
## About
Matthew Holt began developing Caddy in 2014 while studying computer science at Brigham Young University. (The name "Caddy" was chosen because this software helps with the tedious, mundane tasks of serving the Web, and is also a single place for multiple things to be organized together.) It soon became the first web server to use HTTPS automatically and by default, and now has hundreds of contributors and has served trillions of HTTPS requests.
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
- _Project on X: [@caddyserver](https://x.com/caddyserver)_
- _Author on X: [@mholt6](https://x.com/mholt6)_
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.
*Twitter: [@mholt6](https://twitter.com/mholt6)*
-1404
View File
File diff suppressed because it is too large Load Diff
-936
View File
@@ -1,936 +0,0 @@
// 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 caddy
import (
"context"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"sync"
"testing"
"github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
var testCfg = []byte(`{
"apps": {
"http": {
"servers": {
"myserver": {
"listen": ["tcp/localhost:8080-8084"],
"read_timeout": "30s"
},
"yourserver": {
"listen": ["127.0.0.1:5000"],
"read_header_timeout": "15s"
}
}
}
}
}
`)
func TestUnsyncedConfigAccess(t *testing.T) {
// each test is performed in sequence, so
// each change builds on the previous ones;
// the config is not reset between tests
for i, tc := range []struct {
method string
path string // rawConfigKey will be prepended
payload string
expect string // JSON representation of what the whole config is expected to be after the request
shouldErr bool
}{
{
method: "POST",
path: "",
payload: `{"foo": "bar", "list": ["a", "b", "c"]}`, // starting value
expect: `{"foo": "bar", "list": ["a", "b", "c"]}`,
},
{
method: "POST",
path: "/foo",
payload: `"jet"`,
expect: `{"foo": "jet", "list": ["a", "b", "c"]}`,
},
{
method: "POST",
path: "/bar",
payload: `{"aa": "bb", "qq": "zz"}`,
expect: `{"foo": "jet", "bar": {"aa": "bb", "qq": "zz"}, "list": ["a", "b", "c"]}`,
},
{
method: "DELETE",
path: "/bar/qq",
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",
path: "/list",
payload: `"e"`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
},
{
method: "PUT",
path: "/list/3",
payload: `"d"`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e"]}`,
},
{
method: "DELETE",
path: "/list/3",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "e"]}`,
},
{
method: "PATCH",
path: "/list/3",
payload: `"d"`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d"]}`,
},
{
method: "POST",
path: "/list/...",
payload: `["e", "f", "g"]`,
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c", "d", "e", "f", "g"]}`,
},
} {
err := unsyncedConfigAccess(tc.method, rawConfigKey+tc.path, []byte(tc.payload), nil)
if tc.shouldErr && err == nil {
t.Fatalf("Test %d: Expected error return value, but got: %v", i, err)
}
if !tc.shouldErr && err != nil {
t.Fatalf("Test %d: Should not have had error return value, but got: %v", i, err)
}
// decode the expected config so we can do a convenient DeepEqual
var expectedDecoded any
err = json.Unmarshal([]byte(tc.expect), &expectedDecoded)
if err != nil {
t.Fatalf("Test %d: Unmarshaling expected config: %v", i, err)
}
// make sure the resulting config is as we expect it
if !reflect.DeepEqual(rawCfg[rawConfigKey], expectedDecoded) {
t.Fatalf("Test %d:\nExpected:\n\t%#v\nActual:\n\t%#v",
i, expectedDecoded, rawCfg[rawConfigKey])
}
}
}
// TestLoadConcurrent exercises Load under concurrent conditions
// and is most useful under test with `-race` enabled.
func TestLoadConcurrent(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
_ = Load(testCfg, true)
wg.Done()
}()
}
wg.Wait()
}
type fooModule struct {
IntField int
StrField string
}
func (fooModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "foo",
New: func() Module { return new(fooModule) },
}
}
func (fooModule) Start() error { return nil }
func (fooModule) Stop() error { return nil }
func TestETags(t *testing.T) {
RegisterModule(fooModule{})
if err := Load([]byte(`{"admin": {"listen": "localhost:2999"}, "apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
t.Fatalf("loading: %s", err)
}
const key = "/" + rawConfigKey + "/apps/foo"
// try update the config with the wrong etag
err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
// get the etag
hash := etagHasher()
if err := readConfig(key, hash); err != nil {
t.Fatalf("reading: %s", err)
}
// do the same update with the correct key
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
if err != nil {
t.Fatalf("expected update to work; got %v", err)
}
// now try another update. The hash should no longer match and we should get precondition failed
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
t.Fatalf("expected precondition failed; got %v", err)
}
}
func BenchmarkLoad(b *testing.B) {
for i := 0; i < b.N; i++ {
Load(testCfg, true)
}
}
func TestAdminHandlerErrorHandling(t *testing.T) {
initAdminMetrics()
handler := adminHandler{
mux: http.NewServeMux(),
}
handler.mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := fmt.Errorf("test error")
handler.handleError(w, r, err)
}))
req := httptest.NewRequest(http.MethodGet, "/error", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code == http.StatusOK {
t.Error("expected error response, got success")
}
var apiErr APIError
if err := json.NewDecoder(rr.Body).Decode(&apiErr); err != nil {
t.Fatalf("decoding response: %v", err)
}
if apiErr.Message != "test error" {
t.Errorf("expected error message 'test error', got '%s'", apiErr.Message)
}
}
func initAdminMetrics() {
if adminMetrics.requestErrors != nil {
prometheus.Unregister(adminMetrics.requestErrors)
}
if adminMetrics.requestCount != nil {
prometheus.Unregister(adminMetrics.requestCount)
}
adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "caddy",
Subsystem: "admin_http",
Name: "request_errors_total",
Help: "Number of errors that occurred handling admin endpoint requests",
}, []string{"handler", "path", "method"})
adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "caddy",
Subsystem: "admin_http",
Name: "requests_total",
Help: "Count of requests to the admin endpoint",
}, []string{"handler", "path", "code", "method"}) // Added code and method labels
prometheus.MustRegister(adminMetrics.requestErrors)
prometheus.MustRegister(adminMetrics.requestCount)
}
func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
initAdminMetrics()
cfg := &Config{
Admin: &AdminConfig{
Listen: "localhost:2019",
},
}
err := replaceLocalAdminServer(cfg, Context{})
if err != nil {
t.Fatalf("setting up admin server: %v", err)
}
defer func() {
stopAdminServer(localAdminServer)
}()
tests := []struct {
name string
path string
method string
expectedStatus int
}{
{
name: "stop endpoint wrong method",
path: "/stop",
method: http.MethodGet,
expectedStatus: http.StatusMethodNotAllowed,
},
{
name: "config endpoint wrong content-type",
path: "/config/",
method: http.MethodPost,
expectedStatus: http.StatusBadRequest,
},
{
name: "config ID missing ID",
path: "/id/",
method: http.MethodGet,
expectedStatus: http.StatusBadRequest,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
rr := httptest.NewRecorder()
localAdminServer.Handler.ServeHTTP(rr, req)
if rr.Code != test.expectedStatus {
t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
}
metricValue := testGetMetricValue(map[string]string{
"path": test.path,
"handler": "admin",
"method": test.method,
})
if metricValue != 1 {
t.Errorf("expected error metric to be incremented once, got %v", metricValue)
}
})
}
}
func testGetMetricValue(labels map[string]string) float64 {
promLabels := prometheus.Labels{}
for k, v := range labels {
promLabels[k] = v
}
metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels)
if err != nil {
return 0
}
pb := &dto.Metric{}
metric.Write(pb)
return pb.GetCounter().GetValue()
}
type mockRouter struct {
routes []AdminRoute
}
func (m mockRouter) Routes() []AdminRoute {
return m.routes
}
type mockModule struct {
mockRouter
}
func (m *mockModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "admin.api.mock",
New: func() Module {
mm := &mockModule{
mockRouter: mockRouter{
routes: m.routes,
},
}
return mm
},
}
}
func TestNewAdminHandlerRouterRegistration(t *testing.T) {
originalModules := make(map[string]ModuleInfo)
for k, v := range modules {
originalModules[k] = v
}
defer func() {
modules = originalModules
}()
mockRoute := AdminRoute{
Pattern: "/mock",
Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(http.StatusOK)
return nil
}),
}
mock := &mockModule{
mockRouter: mockRouter{
routes: []AdminRoute{mockRoute},
},
}
RegisterModule(mock)
addr, err := ParseNetworkAddress("localhost:2019")
if err != nil {
t.Fatalf("Failed to parse address: %v", err)
}
admin := &AdminConfig{
EnforceOrigin: false,
}
handler := admin.newAdminHandler(addr, false, Context{})
req := httptest.NewRequest("GET", "/mock", nil)
req.Host = "localhost:2019"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
t.Logf("Response body: %s", rr.Body.String())
}
if len(admin.routers) != 1 {
t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
}
}
type mockProvisionableRouter struct {
mockRouter
provisionErr error
provisioned bool
}
func (m *mockProvisionableRouter) Provision(Context) error {
m.provisioned = true
return m.provisionErr
}
type mockProvisionableModule struct {
*mockProvisionableRouter
}
func (m *mockProvisionableModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "admin.api.mock_provision",
New: func() Module {
mm := &mockProvisionableModule{
mockProvisionableRouter: &mockProvisionableRouter{
mockRouter: m.mockRouter,
provisionErr: m.provisionErr,
},
}
return mm
},
}
}
func TestAdminRouterProvisioning(t *testing.T) {
tests := []struct {
name string
provisionErr error
wantErr bool
routersAfter int // expected number of routers after provisioning
}{
{
name: "successful provisioning",
provisionErr: nil,
wantErr: false,
routersAfter: 0,
},
{
name: "provisioning error",
provisionErr: fmt.Errorf("provision failed"),
wantErr: true,
routersAfter: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
originalModules := make(map[string]ModuleInfo)
for k, v := range modules {
originalModules[k] = v
}
defer func() {
modules = originalModules
}()
mockRoute := AdminRoute{
Pattern: "/mock",
Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}),
}
// Create provisionable module
mock := &mockProvisionableModule{
mockProvisionableRouter: &mockProvisionableRouter{
mockRouter: mockRouter{
routes: []AdminRoute{mockRoute},
},
provisionErr: test.provisionErr,
},
}
RegisterModule(mock)
admin := &AdminConfig{}
addr, err := ParseNetworkAddress("localhost:2019")
if err != nil {
t.Fatalf("Failed to parse address: %v", err)
}
_ = admin.newAdminHandler(addr, false, Context{})
err = admin.provisionAdminRouters(Context{})
if test.wantErr {
if err == nil {
t.Error("Expected error but got nil")
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
}
if len(admin.routers) != test.routersAfter {
t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
}
})
}
}
func TestAllowedOriginsUnixSocket(t *testing.T) {
// see comment in allowedOrigins() as to why we do not fill out allowed origins for UDS
tests := []struct {
name string
addr NetworkAddress
origins []string
expectOrigins []string
}{
{
name: "unix socket with default origins",
addr: NetworkAddress{
Network: "unix",
Host: "/tmp/caddy.sock",
},
origins: nil, // default origins
expectOrigins: []string{},
},
{
name: "unix socket with custom origins",
addr: NetworkAddress{
Network: "unix",
Host: "/tmp/caddy.sock",
},
origins: []string{"example.com"},
expectOrigins: []string{
"example.com",
},
},
{
name: "tcp socket on localhost gets all loopback addresses",
addr: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 2019,
EndPort: 2019,
},
origins: nil,
expectOrigins: []string{
"localhost:2019",
"[::1]:2019",
"127.0.0.1:2019",
},
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
admin := AdminConfig{
Origins: test.origins,
}
got := admin.allowedOrigins(test.addr)
var gotOrigins []string
for _, u := range got {
gotOrigins = append(gotOrigins, u.Host)
}
if len(gotOrigins) != len(test.expectOrigins) {
t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins))
return
}
expectMap := make(map[string]struct{})
for _, origin := range test.expectOrigins {
expectMap[origin] = struct{}{}
}
gotMap := make(map[string]struct{})
for _, origin := range gotOrigins {
gotMap[origin] = struct{}{}
}
if !reflect.DeepEqual(expectMap, gotMap) {
t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins)
}
})
}
}
func TestReplaceRemoteAdminServer(t *testing.T) {
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
MTAwMDAwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA4O4S6BSoYcoxvRqI+h7yPOjF6KjntjzVVm9M+uHK4lzX
F1L3pSxJ2nDD4wZEV3FJ5yFOHVFqkG2vXG3BIczOlYG7UeNmKbQnKc5kZj3HGUrS
VGEktA4OJbeZhhWP15gcXN5eDM2eH3g9BFXVX6AURxLiUXzhNBUEZuj/OEyH9yEF
/qPCE+EjzVvWxvBXwgz/io4r4yok/Vq/bxJ6FlV6R7DX5oJSXyO0VEHZPi9DIyNU
kK3F/r4U1sWiJGWOs8i3YQWZ2ejh1C0aLFZpPcCGGgMNpoF31gyYP6ZuPDUyCXsE
g36UUw1JHNtIXYcLhnXuqj4A8TybTDpgXLqvwA9DBQIDAQABo1MwUTAdBgNVHQ4E
FgQUc13z30pFC63rr/HGKOE7E82vjXwwHwYDVR0jBBgwFoAUc13z30pFC63rr/HG
KOE7E82vjXwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHO3j
oeiUXXJ7xD4P8Wj5t9d+E8lE1Xv1Dk3Z+EdG5+dan+RcToE42JJp9zB7FIh5Qz8g
W77LAjqh5oyqz3A2VJcyVgfE3uJP1R1mJM7JfGHf84QH4TZF2Q1RZY4SZs0VQ6+q
5wSlIZ4NXDy4Q4XkIJBGS61wT8IzYFXYBpx4PCP1Qj0PIE4sevEGwjsBIgxK307o
BxF8AWe6N6e4YZmQLGjQ+SeH0iwZb6vpkHyAY8Kj2hvK+cq2P7vU3VGi0t3r1F8L
IvrXHCvO2BMNJ/1UK1M4YNX8LYJqQhg9hEsIROe1OE/m3VhxIYMJI+qZXk9yHfgJ
vq+SH04xKhtFudVBAQ==`
tests := []struct {
name string
cfg *Config
wantErr bool
}{
{
name: "nil config",
cfg: nil,
wantErr: false,
},
{
name: "nil admin config",
cfg: &Config{
Admin: nil,
},
wantErr: false,
},
{
name: "nil remote config",
cfg: &Config{
Admin: &AdminConfig{},
},
wantErr: false,
},
{
name: "invalid listen address",
cfg: &Config{
Admin: &AdminConfig{
Remote: &RemoteAdmin{
Listen: "invalid:address",
},
},
},
wantErr: true,
},
{
name: "valid config",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{},
Remote: &RemoteAdmin{
Listen: "localhost:2021",
AccessControl: []*AdminAccess{
{
PublicKeys: []string{testCert},
Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
},
},
},
},
},
wantErr: false,
},
{
name: "invalid certificate",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{},
Remote: &RemoteAdmin{
Listen: "localhost:2021",
AccessControl: []*AdminAccess{
{
PublicKeys: []string{"invalid-cert-data"},
Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
},
},
},
},
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := Context{
Context: context.Background(),
cfg: test.cfg,
}
if test.cfg != nil {
test.cfg.storage = &certmagic.FileStorage{Path: t.TempDir()}
}
if test.cfg != nil && test.cfg.Admin != nil && test.cfg.Admin.Identity != nil {
identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
return &certmagic.Config{}, nil
},
})
}
err := replaceRemoteAdminServer(ctx, test.cfg)
if test.wantErr {
if err == nil {
t.Error("Expected error but got nil")
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
}
// Clean up
if remoteAdminServer != nil {
_ = stopAdminServer(remoteAdminServer)
}
})
}
}
type mockIssuer struct {
configSet *certmagic.Config
}
func (m *mockIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
return &certmagic.IssuedCertificate{
Certificate: []byte(csr.Raw),
}, nil
}
func (m *mockIssuer) SetConfig(cfg *certmagic.Config) {
m.configSet = cfg
}
func (m *mockIssuer) IssuerKey() string {
return "mock"
}
type mockIssuerModule struct {
*mockIssuer
}
func (m *mockIssuerModule) CaddyModule() ModuleInfo {
return ModuleInfo{
ID: "tls.issuance.acme",
New: func() Module {
return &mockIssuerModule{mockIssuer: new(mockIssuer)}
},
}
}
func TestManageIdentity(t *testing.T) {
originalModules := make(map[string]ModuleInfo)
for k, v := range modules {
originalModules[k] = v
}
defer func() {
modules = originalModules
}()
RegisterModule(&mockIssuerModule{})
certPEM := []byte(`-----BEGIN CERTIFICATE-----
MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3lcub2pUwkjC
5GJQA2ZZfJJi6d1QHhEmkX9VxKYGp6gagZuRqJWy9TXP6++1ZzQQxqZLD0TkuxZ9
8i9Nz00000CCBjCCAQQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgG
CCsGAQUFBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29t
L0dJQUcyLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5j
b20vb2NzcDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/
BAIwADAfBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHREEEDAO
ggxtYWlsLmdvb2dsZTANBgkqhkiG9w0BAQUFAAOCAQEAMP6IWgNGZE8wP9TjFjSZ
3mmW3A1eIr0CuPwNZ2LJ5ZD1i70ojzcj4I9IdP5yPg9CAEV4hNASbM1LzfC7GmJE
tPzW5tRmpKVWZGRgTgZI8Hp/xZXMwLh9ZmXV4kESFAGj5G5FNvJyUV7R5Eh+7OZX
7G4jJ4ZGJh+5jzN9HdJJHQHGYNIYOzC7+HH9UMwCjX9vhQ4RjwFZJThS2Yb+y7pb
9yxTJZoXC6J0H5JpnZb7kZEJ+Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END CERTIFICATE-----`)
keyPEM := []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
...
-----END PRIVATE KEY-----`)
testStorage := certmagic.FileStorage{Path: t.TempDir()}
err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
if err != nil {
t.Fatal(err)
}
err = testStorage.Store(context.Background(), "localhost/localhost.key", keyPEM)
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
cfg *Config
wantErr bool
checkState func(*testing.T, *Config)
}{
{
name: "nil config",
cfg: nil,
},
{
name: "nil admin config",
cfg: &Config{
Admin: nil,
},
},
{
name: "nil identity config",
cfg: &Config{
Admin: &AdminConfig{},
},
},
{
name: "default issuer when none specified",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{
Identifiers: []string{"localhost"},
},
},
storage: &testStorage,
},
checkState: func(t *testing.T, cfg *Config) {
if len(cfg.Admin.Identity.issuers) == 0 {
t.Error("Expected at least 1 issuer to be configured")
return
}
if _, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule); !ok {
t.Error("Expected mock issuer to be configured")
}
},
},
{
name: "custom issuer",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{
Identifiers: []string{"localhost"},
IssuersRaw: []json.RawMessage{
json.RawMessage(`{"module": "acme"}`),
},
},
},
storage: &certmagic.FileStorage{Path: "testdata"},
},
checkState: func(t *testing.T, cfg *Config) {
if len(cfg.Admin.Identity.issuers) != 1 {
t.Fatalf("Expected 1 issuer, got %d", len(cfg.Admin.Identity.issuers))
}
mockIss, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule)
if !ok {
t.Fatal("Expected mock issuer")
}
if mockIss.configSet == nil {
t.Error("Issuer config was not set")
}
},
},
{
name: "invalid issuer module",
cfg: &Config{
Admin: &AdminConfig{
Identity: &IdentityConfig{
Identifiers: []string{"localhost"},
IssuersRaw: []json.RawMessage{
json.RawMessage(`{"module": "doesnt_exist"}`),
},
},
},
},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if identityCertCache != nil {
// Reset the cert cache before each test
identityCertCache.Stop()
identityCertCache = nil
}
ctx := Context{
Context: context.Background(),
cfg: test.cfg,
moduleInstances: make(map[string][]Module),
}
err := manageIdentity(ctx, test.cfg)
if test.wantErr {
if err == nil {
t.Error("Expected error but got nil")
}
return
}
if err != nil {
t.Fatalf("Expected no error but got: %v", err)
}
if test.checkState != nil {
test.checkState(t, test.cfg)
}
})
}
}
+26
View File
@@ -0,0 +1,26 @@
version: "{build}"
os: Windows Server 2012 R2
clone_folder: c:\gopath\src\github.com\mholt\caddy
environment:
GOPATH: c:\gopath
CGO_ENABLED: 0
install:
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.6.windows-amd64.zip
- 7z x go1.6.windows-amd64.zip -y -oC:\ > NUL
- go version
- go env
- go get golang.org/x/tools/cmd/vet
- go get -t ./...
build: off
test_script:
- go vet ./...
- go test ./...
deploy: off
-1185
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
package assets
import (
"os"
"path/filepath"
"runtime"
)
// Path returns the path to the folder
// where the application may store data. This
// currently resolves to ~/.caddy
func Path() string {
return filepath.Join(userHomeDir(), ".caddy")
}
// userHomeDir returns the user's home directory according to
// environment variables.
//
// Credit: http://stackoverflow.com/a/7922977/1048862
func userHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
}
return os.Getenv("HOME")
}
+12
View File
@@ -0,0 +1,12 @@
package assets
import (
"strings"
"testing"
)
func TestPath(t *testing.T) {
if actual := Path(); !strings.HasSuffix(actual, ".caddy") {
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
}
}
+389
View File
@@ -0,0 +1,389 @@
// Package caddy implements the Caddy web server as a service
// in your own Go programs.
//
// To use this package, follow a few simple steps:
//
// 1. Set the AppName and AppVersion variables.
// 2. Call LoadCaddyfile() to get the Caddyfile (it
// might have been piped in as part of a restart).
// You should pass in your own Caddyfile loader.
// 3. Call caddy.Start() to start Caddy, caddy.Stop()
// to stop it, or caddy.Restart() to restart it.
//
// You should use caddy.Wait() to wait for all Caddy servers
// to quit before your process exits.
package caddy
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"path"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/mholt/caddy/caddy/https"
"github.com/mholt/caddy/server"
)
// Configurable application parameters
var (
// AppName is the name of the application.
AppName string
// AppVersion is the version of the application.
AppVersion string
// Quiet when set to true, will not show any informative output on initialization.
Quiet bool
// HTTP2 indicates whether HTTP2 is enabled or not.
HTTP2 bool
// PidFile is the path to the pidfile to create.
PidFile string
// GracefulTimeout is the maximum duration of a graceful shutdown.
GracefulTimeout time.Duration
)
var (
// caddyfile is the input configuration text used for this process
caddyfile Input
// caddyfileMu protects caddyfile during changes
caddyfileMu sync.Mutex
// errIncompleteRestart occurs if this process is a fork
// of the parent but no Caddyfile was piped in
errIncompleteRestart = errors.New("incomplete restart")
// servers is a list of all the currently-listening servers
servers []*server.Server
// serversMu protects the servers slice during changes
serversMu sync.Mutex
// wg is used to wait for all servers to shut down
wg sync.WaitGroup
// loadedGob is used if this is a child process as part of
// a graceful restart; it is used to map listeners to their
// index in the list of inherited file descriptors. This
// variable is not safe for concurrent access.
loadedGob caddyfileGob
// startedBefore should be set to true if caddy has been started
// at least once (does not indicate whether currently running).
startedBefore bool
)
const (
// DefaultHost is the default host.
DefaultHost = ""
// DefaultPort is the default port.
DefaultPort = "2015"
// DefaultRoot is the default root folder.
DefaultRoot = "."
)
// Start starts Caddy with the given Caddyfile. If cdyfile
// is nil, the LoadCaddyfile function will be called to get
// one.
//
// This function blocks until all the servers are listening.
//
// Note (POSIX): If Start is called in the child process of a
// restart more than once within the duration of the graceful
// cutoff (i.e. the child process called Start a first time,
// then called Stop, then Start again within the first 5 seconds
// or however long GracefulTimeout is) and the Caddyfiles have
// at least one listener address in common, the second Start
// may fail with "address already in use" as there's no
// guarantee that the parent process has relinquished the
// address before the grace period ends.
func Start(cdyfile Input) (err error) {
// If we return with no errors, we must do two things: tell the
// parent that we succeeded and write to the pidfile.
defer func() {
if err == nil {
signalSuccessToParent() // TODO: Is doing this more than once per process a bad idea? Start could get called more than once in other apps.
if PidFile != "" {
err := writePidFile()
if err != nil {
log.Printf("[ERROR] Could not write pidfile: %v", err)
}
}
}
}()
// Input must never be nil; try to load something
if cdyfile == nil {
cdyfile, err = LoadCaddyfile(nil)
if err != nil {
return err
}
}
caddyfileMu.Lock()
caddyfile = cdyfile
caddyfileMu.Unlock()
// load the server configs (activates Let's Encrypt)
configs, err := loadConfigs(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
if err != nil {
return err
}
// group virtualhosts by address
groupings, err := arrangeBindings(configs)
if err != nil {
return err
}
// Start each server with its one or more configurations
err = startServers(groupings)
if err != nil {
return err
}
startedBefore = true
// Show initialization output
if !Quiet && !IsRestart() {
var checkedFdLimit bool
for _, group := range groupings {
for _, conf := range group.Configs {
// Print address of site
fmt.Println(conf.Address())
// Note if non-localhost site resolves to loopback interface
if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
fmt.Printf("Notice: %s is only accessible on this machine (%s)\n",
conf.Host, group.BindAddr.IP.String())
}
if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
checkFdlimit()
checkedFdLimit = true
}
}
}
}
return nil
}
// startServers starts all the servers in groupings,
// taking into account whether or not this process is
// a child from a graceful restart or not. It blocks
// until the servers are listening.
func startServers(groupings bindingGroup) error {
var startupWg sync.WaitGroup
errChan := make(chan error, len(groupings)) // must be buffered to allow Serve functions below to return if stopped later
for _, group := range groupings {
s, err := server.New(group.BindAddr.String(), group.Configs, GracefulTimeout)
if err != nil {
return err
}
s.HTTP2 = HTTP2
s.ReqCallback = https.RequestCallback // ensures we can solve ACME challenges while running
if s.OnDemandTLS {
s.TLSConfig.GetCertificate = https.GetOrObtainCertificate // TLS on demand -- awesome!
} else {
s.TLSConfig.GetCertificate = https.GetCertificate
}
var ln server.ListenerFile
if IsRestart() {
// Look up this server's listener in the map of inherited file descriptors;
// if we don't have one, we must make a new one (later).
if fdIndex, ok := loadedGob.ListenerFds[s.Addr]; ok {
file := os.NewFile(fdIndex, "")
fln, err := net.FileListener(file)
if err != nil {
return err
}
ln, ok = fln.(server.ListenerFile)
if !ok {
return errors.New("listener for " + s.Addr + " was not a ListenerFile")
}
file.Close()
delete(loadedGob.ListenerFds, s.Addr)
}
}
wg.Add(1)
go func(s *server.Server, ln server.ListenerFile) {
defer wg.Done()
// run startup functions that should only execute when
// the original parent process is starting.
if !IsRestart() && !startedBefore {
err := s.RunFirstStartupFuncs()
if err != nil {
errChan <- err
return
}
}
// start the server
if ln != nil {
errChan <- s.Serve(ln)
} else {
errChan <- s.ListenAndServe()
}
}(s, ln)
startupWg.Add(1)
go func(s *server.Server) {
defer startupWg.Done()
s.WaitUntilStarted()
}(s)
serversMu.Lock()
servers = append(servers, s)
serversMu.Unlock()
}
// Close the remaining (unused) file descriptors to free up resources
if IsRestart() {
for key, fdIndex := range loadedGob.ListenerFds {
os.NewFile(fdIndex, "").Close()
delete(loadedGob.ListenerFds, key)
}
}
// Wait for all servers to finish starting
startupWg.Wait()
// Return the first error, if any
select {
case err := <-errChan:
// "use of closed network connection" is normal if it was a graceful shutdown
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
return err
}
default:
}
return nil
}
// Stop stops all servers. It blocks until they are all stopped.
// It does NOT execute shutdown callbacks that may have been
// configured by middleware (they must be executed separately).
func Stop() error {
https.Deactivate()
serversMu.Lock()
for _, s := range servers {
if err := s.Stop(); err != nil {
log.Printf("[ERROR] Stopping %s: %v", s.Addr, err)
}
}
servers = []*server.Server{} // don't reuse servers
serversMu.Unlock()
return nil
}
// Wait blocks until all servers are stopped.
func Wait() {
wg.Wait()
}
// LoadCaddyfile loads a Caddyfile, prioritizing a Caddyfile
// piped from stdin as part of a restart (only happens on first call
// to LoadCaddyfile). If it is not a restart, this function tries
// calling the user's loader function, and if that returns nil, then
// this function resorts to the default configuration. Thus, if there
// are no other errors, this function always returns at least the
// default Caddyfile.
func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) {
// If we are a fork, finishing the restart is highest priority;
// piped input is required in this case.
if IsRestart() {
err := gob.NewDecoder(os.Stdin).Decode(&loadedGob)
if err != nil {
return nil, err
}
cdyfile = loadedGob.Caddyfile
atomic.StoreInt32(https.OnDemandIssuedCount, loadedGob.OnDemandTLSCertsIssued)
}
// Try user's loader
if cdyfile == nil && loader != nil {
cdyfile, err = loader()
}
// Otherwise revert to default
if cdyfile == nil {
cdyfile = DefaultInput()
}
return
}
// CaddyfileFromPipe loads the Caddyfile input from f if f is
// not interactive input. f is assumed to be a pipe or stream,
// such as os.Stdin. If f is not a pipe, no error is returned
// but the Input value will be nil. An error is only returned
// if there was an error reading the pipe, even if the length
// of what was read is 0.
func CaddyfileFromPipe(f *os.File) (Input, error) {
fi, err := f.Stat()
if err == nil && fi.Mode()&os.ModeCharDevice == 0 {
// Note that a non-nil error is not a problem. Windows
// will not create a stdin if there is no pipe, which
// produces an error when calling Stat(). But Unix will
// make one either way, which is why we also check that
// bitmask.
// BUG: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X)
confBody, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
return CaddyfileInput{
Contents: confBody,
Filepath: f.Name(),
}, nil
}
// not having input from the pipe is not itself an error,
// just means no input to return.
return nil, nil
}
// Caddyfile returns the current Caddyfile
func Caddyfile() Input {
caddyfileMu.Lock()
defer caddyfileMu.Unlock()
return caddyfile
}
// Input represents a Caddyfile; its contents and file path
// (which should include the file name at the end of the path).
// If path does not apply (e.g. piped input) you may use
// any understandable value. The path is mainly used for logging,
// error messages, and debugging.
type Input interface {
// Gets the Caddyfile contents
Body() []byte
// Gets the path to the origin file
Path() string
// IsFile returns true if the original input was a file on the file system
// that could be loaded again later if requested.
IsFile() bool
}
+32
View File
@@ -0,0 +1,32 @@
package caddy
import (
"net/http"
"testing"
"time"
)
func TestCaddyStartStop(t *testing.T) {
caddyfile := "localhost:1984"
for i := 0; i < 2; i++ {
err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
if err != nil {
t.Fatalf("Error starting, iteration %d: %v", i, err)
}
client := http.Client{
Timeout: time.Duration(2 * time.Second),
}
resp, err := client.Get("http://localhost:1984")
if err != nil {
t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err)
}
resp.Body.Close()
err = Stop()
if err != nil {
t.Fatalf("Error stopping, iteration %d: %v", i, err)
}
}
}
+185
View File
@@ -0,0 +1,185 @@
package caddyfile
import (
"bytes"
"encoding/json"
"fmt"
"net"
"sort"
"strconv"
"strings"
"github.com/mholt/caddy/caddy/parse"
)
const filename = "Caddyfile"
// ToJSON converts caddyfile to its JSON representation.
func ToJSON(caddyfile []byte) ([]byte, error) {
var j Caddyfile
serverBlocks, err := parse.ServerBlocks(filename, bytes.NewReader(caddyfile), false)
if err != nil {
return nil, err
}
for _, sb := range serverBlocks {
block := ServerBlock{Body: [][]interface{}{}}
// Fill up host list
for _, host := range sb.HostList() {
block.Hosts = append(block.Hosts, standardizeScheme(host))
}
// Extract directives deterministically by sorting them
var directives = make([]string, len(sb.Tokens))
for dir := range sb.Tokens {
directives = append(directives, dir)
}
sort.Strings(directives)
// Convert each directive's tokens into our JSON structure
for _, dir := range directives {
disp := parse.NewDispenserTokens(filename, sb.Tokens[dir])
for disp.Next() {
block.Body = append(block.Body, constructLine(&disp))
}
}
// tack this block onto the end of the list
j = append(j, block)
}
result, err := json.Marshal(j)
if err != nil {
return nil, err
}
return result, nil
}
// constructLine transforms tokens into a JSON-encodable structure;
// but only one line at a time, to be used at the top-level of
// a server block only (where the first token on each line is a
// directive) - not to be used at any other nesting level.
func constructLine(d *parse.Dispenser) []interface{} {
var args []interface{}
args = append(args, d.Val())
for d.NextArg() {
if d.Val() == "{" {
args = append(args, constructBlock(d))
continue
}
args = append(args, d.Val())
}
return args
}
// constructBlock recursively processes tokens into a
// JSON-encodable structure. To be used in a directive's
// block. Goes to end of block.
func constructBlock(d *parse.Dispenser) [][]interface{} {
block := [][]interface{}{}
for d.Next() {
if d.Val() == "}" {
break
}
block = append(block, constructLine(d))
}
return block
}
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
func FromJSON(jsonBytes []byte) ([]byte, error) {
var j Caddyfile
var result string
err := json.Unmarshal(jsonBytes, &j)
if err != nil {
return nil, err
}
for sbPos, sb := range j {
if sbPos > 0 {
result += "\n\n"
}
for i, host := range sb.Hosts {
if i > 0 {
result += ", "
}
result += standardizeScheme(host)
}
result += jsonToText(sb.Body, 1)
}
return []byte(result), nil
}
// jsonToText recursively transforms a scope of JSON into plain
// Caddyfile text.
func jsonToText(scope interface{}, depth int) string {
var result string
switch val := scope.(type) {
case string:
if strings.ContainsAny(val, "\" \n\t\r") {
result += `"` + strings.Replace(val, "\"", "\\\"", -1) + `"`
} else {
result += val
}
case int:
result += strconv.Itoa(val)
case float64:
result += fmt.Sprintf("%v", val)
case bool:
result += fmt.Sprintf("%t", val)
case [][]interface{}:
result += " {\n"
for _, arg := range val {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
case []interface{}:
for i, v := range val {
if block, ok := v.([]interface{}); ok {
result += "{\n"
for _, arg := range block {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
continue
}
result += jsonToText(v, depth)
if i < len(val)-1 {
result += " "
}
}
}
return result
}
// standardizeScheme turns an address like host:https into https://host,
// or "host:" into "host".
func standardizeScheme(addr string) string {
if hostname, port, err := net.SplitHostPort(addr); err == nil {
if port == "http" || port == "https" {
addr = port + "://" + hostname
}
}
return strings.TrimSuffix(addr, ":")
}
// Caddyfile encapsulates a slice of ServerBlocks.
type Caddyfile []ServerBlock
// ServerBlock represents a server block.
type ServerBlock struct {
Hosts []string `json:"hosts"`
Body [][]interface{} `json:"body"`
}
+161
View File
@@ -0,0 +1,161 @@
package caddyfile
import "testing"
var tests = []struct {
caddyfile, json string
}{
{ // 0
caddyfile: `foo {
root /bar
}`,
json: `[{"hosts":["foo"],"body":[["root","/bar"]]}]`,
},
{ // 1
caddyfile: `host1, host2 {
dir {
def
}
}`,
json: `[{"hosts":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
},
{ // 2
caddyfile: `host1, host2 {
dir abc {
def ghi
jkl
}
}`,
json: `[{"hosts":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
},
{ // 3
caddyfile: `host1:1234, host2:5678 {
dir abc {
}
}`,
json: `[{"hosts":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
},
{ // 4
caddyfile: `host {
foo "bar baz"
}`,
json: `[{"hosts":["host"],"body":[["foo","bar baz"]]}]`,
},
{ // 5
caddyfile: `host, host:80 {
foo "bar \"baz\""
}`,
json: `[{"hosts":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
},
{ // 6
caddyfile: `host {
foo "bar
baz"
}`,
json: `[{"hosts":["host"],"body":[["foo","bar\nbaz"]]}]`,
},
{ // 7
caddyfile: `host {
dir 123 4.56 true
}`,
json: `[{"hosts":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
},
{ // 8
caddyfile: `http://host, https://host {
}`,
json: `[{"hosts":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
},
{ // 9
caddyfile: `host {
dir1 a b
dir2 c d
}`,
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
},
{ // 10
caddyfile: `host {
dir a b
dir c d
}`,
json: `[{"hosts":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
},
{ // 11
caddyfile: `host {
dir1 a b
dir2 {
c
d
}
}`,
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
},
{ // 12
caddyfile: `host1 {
dir1
}
host2 {
dir2
}`,
json: `[{"hosts":["host1"],"body":[["dir1"]]},{"hosts":["host2"],"body":[["dir2"]]}]`,
},
}
func TestToJSON(t *testing.T) {
for i, test := range tests {
output, err := ToJSON([]byte(test.caddyfile))
if err != nil {
t.Errorf("Test %d: %v", i, err)
}
if string(output) != test.json {
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.json, string(output))
}
}
}
func TestFromJSON(t *testing.T) {
for i, test := range tests {
output, err := FromJSON([]byte(test.json))
if err != nil {
t.Errorf("Test %d: %v", i, err)
}
if string(output) != test.caddyfile {
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.caddyfile, string(output))
}
}
}
func TestStandardizeAddress(t *testing.T) {
// host:https should be converted to https://host
output, err := ToJSON([]byte(`host:https`))
if err != nil {
t.Fatal(err)
}
if expected, actual := `[{"hosts":["https://host"],"body":[]}]`, string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
output, err = FromJSON([]byte(`[{"hosts":["https://host"],"body":[]}]`))
if err != nil {
t.Fatal(err)
}
if expected, actual := "https://host {\n}", string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
// host: should be converted to just host
output, err = ToJSON([]byte(`host:`))
if err != nil {
t.Fatal(err)
}
if expected, actual := `[{"hosts":["host"],"body":[]}]`, string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
output, err = FromJSON([]byte(`[{"hosts":["host:"],"body":[]}]`))
if err != nil {
t.Fatal(err)
}
if expected, actual := "host {\n}", string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
}
+348
View File
@@ -0,0 +1,348 @@
package caddy
import (
"bytes"
"fmt"
"io"
"log"
"net"
"sync"
"github.com/mholt/caddy/caddy/https"
"github.com/mholt/caddy/caddy/parse"
"github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/server"
)
const (
// DefaultConfigFile is the name of the configuration file that is loaded
// by default if no other file is specified.
DefaultConfigFile = "Caddyfile"
)
// loadConfigsUpToIncludingTLS loads the configs from input with name filename and returns them,
// the parsed server blocks, the index of the last directive it processed, and an error (if any).
func loadConfigsUpToIncludingTLS(filename string, input io.Reader) ([]server.Config, []parse.ServerBlock, int, error) {
var configs []server.Config
// Each server block represents similar hosts/addresses, since they
// were grouped together in the Caddyfile.
serverBlocks, err := parse.ServerBlocks(filename, input, true)
if err != nil {
return nil, nil, 0, err
}
if len(serverBlocks) == 0 {
newInput := DefaultInput()
serverBlocks, err = parse.ServerBlocks(newInput.Path(), bytes.NewReader(newInput.Body()), true)
if err != nil {
return nil, nil, 0, err
}
}
var lastDirectiveIndex int // we set up directives in two parts; this stores where we left off
// Iterate each server block and make a config for each one,
// executing the directives that were parsed in order up to the tls
// directive; this is because we must activate Let's Encrypt.
for i, sb := range serverBlocks {
onces := makeOnces()
storages := makeStorages()
for j, addr := range sb.Addresses {
config := server.Config{
Host: addr.Host,
Port: addr.Port,
Scheme: addr.Scheme,
Root: Root,
ConfigFile: filename,
AppName: AppName,
AppVersion: AppVersion,
}
// It is crucial that directives are executed in the proper order.
for k, dir := range directiveOrder {
// Execute directive if it is in the server block
if tokens, ok := sb.Tokens[dir.name]; ok {
// Each setup function gets a controller, from which setup functions
// get access to the config, tokens, and other state information useful
// to set up its own host only.
controller := &setup.Controller{
Config: &config,
Dispenser: parse.NewDispenserTokens(filename, tokens),
OncePerServerBlock: func(f func() error) error {
var err error
onces[dir.name].Do(func() {
err = f()
})
return err
},
ServerBlockIndex: i,
ServerBlockHostIndex: j,
ServerBlockHosts: sb.HostList(),
ServerBlockStorage: storages[dir.name],
}
// execute setup function and append middleware handler, if any
midware, err := dir.setup(controller)
if err != nil {
return nil, nil, lastDirectiveIndex, err
}
if midware != nil {
config.Middleware = append(config.Middleware, midware)
}
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
}
// Stop after TLS setup, since we need to activate Let's Encrypt before continuing;
// it makes some changes to the configs that middlewares might want to know about.
if dir.name == "tls" {
lastDirectiveIndex = k
break
}
}
configs = append(configs, config)
}
}
return configs, serverBlocks, lastDirectiveIndex, nil
}
// loadConfigs reads input (named filename) and parses it, returning the
// server configurations in the order they appeared in the input. As part
// of this, it activates Let's Encrypt for the configs that are produced.
// Thus, the returned configs are already optimally configured for HTTPS.
func loadConfigs(filename string, input io.Reader) ([]server.Config, error) {
configs, serverBlocks, lastDirectiveIndex, err := loadConfigsUpToIncludingTLS(filename, input)
if err != nil {
return nil, err
}
// Now we have all the configs, but they have only been set up to the
// point of tls. We need to activate Let's Encrypt before setting up
// the rest of the middlewares so they have correct information regarding
// TLS configuration, if necessary. (this only appends, so our iterations
// over server blocks below shouldn't be affected)
if !IsRestart() && !Quiet {
fmt.Print("Activating privacy features...")
}
configs, err = https.Activate(configs)
if err != nil {
return nil, err
} else if !IsRestart() && !Quiet {
fmt.Println(" done.")
}
// Finish setting up the rest of the directives, now that TLS is
// optimally configured. These loops are similar to above except
// we don't iterate all the directives from the beginning and we
// don't create new configs.
configIndex := -1
for i, sb := range serverBlocks {
onces := makeOnces()
storages := makeStorages()
for j := range sb.Addresses {
configIndex++
for k := lastDirectiveIndex + 1; k < len(directiveOrder); k++ {
dir := directiveOrder[k]
if tokens, ok := sb.Tokens[dir.name]; ok {
controller := &setup.Controller{
Config: &configs[configIndex],
Dispenser: parse.NewDispenserTokens(filename, tokens),
OncePerServerBlock: func(f func() error) error {
var err error
onces[dir.name].Do(func() {
err = f()
})
return err
},
ServerBlockIndex: i,
ServerBlockHostIndex: j,
ServerBlockHosts: sb.HostList(),
ServerBlockStorage: storages[dir.name],
}
midware, err := dir.setup(controller)
if err != nil {
return nil, err
}
if midware != nil {
configs[configIndex].Middleware = append(configs[configIndex].Middleware, midware)
}
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
}
}
}
}
return configs, nil
}
// makeOnces makes a map of directive name to sync.Once
// instance. This is intended to be called once per server
// block when setting up configs so that Setup functions
// for each directive can perform a task just once per
// server block, even if there are multiple hosts on the block.
//
// We need one Once per directive, otherwise the first
// directive to use it would exclude other directives from
// using it at all, which would be a bug.
func makeOnces() map[string]*sync.Once {
onces := make(map[string]*sync.Once)
for _, dir := range directiveOrder {
onces[dir.name] = new(sync.Once)
}
return onces
}
// makeStorages makes a map of directive name to interface{}
// so that directives' setup functions can persist state
// between different hosts on the same server block during the
// setup phase.
func makeStorages() map[string]interface{} {
storages := make(map[string]interface{})
for _, dir := range directiveOrder {
storages[dir.name] = nil
}
return storages
}
// arrangeBindings groups configurations by their bind address. For example,
// a server that should listen on localhost and another on 127.0.0.1 will
// be grouped into the same address: 127.0.0.1. It will return an error
// if an address is malformed or a TLS listener is configured on the
// same address as a plaintext HTTP listener. The return value is a map of
// bind address to list of configs that would become VirtualHosts on that
// server. Use the keys of the returned map to create listeners, and use
// the associated values to set up the virtualhosts.
func arrangeBindings(allConfigs []server.Config) (bindingGroup, error) {
var groupings bindingGroup
// Group configs by bind address
for _, conf := range allConfigs {
// use default port if none is specified
if conf.Port == "" {
conf.Port = Port
}
bindAddr, warnErr, fatalErr := resolveAddr(conf)
if fatalErr != nil {
return groupings, fatalErr
}
if warnErr != nil {
log.Printf("[WARNING] Resolving bind address for %s: %v", conf.Address(), warnErr)
}
// Make sure to compare the string representation of the address,
// not the pointer, since a new *TCPAddr is created each time.
var existing bool
for i := 0; i < len(groupings); i++ {
if groupings[i].BindAddr.String() == bindAddr.String() {
groupings[i].Configs = append(groupings[i].Configs, conf)
existing = true
break
}
}
if !existing {
groupings = append(groupings, bindingMapping{
BindAddr: bindAddr,
Configs: []server.Config{conf},
})
}
}
// Don't allow HTTP and HTTPS to be served on the same address
for _, group := range groupings {
isTLS := group.Configs[0].TLS.Enabled
for _, config := range group.Configs {
if config.TLS.Enabled != isTLS {
thisConfigProto, otherConfigProto := "HTTP", "HTTP"
if config.TLS.Enabled {
thisConfigProto = "HTTPS"
}
if group.Configs[0].TLS.Enabled {
otherConfigProto = "HTTPS"
}
return groupings, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address",
group.Configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto)
}
}
}
return groupings, nil
}
// resolveAddr determines the address (host and port) that a config will
// bind to. The returned address, resolvAddr, should be used to bind the
// listener or group the config with other configs using the same address.
// The first error, if not nil, is just a warning and should be reported
// but execution may continue. The second error, if not nil, is a real
// problem and the server should not be started.
//
// This function does not handle edge cases like port "http" or "https" if
// they are not known to the system. It does, however, serve on the wildcard
// host if resolving the address of the specific hostname fails.
func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr, fatalErr error) {
resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.BindHost, conf.Port))
if warnErr != nil {
// the hostname probably couldn't be resolved, just bind to wildcard then
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("", conf.Port))
if fatalErr != nil {
return
}
}
return
}
// validDirective returns true if d is a valid
// directive; false otherwise.
func validDirective(d string) bool {
for _, dir := range directiveOrder {
if dir.name == d {
return true
}
}
return false
}
// DefaultInput returns the default Caddyfile input
// to use when it is otherwise empty or missing.
// It uses the default host and port (depends on
// host, e.g. localhost is 2015, otherwise 443) and
// root.
func DefaultInput() CaddyfileInput {
port := Port
if https.HostQualifies(Host) && port == DefaultPort {
port = "443"
}
return CaddyfileInput{
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, port, Root)),
}
}
// These defaults are configurable through the command line
var (
// Root is the site root
Root = DefaultRoot
// Host is the site host
Host = DefaultHost
// Port is the site port
Port = DefaultPort
)
// bindingMapping maps a network address to configurations
// that will bind to it. The order of the configs is important.
type bindingMapping struct {
BindAddr *net.TCPAddr
Configs []server.Config
}
// bindingGroup maps network addresses to their configurations.
// Preserving the order of the groupings is important
// (related to graceful shutdown and restart)
// so this is a slice, not a literal map.
type bindingGroup []bindingMapping
+159
View File
@@ -0,0 +1,159 @@
package caddy
import (
"reflect"
"sync"
"testing"
"github.com/mholt/caddy/server"
)
func TestDefaultInput(t *testing.T) {
if actual, expected := string(DefaultInput().Body()), ":2015\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
// next few tests simulate user providing -host and/or -port flags
Host = "not-localhost.com"
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:443\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
Host = "[::1]"
if actual, expected := string(DefaultInput().Body()), "[::1]:2015\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
Host = "127.0.1.1"
if actual, expected := string(DefaultInput().Body()), "127.0.1.1:2015\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
Host = "not-localhost.com"
Port = "1234"
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:1234\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
Host = DefaultHost
Port = "1234"
if actual, expected := string(DefaultInput().Body()), ":1234\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
}
func TestResolveAddr(t *testing.T) {
// NOTE: If tests fail due to comparing to string "127.0.0.1",
// it's possible that system env resolves with IPv6, or ::1.
// If that happens, maybe we should use actualAddr.IP.IsLoopback()
// for the assertion, rather than a direct string comparison.
// NOTE: Tests with {Host: "", Port: ""} and {Host: "localhost", Port: ""}
// will not behave the same cross-platform, so they have been omitted.
for i, test := range []struct {
config server.Config
shouldWarnErr bool
shouldFatalErr bool
expectedIP string
expectedPort int
}{
{server.Config{Host: "127.0.0.1", Port: "1234"}, false, false, "<nil>", 1234},
{server.Config{Host: "localhost", Port: "80"}, false, false, "<nil>", 80},
{server.Config{BindHost: "localhost", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{BindHost: "127.0.0.1", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{BindHost: "should-not-resolve", Port: "1234"}, true, false, "<nil>", 1234},
{server.Config{BindHost: "localhost", Port: "http"}, false, false, "127.0.0.1", 80},
{server.Config{BindHost: "localhost", Port: "https"}, false, false, "127.0.0.1", 443},
{server.Config{BindHost: "", Port: "1234"}, false, false, "<nil>", 1234},
{server.Config{BindHost: "localhost", Port: "abcd"}, false, true, "", 0},
{server.Config{BindHost: "127.0.0.1", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{BindHost: "localhost", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{BindHost: "should-not-resolve", Host: "localhost", Port: "1234"}, true, false, "<nil>", 1234},
} {
actualAddr, warnErr, fatalErr := resolveAddr(test.config)
if test.shouldFatalErr && fatalErr == nil {
t.Errorf("Test %d: Expected error, but there wasn't any", i)
}
if !test.shouldFatalErr && fatalErr != nil {
t.Errorf("Test %d: Expected no error, but there was one: %v", i, fatalErr)
}
if fatalErr != nil {
continue
}
if test.shouldWarnErr && warnErr == nil {
t.Errorf("Test %d: Expected warning, but there wasn't any", i)
}
if !test.shouldWarnErr && warnErr != nil {
t.Errorf("Test %d: Expected no warning, but there was one: %v", i, warnErr)
}
if actual, expected := actualAddr.IP.String(), test.expectedIP; actual != expected {
t.Errorf("Test %d: IP was %s but expected %s", i, actual, expected)
}
if actual, expected := actualAddr.Port, test.expectedPort; actual != expected {
t.Errorf("Test %d: Port was %d but expected %d", i, actual, expected)
}
}
}
func TestMakeOnces(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
onces := makeOnces()
if len(onces) != len(directives) {
t.Errorf("onces had len %d , expected %d", len(onces), len(directives))
}
expected := map[string]*sync.Once{
"dummy": new(sync.Once),
"dummy2": new(sync.Once),
}
if !reflect.DeepEqual(onces, expected) {
t.Errorf("onces was %v, expected %v", onces, expected)
}
}
func TestMakeStorages(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
storages := makeStorages()
if len(storages) != len(directives) {
t.Errorf("storages had len %d , expected %d", len(storages), len(directives))
}
expected := map[string]interface{}{
"dummy": nil,
"dummy2": nil,
}
if !reflect.DeepEqual(storages, expected) {
t.Errorf("storages was %v, expected %v", storages, expected)
}
}
func TestValidDirective(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
for i, test := range []struct {
directive string
valid bool
}{
{"dummy", true},
{"dummy2", true},
{"dummy3", false},
} {
if actual, expected := validDirective(test.directive), test.valid; actual != expected {
t.Errorf("Test %d: valid was %t, expected %t", i, actual, expected)
}
}
}
+98
View File
@@ -0,0 +1,98 @@
package caddy
import (
"github.com/mholt/caddy/caddy/https"
"github.com/mholt/caddy/caddy/parse"
"github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/middleware"
)
func init() {
// The parse package must know which directives
// are valid, but it must not import the setup
// or config package. To solve this problem, we
// fill up this map in our init function here.
// The parse package does not need to know the
// ordering of the directives.
for _, dir := range directiveOrder {
parse.ValidDirectives[dir.name] = struct{}{}
}
}
// Directives are registered in the order they should be
// executed. Middleware (directives that inject a handler)
// are executed in the order A-B-C-*-C-B-A, assuming
// they all call the Next handler in the chain.
//
// Ordering is VERY important. Every middleware will
// feel the effects of all other middleware below
// (after) them during a request, but they must not
// care what middleware above them are doing.
//
// For example, log needs to know the status code and
// exactly how many bytes were written to the client,
// which every other middleware can affect, so it gets
// registered first. The errors middleware does not
// care if gzip or log modifies its response, so it
// gets registered below them. Gzip, on the other hand,
// DOES care what errors does to the response since it
// must compress every output to the client, even error
// pages, so it must be registered before the errors
// middleware and any others that would write to the
// response.
var directiveOrder = []directive{
// Essential directives that initialize vital configuration settings
{"root", setup.Root},
{"bind", setup.BindHost},
{"tls", https.Setup},
// Other directives that don't create HTTP handlers
{"startup", setup.Startup},
{"shutdown", setup.Shutdown},
// Directives that inject handlers (middleware)
{"log", setup.Log},
{"gzip", setup.Gzip},
{"errors", setup.Errors},
{"header", setup.Headers},
{"rewrite", setup.Rewrite},
{"redir", setup.Redir},
{"ext", setup.Ext},
{"mime", setup.Mime},
{"basicauth", setup.BasicAuth},
{"internal", setup.Internal},
{"proxy", setup.Proxy},
{"fastcgi", setup.FastCGI},
{"websocket", setup.WebSocket},
{"markdown", setup.Markdown},
{"templates", setup.Templates},
{"browse", setup.Browse},
}
// RegisterDirective adds the given directive to caddy's list of directives.
// Pass the name of a directive you want it to be placed after,
// otherwise it will be placed at the bottom of the stack.
func RegisterDirective(name string, setup SetupFunc, after string) {
dir := directive{name: name, setup: setup}
idx := len(directiveOrder)
for i := range directiveOrder {
if directiveOrder[i].name == after {
idx = i + 1
break
}
}
newDirectives := append(directiveOrder[:idx], append([]directive{dir}, directiveOrder[idx:]...)...)
directiveOrder = newDirectives
parse.ValidDirectives[name] = struct{}{}
}
// directive ties together a directive name with its setup function.
type directive struct {
name string
setup SetupFunc
}
// SetupFunc takes a controller and may optionally return a middleware.
// If the resulting middleware is not nil, it will be chained into
// the HTTP handlers in the order specified in this package.
type SetupFunc func(c *setup.Controller) (middleware.Middleware, error)
+31
View File
@@ -0,0 +1,31 @@
package caddy
import (
"reflect"
"testing"
)
func TestRegister(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
RegisterDirective("foo", nil, "dummy")
if len(directiveOrder) != 3 {
t.Fatal("Should have 3 directives now")
}
getNames := func() (s []string) {
for _, d := range directiveOrder {
s = append(s, d.name)
}
return s
}
if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2"}) {
t.Fatalf("directive order doesn't match: %s", getNames())
}
RegisterDirective("bar", nil, "ASDASD")
if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2", "bar"}) {
t.Fatalf("directive order doesn't match: %s", getNames())
}
}
+102
View File
@@ -0,0 +1,102 @@
package caddy
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
)
// isLocalhost returns true if host looks explicitly like a localhost address.
func isLocalhost(host string) bool {
return host == "localhost" || host == "::1" || strings.HasPrefix(host, "127.")
}
// checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum.
func checkFdlimit() {
const min = 4096
// Warn if ulimit is too low for production sites
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH
if err == nil {
// Note that an error here need not be reported
lim, err := strconv.Atoi(string(bytes.TrimSpace(out)))
if err == nil && lim < min {
fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min)
}
}
}
}
// signalSuccessToParent tells the parent our status using pipe at index 3.
// If this process is not a restart, this function does nothing.
// Calling this function once this process has successfully initialized
// is vital so that the parent process can unblock and kill itself.
// This function is idempotent; it executes at most once per process.
func signalSuccessToParent() {
signalParentOnce.Do(func() {
if IsRestart() {
ppipe := os.NewFile(3, "") // parent is reading from pipe at index 3
_, err := ppipe.Write([]byte("success")) // we must send some bytes to the parent
if err != nil {
log.Printf("[ERROR] Communicating successful init to parent: %v", err)
}
ppipe.Close()
}
})
}
// signalParentOnce is used to make sure that the parent is only
// signaled once; doing so more than once breaks whatever socket is
// at fd 4 (the reason for this is still unclear - to reproduce,
// call Stop() and Start() in succession at least once after a
// restart, then try loading first host of Caddyfile in the browser).
// Do not use this directly - call signalSuccessToParent instead.
var signalParentOnce sync.Once
// caddyfileGob maps bind address to index of the file descriptor
// in the Files array passed to the child process. It also contains
// the caddyfile contents and other state needed by the new process.
// Used only during graceful restarts where a new process is spawned.
type caddyfileGob struct {
ListenerFds map[string]uintptr
Caddyfile Input
OnDemandTLSCertsIssued int32
}
// IsRestart returns whether this process is, according
// to env variables, a fork as part of a graceful restart.
func IsRestart() bool {
return os.Getenv("CADDY_RESTART") == "true"
}
// writePidFile writes the process ID to the file at PidFile, if specified.
func writePidFile() error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
return ioutil.WriteFile(PidFile, pid, 0644)
}
// CaddyfileInput represents a Caddyfile as input
// and is simply a convenient way to implement
// the Input interface.
type CaddyfileInput struct {
Filepath string
Contents []byte
RealFile bool
}
// Body returns c.Contents.
func (c CaddyfileInput) Body() []byte { return c.Contents }
// Path returns c.Filepath.
func (c CaddyfileInput) Path() string { return c.Filepath }
// IsFile returns true if the original input was a real file on the file system.
func (c CaddyfileInput) IsFile() bool { return c.RealFile }
+234
View File
@@ -0,0 +1,234 @@
package https
import (
"crypto/tls"
"crypto/x509"
"errors"
"io/ioutil"
"log"
"strings"
"sync"
"time"
"github.com/xenolf/lego/acme"
"golang.org/x/crypto/ocsp"
)
// certCache stores certificates in memory,
// keying certificates by name.
var certCache = make(map[string]Certificate)
var certCacheMu sync.RWMutex
// Certificate is a tls.Certificate with associated metadata tacked on.
// Even if the metadata can be obtained by parsing the certificate,
// we can be more efficient by extracting the metadata once so it's
// just there, ready to use.
type Certificate struct {
tls.Certificate
// Names is the list of names this certificate is written for.
// The first is the CommonName (if any), the rest are SAN.
Names []string
// NotAfter is when the certificate expires.
NotAfter time.Time
// Managed certificates are certificates that Caddy is managing,
// as opposed to the user specifying a certificate and key file
// or directory and managing the certificate resources themselves.
Managed bool
// OnDemand certificates are obtained or loaded on-demand during TLS
// handshakes (as opposed to preloaded certificates, which are loaded
// at startup). If OnDemand is true, Managed must necessarily be true.
// OnDemand certificates are maintained in the background just like
// preloaded ones, however, if an OnDemand certificate fails to renew,
// it is removed from the in-memory cache.
OnDemand bool
// OCSP contains the certificate's parsed OCSP response.
OCSP *ocsp.Response
}
// getCertificate gets a certificate that matches name (a server name)
// from the in-memory cache. If there is no exact match for name, it
// will be checked against names of the form '*.example.com' (wildcard
// certificates) according to RFC 6125. If a match is found, matched will
// be true. If no matches are found, matched will be false and a default
// certificate will be returned with defaulted set to true. If no default
// certificate is set, defaulted will be set to false.
//
// The logic in this function is adapted from the Go standard library,
// which is by the Go Authors.
//
// This function is safe for concurrent use.
func getCertificate(name string) (cert Certificate, matched, defaulted bool) {
var ok bool
// Not going to trim trailing dots here since RFC 3546 says,
// "The hostname is represented ... without a trailing dot."
// Just normalize to lowercase.
name = strings.ToLower(name)
certCacheMu.RLock()
defer certCacheMu.RUnlock()
// exact match? great, let's use it
if cert, ok = certCache[name]; ok {
matched = true
return
}
// try replacing labels in the name with wildcards until we get a match
labels := strings.Split(name, ".")
for i := range labels {
labels[i] = "*"
candidate := strings.Join(labels, ".")
if cert, ok = certCache[candidate]; ok {
matched = true
return
}
}
// if nothing matches, use the default certificate or bust
cert, defaulted = certCache[""]
return
}
// cacheManagedCertificate loads the certificate for domain into the
// cache, flagging it as Managed and, if onDemand is true, as OnDemand
// (meaning that it was obtained or loaded during a TLS handshake).
//
// This function is safe for concurrent use.
func cacheManagedCertificate(domain string, onDemand bool) (Certificate, error) {
cert, err := makeCertificateFromDisk(storage.SiteCertFile(domain), storage.SiteKeyFile(domain))
if err != nil {
return cert, err
}
cert.Managed = true
cert.OnDemand = onDemand
cacheCertificate(cert)
return cert, nil
}
// cacheUnmanagedCertificatePEMFile loads a certificate for host using certFile
// and keyFile, which must be in PEM format. It stores the certificate in
// memory. The Managed and OnDemand flags of the certificate will be set to
// false.
//
// This function is safe for concurrent use.
func cacheUnmanagedCertificatePEMFile(certFile, keyFile string) error {
cert, err := makeCertificateFromDisk(certFile, keyFile)
if err != nil {
return err
}
cacheCertificate(cert)
return nil
}
// cacheUnmanagedCertificatePEMBytes makes a certificate out of the PEM bytes
// of the certificate and key, then caches it in memory.
//
// This function is safe for concurrent use.
func cacheUnmanagedCertificatePEMBytes(certBytes, keyBytes []byte) error {
cert, err := makeCertificate(certBytes, keyBytes)
if err != nil {
return err
}
cacheCertificate(cert)
return nil
}
// makeCertificateFromDisk makes a Certificate by loading the
// certificate and key files. It fills out all the fields in
// the certificate except for the Managed and OnDemand flags.
// (It is up to the caller to set those.)
func makeCertificateFromDisk(certFile, keyFile string) (Certificate, error) {
certPEMBlock, err := ioutil.ReadFile(certFile)
if err != nil {
return Certificate{}, err
}
keyPEMBlock, err := ioutil.ReadFile(keyFile)
if err != nil {
return Certificate{}, err
}
return makeCertificate(certPEMBlock, keyPEMBlock)
}
// makeCertificate turns a certificate PEM bundle and a key PEM block into
// a Certificate, with OCSP and other relevant metadata tagged with it,
// except for the OnDemand and Managed flags. It is up to the caller to
// set those properties.
func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
var cert Certificate
// Convert to a tls.Certificate
tlsCert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return cert, err
}
if len(tlsCert.Certificate) == 0 {
return cert, errors.New("certificate is empty")
}
// Parse leaf certificate and extract relevant metadata
leaf, err := x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
return cert, err
}
if leaf.Subject.CommonName != "" {
cert.Names = []string{strings.ToLower(leaf.Subject.CommonName)}
}
for _, name := range leaf.DNSNames {
if name != leaf.Subject.CommonName {
cert.Names = append(cert.Names, strings.ToLower(name))
}
}
cert.NotAfter = leaf.NotAfter
// Staple OCSP
ocspBytes, ocspResp, err := acme.GetOCSPForCert(certPEMBlock)
if err != nil {
// An error here is not a problem because a certificate may simply
// not contain a link to an OCSP server. But we should log it anyway.
log.Printf("[WARNING] No OCSP stapling for %v: %v", cert.Names, err)
} else if ocspResp.Status == ocsp.Good {
tlsCert.OCSPStaple = ocspBytes
cert.OCSP = ocspResp
}
cert.Certificate = tlsCert
return cert, nil
}
// cacheCertificate adds cert to the in-memory cache. If the cache is
// empty, cert will be used as the default certificate. If the cache is
// full, random entries are deleted until there is room to map all the
// names on the certificate.
//
// This certificate will be keyed to the names in cert.Names. Any name
// that is already a key in the cache will be replaced with this cert.
//
// This function is safe for concurrent use.
func cacheCertificate(cert Certificate) {
certCacheMu.Lock()
if _, ok := certCache[""]; !ok {
// use as default
cert.Names = append(cert.Names, "")
certCache[""] = cert
}
for len(certCache)+len(cert.Names) > 10000 {
// for simplicity, just remove random elements
for key := range certCache {
if key == "" { // ... but not the default cert
continue
}
delete(certCache, key)
break
}
}
for _, name := range cert.Names {
certCache[name] = cert
}
certCacheMu.Unlock()
}
+59
View File
@@ -0,0 +1,59 @@
package https
import "testing"
func TestUnexportedGetCertificate(t *testing.T) {
defer func() { certCache = make(map[string]Certificate) }()
// When cache is empty
if _, matched, defaulted := getCertificate("example.com"); matched || defaulted {
t.Errorf("Got a certificate when cache was empty; matched=%v, defaulted=%v", matched, defaulted)
}
// When cache has one certificate in it (also is default)
defaultCert := Certificate{Names: []string{"example.com", ""}}
certCache[""] = defaultCert
certCache["example.com"] = defaultCert
if cert, matched, defaulted := getCertificate("Example.com"); !matched || defaulted || cert.Names[0] != "example.com" {
t.Errorf("Didn't get a cert for 'Example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
}
if cert, matched, defaulted := getCertificate(""); !matched || defaulted || cert.Names[0] != "example.com" {
t.Errorf("Didn't get a cert for '' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
}
// When retrieving wildcard certificate
certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}}
if cert, matched, defaulted := getCertificate("sub.example.com"); !matched || defaulted || cert.Names[0] != "*.example.com" {
t.Errorf("Didn't get wildcard cert for 'sub.example.com' or got the wrong one: %v, matched=%v, defaulted=%v", cert, matched, defaulted)
}
// When no certificate matches, the default is returned
if cert, matched, defaulted := getCertificate("nomatch"); matched || !defaulted {
t.Errorf("Expected matched=false, defaulted=true; but got matched=%v, defaulted=%v (cert: %v)", matched, defaulted, cert)
} else if cert.Names[0] != "example.com" {
t.Errorf("Expected default cert, got: %v", cert)
}
}
func TestCacheCertificate(t *testing.T) {
defer func() { certCache = make(map[string]Certificate) }()
cacheCertificate(Certificate{Names: []string{"example.com", "sub.example.com"}})
if _, ok := certCache["example.com"]; !ok {
t.Error("Expected first cert to be cached by key 'example.com', but it wasn't")
}
if _, ok := certCache["sub.example.com"]; !ok {
t.Error("Expected first cert to be cached by key 'sub.exmaple.com', but it wasn't")
}
if cert, ok := certCache[""]; !ok || cert.Names[2] != "" {
t.Error("Expected first cert to be cached additionally as the default certificate with empty name added, but it wasn't")
}
cacheCertificate(Certificate{Names: []string{"example2.com"}})
if _, ok := certCache["example2.com"]; !ok {
t.Error("Expected second cert to be cached by key 'exmaple2.com', but it wasn't")
}
if cert, ok := certCache[""]; ok && cert.Names[0] == "example2.com" {
t.Error("Expected second cert to NOT be cached as default, but it was")
}
}
+215
View File
@@ -0,0 +1,215 @@
package https
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"sync"
"time"
"github.com/mholt/caddy/server"
"github.com/xenolf/lego/acme"
)
// acmeMu ensures that only one ACME challenge occurs at a time.
var acmeMu sync.Mutex
// ACMEClient is an acme.Client with custom state attached.
type ACMEClient struct {
*acme.Client
AllowPrompts bool // if false, we assume AlternatePort must be used
}
// NewACMEClient creates a new ACMEClient given an email and whether
// prompting the user is allowed. Clients should not be kept and
// re-used over long periods of time, but immediate re-use is more
// efficient than re-creating on every iteration.
var NewACMEClient = func(email string, allowPrompts bool) (*ACMEClient, error) {
// Look up or create the LE user account
leUser, err := getUser(email)
if err != nil {
return nil, err
}
// The client facilitates our communication with the CA server.
client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse)
if err != nil {
return nil, err
}
// If not registered, the user must register an account with the CA
// and agree to terms
if leUser.Registration == nil {
reg, err := client.Register()
if err != nil {
return nil, errors.New("registration error: " + err.Error())
}
leUser.Registration = reg
if allowPrompts { // can't prompt a user who isn't there
if !Agreed && reg.TosURL == "" {
Agreed = promptUserAgreement(saURL, false) // TODO - latest URL
}
if !Agreed && reg.TosURL == "" {
return nil, errors.New("user must agree to terms")
}
}
err = client.AgreeToTOS()
if err != nil {
saveUser(leUser) // Might as well try, right?
return nil, errors.New("error agreeing to terms: " + err.Error())
}
// save user to the file system
err = saveUser(leUser)
if err != nil {
return nil, errors.New("could not save user: " + err.Error())
}
}
return &ACMEClient{
Client: client,
AllowPrompts: allowPrompts,
}, nil
}
// NewACMEClientGetEmail creates a new ACMEClient and gets an email
// address at the same time (a server config is required, since it
// may contain an email address in it).
func NewACMEClientGetEmail(config server.Config, allowPrompts bool) (*ACMEClient, error) {
return NewACMEClient(getEmail(config, allowPrompts), allowPrompts)
}
// Configure configures c according to bindHost, which is the host (not
// whole address) to bind the listener to in solving the http and tls-sni
// challenges.
func (c *ACMEClient) Configure(bindHost string) {
// If we allow prompts, operator must be present. In our case,
// that is synonymous with saying the server is not already
// started. So if the user is still there, we don't use
// AlternatePort because we don't need to proxy the challenges.
// Conversely, if the operator is not there, the server has
// already started and we need to proxy the challenge.
if c.AllowPrompts {
// Operator is present; server is not already listening
c.SetHTTPAddress(net.JoinHostPort(bindHost, ""))
c.SetTLSAddress(net.JoinHostPort(bindHost, ""))
//c.ExcludeChallenges([]acme.Challenge{acme.DNS01})
} else {
// Operator is not present; server is started, so proxy challenges
c.SetHTTPAddress(net.JoinHostPort(bindHost, AlternatePort))
c.SetTLSAddress(net.JoinHostPort(bindHost, AlternatePort))
//c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
}
c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) // TODO: can we proxy TLS challenges? and we should support DNS...
}
// Obtain obtains a single certificate for names. It stores the certificate
// on the disk if successful.
func (c *ACMEClient) Obtain(names []string) error {
Attempts:
for attempts := 0; attempts < 2; attempts++ {
acmeMu.Lock()
certificate, failures := c.ObtainCertificate(names, true, nil)
acmeMu.Unlock()
if len(failures) > 0 {
// Error - try to fix it or report it to the user and abort
var errMsg string // we'll combine all the failures into a single error message
var promptedForAgreement bool // only prompt user for agreement at most once
for errDomain, obtainErr := range failures {
// TODO: Double-check, will obtainErr ever be nil?
if tosErr, ok := obtainErr.(acme.TOSError); ok {
// Terms of Service agreement error; we can probably deal with this
if !Agreed && !promptedForAgreement && c.AllowPrompts {
Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
promptedForAgreement = true
}
if Agreed || !c.AllowPrompts {
err := c.AgreeToTOS()
if err != nil {
return errors.New("error agreeing to updated terms: " + err.Error())
}
continue Attempts
}
}
// If user did not agree or it was any other kind of error, just append to the list of errors
errMsg += "[" + errDomain + "] failed to get certificate: " + obtainErr.Error() + "\n"
}
return errors.New(errMsg)
}
// Success - immediately save the certificate resource
err := saveCertResource(certificate)
if err != nil {
return fmt.Errorf("error saving assets for %v: %v", names, err)
}
break
}
return nil
}
// Renew renews the managed certificate for name. Right now our storage
// mechanism only supports one name per certificate, so this function only
// accepts one domain as input. It can be easily modified to support SAN
// certificates if, one day, they become desperately needed enough that our
// storage mechanism is upgraded to be more complex to support SAN certs.
//
// Anyway, this function is safe for concurrent use.
func (c *ACMEClient) Renew(name string) error {
// Prepare for renewal (load PEM cert, key, and meta)
certBytes, err := ioutil.ReadFile(storage.SiteCertFile(name))
if err != nil {
return err
}
keyBytes, err := ioutil.ReadFile(storage.SiteKeyFile(name))
if err != nil {
return err
}
metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(name))
if err != nil {
return err
}
var certMeta acme.CertificateResource
err = json.Unmarshal(metaBytes, &certMeta)
certMeta.Certificate = certBytes
certMeta.PrivateKey = keyBytes
// Perform renewal and retry if necessary, but not too many times.
var newCertMeta acme.CertificateResource
var success bool
for attempts := 0; attempts < 2; attempts++ {
acmeMu.Lock()
newCertMeta, err = c.RenewCertificate(certMeta, true)
acmeMu.Unlock()
if err == nil {
success = true
break
}
// If the legal terms changed and need to be agreed to again,
// we can handle that.
if _, ok := err.(acme.TOSError); ok {
err := c.AgreeToTOS()
if err != nil {
return err
}
continue
}
// For any other kind of error, wait 10s and try again.
time.Sleep(10 * time.Second)
}
if !success {
return errors.New("too many renewal attempts; last error: " + err.Error())
}
return saveCertResource(newCertMeta)
}
+31
View File
@@ -0,0 +1,31 @@
package https
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"os"
)
// loadRSAPrivateKey loads a PEM-encoded RSA private key from file.
func loadRSAPrivateKey(file string) (*rsa.PrivateKey, error) {
keyBytes, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
keyBlock, _ := pem.Decode(keyBytes)
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
}
// saveRSAPrivateKey saves a PEM-encoded RSA private key to file.
func saveRSAPrivateKey(key *rsa.PrivateKey, file string) error {
pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
keyOut, err := os.Create(file)
if err != nil {
return err
}
keyOut.Chmod(0600)
defer keyOut.Close()
return pem.Encode(keyOut, &pemKey)
}
+65
View File
@@ -0,0 +1,65 @@
package https
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"os"
"runtime"
"testing"
)
func init() {
rsaKeySizeToUse = 128 // make tests faster; small key size OK for testing
}
func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
keyFile := "test.key"
defer os.Remove(keyFile)
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse)
if err != nil {
t.Fatal(err)
}
// test save
err = saveRSAPrivateKey(privateKey, keyFile)
if err != nil {
t.Fatal("error saving private key:", err)
}
// it doesn't make sense to test file permission on windows
if runtime.GOOS != "windows" {
// get info of the key file
info, err := os.Stat(keyFile)
if err != nil {
t.Fatal("error stating private key:", err)
}
// verify permission of key file is correct
if info.Mode().Perm() != 0600 {
t.Error("Expected key file to have permission 0600, but it wasn't")
}
}
// test load
loadedKey, err := loadRSAPrivateKey(keyFile)
if err != nil {
t.Error("error loading private key:", err)
}
// verify loaded key is correct
if !rsaPrivateKeysSame(privateKey, loadedKey) {
t.Error("Expected key bytes to be the same, but they weren't")
}
}
// rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same.
func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool {
return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b))
}
// rsaPrivateKeyBytes returns the bytes of DER-encoded key.
func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte {
return x509.MarshalPKCS1PrivateKey(key)
}
+42
View File
@@ -0,0 +1,42 @@
package https
import (
"crypto/tls"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
const challengeBasePath = "/.well-known/acme-challenge"
// RequestCallback proxies challenge requests to ACME client if the
// request path starts with challengeBasePath. It returns true if it
// handled the request and no more needs to be done; it returns false
// if this call was a no-op and the request still needs handling.
func RequestCallback(w http.ResponseWriter, r *http.Request) bool {
if strings.HasPrefix(r.URL.Path, challengeBasePath) {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
upstream, err := url.Parse(scheme + "://localhost:" + AlternatePort)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("[ERROR] ACME proxy handler: %v", err)
return true
}
proxy := httputil.NewSingleHostReverseProxy(upstream)
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs
}
proxy.ServeHTTP(w, r)
return true
}
return false
}
+63
View File
@@ -0,0 +1,63 @@
package https
import (
"net"
"net/http"
"net/http/httptest"
"testing"
)
func TestRequestCallbackNoOp(t *testing.T) {
// try base paths that aren't handled by this handler
for _, url := range []string{
"http://localhost/",
"http://localhost/foo.html",
"http://localhost/.git",
"http://localhost/.well-known/",
"http://localhost/.well-known/acme-challenging",
} {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Fatalf("Could not craft request, got error: %v", err)
}
rw := httptest.NewRecorder()
if RequestCallback(rw, req) {
t.Errorf("Got true with this URL, but shouldn't have: %s", url)
}
}
}
func TestRequestCallbackSuccess(t *testing.T) {
expectedPath := challengeBasePath + "/asdf"
// Set up fake acme handler backend to make sure proxying succeeds
var proxySuccess bool
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
proxySuccess = true
if r.URL.Path != expectedPath {
t.Errorf("Expected path '%s' but got '%s' instead", expectedPath, r.URL.Path)
}
}))
// Custom listener that uses the port we expect
ln, err := net.Listen("tcp", "127.0.0.1:"+AlternatePort)
if err != nil {
t.Fatalf("Unable to start test server listener: %v", err)
}
ts.Listener = ln
// Start our engines and run the test
ts.Start()
defer ts.Close()
req, err := http.NewRequest("GET", "http://127.0.0.1:"+AlternatePort+expectedPath, nil)
if err != nil {
t.Fatalf("Could not craft request, got error: %v", err)
}
rw := httptest.NewRecorder()
RequestCallback(rw, req)
if !proxySuccess {
t.Fatal("Expected request to be proxied, but it wasn't")
}
}
+320
View File
@@ -0,0 +1,320 @@
package https
import (
"bytes"
"crypto/tls"
"encoding/pem"
"errors"
"fmt"
"log"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/mholt/caddy/server"
"github.com/xenolf/lego/acme"
)
// GetCertificate gets a certificate to satisfy clientHello as long as
// the certificate is already cached in memory. It will not be loaded
// from disk or obtained from the CA during the handshake.
//
// This function is safe for use as a tls.Config.GetCertificate callback.
func GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := getCertDuringHandshake(clientHello.ServerName, false, false)
return &cert.Certificate, err
}
// GetOrObtainCertificate will get a certificate to satisfy clientHello, even
// if that means obtaining a new certificate from a CA during the handshake.
// It first checks the in-memory cache, then accesses disk, then accesses the
// network if it must. An obtained certificate will be stored on disk and
// cached in memory.
//
// This function is safe for use as a tls.Config.GetCertificate callback.
func GetOrObtainCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := getCertDuringHandshake(clientHello.ServerName, true, true)
return &cert.Certificate, err
}
// getCertDuringHandshake will get a certificate for name. It first tries
// the in-memory cache. If no certificate for name is in the cache and if
// loadIfNecessary == true, it goes to disk to load it into the cache and
// serve it. If it's not on disk and if obtainIfNecessary == true, the
// certificate will be obtained from the CA, cached, and served. If
// obtainIfNecessary is true, then loadIfNecessary must also be set to true.
// An error will be returned if and only if no certificate is available.
//
// This function is safe for concurrent use.
func getCertDuringHandshake(name string, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) {
// First check our in-memory cache to see if we've already loaded it
cert, matched, defaulted := getCertificate(name)
if matched {
return cert, nil
}
if loadIfNecessary {
// Then check to see if we have one on disk
loadedCert, err := cacheManagedCertificate(name, true)
if err == nil {
loadedCert, err = handshakeMaintenance(name, loadedCert)
if err != nil {
log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err)
}
return loadedCert, nil
}
if obtainIfNecessary {
// By this point, we need to ask the CA for a certificate
name = strings.ToLower(name)
// Make sure aren't over any applicable limits
err := checkLimitsForObtainingNewCerts(name)
if err != nil {
return Certificate{}, err
}
// Name has to qualify for a certificate
if !HostQualifies(name) {
return cert, errors.New("hostname '" + name + "' does not qualify for certificate")
}
// Obtain certificate from the CA
return obtainOnDemandCertificate(name)
}
}
if defaulted {
return cert, nil
}
return Certificate{}, errors.New("no certificate for " + name)
}
// checkLimitsForObtainingNewCerts checks to see if name can be issued right
// now according to mitigating factors we keep track of and preferences the
// user has set. If a non-nil error is returned, do not issue a new certificate
// for name.
func checkLimitsForObtainingNewCerts(name string) error {
// User can set hard limit for number of certs for the process to issue
if onDemandMaxIssue > 0 && atomic.LoadInt32(OnDemandIssuedCount) >= onDemandMaxIssue {
return fmt.Errorf("%s: maximum certificates issued (%d)", name, onDemandMaxIssue)
}
// Make sure name hasn't failed a challenge recently
failedIssuanceMu.RLock()
when, ok := failedIssuance[name]
failedIssuanceMu.RUnlock()
if ok {
return fmt.Errorf("%s: throttled; refusing to issue cert since last attempt on %s failed", name, when.String())
}
// Make sure, if we've issued a few certificates already, that we haven't
// issued any recently
lastIssueTimeMu.Lock()
since := time.Since(lastIssueTime)
lastIssueTimeMu.Unlock()
if atomic.LoadInt32(OnDemandIssuedCount) >= 10 && since < 10*time.Minute {
return fmt.Errorf("%s: throttled; last certificate was obtained %v ago", name, since)
}
// 👍Good to go
return nil
}
// obtainOnDemandCertificate obtains a certificate for name for the given
// name. If another goroutine has already started obtaining a cert for
// name, it will wait and use what the other goroutine obtained.
//
// This function is safe for use by multiple concurrent goroutines.
func obtainOnDemandCertificate(name string) (Certificate, error) {
// We must protect this process from happening concurrently, so synchronize.
obtainCertWaitChansMu.Lock()
wait, ok := obtainCertWaitChans[name]
if ok {
// lucky us -- another goroutine is already obtaining the certificate.
// wait for it to finish obtaining the cert and then we'll use it.
obtainCertWaitChansMu.Unlock()
<-wait
return getCertDuringHandshake(name, true, false)
}
// looks like it's up to us to do all the work and obtain the cert
wait = make(chan struct{})
obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock()
// Unblock waiters and delete waitgroup when we return
defer func() {
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
}()
log.Printf("[INFO] Obtaining new certificate for %s", name)
// obtain cert
client, err := NewACMEClientGetEmail(server.Config{}, false)
if err != nil {
return Certificate{}, errors.New("error creating client: " + err.Error())
}
client.Configure("") // TODO: which BindHost?
err = client.Obtain([]string{name})
if err != nil {
// Failed to solve challenge, so don't allow another on-demand
// issue for this name to be attempted for a little while.
failedIssuanceMu.Lock()
failedIssuance[name] = time.Now()
go func(name string) {
time.Sleep(5 * time.Minute)
failedIssuanceMu.Lock()
delete(failedIssuance, name)
failedIssuanceMu.Unlock()
}(name)
failedIssuanceMu.Unlock()
return Certificate{}, err
}
// Success - update counters and stuff
atomic.AddInt32(OnDemandIssuedCount, 1)
lastIssueTimeMu.Lock()
lastIssueTime = time.Now()
lastIssueTimeMu.Unlock()
// The certificate is already on disk; now just start over to load it and serve it
return getCertDuringHandshake(name, true, false)
}
// handshakeMaintenance performs a check on cert for expiration and OCSP
// validity.
//
// This function is safe for use by multiple concurrent goroutines.
func handshakeMaintenance(name string, cert Certificate) (Certificate, error) {
// Check cert expiration
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
if timeLeft < renewDurationBefore {
log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft)
return renewDynamicCertificate(name)
}
// Check OCSP staple validity
if cert.OCSP != nil {
refreshTime := cert.OCSP.ThisUpdate.Add(cert.OCSP.NextUpdate.Sub(cert.OCSP.ThisUpdate) / 2)
if time.Now().After(refreshTime) {
err := stapleOCSP(&cert, nil)
if err != nil {
// An error with OCSP stapling is not the end of the world, and in fact, is
// quite common considering not all certs have issuer URLs that support it.
log.Printf("[ERROR] Getting OCSP for %s: %v", name, err)
}
certCacheMu.Lock()
certCache[name] = cert
certCacheMu.Unlock()
}
}
return cert, nil
}
// renewDynamicCertificate renews currentCert using the clientHello. It returns the
// certificate to use and an error, if any. currentCert may be returned even if an
// error occurs, since we perform renewals before they expire and it may still be
// usable. name should already be lower-cased before calling this function.
//
// This function is safe for use by multiple concurrent goroutines.
func renewDynamicCertificate(name string) (Certificate, error) {
obtainCertWaitChansMu.Lock()
wait, ok := obtainCertWaitChans[name]
if ok {
// lucky us -- another goroutine is already renewing the certificate.
// wait for it to finish, then we'll use the new one.
obtainCertWaitChansMu.Unlock()
<-wait
return getCertDuringHandshake(name, true, false)
}
// looks like it's up to us to do all the work and renew the cert
wait = make(chan struct{})
obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock()
// unblock waiters and delete waitgroup when we return
defer func() {
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
}()
log.Printf("[INFO] Renewing certificate for %s", name)
client, err := NewACMEClientGetEmail(server.Config{}, false)
if err != nil {
return Certificate{}, err
}
client.Configure("") // TODO: Bind address of relevant listener, yuck
err = client.Renew(name)
if err != nil {
return Certificate{}, err
}
return getCertDuringHandshake(name, true, false)
}
// stapleOCSP staples OCSP information to cert for hostname name.
// If you have it handy, you should pass in the PEM-encoded certificate
// bundle; otherwise the DER-encoded cert will have to be PEM-encoded.
// If you don't have the PEM blocks handy, just pass in nil.
//
// Errors here are not necessarily fatal, it could just be that the
// certificate doesn't have an issuer URL.
func stapleOCSP(cert *Certificate, pemBundle []byte) error {
if pemBundle == nil {
// The function in the acme package that gets OCSP requires a PEM-encoded cert
bundle := new(bytes.Buffer)
for _, derBytes := range cert.Certificate.Certificate {
pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
}
pemBundle = bundle.Bytes()
}
ocspBytes, ocspResp, err := acme.GetOCSPForCert(pemBundle)
if err != nil {
return err
}
cert.Certificate.OCSPStaple = ocspBytes
cert.OCSP = ocspResp
return nil
}
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
var obtainCertWaitChans = make(map[string]chan struct{})
var obtainCertWaitChansMu sync.Mutex
// OnDemandIssuedCount is the number of certificates that have been issued
// on-demand by this process. It is only safe to modify this count atomically.
// If it reaches onDemandMaxIssue, on-demand issuances will fail.
var OnDemandIssuedCount = new(int32)
// onDemandMaxIssue is set based on max_certs in tls config. It specifies the
// maximum number of certificates that can be issued.
// TODO: This applies globally, but we should probably make a server-specific
// way to keep track of these limits and counts, since it's specified in the
// Caddyfile...
var onDemandMaxIssue int32
// failedIssuance is a set of names that we recently failed to get a
// certificate for from the ACME CA. They are removed after some time.
// When a name is in this map, do not issue a certificate for it on-demand.
var failedIssuance = make(map[string]time.Time)
var failedIssuanceMu sync.RWMutex
// lastIssueTime records when we last obtained a certificate successfully.
// If this value is recent, do not make any on-demand certificate requests.
var lastIssueTime time.Time
var lastIssueTimeMu sync.Mutex
+54
View File
@@ -0,0 +1,54 @@
package https
import (
"crypto/tls"
"crypto/x509"
"testing"
)
func TestGetCertificate(t *testing.T) {
defer func() { certCache = make(map[string]Certificate) }()
hello := &tls.ClientHelloInfo{ServerName: "example.com"}
helloSub := &tls.ClientHelloInfo{ServerName: "sub.example.com"}
helloNoSNI := &tls.ClientHelloInfo{}
helloNoMatch := &tls.ClientHelloInfo{ServerName: "nomatch"}
// When cache is empty
if cert, err := GetCertificate(hello); err == nil {
t.Errorf("GetCertificate should return error when cache is empty, got: %v", cert)
}
if cert, err := GetCertificate(helloNoSNI); err == nil {
t.Errorf("GetCertificate should return error when cache is empty even if server name is blank, got: %v", cert)
}
// When cache has one certificate in it (also is default)
defaultCert := Certificate{Names: []string{"example.com", ""}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"example.com"}}}}
certCache[""] = defaultCert
certCache["example.com"] = defaultCert
if cert, err := GetCertificate(hello); err != nil {
t.Errorf("Got an error but shouldn't have, when cert exists in cache: %v", err)
} else if cert.Leaf.DNSNames[0] != "example.com" {
t.Errorf("Got wrong certificate with exact match; expected 'example.com', got: %v", cert)
}
if cert, err := GetCertificate(helloNoSNI); err != nil {
t.Errorf("Got an error with no SNI but shouldn't have, when cert exists in cache: %v", err)
} else if cert.Leaf.DNSNames[0] != "example.com" {
t.Errorf("Got wrong certificate for no SNI; expected 'example.com' as default, got: %v", cert)
}
// When retrieving wildcard certificate
certCache["*.example.com"] = Certificate{Names: []string{"*.example.com"}, Certificate: tls.Certificate{Leaf: &x509.Certificate{DNSNames: []string{"*.example.com"}}}}
if cert, err := GetCertificate(helloSub); err != nil {
t.Errorf("Didn't get wildcard cert, got: cert=%v, err=%v ", cert, err)
} else if cert.Leaf.DNSNames[0] != "*.example.com" {
t.Errorf("Got wrong certificate, expected wildcard: %v", cert)
}
// When no certificate matches, the default is returned
if cert, err := GetCertificate(helloNoMatch); err != nil {
t.Errorf("Expected default certificate with no error when no matches, got err: %v", err)
} else if cert.Leaf.DNSNames[0] != "example.com" {
t.Errorf("Expected default cert with no matches, got: %v", cert)
}
}
+422
View File
@@ -0,0 +1,422 @@
// Package https facilitates the management of TLS assets and integrates
// Let's Encrypt functionality into Caddy with first-class support for
// creating and renewing certificates automatically. It is designed to
// configure sites for HTTPS by default.
package https
import (
"encoding/json"
"errors"
"io/ioutil"
"net"
"net/http"
"os"
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/redirect"
"github.com/mholt/caddy/server"
"github.com/xenolf/lego/acme"
)
// Activate sets up TLS for each server config in configs
// as needed; this consists of acquiring and maintaining
// certificates and keys for qualifying configs and enabling
// OCSP stapling for all TLS-enabled configs.
//
// This function may prompt the user to provide an email
// address if none is available through other means. It
// prefers the email address specified in the config, but
// if that is not available it will check the command line
// argument. If absent, it will use the most recent email
// address from last time. If there isn't one, the user
// will be prompted and shown SA link.
//
// Also note that calling this function activates asset
// management automatically, which keeps certificates
// renewed and OCSP stapling updated.
//
// Activate returns the updated list of configs, since
// some may have been appended, for example, to redirect
// plaintext HTTP requests to their HTTPS counterpart.
// This function only appends; it does not splice.
func Activate(configs []server.Config) ([]server.Config, error) {
// just in case previous caller forgot...
Deactivate()
// pre-screen each config and earmark the ones that qualify for managed TLS
MarkQualified(configs)
// place certificates and keys on disk
err := ObtainCerts(configs, true, false)
if err != nil {
return configs, err
}
// update TLS configurations
err = EnableTLS(configs, true)
if err != nil {
return configs, err
}
// set up redirects
configs = MakePlaintextRedirects(configs)
// renew all relevant certificates that need renewal. this is important
// to do right away for a couple reasons, mainly because each restart,
// the renewal ticker is reset, so if restarts happen more often than
// the ticker interval, renewals would never happen. but doing
// it right away at start guarantees that renewals aren't missed.
err = renewManagedCertificates(true)
if err != nil {
return configs, err
}
// keep certificates renewed and OCSP stapling updated
go maintainAssets(stopChan)
return configs, nil
}
// Deactivate cleans up long-term, in-memory resources
// allocated by calling Activate(). Essentially, it stops
// the asset maintainer from running, meaning that certificates
// will not be renewed, OCSP staples will not be updated, etc.
func Deactivate() (err error) {
defer func() {
if rec := recover(); rec != nil {
err = errors.New("already deactivated")
}
}()
close(stopChan)
stopChan = make(chan struct{})
return
}
// MarkQualified scans each config and, if it qualifies for managed
// TLS, it sets the Managed field of the TLSConfig to true.
func MarkQualified(configs []server.Config) {
for i := 0; i < len(configs); i++ {
if ConfigQualifies(configs[i]) {
configs[i].TLS.Managed = true
}
}
}
// ObtainCerts obtains certificates for all these configs as long as a
// certificate does not already exist on disk. It does not modify the
// configs at all; it only obtains and stores certificates and keys to
// the disk. If allowPrompts is true, the user may be shown a prompt.
// If proxyACME is true, the ACME challenges will be proxied to our alt port.
func ObtainCerts(configs []server.Config, allowPrompts, proxyACME bool) error {
// We group configs by email so we don't make the same clients over and
// over. This has the potential to prompt the user for an email, but we
// prevent that by assuming that if we already have a listener that can
// proxy ACME challenge requests, then the server is already running and
// the operator is no longer present.
groupedConfigs := groupConfigsByEmail(configs, allowPrompts)
for email, group := range groupedConfigs {
// Wait as long as we can before creating the client, because it
// may not be needed, for example, if we already have what we
// need on disk. Creating a client involves the network and
// potentially prompting the user, etc., so only do if necessary.
var client *ACMEClient
for _, cfg := range group {
if cfg.Host == "" || existingCertAndKey(cfg.Host) {
continue
}
// Now we definitely do need a client
if client == nil {
var err error
client, err = NewACMEClient(email, allowPrompts)
if err != nil {
return errors.New("error creating client: " + err.Error())
}
}
// c.Configure assumes that allowPrompts == !proxyACME,
// but that's not always true. For example, a restart where
// the user isn't present and we're not listening on port 80.
// TODO: This could probably be refactored better.
if proxyACME {
client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
} else {
client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, ""))
client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, ""))
client.ExcludeChallenges([]acme.Challenge{acme.DNS01})
}
err := client.Obtain([]string{cfg.Host})
if err != nil {
return err
}
}
}
return nil
}
// groupConfigsByEmail groups configs by the email address to be used by an
// ACME client. It only groups configs that have TLS enabled and that are
// marked as Managed. If userPresent is true, the operator MAY be prompted
// for an email address.
func groupConfigsByEmail(configs []server.Config, userPresent bool) map[string][]server.Config {
initMap := make(map[string][]server.Config)
for _, cfg := range configs {
if !cfg.TLS.Managed {
continue
}
leEmail := getEmail(cfg, userPresent)
initMap[leEmail] = append(initMap[leEmail], cfg)
}
return initMap
}
// EnableTLS configures each config to use TLS according to default settings.
// It will only change configs that are marked as managed, and assumes that
// certificates and keys are already on disk. If loadCertificates is true,
// the certificates will be loaded from disk into the cache for this process
// to use. If false, TLS will still be enabled and configured with default
// settings, but no certificates will be parsed loaded into the cache, and
// the returned error value will always be nil.
func EnableTLS(configs []server.Config, loadCertificates bool) error {
for i := 0; i < len(configs); i++ {
if !configs[i].TLS.Managed {
continue
}
configs[i].TLS.Enabled = true
if loadCertificates && configs[i].Host != "" {
_, err := cacheManagedCertificate(configs[i].Host, false)
if err != nil {
return err
}
}
setDefaultTLSParams(&configs[i])
}
return nil
}
// hostHasOtherPort returns true if there is another config in the list with the same
// hostname that has port otherPort, or false otherwise. All the configs are checked
// against the hostname of allConfigs[thisConfigIdx].
func hostHasOtherPort(allConfigs []server.Config, thisConfigIdx int, otherPort string) bool {
for i, otherCfg := range allConfigs {
if i == thisConfigIdx {
continue // has to be a config OTHER than the one we're comparing against
}
if otherCfg.Host == allConfigs[thisConfigIdx].Host && otherCfg.Port == otherPort {
return true
}
}
return false
}
// MakePlaintextRedirects sets up redirects from port 80 to the relevant HTTPS
// hosts. You must pass in all configs, not just configs that qualify, since
// we must know whether the same host already exists on port 80, and those would
// not be in a list of configs that qualify for automatic HTTPS. This function will
// only set up redirects for configs that qualify. It returns the updated list of
// all configs.
func MakePlaintextRedirects(allConfigs []server.Config) []server.Config {
for i, cfg := range allConfigs {
if cfg.TLS.Managed &&
!hostHasOtherPort(allConfigs, i, "80") &&
(cfg.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) {
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
}
}
return allConfigs
}
// ConfigQualifies returns true if cfg qualifies for
// fully managed TLS (but not on-demand TLS, which is
// not considered here). It does NOT check to see if a
// cert and key already exist for the config. If the
// config does qualify, you should set cfg.TLS.Managed
// to true and check that instead, because the process of
// setting up the config may make it look like it
// doesn't qualify even though it originally did.
func ConfigQualifies(cfg server.Config) bool {
return (!cfg.TLS.Manual || cfg.TLS.OnDemand) && // user might provide own cert and key
// user can force-disable automatic HTTPS for this host
cfg.Scheme != "http" &&
cfg.Port != "80" &&
cfg.TLS.LetsEncryptEmail != "off" &&
// we get can't certs for some kinds of hostnames, but
// on-demand TLS allows empty hostnames at startup
(HostQualifies(cfg.Host) || cfg.TLS.OnDemand)
}
// HostQualifies returns true if the hostname alone
// appears eligible for automatic HTTPS. For example,
// localhost, empty hostname, and IP addresses are
// not eligible because we cannot obtain certificates
// for those names.
func HostQualifies(hostname string) bool {
return hostname != "localhost" && // localhost is ineligible
// hostname must not be empty
strings.TrimSpace(hostname) != "" &&
// cannot be an IP address, see
// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
// (also trim [] from either end, since that special case can sneak through
// for IPv6 addresses using the -host flag and with empty/no Caddyfile)
net.ParseIP(strings.Trim(hostname, "[]")) == nil
}
// existingCertAndKey returns true if the host has a certificate
// and private key in storage already, false otherwise.
func existingCertAndKey(host string) bool {
_, err := os.Stat(storage.SiteCertFile(host))
if err != nil {
return false
}
_, err = os.Stat(storage.SiteKeyFile(host))
if err != nil {
return false
}
return true
}
// saveCertResource saves the certificate resource to disk. This
// includes the certificate file itself, the private key, and the
// metadata file.
func saveCertResource(cert acme.CertificateResource) error {
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
if err != nil {
return err
}
// Save cert
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
if err != nil {
return err
}
// Save private key
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
if err != nil {
return err
}
// Save cert metadata
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
if err != nil {
return err
}
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
if err != nil {
return err
}
return nil
}
// redirPlaintextHost returns a new plaintext HTTP configuration for
// a virtualHost that simply redirects to cfg, which is assumed to
// be the HTTPS configuration. The returned configuration is set
// to listen on port 80.
func redirPlaintextHost(cfg server.Config) server.Config {
toURL := "https://{host}" // serve any host, since cfg.Host could be empty
if cfg.Port != "443" && cfg.Port != "80" {
toURL += ":" + cfg.Port
}
redirMidware := func(next middleware.Handler) middleware.Handler {
return redirect.Redirect{Next: next, Rules: []redirect.Rule{
{
FromScheme: "http",
FromPath: "/",
To: toURL + "{uri}",
Code: http.StatusMovedPermanently,
},
}}
}
return server.Config{
Host: cfg.Host,
BindHost: cfg.BindHost,
Port: "80",
Middleware: []middleware.Middleware{redirMidware},
}
}
// Revoke revokes the certificate for host via ACME protocol.
func Revoke(host string) error {
if !existingCertAndKey(host) {
return errors.New("no certificate and key for " + host)
}
email := getEmail(server.Config{Host: host}, true)
if email == "" {
return errors.New("email is required to revoke")
}
client, err := NewACMEClient(email, true)
if err != nil {
return err
}
certFile := storage.SiteCertFile(host)
certBytes, err := ioutil.ReadFile(certFile)
if err != nil {
return err
}
err = client.RevokeCertificate(certBytes)
if err != nil {
return err
}
err = os.Remove(certFile)
if err != nil {
return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error())
}
return nil
}
var (
// DefaultEmail represents the Let's Encrypt account email to use if none provided
DefaultEmail string
// Agreed indicates whether user has agreed to the Let's Encrypt SA
Agreed bool
// CAUrl represents the base URL to the CA's ACME endpoint
CAUrl string
)
// AlternatePort is the port on which the acme client will open a
// listener and solve the CA's challenges. If this alternate port
// is used instead of the default port (80 or 443), then the
// default port for the challenge must be forwarded to this one.
const AlternatePort = "5033"
// KeySize represents the length of a key in bits.
type KeySize int
// Key sizes are used to determine the strength of a key.
const (
Ecc224 KeySize = 224
Ecc256 = 256
Rsa2048 = 2048
Rsa4096 = 4096
)
// rsaKeySizeToUse is the size to use for new RSA keys.
// This shouldn't need to change except for in tests;
// the size can be drastically reduced for speed.
var rsaKeySizeToUse = Rsa2048
// stopChan is used to signal the maintenance goroutine
// to terminate.
var stopChan chan struct{}
+332
View File
@@ -0,0 +1,332 @@
package https
import (
"io/ioutil"
"net/http"
"os"
"testing"
"github.com/mholt/caddy/middleware/redirect"
"github.com/mholt/caddy/server"
"github.com/xenolf/lego/acme"
)
func TestHostQualifies(t *testing.T) {
for i, test := range []struct {
host string
expect bool
}{
{"localhost", false},
{"127.0.0.1", false},
{"127.0.1.5", false},
{"::1", false},
{"[::1]", false},
{"[::]", false},
{"::", false},
{"", false},
{" ", false},
{"0.0.0.0", false},
{"192.168.1.3", false},
{"10.0.2.1", false},
{"169.112.53.4", false},
{"foobar.com", true},
{"sub.foobar.com", true},
} {
if HostQualifies(test.host) && !test.expect {
t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
}
if !HostQualifies(test.host) && test.expect {
t.Errorf("Test %d: Expected '%s' to qualify, but it did NOT", i, test.host)
}
}
}
func TestConfigQualifies(t *testing.T) {
for i, test := range []struct {
cfg server.Config
expect bool
}{
{server.Config{Host: ""}, false},
{server.Config{Host: "localhost"}, false},
{server.Config{Host: "123.44.3.21"}, false},
{server.Config{Host: "example.com"}, true},
{server.Config{Host: "example.com", TLS: server.TLSConfig{Manual: true}}, false},
{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}}, false},
{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}}, true},
{server.Config{Host: "example.com", Scheme: "http"}, false},
{server.Config{Host: "example.com", Port: "80"}, false},
{server.Config{Host: "example.com", Port: "1234"}, true},
{server.Config{Host: "example.com", Scheme: "https"}, true},
{server.Config{Host: "example.com", Port: "80", Scheme: "https"}, false},
} {
if test.expect && !ConfigQualifies(test.cfg) {
t.Errorf("Test %d: Expected config to qualify, but it did NOT: %#v", i, test.cfg)
}
if !test.expect && ConfigQualifies(test.cfg) {
t.Errorf("Test %d: Expected config to NOT qualify, but it did: %#v", i, test.cfg)
}
}
}
func TestRedirPlaintextHost(t *testing.T) {
cfg := redirPlaintextHost(server.Config{
Host: "example.com",
BindHost: "93.184.216.34",
Port: "1234",
})
// Check host and port
if actual, expected := cfg.Host, "example.com"; actual != expected {
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
}
if actual, expected := cfg.BindHost, "93.184.216.34"; actual != expected {
t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual)
}
if actual, expected := cfg.Port, "80"; actual != expected {
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
}
// Make sure redirect handler is set up properly
if cfg.Middleware == nil || len(cfg.Middleware) != 1 {
t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.Middleware)
}
handler, ok := cfg.Middleware[0](nil).(redirect.Redirect)
if !ok {
t.Fatalf("Expected a redirect.Redirect middleware, but got: %#v", handler)
}
if len(handler.Rules) != 1 {
t.Fatalf("Expected one redirect rule, got: %#v", handler.Rules)
}
// Check redirect rule for correctness
if actual, expected := handler.Rules[0].FromScheme, "http"; actual != expected {
t.Errorf("Expected redirect rule to be from scheme '%s' but is actually from '%s'", expected, actual)
}
if actual, expected := handler.Rules[0].FromPath, "/"; actual != expected {
t.Errorf("Expected redirect rule to be for path '%s' but is actually for '%s'", expected, actual)
}
if actual, expected := handler.Rules[0].To, "https://{host}:1234{uri}"; actual != expected {
t.Errorf("Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
}
if actual, expected := handler.Rules[0].Code, http.StatusMovedPermanently; actual != expected {
t.Errorf("Expected redirect rule to have code %d but was %d", expected, actual)
}
// browsers can infer a default port from scheme, so make sure the port
// doesn't get added in explicitly for default ports like 443 for https.
cfg = redirPlaintextHost(server.Config{Host: "example.com", Port: "443"})
handler, ok = cfg.Middleware[0](nil).(redirect.Redirect)
if actual, expected := handler.Rules[0].To, "https://{host}{uri}"; actual != expected {
t.Errorf("(Default Port) Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
}
}
func TestSaveCertResource(t *testing.T) {
storage = Storage("./le_test_save")
defer func() {
err := os.RemoveAll(string(storage))
if err != nil {
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
}
}()
domain := "example.com"
certContents := "certificate"
keyContents := "private key"
metaContents := `{
"domain": "example.com",
"certUrl": "https://example.com/cert",
"certStableUrl": "https://example.com/cert/stable"
}`
cert := acme.CertificateResource{
Domain: domain,
CertURL: "https://example.com/cert",
CertStableURL: "https://example.com/cert/stable",
PrivateKey: []byte(keyContents),
Certificate: []byte(certContents),
}
err := saveCertResource(cert)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
certFile, err := ioutil.ReadFile(storage.SiteCertFile(domain))
if err != nil {
t.Errorf("Expected no error reading certificate file, got: %v", err)
}
if string(certFile) != certContents {
t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(certFile))
}
keyFile, err := ioutil.ReadFile(storage.SiteKeyFile(domain))
if err != nil {
t.Errorf("Expected no error reading private key file, got: %v", err)
}
if string(keyFile) != keyContents {
t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(keyFile))
}
metaFile, err := ioutil.ReadFile(storage.SiteMetaFile(domain))
if err != nil {
t.Errorf("Expected no error reading meta file, got: %v", err)
}
if string(metaFile) != metaContents {
t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(metaFile))
}
}
func TestExistingCertAndKey(t *testing.T) {
storage = Storage("./le_test_existing")
defer func() {
err := os.RemoveAll(string(storage))
if err != nil {
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
}
}()
domain := "example.com"
if existingCertAndKey(domain) {
t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain)
}
err := saveCertResource(acme.CertificateResource{
Domain: domain,
PrivateKey: []byte("key"),
Certificate: []byte("cert"),
})
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if !existingCertAndKey(domain) {
t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain)
}
}
func TestHostHasOtherPort(t *testing.T) {
configs := []server.Config{
{Host: "example.com", Port: "80"},
{Host: "sub1.example.com", Port: "80"},
{Host: "sub1.example.com", Port: "443"},
}
if hostHasOtherPort(configs, 0, "80") {
t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`)
}
if hostHasOtherPort(configs, 0, "443") {
t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`)
}
if !hostHasOtherPort(configs, 1, "443") {
t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`)
}
}
func TestMakePlaintextRedirects(t *testing.T) {
configs := []server.Config{
// Happy path = standard redirect from 80 to 443
{Host: "example.com", TLS: server.TLSConfig{Managed: true}},
// Host on port 80 already defined; don't change it (no redirect)
{Host: "sub1.example.com", Port: "80", Scheme: "http"},
{Host: "sub1.example.com", TLS: server.TLSConfig{Managed: true}},
// Redirect from port 80 to port 5000 in this case
{Host: "sub2.example.com", Port: "5000", TLS: server.TLSConfig{Managed: true}},
// Can redirect from 80 to either 443 or 5001, but choose 443
{Host: "sub3.example.com", Port: "443", TLS: server.TLSConfig{Managed: true}},
{Host: "sub3.example.com", Port: "5001", Scheme: "https", TLS: server.TLSConfig{Managed: true}},
}
result := MakePlaintextRedirects(configs)
expectedRedirCount := 3
if len(result) != len(configs)+expectedRedirCount {
t.Errorf("Expected %d redirect(s) to be added, but got %d",
expectedRedirCount, len(result)-len(configs))
}
}
func TestEnableTLS(t *testing.T) {
configs := []server.Config{
{Host: "example.com", TLS: server.TLSConfig{Managed: true}},
{}, // not managed - no changes!
}
EnableTLS(configs, false)
if !configs[0].TLS.Enabled {
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
}
if configs[1].TLS.Enabled {
t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true")
}
}
func TestGroupConfigsByEmail(t *testing.T) {
if groupConfigsByEmail([]server.Config{}, false) == nil {
t.Errorf("With empty input, returned map was nil, but expected non-nil map")
}
configs := []server.Config{
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
{Host: "sub1.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
{Host: "sub2.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
{Host: "sub3.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
{Host: "sub4.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
{Host: "sub5.example.com", TLS: server.TLSConfig{LetsEncryptEmail: ""}}, // not managed
}
DefaultEmail = "test@example.com"
groups := groupConfigsByEmail(configs, true)
if groups == nil {
t.Fatalf("Returned map was nil, but expected values")
}
if len(groups) != 2 {
t.Errorf("Expected 2 groups, got %d: %#v", len(groups), groups)
}
if len(groups["foo@bar"]) != 2 {
t.Errorf("Expected 2 configs for foo@bar, got %d: %#v", len(groups["foobar"]), groups["foobar"])
}
if len(groups[DefaultEmail]) != 3 {
t.Errorf("Expected 3 configs for %s, got %d: %#v", DefaultEmail, len(groups["foobar"]), groups["foobar"])
}
}
func TestMarkQualified(t *testing.T) {
// TODO: TestConfigQualifies and this test share the same config list...
configs := []server.Config{
{Host: ""},
{Host: "localhost"},
{Host: "123.44.3.21"},
{Host: "example.com"},
{Host: "example.com", TLS: server.TLSConfig{Manual: true}},
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}},
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}},
{Host: "example.com", Scheme: "http"},
{Host: "example.com", Port: "80"},
{Host: "example.com", Port: "1234"},
{Host: "example.com", Scheme: "https"},
{Host: "example.com", Port: "80", Scheme: "https"},
}
expectedManagedCount := 4
MarkQualified(configs)
count := 0
for _, cfg := range configs {
if cfg.TLS.Managed {
count++
}
}
if count != expectedManagedCount {
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
}
}
+206
View File
@@ -0,0 +1,206 @@
package https
import (
"log"
"time"
"github.com/mholt/caddy/server"
"golang.org/x/crypto/ocsp"
)
const (
// RenewInterval is how often to check certificates for renewal.
RenewInterval = 12 * time.Hour
// OCSPInterval is how often to check if OCSP stapling needs updating.
OCSPInterval = 1 * time.Hour
)
// maintainAssets is a permanently-blocking function
// that loops indefinitely and, on a regular schedule, checks
// certificates for expiration and initiates a renewal of certs
// that are expiring soon. It also updates OCSP stapling and
// performs other maintenance of assets.
//
// You must pass in the channel which you'll close when
// maintenance should stop, to allow this goroutine to clean up
// after itself and unblock.
func maintainAssets(stopChan chan struct{}) {
renewalTicker := time.NewTicker(RenewInterval)
ocspTicker := time.NewTicker(OCSPInterval)
for {
select {
case <-renewalTicker.C:
log.Println("[INFO] Scanning for expiring certificates")
renewManagedCertificates(false)
log.Println("[INFO] Done checking certificates")
case <-ocspTicker.C:
log.Println("[INFO] Scanning for stale OCSP staples")
updateOCSPStaples()
log.Println("[INFO] Done checking OCSP staples")
case <-stopChan:
renewalTicker.Stop()
ocspTicker.Stop()
log.Println("[INFO] Stopped background maintenance routine")
return
}
}
}
func renewManagedCertificates(allowPrompts bool) (err error) {
var renewed, deleted []Certificate
var client *ACMEClient
visitedNames := make(map[string]struct{})
certCacheMu.RLock()
for name, cert := range certCache {
if !cert.Managed {
continue
}
// the list of names on this cert should never be empty...
if cert.Names == nil || len(cert.Names) == 0 {
log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v", name, cert.Names)
deleted = append(deleted, cert)
continue
}
// skip names whose certificate we've already renewed
if _, ok := visitedNames[name]; ok {
continue
}
for _, name := range cert.Names {
visitedNames[name] = struct{}{}
}
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
if timeLeft < renewDurationBefore {
log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft)
if client == nil {
client, err = NewACMEClientGetEmail(server.Config{}, allowPrompts)
if err != nil {
return err
}
client.Configure("") // TODO: Bind address of relevant listener, yuck
}
err := client.Renew(cert.Names[0]) // managed certs better have only one name
if err != nil {
if client.AllowPrompts {
// User is present, so stop immediately and report the error
certCacheMu.RUnlock()
return err
}
log.Printf("[ERROR] %v", err)
if cert.OnDemand {
deleted = append(deleted, cert)
}
} else {
renewed = append(renewed, cert)
}
}
}
certCacheMu.RUnlock()
// Apply changes to the cache
for _, cert := range renewed {
_, err := cacheManagedCertificate(cert.Names[0], cert.OnDemand)
if err != nil {
if client.AllowPrompts {
return err // operator is present, so report error immediately
}
log.Printf("[ERROR] %v", err)
}
}
for _, cert := range deleted {
certCacheMu.Lock()
for _, name := range cert.Names {
delete(certCache, name)
}
certCacheMu.Unlock()
}
return nil
}
func updateOCSPStaples() {
// Create a temporary place to store updates
// until we release the potentially long-lived
// read lock and use a short-lived write lock.
type ocspUpdate struct {
rawBytes []byte
parsed *ocsp.Response
}
updated := make(map[string]ocspUpdate)
// A single SAN certificate maps to multiple names, so we use this
// set to make sure we don't waste cycles checking OCSP for the same
// certificate multiple times.
visited := make(map[string]struct{})
certCacheMu.RLock()
for name, cert := range certCache {
// skip this certificate if we've already visited it,
// and if not, mark all the names as visited
if _, ok := visited[name]; ok {
continue
}
for _, n := range cert.Names {
visited[n] = struct{}{}
}
// no point in updating OCSP for expired certificates
if time.Now().After(cert.NotAfter) {
continue
}
var lastNextUpdate time.Time
if cert.OCSP != nil {
// start checking OCSP staple about halfway through validity period for good measure
lastNextUpdate = cert.OCSP.NextUpdate
refreshTime := cert.OCSP.ThisUpdate.Add(lastNextUpdate.Sub(cert.OCSP.ThisUpdate) / 2)
// since OCSP is already stapled, we need only check if we're in that "refresh window"
if time.Now().Before(refreshTime) {
continue
}
}
err := stapleOCSP(&cert, nil)
if err != nil {
if cert.OCSP != nil {
// if it was no staple before, that's fine, otherwise we should log the error
log.Printf("[ERROR] Checking OCSP for %s: %v", name, err)
}
continue
}
// By this point, we've obtained the latest OCSP response.
// If there was no staple before, or if the response is updated, make
// sure we apply the update to all names on the certificate.
if lastNextUpdate.IsZero() || lastNextUpdate != cert.OCSP.NextUpdate {
log.Printf("[INFO] Advancing OCSP staple for %v from %s to %s",
cert.Names, lastNextUpdate, cert.OCSP.NextUpdate)
for _, n := range cert.Names {
updated[n] = ocspUpdate{rawBytes: cert.Certificate.OCSPStaple, parsed: cert.OCSP}
}
}
}
certCacheMu.RUnlock()
// This write lock should be brief since we have all the info we need now.
certCacheMu.Lock()
for name, update := range updated {
cert := certCache[name]
cert.OCSP = update.parsed
cert.Certificate.OCSPStaple = update.rawBytes
certCache[name] = cert
}
certCacheMu.Unlock()
}
// renewDurationBefore is how long before expiration to renew certificates.
const renewDurationBefore = (24 * time.Hour) * 30
+318
View File
@@ -0,0 +1,318 @@
package https
import (
"bytes"
"crypto/tls"
"encoding/pem"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/server"
)
// Setup sets up the TLS configuration and installs certificates that
// are specified by the user in the config file. All the automatic HTTPS
// stuff comes later outside of this function.
func Setup(c *setup.Controller) (middleware.Middleware, error) {
if c.Port == "80" || c.Scheme == "http" {
c.TLS.Enabled = false
log.Printf("[WARNING] TLS disabled for %s://%s.", c.Scheme, c.Address())
return nil, nil
}
c.TLS.Enabled = true
for c.Next() {
var certificateFile, keyFile, loadDir, maxCerts string
args := c.RemainingArgs()
switch len(args) {
case 1:
c.TLS.LetsEncryptEmail = args[0]
// user can force-disable managed TLS this way
if c.TLS.LetsEncryptEmail == "off" {
c.TLS.Enabled = false
return nil, nil
}
case 2:
certificateFile = args[0]
keyFile = args[1]
c.TLS.Manual = true
}
// Optional block with extra parameters
var hadBlock bool
for c.NextBlock() {
hadBlock = true
switch c.Val() {
case "protocols":
args := c.RemainingArgs()
if len(args) != 2 {
return nil, c.ArgErr()
}
value, ok := supportedProtocols[strings.ToLower(args[0])]
if !ok {
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
}
c.TLS.ProtocolMinVersion = value
value, ok = supportedProtocols[strings.ToLower(args[1])]
if !ok {
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
}
c.TLS.ProtocolMaxVersion = value
case "ciphers":
for c.NextArg() {
value, ok := supportedCiphersMap[strings.ToUpper(c.Val())]
if !ok {
return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val())
}
c.TLS.Ciphers = append(c.TLS.Ciphers, value)
}
case "clients":
c.TLS.ClientCerts = c.RemainingArgs()
if len(c.TLS.ClientCerts) == 0 {
return nil, c.ArgErr()
}
case "load":
c.Args(&loadDir)
c.TLS.Manual = true
case "max_certs":
c.Args(&maxCerts)
c.TLS.OnDemand = true
default:
return nil, c.Errf("Unknown keyword '%s'", c.Val())
}
}
// tls requires at least one argument if a block is not opened
if len(args) == 0 && !hadBlock {
return nil, c.ArgErr()
}
// set certificate limit if on-demand TLS is enabled
if maxCerts != "" {
maxCertsNum, err := strconv.Atoi(maxCerts)
if err != nil || maxCertsNum < 1 {
return nil, c.Err("max_certs must be a positive integer")
}
if onDemandMaxIssue == 0 || int32(maxCertsNum) < onDemandMaxIssue { // keep the minimum; TODO: We have to do this because it is global; should be per-server or per-vhost...
onDemandMaxIssue = int32(maxCertsNum)
}
}
// don't try to load certificates unless we're supposed to
if !c.TLS.Enabled || !c.TLS.Manual {
continue
}
// load a single certificate and key, if specified
if certificateFile != "" && keyFile != "" {
err := cacheUnmanagedCertificatePEMFile(certificateFile, keyFile)
if err != nil {
return nil, c.Errf("Unable to load certificate and key files for %s: %v", c.Host, err)
}
log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile)
}
// load a directory of certificates, if specified
if loadDir != "" {
err := loadCertsInDir(c, loadDir)
if err != nil {
return nil, err
}
}
}
setDefaultTLSParams(c.Config)
return nil, nil
}
// loadCertsInDir loads all the certificates/keys in dir, as long as
// the file ends with .pem. This method of loading certificates is
// modeled after haproxy, which expects the certificate and key to
// be bundled into the same file:
// https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#5.1-crt
//
// This function may write to the log as it walks the directory tree.
func loadCertsInDir(c *setup.Controller, dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Printf("[WARNING] Unable to traverse into %s; skipping", path)
return nil
}
if info.IsDir() {
return nil
}
if strings.HasSuffix(strings.ToLower(info.Name()), ".pem") {
certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer)
var foundKey bool // use only the first key in the file
bundle, err := ioutil.ReadFile(path)
if err != nil {
return err
}
for {
// Decode next block so we can see what type it is
var derBlock *pem.Block
derBlock, bundle = pem.Decode(bundle)
if derBlock == nil {
break
}
if derBlock.Type == "CERTIFICATE" {
// Re-encode certificate as PEM, appending to certificate chain
pem.Encode(certBuilder, derBlock)
} else if derBlock.Type == "EC PARAMETERS" {
// EC keys generated from openssl can be composed of two blocks:
// parameters and key (parameter block should come first)
if !foundKey {
// Encode parameters
pem.Encode(keyBuilder, derBlock)
// Key must immediately follow
derBlock, bundle = pem.Decode(bundle)
if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" {
return c.Errf("%s: expected elliptic private key to immediately follow EC parameters", path)
}
pem.Encode(keyBuilder, derBlock)
foundKey = true
}
} else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") {
// RSA key
if !foundKey {
pem.Encode(keyBuilder, derBlock)
foundKey = true
}
} else {
return c.Errf("%s: unrecognized PEM block type: %s", path, derBlock.Type)
}
}
certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes()
if len(certPEMBytes) == 0 {
return c.Errf("%s: failed to parse PEM data", path)
}
if len(keyPEMBytes) == 0 {
return c.Errf("%s: no private key block found", path)
}
err = cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes)
if err != nil {
return c.Errf("%s: failed to load cert and key for %s: %v", path, c.Host, err)
}
log.Printf("[INFO] Successfully loaded TLS assets from %s", path)
}
return nil
})
}
// setDefaultTLSParams sets the default TLS cipher suites, protocol versions,
// and server preferences of a server.Config if they were not previously set
// (it does not overwrite; only fills in missing values). It will also set the
// port to 443 if not already set, TLS is enabled, TLS is manual, and the host
// does not equal localhost.
func setDefaultTLSParams(c *server.Config) {
// If no ciphers provided, use default list
if len(c.TLS.Ciphers) == 0 {
c.TLS.Ciphers = defaultCiphers
}
// Not a cipher suite, but still important for mitigating protocol downgrade attacks
// (prepend since having it at end breaks http2 due to non-h2-approved suites before it)
c.TLS.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, c.TLS.Ciphers...)
// Set default protocol min and max versions - must balance compatibility and security
if c.TLS.ProtocolMinVersion == 0 {
c.TLS.ProtocolMinVersion = tls.VersionTLS10
}
if c.TLS.ProtocolMaxVersion == 0 {
c.TLS.ProtocolMaxVersion = tls.VersionTLS12
}
// Prefer server cipher suites
c.TLS.PreferServerCipherSuites = true
// Default TLS port is 443; only use if port is not manually specified,
// TLS is enabled, and the host is not localhost
if c.Port == "" && c.TLS.Enabled && (!c.TLS.Manual || c.TLS.OnDemand) && c.Host != "localhost" {
c.Port = "443"
}
}
// Map of supported protocols.
// SSLv3 will be not supported in future release.
// HTTP/2 only supports TLS 1.2 and higher.
var supportedProtocols = map[string]uint16{
"ssl3.0": tls.VersionSSL30,
"tls1.0": tls.VersionTLS10,
"tls1.1": tls.VersionTLS11,
"tls1.2": tls.VersionTLS12,
}
// Map of supported ciphers, used only for parsing config.
//
// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites,
// including all but two of the suites below (the two GCM suites).
// See https://http2.github.io/http2-spec/#BadCipherSuites
//
// TLS_FALLBACK_SCSV is not in this list because we manually ensure
// it is always added (even though it is not technically a cipher suite).
//
// This map, like any map, is NOT ORDERED. Do not range over this map.
var supportedCiphersMap = map[string]uint16{
"ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
"ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
"ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
"ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
"ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
"RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
"RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
"ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
"RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
}
// List of supported cipher suites in descending order of preference.
// Ordering is very important! Getting the wrong order will break
// mainstream clients, especially with HTTP/2.
//
// Note that TLS_FALLBACK_SCSV is not in this list since it is always
// added manually.
var supportedCiphers = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
}
// List of all the ciphers we want to use by default
var defaultCiphers = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
}
+232
View File
@@ -0,0 +1,232 @@
package https
import (
"crypto/tls"
"io/ioutil"
"log"
"os"
"testing"
"github.com/mholt/caddy/caddy/setup"
)
func TestMain(m *testing.M) {
// Write test certificates to disk before tests, and clean up
// when we're done.
err := ioutil.WriteFile(certFile, testCert, 0644)
if err != nil {
log.Fatal(err)
}
err = ioutil.WriteFile(keyFile, testKey, 0644)
if err != nil {
os.Remove(certFile)
log.Fatal(err)
}
result := m.Run()
os.Remove(certFile)
os.Remove(keyFile)
os.Exit(result)
}
func TestSetupParseBasic(t *testing.T) {
c := setup.NewTestController(`tls ` + certFile + ` ` + keyFile + ``)
_, err := Setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
// Basic checks
if !c.TLS.Manual {
t.Error("Expected TLS Manual=true, but was false")
}
if !c.TLS.Enabled {
t.Error("Expected TLS Enabled=true, but was false")
}
// Security defaults
if c.TLS.ProtocolMinVersion != tls.VersionTLS10 {
t.Errorf("Expected 'tls1.0 (0x0301)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion)
}
if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 {
t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %v", c.TLS.ProtocolMaxVersion)
}
// Cipher checks
expectedCiphers := []uint16{
tls.TLS_FALLBACK_SCSV,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
}
// Ensure count is correct (plus one for TLS_FALLBACK_SCSV)
if len(c.TLS.Ciphers) != len(expectedCiphers) {
t.Errorf("Expected %v Ciphers (including TLS_FALLBACK_SCSV), got %v",
len(expectedCiphers), len(c.TLS.Ciphers))
}
// Ensure ordering is correct
for i, actual := range c.TLS.Ciphers {
if actual != expectedCiphers[i] {
t.Errorf("Expected cipher in position %d to be %0x, got %0x", i, expectedCiphers[i], actual)
}
}
if !c.TLS.PreferServerCipherSuites {
t.Error("Expected PreferServerCipherSuites = true, but was false")
}
}
func TestSetupParseIncompleteParams(t *testing.T) {
// Using tls without args is an error because it's unnecessary.
c := setup.NewTestController(`tls`)
_, err := Setup(c)
if err == nil {
t.Error("Expected an error, but didn't get one")
}
}
func TestSetupParseWithOptionalParams(t *testing.T) {
params := `tls ` + certFile + ` ` + keyFile + ` {
protocols ssl3.0 tls1.2
ciphers RSA-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384
}`
c := setup.NewTestController(params)
_, err := Setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if c.TLS.ProtocolMinVersion != tls.VersionSSL30 {
t.Errorf("Expected 'ssl3.0 (0x0300)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion)
}
if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 {
t.Errorf("Expected 'tls1.2 (0x0302)' as ProtocolMaxVersion, got %#v", c.TLS.ProtocolMaxVersion)
}
if len(c.TLS.Ciphers)-1 != 3 {
t.Errorf("Expected 3 Ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers)-1)
}
}
func TestSetupDefaultWithOptionalParams(t *testing.T) {
params := `tls {
ciphers RSA-3DES-EDE-CBC-SHA
}`
c := setup.NewTestController(params)
_, err := Setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if len(c.TLS.Ciphers)-1 != 1 {
t.Errorf("Expected 1 ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers)-1)
}
}
// TODO: If we allow this... but probably not a good idea.
// func TestSetupDisableHTTPRedirect(t *testing.T) {
// c := NewTestController(`tls {
// allow_http
// }`)
// _, err := TLS(c)
// if err != nil {
// t.Errorf("Expected no error, but got %v", err)
// }
// if !c.TLS.DisableHTTPRedir {
// t.Error("Expected HTTP redirect to be disabled, but it wasn't")
// }
// }
func TestSetupParseWithWrongOptionalParams(t *testing.T) {
// Test protocols wrong params
params := `tls ` + certFile + ` ` + keyFile + ` {
protocols ssl tls
}`
c := setup.NewTestController(params)
_, err := Setup(c)
if err == nil {
t.Errorf("Expected errors, but no error returned")
}
// Test ciphers wrong params
params = `tls ` + certFile + ` ` + keyFile + ` {
ciphers not-valid-cipher
}`
c = setup.NewTestController(params)
_, err = Setup(c)
if err == nil {
t.Errorf("Expected errors, but no error returned")
}
}
func TestSetupParseWithClientAuth(t *testing.T) {
params := `tls ` + certFile + ` ` + keyFile + ` {
clients client_ca.crt client2_ca.crt
}`
c := setup.NewTestController(params)
_, err := Setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if count := len(c.TLS.ClientCerts); count != 2 {
t.Fatalf("Expected two client certs, had %d", count)
}
if actual := c.TLS.ClientCerts[0]; actual != "client_ca.crt" {
t.Errorf("Expected first client cert file to be '%s', but was '%s'", "client_ca.crt", actual)
}
if actual := c.TLS.ClientCerts[1]; actual != "client2_ca.crt" {
t.Errorf("Expected second client cert file to be '%s', but was '%s'", "client2_ca.crt", actual)
}
// Test missing client cert file
params = `tls ` + certFile + ` ` + keyFile + ` {
clients
}`
c = setup.NewTestController(params)
_, err = Setup(c)
if err == nil {
t.Errorf("Expected an error, but no error returned")
}
}
const (
certFile = "test_cert.pem"
keyFile = "test_key.pem"
)
var testCert = []byte(`-----BEGIN CERTIFICATE-----
MIIBkjCCATmgAwIBAgIJANfFCBcABL6LMAkGByqGSM49BAEwFDESMBAGA1UEAxMJ
bG9jYWxob3N0MB4XDTE2MDIxMDIyMjAyNFoXDTE4MDIwOTIyMjAyNFowFDESMBAG
A1UEAxMJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs22MtnG7
9K1mvIyjEO9GLx7BFD0tBbGnwQ0VPsuCxC6IeVuXbQDLSiVQvFZ6lUszTlczNxVk
pEfqrM6xAupB7qN1MHMwHQYDVR0OBBYEFHxYDvAxUwL4XrjPev6qZ/BiLDs5MEQG
A1UdIwQ9MDuAFHxYDvAxUwL4XrjPev6qZ/BiLDs5oRikFjAUMRIwEAYDVQQDEwls
b2NhbGhvc3SCCQDXxQgXAAS+izAMBgNVHRMEBTADAQH/MAkGByqGSM49BAEDSAAw
RQIgRvBqbyJM2JCJqhA1FmcoZjeMocmhxQHTt1c+1N2wFUgCIQDtvrivbBPA688N
Qh3sMeAKNKPsx5NxYdoWuu9KWcKz9A==
-----END CERTIFICATE-----
`)
var testKey = []byte(`-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIGLtRmwzYVcrH3J0BnzYbGPdWVF10i9p6mxkA4+b2fURoAoGCCqGSM49
AwEHoUQDQgAEs22MtnG79K1mvIyjEO9GLx7BFD0tBbGnwQ0VPsuCxC6IeVuXbQDL
SiVQvFZ6lUszTlczNxVkpEfqrM6xAupB7g==
-----END EC PRIVATE KEY-----
`)
+94
View File
@@ -0,0 +1,94 @@
package https
import (
"path/filepath"
"strings"
"github.com/mholt/caddy/caddy/assets"
)
// storage is used to get file paths in a consistent,
// cross-platform way for persisting Let's Encrypt assets
// on the file system.
var storage = Storage(filepath.Join(assets.Path(), "letsencrypt"))
// Storage is a root directory and facilitates
// forming file paths derived from it.
type Storage string
// Sites gets the directory that stores site certificate and keys.
func (s Storage) Sites() string {
return filepath.Join(string(s), "sites")
}
// Site returns the path to the folder containing assets for domain.
func (s Storage) Site(domain string) string {
return filepath.Join(s.Sites(), domain)
}
// SiteCertFile returns the path to the certificate file for domain.
func (s Storage) SiteCertFile(domain string) string {
return filepath.Join(s.Site(domain), domain+".crt")
}
// SiteKeyFile returns the path to domain's private key file.
func (s Storage) SiteKeyFile(domain string) string {
return filepath.Join(s.Site(domain), domain+".key")
}
// SiteMetaFile returns the path to the domain's asset metadata file.
func (s Storage) SiteMetaFile(domain string) string {
return filepath.Join(s.Site(domain), domain+".json")
}
// Users gets the directory that stores account folders.
func (s Storage) Users() string {
return filepath.Join(string(s), "users")
}
// User gets the account folder for the user with email.
func (s Storage) User(email string) string {
if email == "" {
email = emptyEmail
}
return filepath.Join(s.Users(), email)
}
// UserRegFile gets the path to the registration file for
// the user with the given email address.
func (s Storage) UserRegFile(email string) string {
if email == "" {
email = emptyEmail
}
fileName := emailUsername(email)
if fileName == "" {
fileName = "registration"
}
return filepath.Join(s.User(email), fileName+".json")
}
// UserKeyFile gets the path to the private key file for
// the user with the given email address.
func (s Storage) UserKeyFile(email string) string {
if email == "" {
email = emptyEmail
}
fileName := emailUsername(email)
if fileName == "" {
fileName = "private"
}
return filepath.Join(s.User(email), fileName+".key")
}
// emailUsername returns the username portion of an
// email address (part before '@') or the original
// input if it can't find the "@" symbol.
func emailUsername(email string) string {
at := strings.Index(email, "@")
if at == -1 {
return email
} else if at == 0 {
return email[1:]
}
return email[:at]
}
+88
View File
@@ -0,0 +1,88 @@
package https
import (
"path/filepath"
"testing"
)
func TestStorage(t *testing.T) {
storage = Storage("./le_test")
if expected, actual := filepath.Join("le_test", "sites"), storage.Sites(); actual != expected {
t.Errorf("Expected Sites() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com"), storage.Site("test.com"); actual != expected {
t.Errorf("Expected Site() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.crt"), storage.SiteCertFile("test.com"); actual != expected {
t.Errorf("Expected SiteCertFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.key"), storage.SiteKeyFile("test.com"); actual != expected {
t.Errorf("Expected SiteKeyFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.json"), storage.SiteMetaFile("test.com"); actual != expected {
t.Errorf("Expected SiteMetaFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users"), storage.Users(); actual != expected {
t.Errorf("Expected Users() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", "me@example.com"), storage.User("me@example.com"); actual != expected {
t.Errorf("Expected User() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.json"), storage.UserRegFile("me@example.com"); actual != expected {
t.Errorf("Expected UserRegFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.key"), storage.UserKeyFile("me@example.com"); actual != expected {
t.Errorf("Expected UserKeyFile() to return '%s' but got '%s'", expected, actual)
}
// Test with empty emails
if expected, actual := filepath.Join("le_test", "users", emptyEmail), storage.User(emptyEmail); actual != expected {
t.Errorf("Expected User(\"\") to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".json"), storage.UserRegFile(""); actual != expected {
t.Errorf("Expected UserRegFile(\"\") to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".key"), storage.UserKeyFile(""); actual != expected {
t.Errorf("Expected UserKeyFile(\"\") to return '%s' but got '%s'", expected, actual)
}
}
func TestEmailUsername(t *testing.T) {
for i, test := range []struct {
input, expect string
}{
{
input: "username@example.com",
expect: "username",
},
{
input: "plus+addressing@example.com",
expect: "plus+addressing",
},
{
input: "me+plus-addressing@example.com",
expect: "me+plus-addressing",
},
{
input: "not-an-email",
expect: "not-an-email",
},
{
input: "@foobar.com",
expect: "foobar.com",
},
{
input: emptyEmail,
expect: emptyEmail,
},
{
input: "",
expect: "",
},
} {
if actual := emailUsername(test.input); actual != test.expect {
t.Errorf("Test %d: Expected username to be '%s' but was '%s'", i, test.expect, actual)
}
}
}
+198
View File
@@ -0,0 +1,198 @@
package https
import (
"bufio"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/mholt/caddy/server"
"github.com/xenolf/lego/acme"
)
// User represents a Let's Encrypt user account.
type User struct {
Email string
Registration *acme.RegistrationResource
key *rsa.PrivateKey
}
// GetEmail gets u's email.
func (u User) GetEmail() string {
return u.Email
}
// GetRegistration gets u's registration resource.
func (u User) GetRegistration() *acme.RegistrationResource {
return u.Registration
}
// GetPrivateKey gets u's private key.
func (u User) GetPrivateKey() *rsa.PrivateKey {
return u.key
}
// getUser loads the user with the given email from disk.
// If the user does not exist, it will create a new one,
// but it does NOT save new users to the disk or register
// them via ACME. It does NOT prompt the user.
func getUser(email string) (User, error) {
var user User
// open user file
regFile, err := os.Open(storage.UserRegFile(email))
if err != nil {
if os.IsNotExist(err) {
// create a new user
return newUser(email)
}
return user, err
}
defer regFile.Close()
// load user information
err = json.NewDecoder(regFile).Decode(&user)
if err != nil {
return user, err
}
// load their private key
user.key, err = loadRSAPrivateKey(storage.UserKeyFile(email))
if err != nil {
return user, err
}
return user, nil
}
// saveUser persists a user's key and account registration
// to the file system. It does NOT register the user via ACME
// or prompt the user.
func saveUser(user User) error {
// make user account folder
err := os.MkdirAll(storage.User(user.Email), 0700)
if err != nil {
return err
}
// save private key file
err = saveRSAPrivateKey(user.key, storage.UserKeyFile(user.Email))
if err != nil {
return err
}
// save registration file
jsonBytes, err := json.MarshalIndent(&user, "", "\t")
if err != nil {
return err
}
return ioutil.WriteFile(storage.UserRegFile(user.Email), jsonBytes, 0600)
}
// newUser creates a new User for the given email address
// with a new private key. This function does NOT save the
// user to disk or register it via ACME. If you want to use
// a user account that might already exist, call getUser
// instead. It does NOT prompt the user.
func newUser(email string) (User, error) {
user := User{Email: email}
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse)
if err != nil {
return user, errors.New("error generating private key: " + err.Error())
}
user.key = privateKey
return user, nil
}
// getEmail does everything it can to obtain an email
// address from the user to use for TLS for cfg. If it
// cannot get an email address, it returns empty string.
// (It will warn the user of the consequences of an
// empty email.) This function MAY prompt the user for
// input. If userPresent is false, the operator will
// NOT be prompted and an empty email may be returned.
func getEmail(cfg server.Config, userPresent bool) string {
// First try the tls directive from the Caddyfile
leEmail := cfg.TLS.LetsEncryptEmail
if leEmail == "" {
// Then try memory (command line flag or typed by user previously)
leEmail = DefaultEmail
}
if leEmail == "" {
// Then try to get most recent user email ~/.caddy/users file
userDirs, err := ioutil.ReadDir(storage.Users())
if err == nil {
var mostRecent os.FileInfo
for _, dir := range userDirs {
if !dir.IsDir() {
continue
}
if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) {
leEmail = dir.Name()
DefaultEmail = leEmail // save for next time
}
}
}
}
if leEmail == "" && userPresent {
// Alas, we must bother the user and ask for an email address;
// if they proceed they also agree to the SA.
reader := bufio.NewReader(stdin)
fmt.Println("\nYour sites will be served over HTTPS automatically using Let's Encrypt.")
fmt.Println("By continuing, you agree to the Let's Encrypt Subscriber Agreement at:")
fmt.Println(" " + saURL) // TODO: Show current SA link
fmt.Println("Please enter your email address so you can recover your account if needed.")
fmt.Println("You can leave it blank, but you'll lose the ability to recover your account.")
fmt.Print("Email address: ")
var err error
leEmail, err = reader.ReadString('\n')
if err != nil {
return ""
}
leEmail = strings.TrimSpace(leEmail)
DefaultEmail = leEmail
Agreed = true
}
return leEmail
}
// promptUserAgreement prompts the user to agree to the agreement
// at agreementURL via stdin. If the agreement has changed, then pass
// true as the second argument. If this is the user's first time
// agreeing, pass false. It returns whether the user agreed or not.
func promptUserAgreement(agreementURL string, changed bool) bool {
if changed {
fmt.Printf("The Let's Encrypt Subscriber Agreement has changed:\n %s\n", agreementURL)
fmt.Print("Do you agree to the new terms? (y/n): ")
} else {
fmt.Printf("To continue, you must agree to the Let's Encrypt Subscriber Agreement:\n %s\n", agreementURL)
fmt.Print("Do you agree to the terms? (y/n): ")
}
reader := bufio.NewReader(stdin)
answer, err := reader.ReadString('\n')
if err != nil {
return false
}
answer = strings.ToLower(strings.TrimSpace(answer))
return answer == "y" || answer == "yes"
}
// stdin is used to read the user's input if prompted;
// this is changed by tests during tests.
var stdin = io.ReadWriter(os.Stdin)
// The name of the folder for accounts where the email
// address was not provided; default 'username' if you will.
const emptyEmail = "default"
// TODO: Use latest
const saURL = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
+196
View File
@@ -0,0 +1,196 @@
package https
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"io"
"os"
"strings"
"testing"
"time"
"github.com/mholt/caddy/server"
"github.com/xenolf/lego/acme"
)
func TestUser(t *testing.T) {
privateKey, err := rsa.GenerateKey(rand.Reader, 128)
if err != nil {
t.Fatalf("Could not generate test private key: %v", err)
}
u := User{
Email: "me@mine.com",
Registration: new(acme.RegistrationResource),
key: privateKey,
}
if expected, actual := "me@mine.com", u.GetEmail(); actual != expected {
t.Errorf("Expected email '%s' but got '%s'", expected, actual)
}
if u.GetRegistration() == nil {
t.Error("Expected a registration resource, but got nil")
}
if expected, actual := privateKey, u.GetPrivateKey(); actual != expected {
t.Errorf("Expected the private key at address %p but got one at %p instead ", expected, actual)
}
}
func TestNewUser(t *testing.T) {
email := "me@foobar.com"
user, err := newUser(email)
if err != nil {
t.Fatalf("Error creating user: %v", err)
}
if user.key == nil {
t.Error("Private key is nil")
}
if user.Email != email {
t.Errorf("Expected email to be %s, but was %s", email, user.Email)
}
if user.Registration != nil {
t.Error("New user already has a registration resource; it shouldn't")
}
}
func TestSaveUser(t *testing.T) {
storage = Storage("./testdata")
defer os.RemoveAll(string(storage))
email := "me@foobar.com"
user, err := newUser(email)
if err != nil {
t.Fatalf("Error creating user: %v", err)
}
err = saveUser(user)
if err != nil {
t.Fatalf("Error saving user: %v", err)
}
_, err = os.Stat(storage.UserRegFile(email))
if err != nil {
t.Errorf("Cannot access user registration file, error: %v", err)
}
_, err = os.Stat(storage.UserKeyFile(email))
if err != nil {
t.Errorf("Cannot access user private key file, error: %v", err)
}
}
func TestGetUserDoesNotAlreadyExist(t *testing.T) {
storage = Storage("./testdata")
defer os.RemoveAll(string(storage))
user, err := getUser("user_does_not_exist@foobar.com")
if err != nil {
t.Fatalf("Error getting user: %v", err)
}
if user.key == nil {
t.Error("Expected user to have a private key, but it was nil")
}
}
func TestGetUserAlreadyExists(t *testing.T) {
storage = Storage("./testdata")
defer os.RemoveAll(string(storage))
email := "me@foobar.com"
// Set up test
user, err := newUser(email)
if err != nil {
t.Fatalf("Error creating user: %v", err)
}
err = saveUser(user)
if err != nil {
t.Fatalf("Error saving user: %v", err)
}
// Expect to load user from disk
user2, err := getUser(email)
if err != nil {
t.Fatalf("Error getting user: %v", err)
}
// Assert keys are the same
if !rsaPrivateKeysSame(user.key, user2.key) {
t.Error("Expected private key to be the same after loading, but it wasn't")
}
// Assert emails are the same
if user.Email != user2.Email {
t.Errorf("Expected emails to be equal, but was '%s' before and '%s' after loading", user.Email, user2.Email)
}
}
func TestGetEmail(t *testing.T) {
// let's not clutter up the output
origStdout := os.Stdout
os.Stdout = nil
defer func() { os.Stdout = origStdout }()
storage = Storage("./testdata")
defer os.RemoveAll(string(storage))
DefaultEmail = "test2@foo.com"
// Test1: Use email in config
config := server.Config{
TLS: server.TLSConfig{
LetsEncryptEmail: "test1@foo.com",
},
}
actual := getEmail(config, true)
if actual != "test1@foo.com" {
t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", "test1@foo.com", actual)
}
// Test2: Use default email from flag (or user previously typing it)
actual = getEmail(server.Config{}, true)
if actual != DefaultEmail {
t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", DefaultEmail, actual)
}
// Test3: Get input from user
DefaultEmail = ""
stdin = new(bytes.Buffer)
_, err := io.Copy(stdin, strings.NewReader("test3@foo.com\n"))
if err != nil {
t.Fatalf("Could not simulate user input, error: %v", err)
}
actual = getEmail(server.Config{}, true)
if actual != "test3@foo.com" {
t.Errorf("Did not get correct email from user input prompt; expected '%s' but got '%s'", "test3@foo.com", actual)
}
// Test4: Get most recent email from before
DefaultEmail = ""
for i, eml := range []string{
"test4-3@foo.com",
"test4-2@foo.com",
"test4-1@foo.com",
} {
u, err := newUser(eml)
if err != nil {
t.Fatalf("Error creating user %d: %v", i, err)
}
err = saveUser(u)
if err != nil {
t.Fatalf("Error saving user %d: %v", i, err)
}
// Change modified time so they're all different, so the test becomes deterministic
f, err := os.Stat(storage.User(eml))
if err != nil {
t.Fatalf("Could not access user folder for '%s': %v", eml, err)
}
chTime := f.ModTime().Add(-(time.Duration(i) * time.Second))
if err := os.Chtimes(storage.User(eml), chTime, chTime); err != nil {
t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err)
}
}
actual = getEmail(server.Config{}, true)
if actual != "test4-3@foo.com" {
t.Errorf("Did not get correct email from storage; expected '%s' but got '%s'", "test4-3@foo.com", actual)
}
}
+251
View File
@@ -0,0 +1,251 @@
package parse
import (
"errors"
"fmt"
"io"
"strings"
)
// Dispenser is a type that dispenses tokens, similarly to a lexer,
// except that it can do so with some notion of structure and has
// some really convenient methods.
type Dispenser struct {
filename string
tokens []token
cursor int
nesting int
}
// NewDispenser returns a Dispenser, ready to use for parsing the given input.
func NewDispenser(filename string, input io.Reader) Dispenser {
return Dispenser{
filename: filename,
tokens: allTokens(input),
cursor: -1,
}
}
// NewDispenserTokens returns a Dispenser filled with the given tokens.
func NewDispenserTokens(filename string, tokens []token) Dispenser {
return Dispenser{
filename: filename,
tokens: tokens,
cursor: -1,
}
}
// Next loads the next token. Returns true if a token
// was loaded; false otherwise. If false, all tokens
// have been consumed.
func (d *Dispenser) Next() bool {
if d.cursor < len(d.tokens)-1 {
d.cursor++
return true
}
return false
}
// NextArg loads the next token if it is on the same
// line. Returns true if a token was loaded; false
// otherwise. If false, all tokens on the line have
// been consumed. It handles imported tokens correctly.
func (d *Dispenser) NextArg() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
return false
}
if d.cursor < len(d.tokens)-1 &&
d.tokens[d.cursor].file == d.tokens[d.cursor+1].file &&
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].line {
d.cursor++
return true
}
return false
}
// NextLine loads the next token only if it is not on the same
// line as the current token, and returns true if a token was
// loaded; false otherwise. If false, there is not another token
// or it is on the same line. It handles imported tokens correctly.
func (d *Dispenser) NextLine() bool {
if d.cursor < 0 {
d.cursor++
return true
}
if d.cursor >= len(d.tokens) {
return false
}
if d.cursor < len(d.tokens)-1 &&
(d.tokens[d.cursor].file != d.tokens[d.cursor+1].file ||
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].line) {
d.cursor++
return true
}
return false
}
// NextBlock can be used as the condition of a for loop
// to load the next token as long as it opens a block or
// is already in a block. It returns true if a token was
// loaded, or false when the block's closing curly brace
// was loaded and thus the block ended. Nested blocks are
// not supported.
func (d *Dispenser) NextBlock() bool {
if d.nesting > 0 {
d.Next()
if d.Val() == "}" {
d.nesting--
return false
}
return true
}
if !d.NextArg() { // block must open on same line
return false
}
if d.Val() != "{" {
d.cursor-- // roll back if not opening brace
return false
}
d.Next()
if d.Val() == "}" {
// Open and then closed right away
return false
}
d.nesting++
return true
}
// IncrNest adds a level of nesting to the dispenser.
func (d *Dispenser) IncrNest() {
d.nesting++
return
}
// Val gets the text of the current token. If there is no token
// loaded, it returns empty string.
func (d *Dispenser) Val() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
return d.tokens[d.cursor].text
}
// Line gets the line number of the current token. If there is no token
// loaded, it returns 0.
func (d *Dispenser) Line() int {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return 0
}
return d.tokens[d.cursor].line
}
// File gets the filename of the current token. If there is no token loaded,
// it returns the filename originally given when parsing started.
func (d *Dispenser) File() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return d.filename
}
if tokenFilename := d.tokens[d.cursor].file; tokenFilename != "" {
return tokenFilename
}
return d.filename
}
// Args is a convenience function that loads the next arguments
// (tokens on the same line) into an arbitrary number of strings
// pointed to in targets. If there are fewer tokens available
// than string pointers, the remaining strings will not be changed
// and false will be returned. If there were enough tokens available
// to fill the arguments, then true will be returned.
func (d *Dispenser) Args(targets ...*string) bool {
enough := true
for i := 0; i < len(targets); i++ {
if !d.NextArg() {
enough = false
break
}
*targets[i] = d.Val()
}
return enough
}
// RemainingArgs loads any more arguments (tokens on the same line)
// into a slice and returns them. Open curly brace tokens also indicate
// the end of arguments, and the curly brace is not included in
// the return value nor is it loaded.
func (d *Dispenser) RemainingArgs() []string {
var args []string
for d.NextArg() {
if d.Val() == "{" {
d.cursor--
break
}
args = append(args, d.Val())
}
return args
}
// ArgErr returns an argument error, meaning that another
// argument was expected but not found. In other words,
// a line break or open curly brace was encountered instead of
// an argument.
func (d *Dispenser) ArgErr() error {
if d.Val() == "{" {
return d.Err("Unexpected token '{', expecting argument")
}
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
}
// SyntaxErr creates a generic syntax error which explains what was
// found and what was expected.
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)
return errors.New(msg)
}
// EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error {
return d.Errf("Unexpected EOF")
}
// Err generates a custom parse error with a message of msg.
func (d *Dispenser) Err(msg string) error {
msg = fmt.Sprintf("%s:%d - Parse error: %s", d.File(), d.Line(), msg)
return errors.New(msg)
}
// Errf is like Err, but for formatted error messages
func (d *Dispenser) Errf(format string, args ...interface{}) error {
return d.Err(fmt.Sprintf(format, args...))
}
// numLineBreaks counts how many line breaks are in the token
// value given by the token index tknIdx. It returns 0 if the
// token does not exist or there are no line breaks.
func (d *Dispenser) numLineBreaks(tknIdx int) int {
if tknIdx < 0 || tknIdx >= len(d.tokens) {
return 0
}
return strings.Count(d.tokens[tknIdx].text, "\n")
}
// isNewLine determines whether the current token is on a different
// line (higher line number) than the previous token. It handles imported
// tokens correctly. If there isn't a previous token, it returns true.
func (d *Dispenser) isNewLine() bool {
if d.cursor < 1 {
return true
}
if d.cursor > len(d.tokens)-1 {
return false
}
return d.tokens[d.cursor-1].file != d.tokens[d.cursor].file ||
d.tokens[d.cursor-1].line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].line
}
@@ -1,21 +1,6 @@
// 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
package parse
import (
"errors"
"reflect"
"strings"
"testing"
@@ -26,7 +11,7 @@ func TestDispenser_Val_Next(t *testing.T) {
dir1 arg1
dir2 arg2 arg3
dir3`
d := NewTestDispenser(input)
d := NewDispenser("Testfile", strings.NewReader(input))
if val := d.Val(); val != "" {
t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val)
@@ -64,7 +49,7 @@ func TestDispenser_NextArg(t *testing.T) {
input := `dir1 arg1
dir2 arg2 arg3
dir3`
d := NewTestDispenser(input)
d := NewDispenser("Testfile", strings.NewReader(input))
assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {
if d.Next() != shouldLoad {
@@ -79,7 +64,7 @@ func TestDispenser_NextArg(t *testing.T) {
}
assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) {
if !d.NextArg() {
if d.NextArg() != true {
t.Error("NextArg(): Should load next argument but got false instead")
}
if d.cursor != expectedCursor {
@@ -89,7 +74,7 @@ func TestDispenser_NextArg(t *testing.T) {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
if !loadAnother {
if d.NextArg() {
if d.NextArg() != false {
t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val())
}
if d.cursor != expectedCursor {
@@ -111,7 +96,7 @@ func TestDispenser_NextLine(t *testing.T) {
input := `host:port
dir1 arg1
dir2 arg2 arg3`
d := NewTestDispenser(input)
d := NewDispenser("Testfile", strings.NewReader(input))
assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {
if d.NextLine() != shouldLoad {
@@ -144,10 +129,10 @@ func TestDispenser_NextBlock(t *testing.T) {
}
foobar2 {
}`
d := NewTestDispenser(input)
d := NewDispenser("Testfile", strings.NewReader(input))
assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {
if loaded := d.NextBlock(0); loaded != shouldLoad {
if loaded := d.NextBlock(); loaded != shouldLoad {
t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded)
}
if d.cursor != expectedCursor {
@@ -174,7 +159,7 @@ func TestDispenser_Args(t *testing.T) {
dir2 arg4 arg5
dir3 arg6 arg7
dir4`
d := NewTestDispenser(input)
d := NewDispenser("Testfile", strings.NewReader(input))
d.Next() // dir1
@@ -241,7 +226,7 @@ func TestDispenser_RemainingArgs(t *testing.T) {
dir2 arg4 arg5
dir3 arg6 { arg7
dir4`
d := NewTestDispenser(input)
d := NewDispenser("Testfile", strings.NewReader(input))
d.Next() // dir1
@@ -278,7 +263,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
input := `dir1 {
}
dir2 arg1 arg2`
d := NewTestDispenser(input)
d := NewDispenser("Testfile", strings.NewReader(input))
d.cursor = 1 // {
@@ -304,10 +289,4 @@ func TestDispenser_ArgErr_Err(t *testing.T) {
if !strings.Contains(err.Error(), "foobar") {
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
}
ErrBarIsFull := errors.New("bar is full")
bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull)
if !errors.Is(bookingError, ErrBarIsFull) {
t.Errorf("Errf(): should be able to unwrap the error chain")
}
}
+122
View File
@@ -0,0 +1,122 @@
package parse
import (
"bufio"
"io"
"unicode"
)
type (
// lexer is a utility which can get values, token by
// token, from a Reader. A token is a word, and tokens
// are separated by whitespace. A word can be enclosed
// in quotes if it contains whitespace.
lexer struct {
reader *bufio.Reader
token token
line int
}
// token represents a single parsable unit.
token struct {
file string
line int
text string
}
)
// load prepares the lexer to scan an input for tokens.
func (l *lexer) load(input io.Reader) error {
l.reader = bufio.NewReader(input)
l.line = 1
return nil
}
// next loads the next token into the lexer.
// A token is delimited by whitespace, unless
// the token starts with a quotes character (")
// in which case the token goes until the closing
// quotes (the enclosing quotes are not included).
// Inside quoted strings, quotes may be escaped
// with a preceding \ character. No other chars
// may be escaped. The rest of the line is skipped
// if a "#" character is read in. Returns true if
// a token was loaded; false otherwise.
func (l *lexer) next() bool {
var val []rune
var comment, quoted, escaped bool
makeToken := func() bool {
l.token.text = string(val)
return true
}
for {
ch, _, err := l.reader.ReadRune()
if err != nil {
if len(val) > 0 {
return makeToken()
}
if err == io.EOF {
return false
}
panic(err)
}
if quoted {
if !escaped {
if ch == '\\' {
escaped = true
continue
} else if ch == '"' {
quoted = false
return makeToken()
}
}
if ch == '\n' {
l.line++
}
if escaped {
// only escape quotes
if ch != '"' {
val = append(val, '\\')
}
}
val = append(val, ch)
escaped = false
continue
}
if unicode.IsSpace(ch) {
if ch == '\r' {
continue
}
if ch == '\n' {
l.line++
comment = false
}
if len(val) > 0 {
return makeToken()
}
continue
}
if ch == '#' {
comment = true
}
if comment {
continue
}
if len(val) == 0 {
l.token = token{line: l.line}
if ch == '"' {
quoted = true
continue
}
}
val = append(val, ch)
}
}
+165
View File
@@ -0,0 +1,165 @@
package parse
import (
"strings"
"testing"
)
type lexerTestCase struct {
input string
expected []token
}
func TestLexer(t *testing.T) {
testCases := []lexerTestCase{
{
input: `host:123`,
expected: []token{
{line: 1, text: "host:123"},
},
},
{
input: `host:123
directive`,
expected: []token{
{line: 1, text: "host:123"},
{line: 3, text: "directive"},
},
},
{
input: `host:123 {
directive
}`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 2, text: "directive"},
{line: 3, text: "}"},
},
},
{
input: `host:123 { directive }`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 1, text: "directive"},
{line: 1, text: "}"},
},
},
{
input: `host:123 {
#comment
directive
# comment
foobar # another comment
}`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 3, text: "directive"},
{line: 5, text: "foobar"},
{line: 6, text: "}"},
},
},
{
input: `a "quoted value" b
foobar`,
expected: []token{
{line: 1, text: "a"},
{line: 1, text: "quoted value"},
{line: 1, text: "b"},
{line: 2, text: "foobar"},
},
},
{
input: `A "quoted \"value\" inside" B`,
expected: []token{
{line: 1, text: "A"},
{line: 1, text: `quoted "value" inside`},
{line: 1, text: "B"},
},
},
{
input: `"don't\escape"`,
expected: []token{
{line: 1, text: `don't\escape`},
},
},
{
input: `"don't\\escape"`,
expected: []token{
{line: 1, text: `don't\\escape`},
},
},
{
input: `A "quoted value with line
break inside" {
foobar
}`,
expected: []token{
{line: 1, text: "A"},
{line: 1, text: "quoted value with line\n\t\t\t\t\tbreak inside"},
{line: 2, text: "{"},
{line: 3, text: "foobar"},
{line: 4, text: "}"},
},
},
{
input: `"C:\php\php-cgi.exe"`,
expected: []token{
{line: 1, text: `C:\php\php-cgi.exe`},
},
},
{
input: `empty "" string`,
expected: []token{
{line: 1, text: `empty`},
{line: 1, text: ``},
{line: 1, text: `string`},
},
},
{
input: "skip those\r\nCR characters",
expected: []token{
{line: 1, text: "skip"},
{line: 1, text: "those"},
{line: 2, text: "CR"},
{line: 2, text: "characters"},
},
},
}
for i, testCase := range testCases {
actual := tokenize(testCase.input)
lexerCompare(t, i, testCase.expected, actual)
}
}
func tokenize(input string) (tokens []token) {
l := lexer{}
l.load(strings.NewReader(input))
for l.next() {
tokens = append(tokens, l.token)
}
return
}
func lexerCompare(t *testing.T, n int, expected, actual []token) {
if len(expected) != len(actual) {
t.Errorf("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++ {
if actual[i].line != expected[i].line {
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
n, i, expected[i].text, expected[i].line, actual[i].line)
break
}
if actual[i].text != expected[i].text {
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
n, i, expected[i].text, actual[i].text)
break
}
}
}
+32
View File
@@ -0,0 +1,32 @@
// Package parse provides facilities for parsing configuration files.
package parse
import "io"
// ServerBlocks parses the input just enough to organize tokens,
// in order, by server block. No further parsing is performed.
// If checkDirectives is true, only valid directives will be allowed
// otherwise we consider it a parse error. Server blocks are returned
// in the order in which they appear.
func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]ServerBlock, error) {
p := parser{Dispenser: NewDispenser(filename, input)}
p.checkDirectives = checkDirectives
blocks, err := p.parseAll()
return blocks, err
}
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
func allTokens(input io.Reader) (tokens []token) {
l := new(lexer)
l.load(input)
for l.next() {
tokens = append(tokens, l.token)
}
return
}
// ValidDirectives is a set of directives that are valid (unordered). Populated
// by config package's init function.
var ValidDirectives = make(map[string]struct{})
+22
View File
@@ -0,0 +1,22 @@
package parse
import (
"strings"
"testing"
)
func TestAllTokens(t *testing.T) {
input := strings.NewReader("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"}
tokens := allTokens(input)
if len(tokens) != len(expected) {
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
}
for i, val := range expected {
if tokens[i].text != val {
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text)
}
}
}
+409
View File
@@ -0,0 +1,409 @@
package parse
import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
)
type parser struct {
Dispenser
block ServerBlock // current server block being parsed
eof bool // if we encounter a valid EOF in a hard place
checkDirectives bool // if true, directives must be known
}
func (p *parser) parseAll() ([]ServerBlock, error) {
var blocks []ServerBlock
for p.Next() {
err := p.parseOne()
if err != nil {
return blocks, err
}
if len(p.block.Addresses) > 0 {
blocks = append(blocks, p.block)
}
}
return blocks, nil
}
func (p *parser) parseOne() error {
p.block = ServerBlock{Tokens: make(map[string][]token)}
err := p.begin()
if err != nil {
return err
}
return nil
}
func (p *parser) begin() error {
if len(p.tokens) == 0 {
return nil
}
err := p.addresses()
if err != nil {
return err
}
if p.eof {
// this happens if the Caddyfile consists of only
// a line of addresses and nothing else
return nil
}
err = p.blockContents()
if err != nil {
return err
}
return nil
}
func (p *parser) addresses() error {
var expectingAnother bool
for {
tkn := replaceEnvVars(p.Val())
// special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() {
err := p.doImport()
if err != nil {
return err
}
continue
}
// Open brace definitely indicates end of addresses
if tkn == "{" {
if expectingAnother {
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
}
break
}
if tkn != "" { // empty token possible if user typed "" in Caddyfile
// Trailing comma indicates another address will follow, which
// may possibly be on the next line
if tkn[len(tkn)-1] == ',' {
tkn = tkn[:len(tkn)-1]
expectingAnother = true
} else {
expectingAnother = false // but we may still see another one on this line
}
// Parse and save this address
addr, err := standardAddress(tkn)
if err != nil {
return err
}
p.block.Addresses = append(p.block.Addresses, addr)
}
// Advance token and possibly break out of loop or return error
hasNext := p.Next()
if expectingAnother && !hasNext {
return p.EOFErr()
}
if !hasNext {
p.eof = true
break // EOF
}
if !expectingAnother && p.isNewLine() {
break
}
}
return nil
}
func (p *parser) blockContents() error {
errOpenCurlyBrace := p.openCurlyBrace()
if errOpenCurlyBrace != nil {
// single-server configs don't need curly braces
p.cursor--
}
err := p.directives()
if err != nil {
return err
}
// Only look for close curly brace if there was an opening
if errOpenCurlyBrace == nil {
err = p.closeCurlyBrace()
if err != nil {
return err
}
}
return nil
}
// directives parses through all the lines for directives
// and it expects the next token to be the first
// directive. It goes until EOF or closing curly brace
// which ends the server block.
func (p *parser) directives() error {
for p.Next() {
// end of server block
if p.Val() == "}" {
break
}
// special case: import directive replaces tokens during parse-time
if p.Val() == "import" {
err := p.doImport()
if err != nil {
return err
}
p.cursor-- // cursor is advanced when we continue, so roll back one more
continue
}
// normal case: parse a directive on this line
if err := p.directive(); err != nil {
return err
}
}
return nil
}
// doImport swaps out the import directive and its argument
// (a total of 2 tokens) with the tokens in the specified file
// or globbing pattern. When the function returns, the cursor
// is on the token before where the import directive was. In
// other words, call Next() to access the first token that was
// imported.
func (p *parser) doImport() error {
// syntax check
if !p.NextArg() {
return p.ArgErr()
}
importPattern := p.Val()
if p.NextArg() {
return p.Err("Import takes only one argument (glob pattern or file)")
}
// do glob
matches, err := filepath.Glob(importPattern)
if err != nil {
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
}
if len(matches) == 0 {
return p.Errf("No files matching import pattern %s", importPattern)
}
// splice out the import directive and its argument (2 tokens total)
tokensBefore := p.tokens[:p.cursor-1]
tokensAfter := p.tokens[p.cursor+1:]
// collect all the imported tokens
var importedTokens []token
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
if err != nil {
return err
}
importedTokens = append(importedTokens, newTokens...)
}
// splice the imported tokens in the place of the import statement
// and rewind cursor so Next() will land on first imported token
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
p.cursor--
return nil
}
// doSingleImport lexes the individual file at importFile and returns
// its tokens or an error, if any.
func (p *parser) doSingleImport(importFile string) ([]token, error) {
file, err := os.Open(importFile)
if err != nil {
return nil, p.Errf("Could not import %s: %v", importFile, err)
}
defer file.Close()
importedTokens := allTokens(file)
// Tack the filename onto these tokens so errors show the imported file's name
filename := filepath.Base(importFile)
for i := 0; i < len(importedTokens); i++ {
importedTokens[i].file = filename
}
return importedTokens, nil
}
// directive collects tokens until the directive's scope
// closes (either end of line or end of curly brace block).
// It expects the currently-loaded token to be a directive
// (or } that ends a server block). The collected tokens
// are loaded into the current server block for later use
// by directive setup functions.
func (p *parser) directive() error {
dir := p.Val()
nesting := 0
if p.checkDirectives {
if _, ok := ValidDirectives[dir]; !ok {
return p.Errf("Unknown directive '%s'", dir)
}
}
// The directive itself is appended as a relevant token
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
for p.Next() {
if p.Val() == "{" {
nesting++
} else if p.isNewLine() && nesting == 0 {
p.cursor-- // read too far
break
} else if p.Val() == "}" && nesting > 0 {
nesting--
} else if p.Val() == "}" && nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace")
}
p.tokens[p.cursor].text = replaceEnvVars(p.tokens[p.cursor].text)
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
}
if nesting > 0 {
return p.EOFErr()
}
return nil
}
// openCurlyBrace expects the current token to be an
// opening curly brace. This acts like an assertion
// because it returns an error if the token is not
// a opening curly brace. It does NOT advance the token.
func (p *parser) openCurlyBrace() error {
if p.Val() != "{" {
return p.SyntaxErr("{")
}
return nil
}
// closeCurlyBrace expects the current token to be
// a closing curly brace. This acts like an assertion
// because it returns an error if the token is not
// a closing curly brace. It does NOT advance the token.
func (p *parser) closeCurlyBrace() error {
if p.Val() != "}" {
return p.SyntaxErr("}")
}
return nil
}
// standardAddress parses an address string into a structured format with separate
// scheme, host, and port portions, as well as the original input string.
func standardAddress(str string) (address, error) {
var scheme string
var err error
// first check for scheme and strip it off
input := str
if strings.HasPrefix(str, "https://") {
scheme = "https"
str = str[8:]
} else if strings.HasPrefix(str, "http://") {
scheme = "http"
str = str[7:]
}
// separate host and port
host, port, err := net.SplitHostPort(str)
if err != nil {
host, port, err = net.SplitHostPort(str + ":")
// no error check here; return err at end of function
}
// see if we can set port based off scheme
if port == "" {
if scheme == "http" {
port = "80"
} else if scheme == "https" {
port = "443"
}
}
// repeated or conflicting scheme is confusing, so error
if scheme != "" && (port == "http" || port == "https") {
return address{}, fmt.Errorf("[%s] scheme specified twice in address", input)
}
// error if scheme and port combination violate convention
if (scheme == "http" && port == "443") || (scheme == "https" && port == "80") {
return address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
}
// standardize http and https ports to their respective port numbers
if port == "http" {
scheme = "http"
port = "80"
} else if port == "https" {
scheme = "https"
port = "443"
}
return address{Original: input, Scheme: scheme, Host: host, Port: port}, err
}
// replaceEnvVars replaces environment variables that appear in the token
// and understands both the $UNIX and %WINDOWS% syntaxes.
func replaceEnvVars(s string) string {
s = replaceEnvReferences(s, "{%", "%}")
s = replaceEnvReferences(s, "{$", "}")
return s
}
// replaceEnvReferences performs the actual replacement of env variables
// in s, given the placeholder start and placeholder end strings.
func replaceEnvReferences(s, refStart, refEnd string) string {
index := strings.Index(s, refStart)
for index != -1 {
endIndex := strings.Index(s, refEnd)
if endIndex != -1 {
ref := s[index : endIndex+len(refEnd)]
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
} else {
return s
}
index = strings.Index(s, refStart)
}
return s
}
type (
// ServerBlock associates tokens with a list of addresses
// and groups tokens by directive name.
ServerBlock struct {
Addresses []address
Tokens map[string][]token
}
address struct {
Original, Scheme, Host, Port string
}
)
// HostList converts the list of addresses that are
// associated with this server block into a slice of
// strings, where each address is as it was originally
// read from the input.
func (sb ServerBlock) HostList() []string {
sbHosts := make([]string, len(sb.Addresses))
for j, addr := range sb.Addresses {
sbHosts[j] = addr.Original
}
return sbHosts
}
+477
View File
@@ -0,0 +1,477 @@
package parse
import (
"os"
"strings"
"testing"
)
func TestStandardAddress(t *testing.T) {
for i, test := range []struct {
input string
scheme, host, port string
shouldErr bool
}{
{`localhost`, "", "localhost", "", false},
{`localhost:1234`, "", "localhost", "1234", false},
{`localhost:`, "", "localhost", "", false},
{`0.0.0.0`, "", "0.0.0.0", "", false},
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", false},
{`:1234`, "", "", "1234", false},
{`[::1]`, "", "::1", "", false},
{`[::1]:1234`, "", "::1", "1234", false},
{`:`, "", "", "", false},
{`localhost:http`, "http", "localhost", "80", false},
{`localhost:https`, "https", "localhost", "443", false},
{`:http`, "http", "", "80", false},
{`:https`, "https", "", "443", false},
{`http://localhost:https`, "", "", "", true}, // conflict
{`http://localhost:http`, "", "", "", true}, // repeated scheme
{`http://localhost:443`, "", "", "", true}, // not conventional
{`https://localhost:80`, "", "", "", true}, // not conventional
{`http://localhost`, "http", "localhost", "80", false},
{`https://localhost`, "https", "localhost", "443", false},
{`http://127.0.0.1`, "http", "127.0.0.1", "80", false},
{`https://127.0.0.1`, "https", "127.0.0.1", "443", false},
{`http://[::1]`, "http", "::1", "80", false},
{`http://localhost:1234`, "http", "localhost", "1234", false},
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", false},
{`http://[::1]:1234`, "http", "::1", "1234", false},
{``, "", "", "", false},
{`::1`, "", "::1", "", true},
{`localhost::`, "", "localhost::", "", true},
{`#$%@`, "", "#$%@", "", true},
} {
actual, err := standardAddress(test.input)
if err != nil && !test.shouldErr {
t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err)
}
if err == nil && test.shouldErr {
t.Errorf("Test %d (%s): Expected error, but had none", i, test.input)
}
if actual.Scheme != test.scheme {
t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme)
}
if actual.Host != test.host {
t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host)
}
if actual.Port != test.port {
t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port)
}
}
}
func TestParseOneAndImport(t *testing.T) {
setupParseTests()
testParseOne := func(input string) (ServerBlock, error) {
p := testParser(input)
p.Next() // parseOne doesn't call Next() to start, so we must
err := p.parseOne()
return p.block, err
}
for i, test := range []struct {
input string
shouldErr bool
addresses []address
tokens map[string]int // map of directive name to number of tokens expected
}{
{`localhost`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{}},
{`localhost
dir1`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 1,
}},
{`localhost:1234
dir1 foo bar`, false, []address{
{"localhost:1234", "", "localhost", "1234"},
}, map[string]int{
"dir1": 3,
}},
{`localhost {
dir1
}`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 1,
}},
{`localhost:1234 {
dir1 foo bar
dir2
}`, false, []address{
{"localhost:1234", "", "localhost", "1234"},
}, map[string]int{
"dir1": 3,
"dir2": 1,
}},
{`http://localhost https://localhost
dir1 foo bar`, false, []address{
{"http://localhost", "http", "localhost", "80"},
{"https://localhost", "https", "localhost", "443"},
}, map[string]int{
"dir1": 3,
}},
{`http://localhost https://localhost {
dir1 foo bar
}`, false, []address{
{"http://localhost", "http", "localhost", "80"},
{"https://localhost", "https", "localhost", "443"},
}, map[string]int{
"dir1": 3,
}},
{`http://localhost, https://localhost {
dir1 foo bar
}`, false, []address{
{"http://localhost", "http", "localhost", "80"},
{"https://localhost", "https", "localhost", "443"},
}, map[string]int{
"dir1": 3,
}},
{`http://localhost, {
}`, true, []address{
{"http://localhost", "http", "localhost", "80"},
}, map[string]int{}},
{`host1:80, http://host2.com
dir1 foo bar
dir2 baz`, false, []address{
{"host1:80", "", "host1", "80"},
{"http://host2.com", "http", "host2.com", "80"},
}, map[string]int{
"dir1": 3,
"dir2": 2,
}},
{`http://host1.com,
http://host2.com,
https://host3.com`, false, []address{
{"http://host1.com", "http", "host1.com", "80"},
{"http://host2.com", "http", "host2.com", "80"},
{"https://host3.com", "https", "host3.com", "443"},
}, map[string]int{}},
{`http://host1.com:1234, https://host2.com
dir1 foo {
bar baz
}
dir2`, false, []address{
{"http://host1.com:1234", "http", "host1.com", "1234"},
{"https://host2.com", "https", "host2.com", "443"},
}, map[string]int{
"dir1": 6,
"dir2": 1,
}},
{`127.0.0.1
dir1 {
bar baz
}
dir2 {
foo bar
}`, false, []address{
{"127.0.0.1", "", "127.0.0.1", ""},
}, map[string]int{
"dir1": 5,
"dir2": 5,
}},
{`127.0.0.1
unknown_directive`, true, []address{
{"127.0.0.1", "", "127.0.0.1", ""},
}, map[string]int{}},
{`localhost
dir1 {
foo`, true, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
}`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
} }`, true, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
nested {
foo
}
}
dir2 foo bar`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 7,
"dir2": 3,
}},
{``, false, []address{}, map[string]int{}},
{`localhost
dir1 arg1
import import_test1.txt`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 2,
"dir2": 3,
"dir3": 1,
}},
{`import import_test2.txt`, false, []address{
{"host1", "", "host1", ""},
}, map[string]int{
"dir1": 1,
"dir2": 2,
}},
{`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}},
{`import not_found.txt`, true, []address{}, map[string]int{}},
{`""`, false, []address{}, map[string]int{}},
{``, false, []address{}, map[string]int{}},
} {
result, err := testParseOne(test.input)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(result.Addresses) != len(test.addresses) {
t.Errorf("Test %d: Expected %d addresses, got %d",
i, len(test.addresses), len(result.Addresses))
continue
}
for j, addr := range result.Addresses {
if addr.Host != test.addresses[j].Host {
t.Errorf("Test %d, address %d: Expected host to be '%s', but was '%s'",
i, j, test.addresses[j].Host, addr.Host)
}
if addr.Port != test.addresses[j].Port {
t.Errorf("Test %d, address %d: Expected port to be '%s', but was '%s'",
i, j, test.addresses[j].Port, addr.Port)
}
}
if len(result.Tokens) != len(test.tokens) {
t.Errorf("Test %d: Expected %d directives, had %d",
i, len(test.tokens), len(result.Tokens))
continue
}
for directive, tokens := range result.Tokens {
if len(tokens) != test.tokens[directive] {
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
i, directive, test.tokens[directive], len(tokens))
continue
}
}
}
}
func TestParseAll(t *testing.T) {
setupParseTests()
for i, test := range []struct {
input string
shouldErr bool
addresses [][]address // addresses per server block, in order
}{
{`localhost`, false, [][]address{
{{"localhost", "", "localhost", ""}},
}},
{`localhost:1234`, false, [][]address{
{{"localhost:1234", "", "localhost", "1234"}},
}},
{`localhost:1234 {
}
localhost:2015 {
}`, false, [][]address{
{{"localhost:1234", "", "localhost", "1234"}},
{{"localhost:2015", "", "localhost", "2015"}},
}},
{`localhost:1234, http://host2`, false, [][]address{
{{"localhost:1234", "", "localhost", "1234"}, {"http://host2", "http", "host2", "80"}},
}},
{`localhost:1234, http://host2,`, true, [][]address{}},
{`http://host1.com, http://host2.com {
}
https://host3.com, https://host4.com {
}`, false, [][]address{
{{"http://host1.com", "http", "host1.com", "80"}, {"http://host2.com", "http", "host2.com", "80"}},
{{"https://host3.com", "https", "host3.com", "443"}, {"https://host4.com", "https", "host4.com", "443"}},
}},
{`import import_glob*.txt`, false, [][]address{
{{"glob0.host0", "", "glob0.host0", ""}},
{{"glob0.host1", "", "glob0.host1", ""}},
{{"glob1.host0", "", "glob1.host0", ""}},
{{"glob2.host0", "", "glob2.host0", ""}},
}},
} {
p := testParser(test.input)
blocks, err := p.parseAll()
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(blocks) != len(test.addresses) {
t.Errorf("Test %d: Expected %d server blocks, got %d",
i, len(test.addresses), len(blocks))
continue
}
for j, block := range blocks {
if len(block.Addresses) != len(test.addresses[j]) {
t.Errorf("Test %d: Expected %d addresses in block %d, got %d",
i, len(test.addresses[j]), j, len(block.Addresses))
continue
}
for k, addr := range block.Addresses {
if addr.Host != test.addresses[j][k].Host {
t.Errorf("Test %d, block %d, address %d: Expected host to be '%s', but was '%s'",
i, j, k, test.addresses[j][k].Host, addr.Host)
}
if addr.Port != test.addresses[j][k].Port {
t.Errorf("Test %d, block %d, address %d: Expected port to be '%s', but was '%s'",
i, j, k, test.addresses[j][k].Port, addr.Port)
}
}
}
}
}
func TestEnvironmentReplacement(t *testing.T) {
setupParseTests()
os.Setenv("PORT", "8080")
os.Setenv("ADDRESS", "servername.com")
os.Setenv("FOOBAR", "foobar")
// basic test; unix-style env vars
p := testParser(`{$ADDRESS}`)
blocks, _ := p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
// multiple vars per token
p = testParser(`{$ADDRESS}:{$PORT}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// windows-style var and unix style in same token
p = testParser(`{%ADDRESS%}:{$PORT}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// reverse order
p = testParser(`{$ADDRESS}:{%PORT%}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// env var in server block body as argument
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Tokens["dir1"][1].text, "foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// combined windows env vars in argument
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].text, "servername.com/foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// malformed env var (windows)
p = testParser(":1234\ndir1 {%ADDRESS}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].text, "{%ADDRESS}"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
// malformed (non-existent) env var (unix)
p = testParser(`:{$PORT$}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Port, ""; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// in quoted field
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].text, "Test foobar test"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
}
func setupParseTests() {
// Set up some bogus directives for testing
ValidDirectives = map[string]struct{}{
"dir1": {},
"dir2": {},
"dir3": {},
}
}
func testParser(input string) parser {
buf := strings.NewReader(input)
p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true}
return p
}
+166
View File
@@ -0,0 +1,166 @@
// +build !windows
package caddy
import (
"bytes"
"encoding/gob"
"errors"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"path"
"sync/atomic"
"github.com/mholt/caddy/caddy/https"
)
func init() {
gob.Register(CaddyfileInput{})
}
// Restart restarts the entire application; gracefully with zero
// downtime if on a POSIX-compatible system, or forcefully if on
// Windows but with imperceptibly-short downtime.
//
// The restarted application will use newCaddyfile as its input
// configuration. If newCaddyfile is nil, the current (existing)
// Caddyfile configuration will be used.
//
// Note: The process must exist in the same place on the disk in
// order for this to work. Thus, multiple graceful restarts don't
// work if executing with `go run`, since the binary is cleaned up
// when `go run` sees the initial parent process exit.
func Restart(newCaddyfile Input) error {
log.Println("[INFO] Restarting")
if newCaddyfile == nil {
caddyfileMu.Lock()
newCaddyfile = caddyfile
caddyfileMu.Unlock()
}
// Get certificates for any new hosts in the new Caddyfile without causing downtime
err := getCertsForNewCaddyfile(newCaddyfile)
if err != nil {
return errors.New("TLS preload: " + err.Error())
}
if len(os.Args) == 0 { // this should never happen, but...
os.Args = []string{""}
}
// Tell the child that it's a restart
os.Setenv("CADDY_RESTART", "true")
// Prepare our payload to the child process
cdyfileGob := caddyfileGob{
ListenerFds: make(map[string]uintptr),
Caddyfile: newCaddyfile,
OnDemandTLSCertsIssued: atomic.LoadInt32(https.OnDemandIssuedCount),
}
// Prepare a pipe to the fork's stdin so it can get the Caddyfile
rpipe, wpipe, err := os.Pipe()
if err != nil {
return err
}
// Prepare a pipe that the child process will use to communicate
// its success with us by sending > 0 bytes
sigrpipe, sigwpipe, err := os.Pipe()
if err != nil {
return err
}
// Pass along relevant file descriptors to child process; ordering
// is very important since we rely on these being in certain positions.
extraFiles := []*os.File{sigwpipe} // fd 3
// Add file descriptors of all the sockets
serversMu.Lock()
for i, s := range servers {
extraFiles = append(extraFiles, s.ListenerFd())
cdyfileGob.ListenerFds[s.Addr] = uintptr(4 + i) // 4 fds come before any of the listeners
}
serversMu.Unlock()
// Set up the command
cmd := exec.Command(os.Args[0], os.Args[1:]...)
cmd.Stdin = rpipe // fd 0
cmd.Stdout = os.Stdout // fd 1
cmd.Stderr = os.Stderr // fd 2
cmd.ExtraFiles = extraFiles
// Spawn the child process
err = cmd.Start()
if err != nil {
return err
}
// Immediately close our dup'ed fds and the write end of our signal pipe
for _, f := range extraFiles {
f.Close()
}
// Feed Caddyfile to the child
err = gob.NewEncoder(wpipe).Encode(cdyfileGob)
if err != nil {
return err
}
wpipe.Close()
// Determine whether child startup succeeded
answer, readErr := ioutil.ReadAll(sigrpipe)
if answer == nil || len(answer) == 0 {
cmdErr := cmd.Wait() // get exit status
log.Printf("[ERROR] Restart: child failed to initialize (%v) - changes not applied", cmdErr)
if readErr != nil {
log.Printf("[ERROR] Restart: additionally, error communicating with child process: %v", readErr)
}
return errIncompleteRestart
}
// Looks like child is successful; we can exit gracefully.
return Stop()
}
func getCertsForNewCaddyfile(newCaddyfile Input) error {
// parse the new caddyfile only up to (and including) TLS
// so we can know what we need to get certs for.
configs, _, _, err := loadConfigsUpToIncludingTLS(path.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body()))
if err != nil {
return errors.New("loading Caddyfile: " + err.Error())
}
// first mark the configs that are qualified for managed TLS
https.MarkQualified(configs)
// since we group by bind address to obtain certs, we must call
// EnableTLS to make sure the port is set properly first
// (can ignore error since we aren't actually using the certs)
https.EnableTLS(configs, false)
// find out if we can let the acme package start its own challenge listener
// on port 80
var proxyACME bool
serversMu.Lock()
for _, s := range servers {
_, port, _ := net.SplitHostPort(s.Addr)
if port == "80" {
proxyACME = true
break
}
}
serversMu.Unlock()
// place certs on the disk
err = https.ObtainCerts(configs, false, proxyACME)
if err != nil {
return errors.New("obtaining certs: " + err.Error())
}
return nil
}
+31
View File
@@ -0,0 +1,31 @@
package caddy
import "log"
// Restart restarts Caddy forcefully using newCaddyfile,
// or, if nil, the current/existing Caddyfile is reused.
func Restart(newCaddyfile Input) error {
log.Println("[INFO] Restarting")
if newCaddyfile == nil {
caddyfileMu.Lock()
newCaddyfile = caddyfile
caddyfileMu.Unlock()
}
wg.Add(1) // barrier so Wait() doesn't unblock
err := Stop()
if err != nil {
return err
}
err = Start(newCaddyfile)
if err != nil {
return err
}
wg.Done() // take down our barrier
return nil
}
+72
View File
@@ -0,0 +1,72 @@
package setup
import (
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/basicauth"
)
// BasicAuth configures a new BasicAuth middleware instance.
func BasicAuth(c *Controller) (middleware.Middleware, error) {
root := c.Root
rules, err := basicAuthParse(c)
if err != nil {
return nil, err
}
basic := basicauth.BasicAuth{Rules: rules}
return func(next middleware.Handler) middleware.Handler {
basic.Next = next
basic.SiteRoot = root
return basic
}, nil
}
func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
var rules []basicauth.Rule
var err error
for c.Next() {
var rule basicauth.Rule
args := c.RemainingArgs()
switch len(args) {
case 2:
rule.Username = args[0]
if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil {
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
}
for c.NextBlock() {
rule.Resources = append(rule.Resources, c.Val())
if c.NextArg() {
return rules, c.Errf("Expecting only one resource per line (extra '%s')", c.Val())
}
}
case 3:
rule.Resources = append(rule.Resources, args[0])
rule.Username = args[1]
if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil {
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
}
default:
return rules, c.ArgErr()
}
rules = append(rules, rule)
}
return rules, nil
}
func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) {
if !strings.HasPrefix(passw, "htpasswd=") {
return basicauth.PlainMatcher(passw), nil
}
return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot)
}
+132
View File
@@ -0,0 +1,132 @@
package setup
import (
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/mholt/caddy/middleware/basicauth"
)
func TestBasicAuth(t *testing.T) {
c := NewTestController(`basicauth user pwd`)
mid, err := BasicAuth(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(basicauth.BasicAuth)
if !ok {
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestBasicAuthParse(t *testing.T) {
htpasswdPasswd := "IedFOuGmTpT8"
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
var skipHtpassword bool
htfh, err := ioutil.TempFile(".", "basicauth-")
if err != nil {
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
skipHtpassword = true
} else {
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
}
htfh.Close()
defer os.Remove(htfh.Name())
}
tests := []struct {
input string
shouldErr bool
password string
expected []basicauth.Rule
}{
{`basicauth user pwd`, false, "pwd", []basicauth.Rule{
{Username: "user"},
}},
{`basicauth user pwd {
}`, false, "pwd", []basicauth.Rule{
{Username: "user"},
}},
{`basicauth user pwd {
/resource1
/resource2
}`, false, "pwd", []basicauth.Rule{
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
}},
{`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{
{Username: "user", Resources: []string{"/resource"}},
}},
{`basicauth /res1 user1 pwd1
basicauth /res2 user2 pwd2`, false, "pwd", []basicauth.Rule{
{Username: "user1", Resources: []string{"/res1"}},
{Username: "user2", Resources: []string{"/res2"}},
}},
{`basicauth user`, true, "", []basicauth.Rule{}},
{`basicauth`, true, "", []basicauth.Rule{}},
{`basicauth /resource user pwd asdf`, true, "", []basicauth.Rule{}},
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []basicauth.Rule{
{Username: "sha1"},
}},
}
for i, test := range tests {
c := NewTestController(test.input)
actual, err := basicAuthParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, expectedRule := range test.expected {
actualRule := actual[j]
if actualRule.Username != expectedRule.Username {
t.Errorf("Test %d, rule %d: Expected username '%s', got '%s'",
i, j, expectedRule.Username, actualRule.Username)
}
if strings.Contains(test.input, "htpasswd=") && skipHtpassword {
continue
}
pwd := test.password
if len(actual) > 1 {
pwd = fmt.Sprintf("%s%d", pwd, j+1)
}
if !actualRule.Password(pwd) || actualRule.Password(test.password+"!") {
t.Errorf("Test %d, rule %d: Expected password '%v', got '%v'",
i, j, test.password, actualRule.Password(""))
}
expectedRes := fmt.Sprintf("%v", expectedRule.Resources)
actualRes := fmt.Sprintf("%v", actualRule.Resources)
if actualRes != expectedRes {
t.Errorf("Test %d, rule %d: Expected resource list %s, but got %s",
i, j, expectedRes, actualRes)
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
package setup
import "github.com/mholt/caddy/middleware"
// BindHost sets the host to bind the listener to.
func BindHost(c *Controller) (middleware.Middleware, error) {
for c.Next() {
if !c.Args(&c.BindHost) {
return nil, c.ArgErr()
}
}
return nil, nil
}
+368
View File
@@ -0,0 +1,368 @@
package setup
import (
"fmt"
"io/ioutil"
"text/template"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/browse"
)
// Browse configures a new Browse middleware instance.
func Browse(c *Controller) (middleware.Middleware, error) {
configs, err := browseParse(c)
if err != nil {
return nil, err
}
browse := browse.Browse{
Root: c.Root,
Configs: configs,
IgnoreIndexes: false,
}
return func(next middleware.Handler) middleware.Handler {
browse.Next = next
return browse
}, nil
}
func browseParse(c *Controller) ([]browse.Config, error) {
var configs []browse.Config
appendCfg := func(bc browse.Config) error {
for _, c := range configs {
if c.PathScope == bc.PathScope {
return fmt.Errorf("duplicate browsing config for %s", c.PathScope)
}
}
configs = append(configs, bc)
return nil
}
for c.Next() {
var bc browse.Config
// First argument is directory to allow browsing; default is site root
if c.NextArg() {
bc.PathScope = c.Val()
} else {
bc.PathScope = "/"
}
// Second argument would be the template file to use
var tplText string
if c.NextArg() {
tplBytes, err := ioutil.ReadFile(c.Val())
if err != nil {
return configs, err
}
tplText = string(tplBytes)
} else {
tplText = defaultTemplate
}
// Build the template
tpl, err := template.New("listing").Parse(tplText)
if err != nil {
return configs, err
}
bc.Template = tpl
// Save configuration
err = appendCfg(bc)
if err != nil {
return configs, err
}
}
return configs, nil
}
// The default template to use when serving up directory listings
const defaultTemplate = `<!DOCTYPE html>
<html>
<head>
<title>{{.Name}}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { padding: 0; margin: 0; }
body {
font-family: sans-serif;
text-rendering: optimizespeed;
}
a {
color: #006ed3;
text-decoration: none;
}
a:hover,
h1 a:hover {
color: #319cff;
}
header,
.content {
padding-left: 5%;
padding-right: 5%;
}
th:first-child,
td:first-child {
padding-left: 5%;
}
th:last-child,
td:last-child {
padding-right: 5%;
}
header {
padding-top: 25px;
padding-bottom: 15px;
background-color: #f2f2f2;
}
h1 {
font-size: 20px;
font-weight: normal;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
}
h1 a {
color: inherit;
}
h1 a:hover {
text-decoration: underline;
}
main {
display: block;
}
.meta {
font-size: 12px;
font-family: Verdana, sans-serif;
border-bottom: 1px solid #9C9C9C;
padding-top: 15px;
padding-bottom: 15px;
}
.meta-item {
margin-right: 1em;
}
table {
width: 100%;
border-collapse: collapse;
}
tr {
border-bottom: 1px dashed #dadada;
}
tr:not(:first-child):hover {
background-color: #ffffec;
}
th,
td {
text-align: left;
padding: 10px 0;
}
th {
padding-top: 15px;
padding-bottom: 15px;
font-size: 16px;
white-space: nowrap;
}
th a {
color: black;
}
th svg {
vertical-align: middle;
}
td {
font-size: 14px;
}
td:first-child {
width: 50%;
}
th:last-child,
td:last-child {
text-align: right;
}
td:first-child svg {
position: absolute;
}
td .name {
margin-left: 1.75em;
word-break: break-all;
overflow-wrap: break-word;
white-space: pre-wrap;
}
footer {
padding: 40px 20px;
font-size: 12px;
text-align: center;
}
@media (max-width: 600px) {
.hideable {
display: none;
}
td:first-child {
width: auto;
}
th:nth-child(2),
td:nth-child(2) {
padding-right: 5%;
text-align: right;
}
}
</style>
</head>
<body>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="0" width="0" style="position: absolute;">
<defs>
<!-- Folder -->
<linearGradient id="f" y2="640" gradientUnits="userSpaceOnUse" x2="244.84" gradientTransform="matrix(.97319 0 0 1.0135 -.50695 -13.679)" y1="415.75" x1="244.84">
<stop stop-color="#b3ddfd" offset="0"/>
<stop stop-color="#69c" offset="1"/>
</linearGradient>
<linearGradient id="e" y2="571.06" gradientUnits="userSpaceOnUse" x2="238.03" gradientTransform="translate(0,2)" y1="346.05" x1="236.26">
<stop stop-color="#ace" offset="0"/>
<stop stop-color="#369" offset="1"/>
</linearGradient>
<g id="folder" transform="translate(-266.06 -193.36)">
<g transform="matrix(.066019 0 0 .066019 264.2 170.93)">
<g transform="matrix(1.4738 0 0 1.4738 -52.053 -166.93)">
<path fill="#69c" d="m98.424 343.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="409.69" x="54.428" fill="#369"/>
<path fill="url(#e)" d="m98.424 345.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="407.69" x="54.428" fill="url(#f)"/>
</g>
</g>
</g>
<!-- File -->
<linearGradient id="a">
<stop stop-color="#cbcbcb" offset="0"/>
<stop stop-color="#f0f0f0" offset=".34923"/>
<stop stop-color="#e2e2e2" offset="1"/>
</linearGradient>
<linearGradient id="d" y2="686.15" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="207.83" gradientTransform="matrix(.28346 0 0 .31053 -608.52 485.11)" x2="380.1" x1="749.25"/>
<linearGradient id="c" y2="287.74" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="169.44" gradientTransform="matrix(.28342 0 0 .31057 -608.52 485.11)" x2="622.33" x1="741.64"/>
<linearGradient id="b" y2="418.54" gradientUnits="userSpaceOnUse" y1="236.13" gradientTransform="matrix(.29343 0 0 .29999 -608.52 485.11)" x2="330.88" x1="687.96">
<stop stop-color="#fff" offset="0"/>
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
</linearGradient>
<g id="file" transform="translate(-278.15 -216.59)">
<g fill-rule="evenodd" transform="matrix(.19775 0 0 .19775 381.05 112.68)">
<path d="m-520.17 525.5v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke-width=".42649" fill="#fff"/>
<g>
<path d="m-520.11 525.68v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke="#000" stroke-width=".42649" fill="url(#d)"/>
<path d="m-386 562.42c-10.108-2.9925-23.206-2.5682-33.101-0.86253 1.7084-10.962 1.922-24.701-0.4271-35.877l33.528 36.739z" stroke-width=".95407pt" fill="url(#c)"/>
<path d="m-519.13 537-0.60402 134.7h131.68l0.0755-33.296c-2.9446 1.1325-32.692-40.998-70.141-39.186-37.483 1.8137-27.785-56.777-61.006-62.214z" stroke-width="1pt" fill="url(#b)"/>
</g>
</g>
</g>
<!-- Up arrow -->
<g id="up-arrow" transform="translate(-279.22 -208.12)">
<path transform="matrix(.22413 0 0 .12089 335.67 164.35)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/>
</g>
<!-- Down arrow -->
<g id="down-arrow" transform="translate(-279.22 -208.12)">
<path transform="matrix(.22413 0 0 -.12089 335.67 257.93)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/>
</g>
</defs>
</svg>
<header>
<h1>{{.LinkedPath}}</h1>
</header>
<main>
<div class="meta">
<div class="content">
<span class="meta-item"><b>{{.NumDirs}}</b> director{{if eq 1 .NumDirs}}y{{else}}ies{{end}}</span>
<span class="meta-item"><b>{{.NumFiles}}</b> file{{if ne 1 .NumFiles}}s{{end}}</span>
</div>
</div>
<div class="listing">
<table>
<tr>
<th>
{{if and (eq .Sort "name") (ne .Order "desc")}}
<a href="?sort=name&order=desc">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{else if and (eq .Sort "name") (ne .Order "asc")}}
<a href="?sort=name&order=asc">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{else}}
<a href="?sort=name&order=asc">Name</a>
{{end}}
</th>
<th>
{{if and (eq .Sort "size") (ne .Order "desc")}}
<a href="?sort=size&order=desc">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a></a>
{{else if and (eq .Sort "size") (ne .Order "asc")}}
<a href="?sort=size&order=asc">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a></a>
{{else}}
<a href="?sort=size&order=asc">Size</a>
{{end}}
</th>
<th class="hideable">
{{if and (eq .Sort "time") (ne .Order "desc")}}
<a href="?sort=time&order=desc">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a></a>
{{else if and (eq .Sort "time") (ne .Order "asc")}}
<a href="?sort=time&order=asc">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a></a>
{{else}}
<a href="?sort=time&order=asc">Modified</a>
{{end}}
</th>
</tr>
{{range .Items}}
<tr>
<td>
<a href="{{.URL}}">
{{if .IsDir}}
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg>
{{else}}
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg>
{{end}}
<span class="name">{{.Name}}</span>
</a>
</td>
<td>{{.HumanSize}}</td>
<td class="hideable">{{.HumanModTime "01/02/2006 03:04:05 PM"}}</td>
</tr>
{{end}}
</table>
</div>
</main>
<footer>
Served with <a href="https://caddyserver.com">Caddy</a>
</footer>
</body>
</html>`
+65
View File
@@ -0,0 +1,65 @@
package setup
import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/mholt/caddy/middleware/browse"
)
func TestBrowse(t *testing.T) {
tempDirPath, err := getTempDirPath()
if err != nil {
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
}
nonExistantDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano())))
tempTemplate, err := ioutil.TempFile(".", "tempTemplate")
if err != nil {
t.Fatalf("BeforeTest: Failed to create a temporary file in the working directory! Error was: %v", err)
}
defer os.Remove(tempTemplate.Name())
tempTemplatePath := filepath.Join(".", tempTemplate.Name())
for i, test := range []struct {
input string
expectedPathScope []string
shouldErr bool
}{
// test case #0 tests handling of multiple pathscopes
{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false},
// test case #1 tests instantiation of browse.Config with default values
{"browse /", []string{"/"}, false},
// test case #2 tests detectaction of custom template
{"browse . " + tempTemplatePath, []string{"."}, false},
// test case #3 tests detection of non-existent template
{"browse . " + nonExistantDirPath, nil, true},
// test case #4 tests detection of duplicate pathscopes
{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true},
} {
recievedFunc, err := Browse(NewTestController(test.input))
if err != nil && !test.shouldErr {
t.Errorf("Test case #%d recieved an error of %v", i, err)
}
if test.expectedPathScope == nil {
continue
}
recievedConfigs := recievedFunc(nil).(browse.Browse).Configs
for j, config := range recievedConfigs {
if config.PathScope != test.expectedPathScope[j] {
t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope)
}
}
}
}
+83
View File
@@ -0,0 +1,83 @@
package setup
import (
"fmt"
"net/http"
"strings"
"github.com/mholt/caddy/caddy/parse"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/server"
)
// Controller is given to the setup function of middlewares which
// gives them access to be able to read tokens and set config. Each
// virtualhost gets their own server config and dispenser.
type Controller struct {
*server.Config
parse.Dispenser
// OncePerServerBlock is a function that executes f
// exactly once per server block, no matter how many
// hosts are associated with it. If it is the first
// time, the function f is executed immediately
// (not deferred) and may return an error which is
// returned by OncePerServerBlock.
OncePerServerBlock func(f func() error) error
// ServerBlockIndex is the 0-based index of the
// server block as it appeared in the input.
ServerBlockIndex int
// ServerBlockHostIndex is the 0-based index of this
// host as it appeared in the input at the head of the
// server block.
ServerBlockHostIndex int
// ServerBlockHosts is a list of hosts that are
// associated with this server block. All these
// hosts, consequently, share the same tokens.
ServerBlockHosts []string
// ServerBlockStorage is used by a directive's
// setup function to persist state between all
// the hosts on a server block.
ServerBlockStorage interface{}
}
// NewTestController creates a new *Controller for
// the input specified, with a filename of "Testfile".
// The Config is bare, consisting only of a Root of cwd.
//
// Used primarily for testing but needs to be exported so
// add-ons can use this as a convenience. Does not initialize
// the server-block-related fields.
func NewTestController(input string) *Controller {
return &Controller{
Config: &server.Config{
Root: ".",
},
Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)),
OncePerServerBlock: func(f func() error) error {
return f()
},
}
}
// EmptyNext is a no-op function that can be passed into
// middleware.Middleware functions so that the assignment
// to the Next field of the Handler can be tested.
//
// Used primarily for testing but needs to be exported so
// add-ons can use this as a convenience.
var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
return 0, nil
})
// SameNext does a pointer comparison between next1 and next2.
//
// Used primarily for testing but needs to be exported so
// add-ons can use this as a convenience.
func SameNext(next1, next2 middleware.Handler) bool {
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
}
+148
View File
@@ -0,0 +1,148 @@
package setup
import (
"io"
"log"
"os"
"path/filepath"
"strconv"
"github.com/hashicorp/go-syslog"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/errors"
)
// Errors configures a new errors middleware instance.
func Errors(c *Controller) (middleware.Middleware, error) {
handler, err := errorsParse(c)
if err != nil {
return nil, err
}
// Open the log file for writing when the server starts
c.Startup = append(c.Startup, func() error {
var err error
var writer io.Writer
switch handler.LogFile {
case "visible":
handler.Debug = true
case "stdout":
writer = os.Stdout
case "stderr":
writer = os.Stderr
case "syslog":
writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy")
if err != nil {
return err
}
default:
if handler.LogFile == "" {
writer = os.Stderr // default
break
}
var file *os.File
file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return err
}
if handler.LogRoller != nil {
file.Close()
handler.LogRoller.Filename = handler.LogFile
writer = handler.LogRoller.GetLogWriter()
} else {
writer = file
}
}
handler.Log = log.New(writer, "", 0)
return nil
})
return func(next middleware.Handler) middleware.Handler {
handler.Next = next
return handler
}, nil
}
func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
// Very important that we make a pointer because the Startup
// function that opens the log file must have access to the
// same instance of the handler, not a copy.
handler := &errors.ErrorHandler{ErrorPages: make(map[int]string)}
optionalBlock := func() (bool, error) {
var hadBlock bool
for c.NextBlock() {
hadBlock = true
what := c.Val()
if !c.NextArg() {
return hadBlock, c.ArgErr()
}
where := c.Val()
if what == "log" {
if where == "visible" {
handler.Debug = true
} else {
handler.LogFile = where
if c.NextArg() {
if c.Val() == "{" {
c.IncrNest()
logRoller, err := parseRoller(c)
if err != nil {
return hadBlock, err
}
handler.LogRoller = logRoller
}
}
}
} else {
// Error page; ensure it exists
where = filepath.Join(c.Root, where)
f, err := os.Open(where)
if err != nil {
log.Printf("[WARNING] Unable to open error page '%s': %v", where, err)
}
f.Close()
whatInt, err := strconv.Atoi(what)
if err != nil {
return hadBlock, c.Err("Expecting a numeric status code, got '" + what + "'")
}
handler.ErrorPages[whatInt] = where
}
}
return hadBlock, nil
}
for c.Next() {
// weird hack to avoid having the handler values overwritten.
if c.Val() == "}" {
continue
}
// Configuration may be in a block
hadBlock, err := optionalBlock()
if err != nil {
return handler, err
}
// Otherwise, the only argument would be an error log file name or 'visible'
if !hadBlock {
if c.NextArg() {
if c.Val() == "visible" {
handler.Debug = true
} else {
handler.LogFile = c.Val()
}
}
}
}
return handler, nil
}
+158
View File
@@ -0,0 +1,158 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/errors"
)
func TestErrors(t *testing.T) {
c := NewTestController(`errors`)
mid, err := Errors(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(*errors.ErrorHandler)
if !ok {
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
}
if myHandler.LogFile != "" {
t.Errorf("Expected '%s' as the default LogFile", "")
}
if myHandler.LogRoller != nil {
t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
// Test Startup function
if len(c.Startup) == 0 {
t.Fatal("Expected 1 startup function, had 0")
}
err = c.Startup[0]()
if myHandler.Log == nil {
t.Error("Expected Log to be non-nil after startup because Debug is not enabled")
}
}
func TestErrorsParse(t *testing.T) {
tests := []struct {
inputErrorsRules string
shouldErr bool
expectedErrorHandler errors.ErrorHandler
}{
{`errors`, false, errors.ErrorHandler{
LogFile: "",
}},
{`errors errors.txt`, false, errors.ErrorHandler{
LogFile: "errors.txt",
}},
{`errors visible`, false, errors.ErrorHandler{
LogFile: "",
Debug: true,
}},
{`errors { log visible }`, false, errors.ErrorHandler{
LogFile: "",
Debug: true,
}},
{`errors { log errors.txt
404 404.html
500 500.html
}`, false, errors.ErrorHandler{
LogFile: "errors.txt",
ErrorPages: map[int]string{
404: "404.html",
500: "500.html",
},
}},
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, errors.ErrorHandler{
LogFile: "errors.txt",
LogRoller: &middleware.LogRoller{
MaxSize: 2,
MaxAge: 10,
MaxBackups: 3,
LocalTime: true,
},
}},
{`errors { log errors.txt {
size 3
age 11
keep 5
}
404 404.html
503 503.html
}`, false, errors.ErrorHandler{
LogFile: "errors.txt",
ErrorPages: map[int]string{
404: "404.html",
503: "503.html",
},
LogRoller: &middleware.LogRoller{
MaxSize: 3,
MaxAge: 11,
MaxBackups: 5,
LocalTime: true,
},
}},
}
for i, test := range tests {
c := NewTestController(test.inputErrorsRules)
actualErrorsRule, err := errorsParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if actualErrorsRule.LogFile != test.expectedErrorHandler.LogFile {
t.Errorf("Test %d expected LogFile to be %s, but got %s",
i, test.expectedErrorHandler.LogFile, actualErrorsRule.LogFile)
}
if actualErrorsRule.Debug != test.expectedErrorHandler.Debug {
t.Errorf("Test %d expected Debug to be %v, but got %v",
i, test.expectedErrorHandler.Debug, actualErrorsRule.Debug)
}
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller == nil || actualErrorsRule.LogRoller == nil && test.expectedErrorHandler.LogRoller != nil {
t.Fatalf("Test %d expected LogRoller to be %v, but got %v",
i, test.expectedErrorHandler.LogRoller, actualErrorsRule.LogRoller)
}
if len(actualErrorsRule.ErrorPages) != len(test.expectedErrorHandler.ErrorPages) {
t.Fatalf("Test %d expected %d no of Error pages, but got %d ",
i, len(test.expectedErrorHandler.ErrorPages), len(actualErrorsRule.ErrorPages))
}
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller != nil {
if actualErrorsRule.LogRoller.Filename != test.expectedErrorHandler.LogRoller.Filename {
t.Fatalf("Test %d expected LogRoller Filename to be %s, but got %s",
i, test.expectedErrorHandler.LogRoller.Filename, actualErrorsRule.LogRoller.Filename)
}
if actualErrorsRule.LogRoller.MaxAge != test.expectedErrorHandler.LogRoller.MaxAge {
t.Fatalf("Test %d expected LogRoller MaxAge to be %d, but got %d",
i, test.expectedErrorHandler.LogRoller.MaxAge, actualErrorsRule.LogRoller.MaxAge)
}
if actualErrorsRule.LogRoller.MaxBackups != test.expectedErrorHandler.LogRoller.MaxBackups {
t.Fatalf("Test %d expected LogRoller MaxBackups to be %d, but got %d",
i, test.expectedErrorHandler.LogRoller.MaxBackups, actualErrorsRule.LogRoller.MaxBackups)
}
if actualErrorsRule.LogRoller.MaxSize != test.expectedErrorHandler.LogRoller.MaxSize {
t.Fatalf("Test %d expected LogRoller MaxSize to be %d, but got %d",
i, test.expectedErrorHandler.LogRoller.MaxSize, actualErrorsRule.LogRoller.MaxSize)
}
if actualErrorsRule.LogRoller.LocalTime != test.expectedErrorHandler.LogRoller.LocalTime {
t.Fatalf("Test %d expected LogRoller LocalTime to be %t, but got %t",
i, test.expectedErrorHandler.LogRoller.LocalTime, actualErrorsRule.LogRoller.LocalTime)
}
}
}
}
+54
View File
@@ -0,0 +1,54 @@
package setup
import (
"os"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/extensions"
)
// Ext configures a new instance of 'extensions' middleware for clean URLs.
func Ext(c *Controller) (middleware.Middleware, error) {
root := c.Root
exts, err := extParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return extensions.Ext{
Next: next,
Extensions: exts,
Root: root,
}
}, nil
}
// extParse sets up an instance of extension middleware
// from a middleware controller and returns a list of extensions.
func extParse(c *Controller) ([]string, error) {
var exts []string
for c.Next() {
// At least one extension is required
if !c.NextArg() {
return exts, c.ArgErr()
}
exts = append(exts, c.Val())
// Tack on any other extensions that may have been listed
exts = append(exts, c.RemainingArgs()...)
}
return exts, nil
}
// resourceExists returns true if the file specified at
// root + path exists; false otherwise.
func resourceExists(root, path string) bool {
_, err := os.Stat(root + path)
// technically we should use os.IsNotExist(err)
// but we don't handle any other kinds of errors anyway
return err == nil
}
+76
View File
@@ -0,0 +1,76 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware/extensions"
)
func TestExt(t *testing.T) {
c := NewTestController(`ext .html .htm .php`)
mid, err := Ext(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(extensions.Ext)
if !ok {
t.Fatalf("Expected handler to be type Ext, got: %#v", handler)
}
if myHandler.Extensions[0] != ".html" {
t.Errorf("Expected .html in the list of Extensions")
}
if myHandler.Extensions[1] != ".htm" {
t.Errorf("Expected .htm in the list of Extensions")
}
if myHandler.Extensions[2] != ".php" {
t.Errorf("Expected .php in the list of Extensions")
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestExtParse(t *testing.T) {
tests := []struct {
inputExts string
shouldErr bool
expectedExts []string
}{
{`ext .html .htm .php`, false, []string{".html", ".htm", ".php"}},
{`ext .php .html .xml`, false, []string{".php", ".html", ".xml"}},
{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}},
}
for i, test := range tests {
c := NewTestController(test.inputExts)
actualExts, err := extParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualExts) != len(test.expectedExts) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expectedExts), len(actualExts))
}
for j, actualExt := range actualExts {
if actualExt != test.expectedExts[j] {
t.Fatalf("Test %d expected %dth extension to be %s , but got %s",
i, j, test.expectedExts[j], actualExt)
}
}
}
}
+110
View File
@@ -0,0 +1,110 @@
package setup
import (
"errors"
"net/http"
"path/filepath"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/fastcgi"
)
// FastCGI configures a new FastCGI middleware instance.
func FastCGI(c *Controller) (middleware.Middleware, error) {
absRoot, err := filepath.Abs(c.Root)
if err != nil {
return nil, err
}
rules, err := fastcgiParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return fastcgi.Handler{
Next: next,
Rules: rules,
Root: c.Root,
AbsRoot: absRoot,
FileSys: http.Dir(c.Root),
SoftwareName: c.AppName,
SoftwareVersion: c.AppVersion,
ServerName: c.Host,
ServerPort: c.Port,
}
}, nil
}
func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
var rules []fastcgi.Rule
for c.Next() {
var rule fastcgi.Rule
args := c.RemainingArgs()
switch len(args) {
case 0:
return rules, c.ArgErr()
case 1:
rule.Path = "/"
rule.Address = args[0]
case 2:
rule.Path = args[0]
rule.Address = args[1]
case 3:
rule.Path = args[0]
rule.Address = args[1]
err := fastcgiPreset(args[2], &rule)
if err != nil {
return rules, c.Err("Invalid fastcgi rule preset '" + args[2] + "'")
}
}
for c.NextBlock() {
switch c.Val() {
case "ext":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.Ext = c.Val()
case "split":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.SplitPath = c.Val()
case "index":
args := c.RemainingArgs()
if len(args) == 0 {
return rules, c.ArgErr()
}
rule.IndexFiles = args
case "env":
envArgs := c.RemainingArgs()
if len(envArgs) < 2 {
return rules, c.ArgErr()
}
rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]})
}
}
rules = append(rules, rule)
}
return rules, nil
}
// fastcgiPreset configures rule according to name. It returns an error if
// name is not a recognized preset name.
func fastcgiPreset(name string, rule *fastcgi.Rule) error {
switch name {
case "php":
rule.Ext = ".php"
rule.SplitPath = ".php"
rule.IndexFiles = []string{"index.php"}
default:
return errors.New(name + " is not a valid preset name")
}
return nil
}
+107
View File
@@ -0,0 +1,107 @@
package setup
import (
"fmt"
"github.com/mholt/caddy/middleware/fastcgi"
"testing"
)
func TestFastCGI(t *testing.T) {
c := NewTestController(`fastcgi / 127.0.0.1:9000`)
mid, err := FastCGI(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(fastcgi.Handler)
if !ok {
t.Fatalf("Expected handler to be type , got: %#v", handler)
}
if myHandler.Rules[0].Path != "/" {
t.Errorf("Expected / as the Path")
}
if myHandler.Rules[0].Address != "127.0.0.1:9000" {
t.Errorf("Expected 127.0.0.1:9000 as the Address")
}
}
func TestFastcgiParse(t *testing.T) {
tests := []struct {
inputFastcgiConfig string
shouldErr bool
expectedFastcgiConfig []fastcgi.Rule
}{
{`fastcgi /blog 127.0.0.1:9000 php`,
false, []fastcgi.Rule{{
Path: "/blog",
Address: "127.0.0.1:9000",
Ext: ".php",
SplitPath: ".php",
IndexFiles: []string{"index.php"},
}}},
{`fastcgi / 127.0.0.1:9001 {
split .html
}`,
false, []fastcgi.Rule{{
Path: "/",
Address: "127.0.0.1:9001",
Ext: "",
SplitPath: ".html",
IndexFiles: []string{},
}}},
}
for i, test := range tests {
c := NewTestController(test.inputFastcgiConfig)
actualFastcgiConfigs, err := fastcgiParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualFastcgiConfigs) != len(test.expectedFastcgiConfig) {
t.Fatalf("Test %d expected %d no of FastCGI configs, but got %d ",
i, len(test.expectedFastcgiConfig), len(actualFastcgiConfigs))
}
for j, actualFastcgiConfig := range actualFastcgiConfigs {
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
}
if actualFastcgiConfig.Address != test.expectedFastcgiConfig[j].Address {
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Address, actualFastcgiConfig.Address)
}
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
t.Errorf("Test %d expected %dth FastCGI Ext to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Ext, actualFastcgiConfig.Ext)
}
if actualFastcgiConfig.SplitPath != test.expectedFastcgiConfig[j].SplitPath {
t.Errorf("Test %d expected %dth FastCGI SplitPath to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
}
if fmt.Sprint(actualFastcgiConfig.IndexFiles) != fmt.Sprint(test.expectedFastcgiConfig[j].IndexFiles) {
t.Errorf("Test %d expected %dth FastCGI IndexFiles to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].IndexFiles, actualFastcgiConfig.IndexFiles)
}
}
}
}
+117
View File
@@ -0,0 +1,117 @@
package setup
import (
"fmt"
"strconv"
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/gzip"
)
// Gzip configures a new gzip middleware instance.
func Gzip(c *Controller) (middleware.Middleware, error) {
configs, err := gzipParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return gzip.Gzip{Next: next, Configs: configs}
}, nil
}
func gzipParse(c *Controller) ([]gzip.Config, error) {
var configs []gzip.Config
for c.Next() {
config := gzip.Config{}
// Request Filters
pathFilter := gzip.PathFilter{IgnoredPaths: make(gzip.Set)}
extFilter := gzip.ExtFilter{Exts: make(gzip.Set)}
// Response Filters
lengthFilter := gzip.LengthFilter(0)
// No extra args expected
if len(c.RemainingArgs()) > 0 {
return configs, c.ArgErr()
}
for c.NextBlock() {
switch c.Val() {
case "ext":
exts := c.RemainingArgs()
if len(exts) == 0 {
return configs, c.ArgErr()
}
for _, e := range exts {
if !strings.HasPrefix(e, ".") && e != gzip.ExtWildCard && e != "" {
return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e)
}
extFilter.Exts.Add(e)
}
case "not":
paths := c.RemainingArgs()
if len(paths) == 0 {
return configs, c.ArgErr()
}
for _, p := range paths {
if p == "/" {
return configs, fmt.Errorf(`gzip: cannot exclude path "/" - remove directive entirely instead`)
}
if !strings.HasPrefix(p, "/") {
return configs, fmt.Errorf(`gzip: invalid path "%v" (must start with /)`, p)
}
pathFilter.IgnoredPaths.Add(p)
}
case "level":
if !c.NextArg() {
return configs, c.ArgErr()
}
level, _ := strconv.Atoi(c.Val())
config.Level = level
case "min_length":
if !c.NextArg() {
return configs, c.ArgErr()
}
length, err := strconv.ParseInt(c.Val(), 10, 64)
if err != nil {
return configs, err
} else if length == 0 {
return configs, fmt.Errorf(`gzip: min_length must be greater than 0`)
}
lengthFilter = gzip.LengthFilter(length)
default:
return configs, c.ArgErr()
}
}
// Request Filters
config.RequestFilters = []gzip.RequestFilter{}
// If ignored paths are specified, put in front to filter with path first
if len(pathFilter.IgnoredPaths) > 0 {
config.RequestFilters = []gzip.RequestFilter{pathFilter}
}
// Then, if extensions are specified, use those to filter.
// Otherwise, use default extensions filter.
if len(extFilter.Exts) > 0 {
config.RequestFilters = append(config.RequestFilters, extFilter)
} else {
config.RequestFilters = append(config.RequestFilters, gzip.DefaultExtFilter())
}
// Response Filters
// If min_length is specified, use it.
if int64(lengthFilter) != 0 {
config.ResponseFilters = append(config.ResponseFilters, lengthFilter)
}
configs = append(configs, config)
}
return configs, nil
}
+101
View File
@@ -0,0 +1,101 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware/gzip"
)
func TestGzip(t *testing.T) {
c := NewTestController(`gzip`)
mid, err := Gzip(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(gzip.Gzip)
if !ok {
t.Fatalf("Expected handler to be type Gzip, got: %#v", handler)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
tests := []struct {
input string
shouldErr bool
}{
{`gzip {`, true},
{`gzip {}`, true},
{`gzip a b`, true},
{`gzip a {`, true},
{`gzip { not f } `, true},
{`gzip { not } `, true},
{`gzip { not /file
ext .html
level 1
} `, false},
{`gzip { level 9 } `, false},
{`gzip { ext } `, true},
{`gzip { ext /f
} `, true},
{`gzip { not /file
ext .html
level 1
}
gzip`, false},
{`gzip {
ext ""
}`, false},
{`gzip { not /file
ext .html
level 1
}
gzip { not /file1
ext .htm
level 3
}
`, false},
{`gzip { not /file
ext .html
level 1
}
gzip { not /file1
ext .htm
level 3
}
`, false},
{`gzip { not /file
ext *
level 1
}
`, false},
{`gzip { not /file
ext *
level 1
min_length ab
}
`, true},
{`gzip { not /file
ext *
level 1
min_length 1000
}
`, false},
}
for i, test := range tests {
c := NewTestController(test.input)
_, err := gzipParse(c)
if test.shouldErr && err == nil {
t.Errorf("Test %v: Expected error but found nil", i)
} else if !test.shouldErr && err != nil {
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
}
}
}
+84
View File
@@ -0,0 +1,84 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/headers"
)
// Headers configures a new Headers middleware instance.
func Headers(c *Controller) (middleware.Middleware, error) {
rules, err := headersParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return headers.Headers{Next: next, Rules: rules}
}, nil
}
func headersParse(c *Controller) ([]headers.Rule, error) {
var rules []headers.Rule
for c.NextLine() {
var head headers.Rule
var isNewPattern bool
if !c.NextArg() {
return rules, c.ArgErr()
}
pattern := c.Val()
// See if we already have a definition for this Path pattern...
for _, h := range rules {
if h.Path == pattern {
head = h
break
}
}
// ...otherwise, this is a new pattern
if head.Path == "" {
head.Path = pattern
isNewPattern = true
}
for c.NextBlock() {
// A block of headers was opened...
h := headers.Header{Name: c.Val()}
if c.NextArg() {
h.Value = c.Val()
}
head.Headers = append(head.Headers, h)
}
if c.NextArg() {
// ... or single header was defined as an argument instead.
h := headers.Header{Name: c.Val()}
h.Value = c.Val()
if c.NextArg() {
h.Value = c.Val()
}
head.Headers = append(head.Headers, h)
}
if isNewPattern {
rules = append(rules, head)
} else {
for i := 0; i < len(rules); i++ {
if rules[i].Path == pattern {
rules[i] = head
break
}
}
}
}
return rules, nil
}
+85
View File
@@ -0,0 +1,85 @@
package setup
import (
"fmt"
"testing"
"github.com/mholt/caddy/middleware/headers"
)
func TestHeaders(t *testing.T) {
c := NewTestController(`header / Foo Bar`)
mid, err := Headers(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(headers.Headers)
if !ok {
t.Fatalf("Expected handler to be type Headers, got: %#v", handler)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestHeadersParse(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expected []headers.Rule
}{
{`header /foo Foo "Bar Baz"`,
false, []headers.Rule{
{Path: "/foo", Headers: []headers.Header{
{"Foo", "Bar Baz"},
}},
}},
{`header /bar { Foo "Bar Baz" Baz Qux }`,
false, []headers.Rule{
{Path: "/bar", Headers: []headers.Header{
{"Foo", "Bar Baz"},
{"Baz", "Qux"},
}},
}},
}
for i, test := range tests {
c := NewTestController(test.input)
actual, err := headersParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, expectedRule := range test.expected {
actualRule := actual[j]
if actualRule.Path != expectedRule.Path {
t.Errorf("Test %d, rule %d: Expected path %s, but got %s",
i, j, expectedRule.Path, actualRule.Path)
}
expectedHeaders := fmt.Sprintf("%v", expectedRule.Headers)
actualHeaders := fmt.Sprintf("%v", actualRule.Headers)
if actualHeaders != expectedHeaders {
t.Errorf("Test %d, rule %d: Expected headers %s, but got %s",
i, j, expectedHeaders, actualHeaders)
}
}
}
}
+31
View File
@@ -0,0 +1,31 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/inner"
)
// Internal configures a new Internal middleware instance.
func Internal(c *Controller) (middleware.Middleware, error) {
paths, err := internalParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return inner.Internal{Next: next, Paths: paths}
}, nil
}
func internalParse(c *Controller) ([]string, error) {
var paths []string
for c.Next() {
if !c.NextArg() {
return paths, c.ArgErr()
}
paths = append(paths, c.Val())
}
return paths, nil
}
+72
View File
@@ -0,0 +1,72 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware/inner"
)
func TestInternal(t *testing.T) {
c := NewTestController(`internal /internal`)
mid, err := Internal(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(inner.Internal)
if !ok {
t.Fatalf("Expected handler to be type Internal, got: %#v", handler)
}
if myHandler.Paths[0] != "/internal" {
t.Errorf("Expected internal in the list of internal Paths")
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestInternalParse(t *testing.T) {
tests := []struct {
inputInternalPaths string
shouldErr bool
expectedInternalPaths []string
}{
{`internal /internal`, false, []string{"/internal"}},
{`internal /internal1
internal /internal2`, false, []string{"/internal1", "/internal2"}},
}
for i, test := range tests {
c := NewTestController(test.inputInternalPaths)
actualInternalPaths, err := internalParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualInternalPaths) != len(test.expectedInternalPaths) {
t.Fatalf("Test %d expected %d InternalPaths, but got %d",
i, len(test.expectedInternalPaths), len(actualInternalPaths))
}
for j, actualInternalPath := range actualInternalPaths {
if actualInternalPath != test.expectedInternalPaths[j] {
t.Fatalf("Test %d expected %dth Internal Path to be %s , but got %s",
i, j, test.expectedInternalPaths[j], actualInternalPath)
}
}
}
}
+130
View File
@@ -0,0 +1,130 @@
package setup
import (
"io"
"log"
"os"
"github.com/hashicorp/go-syslog"
"github.com/mholt/caddy/middleware"
caddylog "github.com/mholt/caddy/middleware/log"
"github.com/mholt/caddy/server"
)
// Log sets up the logging middleware.
func Log(c *Controller) (middleware.Middleware, error) {
rules, err := logParse(c)
if err != nil {
return nil, err
}
// Open the log files for writing when the server starts
c.Startup = append(c.Startup, func() error {
for i := 0; i < len(rules); i++ {
var err error
var writer io.Writer
if rules[i].OutputFile == "stdout" {
writer = os.Stdout
} else if rules[i].OutputFile == "stderr" {
writer = os.Stderr
} else if rules[i].OutputFile == "syslog" {
writer, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "caddy")
if err != nil {
return err
}
} else {
var file *os.File
file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return err
}
if rules[i].Roller != nil {
file.Close()
rules[i].Roller.Filename = rules[i].OutputFile
writer = rules[i].Roller.GetLogWriter()
} else {
writer = file
}
}
rules[i].Log = log.New(writer, "", 0)
}
return nil
})
return func(next middleware.Handler) middleware.Handler {
return caddylog.Logger{Next: next, Rules: rules, ErrorFunc: server.DefaultErrorFunc}
}, nil
}
func logParse(c *Controller) ([]caddylog.Rule, error) {
var rules []caddylog.Rule
for c.Next() {
args := c.RemainingArgs()
var logRoller *middleware.LogRoller
if c.NextBlock() {
if c.Val() == "rotate" {
if c.NextArg() {
if c.Val() == "{" {
var err error
logRoller, err = parseRoller(c)
if err != nil {
return nil, err
}
// This part doesn't allow having something after the rotate block
if c.Next() {
if c.Val() != "}" {
return nil, c.ArgErr()
}
}
}
}
}
}
if len(args) == 0 {
// Nothing specified; use defaults
rules = append(rules, caddylog.Rule{
PathScope: "/",
OutputFile: caddylog.DefaultLogFilename,
Format: caddylog.DefaultLogFormat,
Roller: logRoller,
})
} else if len(args) == 1 {
// Only an output file specified
rules = append(rules, caddylog.Rule{
PathScope: "/",
OutputFile: args[0],
Format: caddylog.DefaultLogFormat,
Roller: logRoller,
})
} else {
// Path scope, output file, and maybe a format specified
format := caddylog.DefaultLogFormat
if len(args) > 2 {
switch args[2] {
case "{common}":
format = caddylog.CommonLogFormat
case "{combined}":
format = caddylog.CombinedLogFormat
default:
format = args[2]
}
}
rules = append(rules, caddylog.Rule{
PathScope: args[0],
OutputFile: args[1],
Format: format,
Roller: logRoller,
})
}
}
return rules, nil
}
+175
View File
@@ -0,0 +1,175 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware"
caddylog "github.com/mholt/caddy/middleware/log"
)
func TestLog(t *testing.T) {
c := NewTestController(`log`)
mid, err := Log(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(caddylog.Logger)
if !ok {
t.Fatalf("Expected handler to be type Logger, got: %#v", handler)
}
if myHandler.Rules[0].PathScope != "/" {
t.Errorf("Expected / as the default PathScope")
}
if myHandler.Rules[0].OutputFile != caddylog.DefaultLogFilename {
t.Errorf("Expected %s as the default OutputFile", caddylog.DefaultLogFilename)
}
if myHandler.Rules[0].Format != caddylog.DefaultLogFormat {
t.Errorf("Expected %s as the default Log Format", caddylog.DefaultLogFormat)
}
if myHandler.Rules[0].Roller != nil {
t.Errorf("Expected Roller to be nil, got: %v", *myHandler.Rules[0].Roller)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestLogParse(t *testing.T) {
tests := []struct {
inputLogRules string
shouldErr bool
expectedLogRules []caddylog.Rule
}{
{`log`, false, []caddylog.Rule{{
PathScope: "/",
OutputFile: caddylog.DefaultLogFilename,
Format: caddylog.DefaultLogFormat,
}}},
{`log log.txt`, false, []caddylog.Rule{{
PathScope: "/",
OutputFile: "log.txt",
Format: caddylog.DefaultLogFormat,
}}},
{`log /api log.txt`, false, []caddylog.Rule{{
PathScope: "/api",
OutputFile: "log.txt",
Format: caddylog.DefaultLogFormat,
}}},
{`log /serve stdout`, false, []caddylog.Rule{{
PathScope: "/serve",
OutputFile: "stdout",
Format: caddylog.DefaultLogFormat,
}}},
{`log /myapi log.txt {common}`, false, []caddylog.Rule{{
PathScope: "/myapi",
OutputFile: "log.txt",
Format: caddylog.CommonLogFormat,
}}},
{`log /test accesslog.txt {combined}`, false, []caddylog.Rule{{
PathScope: "/test",
OutputFile: "accesslog.txt",
Format: caddylog.CombinedLogFormat,
}}},
{`log /api1 log.txt
log /api2 accesslog.txt {combined}`, false, []caddylog.Rule{{
PathScope: "/api1",
OutputFile: "log.txt",
Format: caddylog.DefaultLogFormat,
}, {
PathScope: "/api2",
OutputFile: "accesslog.txt",
Format: caddylog.CombinedLogFormat,
}}},
{`log /api3 stdout {host}
log /api4 log.txt {when}`, false, []caddylog.Rule{{
PathScope: "/api3",
OutputFile: "stdout",
Format: "{host}",
}, {
PathScope: "/api4",
OutputFile: "log.txt",
Format: "{when}",
}}},
{`log access.log { rotate { size 2 age 10 keep 3 } }`, false, []caddylog.Rule{{
PathScope: "/",
OutputFile: "access.log",
Format: caddylog.DefaultLogFormat,
Roller: &middleware.LogRoller{
MaxSize: 2,
MaxAge: 10,
MaxBackups: 3,
LocalTime: true,
},
}}},
}
for i, test := range tests {
c := NewTestController(test.inputLogRules)
actualLogRules, err := logParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualLogRules) != len(test.expectedLogRules) {
t.Fatalf("Test %d expected %d no of Log rules, but got %d ",
i, len(test.expectedLogRules), len(actualLogRules))
}
for j, actualLogRule := range actualLogRules {
if actualLogRule.PathScope != test.expectedLogRules[j].PathScope {
t.Errorf("Test %d expected %dth LogRule PathScope to be %s , but got %s",
i, j, test.expectedLogRules[j].PathScope, actualLogRule.PathScope)
}
if actualLogRule.OutputFile != test.expectedLogRules[j].OutputFile {
t.Errorf("Test %d expected %dth LogRule OutputFile to be %s , but got %s",
i, j, test.expectedLogRules[j].OutputFile, actualLogRule.OutputFile)
}
if actualLogRule.Format != test.expectedLogRules[j].Format {
t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s",
i, j, test.expectedLogRules[j].Format, actualLogRule.Format)
}
if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller == nil || actualLogRule.Roller == nil && test.expectedLogRules[j].Roller != nil {
t.Fatalf("Test %d expected %dth LogRule Roller to be %v, but got %v",
i, j, test.expectedLogRules[j].Roller, actualLogRule.Roller)
}
if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller != nil {
if actualLogRule.Roller.Filename != test.expectedLogRules[j].Roller.Filename {
t.Fatalf("Test %d expected %dth LogRule Roller Filename to be %s, but got %s",
i, j, test.expectedLogRules[j].Roller.Filename, actualLogRule.Roller.Filename)
}
if actualLogRule.Roller.MaxAge != test.expectedLogRules[j].Roller.MaxAge {
t.Fatalf("Test %d expected %dth LogRule Roller MaxAge to be %d, but got %d",
i, j, test.expectedLogRules[j].Roller.MaxAge, actualLogRule.Roller.MaxAge)
}
if actualLogRule.Roller.MaxBackups != test.expectedLogRules[j].Roller.MaxBackups {
t.Fatalf("Test %d expected %dth LogRule Roller MaxBackups to be %d, but got %d",
i, j, test.expectedLogRules[j].Roller.MaxBackups, actualLogRule.Roller.MaxBackups)
}
if actualLogRule.Roller.MaxSize != test.expectedLogRules[j].Roller.MaxSize {
t.Fatalf("Test %d expected %dth LogRule Roller MaxSize to be %d, but got %d",
i, j, test.expectedLogRules[j].Roller.MaxSize, actualLogRule.Roller.MaxSize)
}
if actualLogRule.Roller.LocalTime != test.expectedLogRules[j].Roller.LocalTime {
t.Fatalf("Test %d expected %dth LogRule Roller LocalTime to be %t, but got %t",
i, j, test.expectedLogRules[j].Roller.LocalTime, actualLogRule.Roller.LocalTime)
}
}
}
}
}
+157
View File
@@ -0,0 +1,157 @@
package setup
import (
"net/http"
"path"
"path/filepath"
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/markdown"
"github.com/russross/blackfriday"
)
// Markdown configures a new Markdown middleware instance.
func Markdown(c *Controller) (middleware.Middleware, error) {
mdconfigs, err := markdownParse(c)
if err != nil {
return nil, err
}
md := markdown.Markdown{
Root: c.Root,
FileSys: http.Dir(c.Root),
Configs: mdconfigs,
IndexFiles: []string{"index.md"},
}
// Sweep the whole path at startup to at least generate link index, maybe generate static site
c.Startup = append(c.Startup, func() error {
for i := range mdconfigs {
cfg := mdconfigs[i]
// Generate link index and static files (if enabled)
if err := markdown.GenerateStatic(md, cfg); err != nil {
return err
}
// Watch file changes for static site generation if not in development mode.
if !cfg.Development {
markdown.Watch(md, cfg, markdown.DefaultInterval)
}
}
return nil
})
return func(next middleware.Handler) middleware.Handler {
md.Next = next
return md
}, nil
}
func markdownParse(c *Controller) ([]*markdown.Config, error) {
var mdconfigs []*markdown.Config
for c.Next() {
md := &markdown.Config{
Renderer: blackfriday.HtmlRenderer(0, "", ""),
Templates: make(map[string]string),
StaticFiles: make(map[string]string),
}
// Get the path scope
args := c.RemainingArgs()
switch len(args) {
case 0:
md.PathScope = "/"
case 1:
md.PathScope = args[0]
default:
return mdconfigs, c.ArgErr()
}
// Load any other configuration parameters
for c.NextBlock() {
if err := loadParams(c, md); err != nil {
return mdconfigs, err
}
}
// If no extensions were specified, assume some defaults
if len(md.Extensions) == 0 {
md.Extensions = []string{".md", ".markdown", ".mdown"}
}
mdconfigs = append(mdconfigs, md)
}
return mdconfigs, nil
}
func loadParams(c *Controller, mdc *markdown.Config) error {
switch c.Val() {
case "ext":
exts := c.RemainingArgs()
if len(exts) == 0 {
return c.ArgErr()
}
mdc.Extensions = append(mdc.Extensions, exts...)
return nil
case "css":
if !c.NextArg() {
return c.ArgErr()
}
mdc.Styles = append(mdc.Styles, c.Val())
return nil
case "js":
if !c.NextArg() {
return c.ArgErr()
}
mdc.Scripts = append(mdc.Scripts, c.Val())
return nil
case "template":
tArgs := c.RemainingArgs()
switch len(tArgs) {
case 0:
return c.ArgErr()
case 1:
if _, ok := mdc.Templates[markdown.DefaultTemplate]; ok {
return c.Err("only one default template is allowed, use alias.")
}
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0]))
mdc.Templates[markdown.DefaultTemplate] = fpath
return nil
case 2:
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1]))
mdc.Templates[tArgs[0]] = fpath
return nil
default:
return c.ArgErr()
}
case "sitegen":
if c.NextArg() {
mdc.StaticDir = path.Join(c.Root, c.Val())
} else {
mdc.StaticDir = path.Join(c.Root, markdown.DefaultStaticDir)
}
if c.NextArg() {
// only 1 argument allowed
return c.ArgErr()
}
return nil
case "dev":
if c.NextArg() {
mdc.Development = strings.ToLower(c.Val()) == "true"
} else {
mdc.Development = true
}
if c.NextArg() {
// only 1 argument allowed
return c.ArgErr()
}
return nil
default:
return c.Err("Expected valid markdown configuration property")
}
}
+184
View File
@@ -0,0 +1,184 @@
package setup
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"testing"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/markdown"
)
func TestMarkdown(t *testing.T) {
c := NewTestController(`markdown /blog`)
mid, err := Markdown(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(markdown.Markdown)
if !ok {
t.Fatalf("Expected handler to be type Markdown, got: %#v", handler)
}
if myHandler.Configs[0].PathScope != "/blog" {
t.Errorf("Expected /blog as the Path Scope")
}
if fmt.Sprint(myHandler.Configs[0].Extensions) != fmt.Sprint([]string{".md", ".markdown", ".mdown"}) {
t.Errorf("Expected .md, .markdown, and .mdown as default extensions")
}
}
func TestMarkdownStaticGen(t *testing.T) {
c := NewTestController(`markdown /blog {
ext .md
template tpl_with_include.html
sitegen
}`)
c.Root = "./testdata"
mid, err := Markdown(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
for _, start := range c.Startup {
err := start()
if err != nil {
t.Errorf("Startup error: %v", err)
}
}
next := middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called")
return 0, nil
})
hndlr := mid(next)
mkdwn, ok := hndlr.(markdown.Markdown)
if !ok {
t.Fatalf("Was expecting a markdown.Markdown but got %T", hndlr)
}
expectedStaticFiles := map[string]string{"/blog/first_post.md": "testdata/generated_site/blog/first_post.md/index.html"}
if fmt.Sprint(expectedStaticFiles) != fmt.Sprint(mkdwn.Configs[0].StaticFiles) {
t.Fatalf("Test expected StaticFiles to be %s, but got %s",
fmt.Sprint(expectedStaticFiles), fmt.Sprint(mkdwn.Configs[0].StaticFiles))
}
filePath := "testdata/generated_site/blog/first_post.md/index.html"
if _, err := os.Stat(filePath); err != nil {
t.Fatalf("An error occured when getting the file information: %v", err)
}
html, err := ioutil.ReadFile(filePath)
if err != nil {
t.Fatalf("An error occured when getting the file content: %v", err)
}
expectedBody := []byte(`<!DOCTYPE html>
<html>
<head>
<title>first_post</title>
</head>
<body>
<h1>Header title</h1>
<h1>Test h1</h1>
</body>
</html>
`)
if !bytes.Equal(html, expectedBody) {
t.Fatalf("Expected file content: %s got: %s", string(expectedBody), string(html))
}
fp := filepath.Join(c.Root, markdown.DefaultStaticDir)
if err = os.RemoveAll(fp); err != nil {
t.Errorf("Error while removing the generated static files: %v", err)
}
}
func TestMarkdownParse(t *testing.T) {
tests := []struct {
inputMarkdownConfig string
shouldErr bool
expectedMarkdownConfig []markdown.Config
}{
{`markdown /blog {
ext .md .txt
css /resources/css/blog.css
js /resources/js/blog.js
}`, false, []markdown.Config{{
PathScope: "/blog",
Extensions: []string{".md", ".txt"},
Styles: []string{"/resources/css/blog.css"},
Scripts: []string{"/resources/js/blog.js"},
}}},
{`markdown /blog {
ext .md
template tpl_with_include.html
sitegen
}`, false, []markdown.Config{{
PathScope: "/blog",
Extensions: []string{".md"},
Templates: map[string]string{markdown.DefaultTemplate: "testdata/tpl_with_include.html"},
StaticDir: markdown.DefaultStaticDir,
}}},
}
for i, test := range tests {
c := NewTestController(test.inputMarkdownConfig)
c.Root = "./testdata"
actualMarkdownConfigs, err := markdownParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualMarkdownConfigs) != len(test.expectedMarkdownConfig) {
t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
i, len(test.expectedMarkdownConfig), len(actualMarkdownConfigs))
}
for j, actualMarkdownConfig := range actualMarkdownConfigs {
if actualMarkdownConfig.PathScope != test.expectedMarkdownConfig[j].PathScope {
t.Errorf("Test %d expected %dth Markdown PathScope to be %s , but got %s",
i, j, test.expectedMarkdownConfig[j].PathScope, actualMarkdownConfig.PathScope)
}
if fmt.Sprint(actualMarkdownConfig.Styles) != fmt.Sprint(test.expectedMarkdownConfig[j].Styles) {
t.Errorf("Test %d expected %dth Markdown Config Styles to be %s , but got %s",
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Styles), fmt.Sprint(actualMarkdownConfig.Styles))
}
if fmt.Sprint(actualMarkdownConfig.Scripts) != fmt.Sprint(test.expectedMarkdownConfig[j].Scripts) {
t.Errorf("Test %d expected %dth Markdown Config Scripts to be %s , but got %s",
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Scripts), fmt.Sprint(actualMarkdownConfig.Scripts))
}
if fmt.Sprint(actualMarkdownConfig.Templates) != fmt.Sprint(test.expectedMarkdownConfig[j].Templates) {
t.Errorf("Test %d expected %dth Markdown Config Templates to be %s , but got %s",
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Templates), fmt.Sprint(actualMarkdownConfig.Templates))
}
}
}
}
+62
View File
@@ -0,0 +1,62 @@
package setup
import (
"fmt"
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/mime"
)
// Mime configures a new mime middleware instance.
func Mime(c *Controller) (middleware.Middleware, error) {
configs, err := mimeParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return mime.Mime{Next: next, Configs: configs}
}, nil
}
func mimeParse(c *Controller) ([]mime.Config, error) {
var configs []mime.Config
for c.Next() {
// At least one extension is required
args := c.RemainingArgs()
switch len(args) {
case 2:
if err := validateExt(args[0]); err != nil {
return configs, err
}
configs = append(configs, mime.Config{Ext: args[0], ContentType: args[1]})
case 1:
return configs, c.ArgErr()
case 0:
for c.NextBlock() {
ext := c.Val()
if err := validateExt(ext); err != nil {
return configs, err
}
if !c.NextArg() {
return configs, c.ArgErr()
}
configs = append(configs, mime.Config{Ext: ext, ContentType: c.Val()})
}
}
}
return configs, nil
}
// validateExt checks for valid file name extension.
func validateExt(ext string) error {
if !strings.HasPrefix(ext, ".") {
return fmt.Errorf(`mime: invalid extension "%v" (must start with dot)`, ext)
}
return nil
}
+59
View File
@@ -0,0 +1,59 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware/mime"
)
func TestMime(t *testing.T) {
c := NewTestController(`mime .txt text/plain`)
mid, err := Mime(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(mime.Mime)
if !ok {
t.Fatalf("Expected handler to be type Mime, got: %#v", handler)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
tests := []struct {
input string
shouldErr bool
}{
{`mime {`, true},
{`mime {}`, true},
{`mime a b`, true},
{`mime a {`, true},
{`mime { txt f } `, true},
{`mime { html } `, true},
{`mime {
.html text/html
.txt text/plain
} `, false},
{`mime { .html text/html } `, false},
{`mime { .html
} `, true},
{`mime .txt text/plain`, false},
}
for i, test := range tests {
c := NewTestController(test.input)
m, err := mimeParse(c)
if test.shouldErr && err == nil {
t.Errorf("Test %v: Expected error but found nil %v", i, m)
} else if !test.shouldErr && err != nil {
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
}
}
}
+17
View File
@@ -0,0 +1,17 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/proxy"
)
// Proxy configures a new Proxy middleware instance.
func Proxy(c *Controller) (middleware.Middleware, error) {
upstreams, err := proxy.NewStaticUpstreams(c.Dispenser)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return proxy.Proxy{Next: next, Upstreams: upstreams}
}, nil
}
+173
View File
@@ -0,0 +1,173 @@
package setup
import (
"net/http"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/redirect"
)
// Redir configures a new Redirect middleware instance.
func Redir(c *Controller) (middleware.Middleware, error) {
rules, err := redirParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return redirect.Redirect{Next: next, Rules: rules}
}, nil
}
func redirParse(c *Controller) ([]redirect.Rule, error) {
var redirects []redirect.Rule
// setRedirCode sets the redirect code for rule if it can, or returns an error
setRedirCode := func(code string, rule *redirect.Rule) error {
if code == "meta" {
rule.Meta = true
} else if codeNumber, ok := httpRedirs[code]; ok {
rule.Code = codeNumber
} else {
return c.Errf("Invalid redirect code '%v'", code)
}
return nil
}
// checkAndSaveRule checks the rule for validity (except the redir code)
// and saves it if it's valid, or returns an error.
checkAndSaveRule := func(rule redirect.Rule) error {
if rule.FromPath == rule.To {
return c.Err("'from' and 'to' values of redirect rule cannot be the same")
}
for _, otherRule := range redirects {
if otherRule.FromPath == rule.FromPath {
return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.FromPath, otherRule.To)
}
}
redirects = append(redirects, rule)
return nil
}
for c.Next() {
args := c.RemainingArgs()
var hadOptionalBlock bool
for c.NextBlock() {
hadOptionalBlock = true
var rule redirect.Rule
if c.Config.TLS.Enabled {
rule.FromScheme = "https"
} else {
rule.FromScheme = "http"
}
// Set initial redirect code
// BUG: If the code is specified for a whole block and that code is invalid,
// the line number will appear on the first line inside the block, even if that
// line overwrites the block-level code with a valid redirect code. The program
// still functions correctly, but the line number in the error reporting is
// misleading to the user.
if len(args) == 1 {
err := setRedirCode(args[0], &rule)
if err != nil {
return redirects, err
}
} else {
rule.Code = http.StatusMovedPermanently // default code
}
// RemainingArgs only gets the values after the current token, but in our
// case we want to include the current token to get an accurate count.
insideArgs := append([]string{c.Val()}, c.RemainingArgs()...)
switch len(insideArgs) {
case 1:
// To specified (catch-all redirect)
// Not sure why user is doing this in a table, as it causes all other redirects to be ignored.
// As such, this feature remains undocumented.
rule.FromPath = "/"
rule.To = insideArgs[0]
case 2:
// From and To specified
rule.FromPath = insideArgs[0]
rule.To = insideArgs[1]
case 3:
// From, To, and Code specified
rule.FromPath = insideArgs[0]
rule.To = insideArgs[1]
err := setRedirCode(insideArgs[2], &rule)
if err != nil {
return redirects, err
}
default:
return redirects, c.ArgErr()
}
err := checkAndSaveRule(rule)
if err != nil {
return redirects, err
}
}
if !hadOptionalBlock {
var rule redirect.Rule
if c.Config.TLS.Enabled {
rule.FromScheme = "https"
} else {
rule.FromScheme = "http"
}
rule.Code = http.StatusMovedPermanently // default
switch len(args) {
case 1:
// To specified (catch-all redirect)
rule.FromPath = "/"
rule.To = args[0]
case 2:
// To and Code specified (catch-all redirect)
rule.FromPath = "/"
rule.To = args[0]
err := setRedirCode(args[1], &rule)
if err != nil {
return redirects, err
}
case 3:
// From, To, and Code specified
rule.FromPath = args[0]
rule.To = args[1]
err := setRedirCode(args[2], &rule)
if err != nil {
return redirects, err
}
default:
return redirects, c.ArgErr()
}
err := checkAndSaveRule(rule)
if err != nil {
return redirects, err
}
}
}
return redirects, nil
}
// httpRedirs is a list of supported HTTP redirect codes.
var httpRedirs = map[string]int{
"300": http.StatusMultipleChoices,
"301": http.StatusMovedPermanently,
"302": http.StatusFound, // (NOT CORRECT for "Temporary Redirect", see 307)
"303": http.StatusSeeOther,
"304": http.StatusNotModified,
"305": http.StatusUseProxy,
"307": http.StatusTemporaryRedirect,
"308": 308, // Permanent Redirect
}
+67
View File
@@ -0,0 +1,67 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware/redirect"
)
func TestRedir(t *testing.T) {
for j, test := range []struct {
input string
shouldErr bool
expectedRules []redirect.Rule
}{
// test case #0 tests the recognition of a valid HTTP status code defined outside of block statement
{"redir 300 {\n/ /foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 300}}},
// test case #1 tests the recognition of an invalid HTTP status code defined outside of block statement
{"redir 9000 {\n/ /foo\n}", true, []redirect.Rule{{}}},
// test case #2 tests the detection of a valid HTTP status code outside of a block statement being overriden by an invalid HTTP status code inside statement of a block statement
{"redir 300 {\n/ /foo 9000\n}", true, []redirect.Rule{{}}},
// test case #3 tests the detection of an invalid HTTP status code outside of a block statement being overriden by a valid HTTP status code inside statement of a block statement
{"redir 9000 {\n/ /foo 300\n}", true, []redirect.Rule{{}}},
// test case #4 tests the recognition of a TO redirection in a block statement.The HTTP status code is set to the default of 301 - MovedPermanently
{"redir 302 {\n/foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 302}}},
// test case #5 tests the recognition of a TO and From redirection in a block statement
{"redir {\n/bar /foo 303\n}", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}},
// test case #6 tests the recognition of a TO redirection in a non-block statement. The HTTP status code is set to the default of 301 - MovedPermanently
{"redir /foo", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 301}}},
// test case #7 tests the recognition of a TO and From redirection in a non-block statement
{"redir /bar /foo 303", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}},
// test case #8 tests the recognition of multiple redirections
{"redir {\n / /foo 304 \n} \n redir {\n /bar /foobar 305 \n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 304}, {FromPath: "/bar", To: "/foobar", Code: 305}}},
// test case #9 tests the detection of duplicate redirections
{"redir {\n /bar /foo 304 \n} redir {\n /bar /foo 304 \n}", true, []redirect.Rule{{}}},
} {
recievedFunc, err := Redir(NewTestController(test.input))
if err != nil && !test.shouldErr {
t.Errorf("Test case #%d recieved an error of %v", j, err)
} else if test.shouldErr {
continue
}
recievedRules := recievedFunc(nil).(redirect.Redirect).Rules
for i, recievedRule := range recievedRules {
if recievedRule.FromPath != test.expectedRules[i].FromPath {
t.Errorf("Test case #%d.%d expected a from path of %s, but recieved a from path of %s", j, i, test.expectedRules[i].FromPath, recievedRule.FromPath)
}
if recievedRule.To != test.expectedRules[i].To {
t.Errorf("Test case #%d.%d expected a TO path of %s, but recieved a TO path of %s", j, i, test.expectedRules[i].To, recievedRule.To)
}
if recievedRule.Code != test.expectedRules[i].Code {
t.Errorf("Test case #%d.%d expected a HTTP status code of %d, but recieved a code of %d", j, i, test.expectedRules[i].Code, recievedRule.Code)
}
}
}
}
+109
View File
@@ -0,0 +1,109 @@
package setup
import (
"net/http"
"strconv"
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/rewrite"
)
// Rewrite configures a new Rewrite middleware instance.
func Rewrite(c *Controller) (middleware.Middleware, error) {
rewrites, err := rewriteParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return rewrite.Rewrite{
Next: next,
FileSys: http.Dir(c.Root),
Rules: rewrites,
}
}, nil
}
func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
var simpleRules []rewrite.Rule
var regexpRules []rewrite.Rule
for c.Next() {
var rule rewrite.Rule
var err error
var base = "/"
var pattern, to string
var status int
var ext []string
args := c.RemainingArgs()
var ifs []rewrite.If
switch len(args) {
case 1:
base = args[0]
fallthrough
case 0:
for c.NextBlock() {
switch c.Val() {
case "r", "regexp":
if !c.NextArg() {
return nil, c.ArgErr()
}
pattern = c.Val()
case "to":
args1 := c.RemainingArgs()
if len(args1) == 0 {
return nil, c.ArgErr()
}
to = strings.Join(args1, " ")
case "ext":
args1 := c.RemainingArgs()
if len(args1) == 0 {
return nil, c.ArgErr()
}
ext = args1
case "if":
args1 := c.RemainingArgs()
if len(args1) != 3 {
return nil, c.ArgErr()
}
ifCond, err := rewrite.NewIf(args1[0], args1[1], args1[2])
if err != nil {
return nil, err
}
ifs = append(ifs, ifCond)
case "status":
if !c.NextArg() {
return nil, c.ArgErr()
}
status, _ = strconv.Atoi(c.Val())
if status < 200 || (status > 299 && status < 400) || status > 499 {
return nil, c.Err("status must be 2xx or 4xx")
}
default:
return nil, c.ArgErr()
}
}
// ensure to or status is specified
if to == "" && status == 0 {
return nil, c.ArgErr()
}
if rule, err = rewrite.NewComplexRule(base, pattern, to, status, ext, ifs); err != nil {
return nil, err
}
regexpRules = append(regexpRules, rule)
// the only unhandled case is 2 and above
default:
rule = rewrite.NewSimpleRule(args[0], strings.Join(args[1:], " "))
simpleRules = append(simpleRules, rule)
}
}
// put simple rules in front to avoid regexp computation for them
return append(simpleRules, regexpRules...), nil
}
+241
View File
@@ -0,0 +1,241 @@
package setup
import (
"fmt"
"regexp"
"testing"
"github.com/mholt/caddy/middleware/rewrite"
)
func TestRewrite(t *testing.T) {
c := NewTestController(`rewrite /from /to`)
mid, err := Rewrite(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(rewrite.Rewrite)
if !ok {
t.Fatalf("Expected handler to be type Rewrite, got: %#v", handler)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
if len(myHandler.Rules) != 1 {
t.Errorf("Expected handler to have %d rule, has %d instead", 1, len(myHandler.Rules))
}
}
func TestRewriteParse(t *testing.T) {
simpleTests := []struct {
input string
shouldErr bool
expected []rewrite.Rule
}{
{`rewrite /from /to`, false, []rewrite.Rule{
rewrite.SimpleRule{From: "/from", To: "/to"},
}},
{`rewrite /from /to
rewrite a b`, false, []rewrite.Rule{
rewrite.SimpleRule{From: "/from", To: "/to"},
rewrite.SimpleRule{From: "a", To: "b"},
}},
{`rewrite a`, true, []rewrite.Rule{}},
{`rewrite`, true, []rewrite.Rule{}},
{`rewrite a b c`, false, []rewrite.Rule{
rewrite.SimpleRule{From: "a", To: "b c"},
}},
}
for i, test := range simpleTests {
c := NewTestController(test.input)
actual, err := rewriteParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
} else if err != nil && test.shouldErr {
continue
}
if len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, e := range test.expected {
actualRule := actual[j].(rewrite.SimpleRule)
expectedRule := e.(rewrite.SimpleRule)
if actualRule.From != expectedRule.From {
t.Errorf("Test %d, rule %d: Expected From=%s, got %s",
i, j, expectedRule.From, actualRule.From)
}
if actualRule.To != expectedRule.To {
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
i, j, expectedRule.To, actualRule.To)
}
}
}
regexpTests := []struct {
input string
shouldErr bool
expected []rewrite.Rule
}{
{`rewrite {
r .*
to /to /index.php?
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to /index.php?", Regexp: regexp.MustCompile(".*")},
}},
{`rewrite {
regexp .*
to /to
ext / html txt
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to", Exts: []string{"/", "html", "txt"}, Regexp: regexp.MustCompile(".*")},
}},
{`rewrite /path {
r rr
to /dest
}
rewrite / {
regexp [a-z]+
to /to /to2
}
`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/path", To: "/dest", Regexp: regexp.MustCompile("rr")},
&rewrite.ComplexRule{Base: "/", To: "/to /to2", Regexp: regexp.MustCompile("[a-z]+")},
}},
{`rewrite {
r .*
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite /`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
to /to
if {path} is a
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to", Ifs: []rewrite.If{{A: "{path}", Operator: "is", B: "a"}}},
}},
{`rewrite {
status 500
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
status 400
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", Status: 400},
}},
{`rewrite {
to /to
status 400
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to", Status: 400},
}},
{`rewrite {
status 399
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
status 200
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", Status: 200},
}},
{`rewrite {
to /to
status 200
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to", Status: 200},
}},
{`rewrite {
status 199
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
status 0
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
to /to
status 0
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
}
for i, test := range regexpTests {
c := NewTestController(test.input)
actual, err := rewriteParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
} else if err != nil && test.shouldErr {
continue
}
if len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, e := range test.expected {
actualRule := actual[j].(*rewrite.ComplexRule)
expectedRule := e.(*rewrite.ComplexRule)
if actualRule.Base != expectedRule.Base {
t.Errorf("Test %d, rule %d: Expected Base=%s, got %s",
i, j, expectedRule.Base, actualRule.Base)
}
if actualRule.To != expectedRule.To {
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
i, j, expectedRule.To, actualRule.To)
}
if fmt.Sprint(actualRule.Exts) != fmt.Sprint(expectedRule.Exts) {
t.Errorf("Test %d, rule %d: Expected Ext=%v, got %v",
i, j, expectedRule.To, actualRule.To)
}
if actualRule.Regexp != nil {
if actualRule.String() != expectedRule.String() {
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
i, j, expectedRule.String(), actualRule.String())
}
}
if fmt.Sprint(actualRule.Ifs) != fmt.Sprint(expectedRule.Ifs) {
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
i, j, fmt.Sprint(expectedRule.Ifs), fmt.Sprint(actualRule.Ifs))
}
}
}
}
+40
View File
@@ -0,0 +1,40 @@
package setup
import (
"strconv"
"github.com/mholt/caddy/middleware"
)
func parseRoller(c *Controller) (*middleware.LogRoller, error) {
var size, age, keep int
// This is kind of a hack to support nested blocks:
// As we are already in a block: either log or errors,
// c.nesting > 0 but, as soon as c meets a }, it thinks
// the block is over and return false for c.NextBlock.
for c.NextBlock() {
what := c.Val()
if !c.NextArg() {
return nil, c.ArgErr()
}
value := c.Val()
var err error
switch what {
case "size":
size, err = strconv.Atoi(value)
case "age":
age, err = strconv.Atoi(value)
case "keep":
keep, err = strconv.Atoi(value)
}
if err != nil {
return nil, err
}
}
return &middleware.LogRoller{
MaxSize: size,
MaxAge: age,
MaxBackups: keep,
LocalTime: true,
}, nil
}
+32
View File
@@ -0,0 +1,32 @@
package setup
import (
"log"
"os"
"github.com/mholt/caddy/middleware"
)
// Root sets up the root file path of the server.
func Root(c *Controller) (middleware.Middleware, error) {
for c.Next() {
if !c.NextArg() {
return nil, c.ArgErr()
}
c.Root = c.Val()
}
// Check if root path exists
_, err := os.Stat(c.Root)
if err != nil {
if os.IsNotExist(err) {
// Allow this, because the folder might appear later.
// But make sure the user knows!
log.Printf("[WARNING] Root path does not exist: %s", c.Root)
} else {
return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err)
}
}
return nil, nil
}
+108
View File
@@ -0,0 +1,108 @@
package setup
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
)
func TestRoot(t *testing.T) {
// Predefined error substrings
parseErrContent := "Parse error:"
unableToAccessErrContent := "Unable to access root path"
existingDirPath, err := getTempDirPath()
if err != nil {
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
}
nonExistingDir := filepath.Join(existingDirPath, "highly_unlikely_to_exist_dir")
existingFile, err := ioutil.TempFile("", "root_test")
if err != nil {
t.Fatalf("BeforeTest: Failed to create temp file for testing! Error was: %v", err)
}
defer func() {
existingFile.Close()
os.Remove(existingFile.Name())
}()
inaccessiblePath := getInaccessiblePath(existingFile.Name())
tests := []struct {
input string
shouldErr bool
expectedRoot string // expected root, set to the controller. Empty for negative cases.
expectedErrContent string // substring from the expected error. Empty for positive cases.
}{
// positive
{
fmt.Sprintf(`root %s`, nonExistingDir), false, nonExistingDir, "",
},
{
fmt.Sprintf(`root %s`, existingDirPath), false, existingDirPath, "",
},
// negative
{
`root `, true, "", parseErrContent,
},
{
fmt.Sprintf(`root %s`, inaccessiblePath), true, "", unableToAccessErrContent,
},
{
fmt.Sprintf(`root {
%s
}`, existingDirPath), true, "", parseErrContent,
},
}
for i, test := range tests {
c := NewTestController(test.input)
mid, err := Root(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErrContent) {
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input)
}
}
// the Root method always returns a nil middleware
if mid != nil {
t.Errorf("Middware, returned from Root() was not nil: %v", mid)
}
// check c.Root only if we are in a positive test.
if !test.shouldErr && test.expectedRoot != c.Root {
t.Errorf("Root not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedRoot, c.Root)
}
}
}
// getTempDirPath returnes the path to the system temp directory. If it does not exists - an error is returned.
func getTempDirPath() (string, error) {
tempDir := os.TempDir()
_, err := os.Stat(tempDir)
if err != nil {
return "", err
}
return tempDir, nil
}
func getInaccessiblePath(file string) string {
// null byte in filename is not allowed on Windows AND unix
return filepath.Join("C:", "file\x00name")
}
+64
View File
@@ -0,0 +1,64 @@
package setup
import (
"os"
"os/exec"
"strings"
"github.com/mholt/caddy/middleware"
)
// Startup registers a startup callback to execute during server start.
func Startup(c *Controller) (middleware.Middleware, error) {
return nil, registerCallback(c, &c.FirstStartup)
}
// Shutdown registers a shutdown callback to execute during process exit.
func Shutdown(c *Controller) (middleware.Middleware, error) {
return nil, registerCallback(c, &c.Shutdown)
}
// registerCallback registers a callback function to execute by
// using c to parse the line. It appends the callback function
// to the list of callback functions passed in by reference.
func registerCallback(c *Controller, list *[]func() error) error {
var funcs []func() error
for c.Next() {
args := c.RemainingArgs()
if len(args) == 0 {
return c.ArgErr()
}
nonblock := false
if len(args) > 1 && args[len(args)-1] == "&" {
// Run command in background; non-blocking
nonblock = true
args = args[:len(args)-1]
}
command, args, err := middleware.SplitCommandAndArgs(strings.Join(args, " "))
if err != nil {
return c.Err(err.Error())
}
fn := func() error {
cmd := exec.Command(command, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if nonblock {
return cmd.Start()
}
return cmd.Run()
}
funcs = append(funcs, fn)
}
return c.OncePerServerBlock(func() error {
*list = append(*list, funcs...)
return nil
})
}

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