Compare commits

...

427 Commits

Author SHA1 Message Date
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
174 changed files with 10514 additions and 2163 deletions
+2
View File
@@ -12,3 +12,5 @@ access.log
/*.conf
Caddyfile
og_static/
+13 -1
View File
@@ -1,2 +1,14 @@
language: go
script: go test ./...
go:
- 1.4.3
- 1.5.1
- tip
install:
- go get -d ./...
- go get golang.org/x/tools/cmd/vet
script:
- go vet ./...
- go test ./...
+20 -8
View File
@@ -1,32 +1,44 @@
## Contributing to Caddy
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup), then join the #caddy channel.)
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with
other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup),
then join the #caddy channel.)
This project gladly accepts contributions and we encourage interested users to get involved!
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. Thank you for helping out in simple ways! Bug fixes should be under test to assert correct behavior.
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time.
Thank you for helping out in simple ways! Bug fixes should be under test to
assert correct behavior.
#### Ideas, questions, bug reports
You should totally [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 reproducing the problem.
You should totally [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
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 Slack or you may email me directly. (My email address is below.)
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 Slack 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.
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.
Thanks for your help! Caddy would not be what it is today without your contributions.
View File
+92 -51
View File
@@ -1,21 +1,30 @@
[![Caddy](https://caddyserver.com/resources/images/caddy-boxed.png)](https://caddyserver.com)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/mholt/caddy) [![Build Status](https://img.shields.io/travis/mholt/caddy.svg?style=flat-square)](https://travis-ci.org/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.
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, Virtual Hosts, TLS + SNI, and easy configuration with a [Caddyfile](https://caddyserver.com/docs/caddyfile). Usually, you have one Caddyfile per site. Most directives for the Caddyfile invoke a layer of middleware which can be [used in your own Go programs](https://github.com/mholt/caddy/wiki/Using-Caddy-Middleware-in-Your-Own-Programs).
[Download](https://github.com/mholt/caddy/releases) · [User Guide](https://caddyserver.com/docs)
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
- [Getting Caddy](#getting-caddy)
- [Running from Source](#running-from-source)
- [Quick Start](#quick-start)
- [Running from Source](#running-from-source)
- [Contributing](#contributing)
- [About the Project](#about-the-project)
@@ -29,42 +38,11 @@ Caddy binaries have no dependencies and are available for nearly every platform.
[Latest release](https://github.com/mholt/caddy/releases/latest)
## Running from Source
Note: You will need **[Go 1.4](https://golang.org/dl)** or newer
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'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.
#### Docker Container
Caddy is available as a Docker container from any of these sources:
- [abiosoft/caddy](https://registry.hub.docker.com/u/abiosoft/caddy/)
- [darron/caddy](https://registry.hub.docker.com/u/darron/caddy/)
- [jumanjiman/caddy](https://registry.hub.docker.com/u/jumanjiman/caddy/)
#### 3rd-party libraries
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.
## Quick Start
The website has [full documentation](https://caddyserver.com/docs) but this will get you started in about 30 seconds:
The website has [full documentation](https://caddyserver.com/docs) but this will
get you started in about 30 seconds:
Place a file named "Caddyfile" with your site. Paste this into it and save:
@@ -79,51 +57,114 @@ log ../access.log
header /api Access-Control-Allow-Origin *
```
Run `caddy` from that directory, and it will automatically use that Caddyfile to configure itself.
Run `caddy` from that directory, and it will automatically use that Caddyfile to
configure itself.
That simple file enables compression, allows directory browsing (for folders without an index file), serves clean URLs, hosts an echo server for WebSocket connections at /echo, logs accesses to access.log, and adds the coveted `Access-Control-Allow-Origin: *` header for all responses from some API.
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.
Wow! Caddy can do a lot with just a few lines.
#### Defining multiple sites
You can run multiple sites from the same Caddyfile, too:
```
http://mysite.com,
http://www.mysite.com {
redir https://mysite.com
site1.com {
# ...
}
https://mysite.com {
tls mysite.crt mysite.key
site2.com, sub.site2.com {
# ...
}
```
Note that the secure host will automatically be served with HTTP/2 if the client supports it.
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.
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.
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.
## Running from Source
Note: You will need **[Go 1.4](https://golang.org/dl)** or a later version.
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'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://registry.hub.docker.com/u/abiosoft/caddy/)
- [darron/caddy](https://registry.hub.docker.com/u/darron/caddy/)
- [joshix/caddy](https://registry.hub.docker.com/u/joshix/caddy/)
- [jumanjiman/caddy](https://registry.hub.docker.com/u/jumanjiman/caddy/)
- [zenithar/nano-caddy](https://registry.hub.docker.com/u/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.
## Contributing
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup), then join the #caddy channel.)
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with
other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup),
then join the #caddy channel.)
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.
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.
Thanks for making Caddy -- and the Web -- better!
Special thanks to
[![DigitalOcean](http://i.imgur.com/sfGr0eY.png)](https://www.digitalocean.com)
for hosting the Caddy project.
## About the project
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, lighttpd, Websocketd, and Vagrant, and provides a pleasant mixture of features from each of them.
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.
*Twitter: [@mholt6](https://twitter.com/mholt6)*
-76
View File
@@ -1,76 +0,0 @@
// Package app holds application-global state to make it accessible
// by other packages in the application.
//
// This package differs from config in that the things in app aren't
// really related to server configuration.
package app
import (
"errors"
"runtime"
"strconv"
"strings"
"sync"
"github.com/mholt/caddy/server"
)
const (
// Name is the program name
Name = "Caddy"
// Version is the program version
Version = "0.7.3"
)
var (
// Servers is a list of all the currently-listening servers
Servers []*server.Server
// ServersMutex protects the Servers slice during changes
ServersMutex sync.Mutex
// Wg is used to wait for all servers to shut down
Wg sync.WaitGroup
// Http2 indicates whether HTTP2 is enabled or not
Http2 bool // TODO: temporary flag until http2 is standard
// Quiet mode hides non-error initialization output
Quiet bool
)
// SetCPU parses string cpu and sets GOMAXPROCS
// according to its value. It accepts either
// a number (e.g. 3) or a percent (e.g. 50%).
func SetCPU(cpu string) error {
var numCPU int
availCPU := runtime.NumCPU()
if strings.HasSuffix(cpu, "%") {
// Percent
var percent float32
pctStr := cpu[:len(cpu)-1]
pctInt, err := strconv.Atoi(pctStr)
if err != nil || pctInt < 1 || pctInt > 100 {
return errors.New("invalid CPU value: percentage must be between 1-100")
}
percent = float32(pctInt) / 100
numCPU = int(float32(availCPU) * percent)
} else {
// Number
num, err := strconv.Atoi(cpu)
if err != nil || num < 1 {
return errors.New("invalid CPU value: provide a number or percent greater than 0")
}
numCPU = num
}
if numCPU > availCPU {
numCPU = availCPU
}
runtime.GOMAXPROCS(numCPU)
return nil
}
+19
View File
@@ -0,0 +1,19 @@
version: "{build}"
os: Windows Server 2012 R2
clone_folder: c:\gopath\src\github.com\mholt\caddy
environment:
GOPATH: c:\gopath
install:
- go get golang.org/x/tools/cmd/vet
- echo %GOPATH%
- go version
- go env
- go get -d ./...
build_script:
- go vet ./...
- go test ./...
+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)
}
}
+380
View File
@@ -0,0 +1,380 @@
// Package caddy implements the Caddy web server as a service.
//
// 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"
"time"
"github.com/mholt/caddy/caddy/letsencrypt"
"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 // TODO: temporary flag until http2 is standard
// 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 // TODO: This setting is temporary
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 {
letsencrypt.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
}
// 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\ntls off"
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)
}
}
}
+180
View File
@@ -0,0 +1,180 @@
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, strings.TrimSuffix(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.
// goes to end of line
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.
// 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 hostname, port, err := net.SplitHostPort(host); err == nil {
if port == "http" || port == "https" {
host = port + "://" + hostname
}
}
if i > 0 {
result += ", "
}
result += strings.TrimSuffix(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
}
// 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"`
}
+126
View File
@@ -0,0 +1,126 @@
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":["host:http","host:https"],"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))
}
}
}
+372
View File
@@ -0,0 +1,372 @@
package caddy
import (
"bytes"
"fmt"
"io"
"log"
"net"
"sync"
"github.com/mholt/caddy/caddy/letsencrypt"
"github.com/mholt/caddy/caddy/parse"
"github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/middleware"
"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"
)
// 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 optimally
// for HTTPS.
func loadConfigs(filename string, input io.Reader) ([]server.Config, 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, err
}
if len(serverBlocks) == 0 {
newInput := DefaultInput()
serverBlocks, err = parse.ServerBlocks(newInput.Path(), bytes.NewReader(newInput.Body()), true)
if err != nil {
return nil, 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,
Root: Root,
Middleware: make(map[string][]middleware.Middleware),
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, err
}
if midware != nil {
// TODO: For now, we only support the default path scope /
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)
}
}
// 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 call is append-only, so our
// iterations below shouldn't be affected)
if !IsRestart() && !Quiet {
fmt.Print("Activating privacy features...")
}
configs, err = letsencrypt.Activate(configs)
if err != nil {
if !Quiet {
fmt.Println()
}
return nil, err
}
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 {
// TODO: For now, we only support the default path scope /
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 handles edge cases gracefully. If a port name like
// "http" or "https" is unknown to the system, this function will
// change them to 80 or 443 respectively. If a hostname fails to
// resolve, that host can still be served but will be listening on
// the wildcard host instead. This function takes care of this for you.
func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr, fatalErr error) {
bindHost := conf.BindHost
// TODO: Do we even need the port? Maybe we just need to look up the host.
resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(bindHost, conf.Port))
if warnErr != nil {
// Most likely the host lookup failed or the port is unknown
tryPort := conf.Port
switch errVal := warnErr.(type) {
case *net.AddrError:
if errVal.Err == "unknown port" {
// some odd Linux machines don't support these port names; see issue #136
switch conf.Port {
case "http":
tryPort = "80"
case "https":
tryPort = "443"
}
}
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(bindHost, tryPort))
if fatalErr != nil {
return
}
default:
// the hostname probably couldn't be resolved, just bind to wildcard then
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("0.0.0.0", tryPort))
if fatalErr != nil {
return
}
}
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 https) and
// root.
func DefaultInput() CaddyfileInput {
port := Port
if letsencrypt.HostQualifies(Host) {
port = "https"
}
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
+147
View File
@@ -0,0 +1,147 @@
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 flag
Host = "not-localhost.com"
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:https\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)
}
}
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, "0.0.0.0", 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, "0.0.0.0", 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)
}
}
}
+7 -6
View File
@@ -1,8 +1,8 @@
package config
package caddy
import (
"github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/config/setup"
"github.com/mholt/caddy/caddy/parse"
"github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/middleware"
)
@@ -42,7 +42,7 @@ func init() {
var directiveOrder = []directive{
// Essential directives that initialize vital configuration settings
{"root", setup.Root},
{"tls", setup.TLS},
{"tls", setup.TLS}, // letsencrypt is set up just after tls
{"bind", setup.BindHost},
// Other directives that don't create HTTP handlers
@@ -57,6 +57,7 @@ var directiveOrder = []directive{
{"rewrite", setup.Rewrite},
{"redir", setup.Redir},
{"ext", setup.Ext},
{"mime", setup.Mime},
{"basicauth", setup.BasicAuth},
{"internal", setup.Internal},
{"proxy", setup.Proxy},
@@ -73,7 +74,7 @@ type directive struct {
setup SetupFunc
}
// A setup function takes a setup controller. Its return values may
// both be nil. If middleware is not nil, it will be chained into
// 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)
+106
View File
@@ -0,0 +1,106 @@
package caddy
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"github.com/mholt/caddy/caddy/letsencrypt"
)
func init() {
letsencrypt.OnChange = func() error { return Restart(nil) }
}
// 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. Used only during graceful restarts.
type caddyfileGob struct {
ListenerFds map[string]uintptr
Caddyfile Input
}
// 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 }
+30
View File
@@ -0,0 +1,30 @@
package letsencrypt
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
}
defer keyOut.Close()
return pem.Encode(keyOut, &pemKey)
}
+51
View File
@@ -0,0 +1,51 @@
package letsencrypt
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"os"
"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)
}
// test load
loadedKey, err := loadRSAPrivateKey(keyFile)
if err != nil {
t.Error("error loading private key:", err)
}
// very loaded key is correct
if !rsaPrivateKeysSame(privateKey, loadedKey) {
t.Error("Expected key bytes to be the same, but they weren't")
}
}
// rsaPrivateKeyBytes returns the bytes of DER-encoded key.
func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte {
return x509.MarshalPKCS1PrivateKey(key)
}
// 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))
}
+55
View File
@@ -0,0 +1,55 @@
package letsencrypt
import (
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/mholt/caddy/middleware"
)
const challengeBasePath = "/.well-known/acme-challenge"
// Handler is a Caddy middleware that can proxy ACME challenge
// requests to the real ACME client endpoint. This is necessary
// to renew certificates while the server is running.
type Handler struct {
Next middleware.Handler
//ChallengeActive int32 // (TODO) use sync/atomic to set/get this flag safely and efficiently
}
// ServeHTTP is basically a no-op unless an ACME challenge is active on this host
// and the request path matches the expected path exactly.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
// Proxy challenge requests to ACME client
// TODO: Only do this if a challenge is active?
if strings.HasPrefix(r.URL.Path, challengeBasePath) {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
hostname, _, err := net.SplitHostPort(r.URL.Host)
if err != nil {
hostname = r.URL.Host
}
upstream, err := url.Parse(scheme + "://" + hostname + ":" + alternatePort)
if err != nil {
return http.StatusInternalServerError, err
}
proxy := httputil.NewSingleHostReverseProxy(upstream)
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client would use self-signed cert
}
proxy.ServeHTTP(w, r)
return 0, nil
}
return h.Next.ServeHTTP(w, r)
}
+540
View File
@@ -0,0 +1,540 @@
// Package letsencrypt 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 letsencrypt
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
"github.com/mholt/caddy/caddy/setup"
"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. It only skips the config if the cert and key
// are already provided, if plaintext http is explicitly
// specified as the port, TLS is explicitly disabled, or
// the host looks like a loopback or wildcard address.
//
// 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. This has the effect
// of causing restarts when assets are 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 prepend or splice.
func Activate(configs []server.Config) ([]server.Config, error) {
// just in case previous caller forgot...
Deactivate()
// reset cached ocsp statuses from any previous activations
ocspStatus = make(map[*[]byte]int)
// Identify and configure any eligible hosts for which
// we already have certs and keys in storage from last time.
configLen := len(configs) // avoid infinite loop since this loop appends plaintext to the slice
for i := 0; i < configLen; i++ {
if existingCertAndKey(configs[i].Host) && configQualifies(configs, i) {
configs = autoConfigure(configs, i)
}
}
// Group configs by email address; only configs that are eligible
// for TLS management are included. We group by email so that we
// can request certificates in batches with the same client.
// Note: The return value is a map, and iteration over a map is
// not ordered. I don't think it will be a problem, but if an
// ordering problem arises, look at this carefully.
groupedConfigs, err := groupConfigsByEmail(configs)
if err != nil {
return configs, err
}
// obtain certificates for configs that need one, and reconfigure each
// config to use the certificates
for leEmail, cfgIndexes := range groupedConfigs {
// make client to service this email address with CA server
client, err := newClient(leEmail)
if err != nil {
return configs, errors.New("error creating client: " + err.Error())
}
// little bit of housekeeping; gather the hostnames into a slice
var hosts []string
for _, idx := range cfgIndexes {
// don't allow duplicates (happens when serving same host on multiple ports!)
var duplicate bool
for _, otherHost := range hosts {
if configs[idx].Host == otherHost {
duplicate = true
break
}
}
if !duplicate {
hosts = append(hosts, configs[idx].Host)
}
}
// client is ready, so let's get free, trusted SSL certificates!
Obtain:
certificates, failures := client.ObtainCertificates(hosts, true)
if len(failures) > 0 {
// Build an error string to return, using all the failures in the list.
var errMsg string
// If an error is because of updated SA, only prompt user for agreement once
var promptedForAgreement bool
for domain, obtainErr := range failures {
// If the failure was simply because the terms have changed, re-prompt and re-try
if tosErr, ok := obtainErr.(acme.TOSError); ok {
if !Agreed && !promptedForAgreement {
Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
promptedForAgreement = true
}
if Agreed {
err := client.AgreeToTOS()
if err != nil {
return configs, errors.New("error agreeing to updated terms: " + err.Error())
}
goto Obtain
}
}
// If user did not agree or it was any other kind of error, just append to the list of errors
errMsg += "[" + domain + "] failed to get certificate: " + obtainErr.Error() + "\n"
}
return configs, errors.New(errMsg)
}
// ... that's it. save the certs, keys, and metadata files to disk
err = saveCertsAndKeys(certificates)
if err != nil {
return configs, errors.New("error saving assets: " + err.Error())
}
// it all comes down to this: turning on TLS with all the new certs
for _, idx := range cfgIndexes {
configs = autoConfigure(configs, idx)
}
}
// renew all certificates that need renewal
renewCertificates(configs, false)
// keep certificates renewed and OCSP stapling updated
go maintainAssets(configs, 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
}
// configQualifies returns true if the config at cfgIndex (within allConfigs)
// qualifes for automatic LE activation. It does NOT check to see if a cert
// and key already exist for the config.
func configQualifies(allConfigs []server.Config, cfgIndex int) bool {
cfg := allConfigs[cfgIndex]
return cfg.TLS.Certificate == "" && // user could provide their own cert and key
cfg.TLS.Key == "" &&
// user can force-disable automatic HTTPS for this host
cfg.Port != "http" &&
cfg.TLS.LetsEncryptEmail != "off" &&
// obviously we get can't certs for loopback or internal hosts
HostQualifies(cfg.Host) &&
// make sure another HTTPS version of this config doesn't exist in the list already
!otherHostHasScheme(allConfigs, cfgIndex, "https")
}
// HostQualifies returns true if the hostname alone
// appears eligible for automatic HTTPS. For example,
// localhost, empty hostname, and wildcard hosts are
// not eligible because we cannot obtain certificates
// for those names.
func HostQualifies(hostname string) bool {
return hostname != "localhost" &&
strings.TrimSpace(hostname) != "" &&
hostname != "0.0.0.0" &&
hostname != "[::]" && // before parsing
hostname != "::" && // after parsing
hostname != "[::1]" && // before parsing
hostname != "::1" && // after parsing
!strings.HasPrefix(hostname, "127.") // to use boulder on your own machine, add fake domain to hosts file
// not excluding 10.* and 192.168.* hosts for possibility of running internal Boulder instance
}
// groupConfigsByEmail groups configs by user email address. The returned map is
// a map of email address to the configs that are serviced under that account.
// If an email address is not available for an eligible config, the user will be
// prompted to provide one. The returned map contains pointers to the original
// server config values.
func groupConfigsByEmail(configs []server.Config) (map[string][]int, error) {
initMap := make(map[string][]int)
for i := 0; i < len(configs); i++ {
// filter out configs that we already have certs for and
// that we won't be obtaining certs for - this way we won't
// bother the user for an email address unnecessarily and
// we don't obtain new certs for a host we already have certs for.
if existingCertAndKey(configs[i].Host) || !configQualifies(configs, i) {
continue
}
leEmail := getEmail(configs[i])
initMap[leEmail] = append(initMap[leEmail], i)
}
return initMap, 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
}
// newClient creates a new ACME client to facilitate communication
// with the Let's Encrypt CA server on behalf of the user specified
// by leEmail. As part of this process, a user will be loaded from
// disk (if already exists) or created new and registered via ACME
// and saved to the file system for next time.
func newClient(leEmail string) (*acme.Client, error) {
return newClientPort(leEmail, "")
}
// newClientPort does the same thing as newClient, except it creates a
// new client with a custom port used for ACME transactions instead of
// the default port. This is important if the default port is already in
// use or is not exposed to the public, etc.
func newClientPort(leEmail, port string) (*acme.Client, error) {
// Look up or create the LE user account
leUser, err := getUser(leEmail)
if err != nil {
return nil, err
}
// The client facilitates our communication with the CA server.
client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse, port)
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 !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) // TODO: Might as well try, right? Error check?
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 client, nil
}
// obtainCertificates obtains certificates from the CA server for
// the configurations in serverConfigs using client.
func obtainCertificates(client *acme.Client, serverConfigs []server.Config) ([]acme.CertificateResource, map[string]error) {
var hosts []string
for _, cfg := range serverConfigs {
hosts = append(hosts, cfg.Host)
}
return client.ObtainCertificates(hosts, true)
}
// saveCertificates saves each certificate resource to disk. This
// includes the certificate file itself, the private key, and the
// metadata file.
func saveCertsAndKeys(certificates []acme.CertificateResource) error {
for _, cert := range certificates {
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
}
// autoConfigure enables TLS on allConfigs[cfgIndex] and appends, if necessary,
// a new config to allConfigs that redirects plaintext HTTP to its new HTTPS
// counterpart. It expects the certificate and key to already be in storage. It
// returns the new list of allConfigs, since it may append a new config. This
// function assumes that allConfigs[cfgIndex] is already set up for HTTPS.
func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config {
cfg := &allConfigs[cfgIndex]
bundleBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
// TODO: Handle these errors better
if err == nil {
ocsp, status, err := acme.GetOCSPForCert(bundleBytes)
ocspStatus[&bundleBytes] = status
if err == nil && status == acme.OCSPGood {
cfg.TLS.OCSPStaple = ocsp
}
}
cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host)
cfg.TLS.Key = storage.SiteKeyFile(cfg.Host)
cfg.TLS.Enabled = true
// Ensure all defaults are set for the TLS config
setup.SetDefaultTLSParams(cfg)
if cfg.Port == "" {
cfg.Port = "https"
}
// Set up http->https redirect as long as there isn't already a http counterpart
// in the configs and this isn't, for some reason, already on port 80.
// Also, the port 80 variant of this config is necessary for proxying challenge requests.
if !otherHostHasScheme(allConfigs, cfgIndex, "http") &&
cfg.Port != "80" && cfg.Port != "http" { // (would not be http port with current program flow, but just in case)
allConfigs = append(allConfigs, redirPlaintextHost(*cfg))
}
// To support renewals, we need handlers at ports 80 and 443,
// depending on the challenge type that is used to complete renewal.
for i, c := range allConfigs {
if c.Address() == cfg.Host+":80" ||
c.Address() == cfg.Host+":443" ||
c.Address() == cfg.Host+":http" ||
c.Address() == cfg.Host+":https" {
// Each virtualhost must have their own handlers, or the chaining gets messed up when middlewares are compiled!
handler := new(Handler)
mid := func(next middleware.Handler) middleware.Handler {
handler.Next = next
return handler
}
// TODO: Currently, acmeHandlers are not referenced, but we need to add a way to toggle
// their proxy functionality -- or maybe not. Gotta figure this out for sure.
acmeHandlers[c.Address()] = handler
allConfigs[i].Middleware["/"] = append(allConfigs[i].Middleware["/"], mid)
}
}
return allConfigs
}
// otherHostHasScheme tells you whether there is ANOTHER config in allConfigs
// for the same host but with the port equal to scheme as allConfigs[cfgIndex].
// This function considers "443" and "https" to be the same scheme, as well as
// "http" and "80". It does not tell you whether there is ANY config with scheme,
// only if there's a different one with it.
func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string) bool {
if scheme == "80" {
scheme = "http"
} else if scheme == "443" {
scheme = "https"
}
for i, otherCfg := range allConfigs {
if i == cfgIndex {
continue // has to be a config OTHER than the one we're comparing against
}
if otherCfg.Host == allConfigs[cfgIndex].Host {
if (otherCfg.Port == scheme) ||
(scheme == "https" && otherCfg.Port == "443") ||
(scheme == "http" && otherCfg.Port == "80") {
return true
}
}
}
return false
}
// 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 the "http" port (port 80).
func redirPlaintextHost(cfg server.Config) server.Config {
toURL := "https://" + cfg.Host
if cfg.Port != "https" && cfg.Port != "http" {
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,
Port: "http",
Middleware: map[string][]middleware.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})
if email == "" {
return errors.New("email is required to revoke")
}
client, err := newClient(email)
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
)
// Some essential values related to the Let's Encrypt process
const (
// 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.
alternatePort = "5033"
// How often to check certificates for renewal.
renewInterval = 24 * time.Hour
// How often to update OCSP stapling.
ocspInterval = 1 * time.Hour
)
// KeySize represents the length of a key in bits.
type KeySize int
// Key sizes are used to determine the strength of a key.
const (
ECC_224 KeySize = 224
ECC_256 = 256
RSA_2048 = 2048
RSA_4096 = 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 = RSA_2048
// stopChan is used to signal the maintenance goroutine
// to terminate.
var stopChan chan struct{}
// ocspStatus maps certificate bundle to OCSP status at start.
// It is used during regular OCSP checks to see if the OCSP
// status has changed.
var ocspStatus = make(map[*[]byte]int)
+79
View File
@@ -0,0 +1,79 @@
package letsencrypt
import (
"net/http"
"testing"
"github.com/mholt/caddy/middleware/redirect"
"github.com/mholt/caddy/server"
)
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", true},
{"10.0.2.1", true},
{"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 TestRedirPlaintextHost(t *testing.T) {
cfg := redirPlaintextHost(server.Config{
Host: "example.com",
Port: "http",
})
// 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.Port, "http"; 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://example.com{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)
}
}
+168
View File
@@ -0,0 +1,168 @@
package letsencrypt
import (
"encoding/json"
"io/ioutil"
"log"
"time"
"github.com/mholt/caddy/server"
"github.com/xenolf/lego/acme"
)
// OnChange is a callback function that will be used to restart
// the application or the part of the application that uses
// the certificates maintained by this package. When at least
// one certificate is renewed or an OCSP status changes, this
// function will be called.
var OnChange func() error
// 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 server configs to maintain and the channel
// which you'll close when maintenance should stop, to allow this
// goroutine to clean up after itself and unblock.
func maintainAssets(configs []server.Config, stopChan chan struct{}) {
renewalTicker := time.NewTicker(renewInterval)
ocspTicker := time.NewTicker(ocspInterval)
for {
select {
case <-renewalTicker.C:
n, errs := renewCertificates(configs, true)
if len(errs) > 0 {
for _, err := range errs {
log.Printf("[ERROR] Certificate renewal: %v", err)
}
}
// even if there was an error, some renewals may have succeeded
if n > 0 && OnChange != nil {
err := OnChange()
if err != nil {
log.Printf("[ERROR] OnChange after cert renewal: %v", err)
}
}
case <-ocspTicker.C:
for bundle, oldStatus := range ocspStatus {
_, newStatus, err := acme.GetOCSPForCert(*bundle)
if err == nil && newStatus != oldStatus && OnChange != nil {
log.Printf("[INFO] OCSP status changed from %v to %v", oldStatus, newStatus)
err := OnChange()
if err != nil {
log.Printf("[ERROR] OnChange after OCSP update: %v", err)
}
break
}
}
case <-stopChan:
renewalTicker.Stop()
ocspTicker.Stop()
return
}
}
}
// renewCertificates loops through all configured site and
// looks for certificates to renew. Nothing is mutated
// through this function; all changes happen directly on disk.
// It returns the number of certificates renewed and any errors
// that occurred. It only performs a renewal if necessary.
// If useCustomPort is true, a custom port will be used, and
// whatever is listening at 443 better proxy ACME requests to it.
// Otherwise, the acme package will create its own listener on 443.
func renewCertificates(configs []server.Config, useCustomPort bool) (int, []error) {
log.Printf("[INFO] Checking certificates for %d hosts", len(configs))
var errs []error
var n int
for _, cfg := range configs {
// Host must be TLS-enabled and have existing assets managed by LE
if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) {
continue
}
// Read the certificate and get the NotAfter time.
certBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
if err != nil {
errs = append(errs, err)
continue // still have to check other certificates
}
expTime, err := acme.GetPEMCertExpiration(certBytes)
if err != nil {
errs = append(errs, err)
continue
}
// The time returned from the certificate is always in UTC.
// So calculate the time left with local time as UTC.
// Directly convert it to days for the following checks.
daysLeft := int(expTime.Sub(time.Now().UTC()).Hours() / 24)
// Renew with two weeks or less remaining.
if daysLeft <= 14 {
log.Printf("[INFO] Certificate for %s has %d days remaining; attempting renewal", cfg.Host, daysLeft)
var client *acme.Client
if useCustomPort {
client, err = newClientPort("", alternatePort) // email not used for renewal
} else {
client, err = newClient("")
}
if err != nil {
errs = append(errs, err)
continue
}
// Read and set up cert meta, required for renewal
metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(cfg.Host))
if err != nil {
errs = append(errs, err)
continue
}
privBytes, err := ioutil.ReadFile(storage.SiteKeyFile(cfg.Host))
if err != nil {
errs = append(errs, err)
continue
}
var certMeta acme.CertificateResource
err = json.Unmarshal(metaBytes, &certMeta)
certMeta.Certificate = certBytes
certMeta.PrivateKey = privBytes
// Renew certificate
Renew:
newCertMeta, err := client.RenewCertificate(certMeta, true, true)
if err != nil {
if _, ok := err.(acme.TOSError); ok {
err := client.AgreeToTOS()
if err != nil {
errs = append(errs, err)
}
goto Renew
}
time.Sleep(10 * time.Second)
newCertMeta, err = client.RenewCertificate(certMeta, true, true)
if err != nil {
errs = append(errs, err)
continue
}
}
saveCertsAndKeys([]acme.CertificateResource{newCertMeta})
n++
} else if daysLeft <= 30 {
// Warn on 30 days remaining. TODO: Just do this once...
log.Printf("[WARNING] Certificate for %s has %d days remaining; will automatically renew when 14 days remain\n", cfg.Host, daysLeft)
}
}
return n, errs
}
// acmeHandlers is a map of host to ACME handler. These
// are used to proxy ACME requests to the ACME client.
var acmeHandlers = make(map[string]*Handler)
+94
View File
@@ -0,0 +1,94 @@
package letsencrypt
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 letsencrypt
import (
"path/filepath"
"testing"
)
func TestStorage(t *testing.T) {
storage = Storage("./letsencrypt")
if expected, actual := filepath.Join("letsencrypt", "sites"), storage.Sites(); actual != expected {
t.Errorf("Expected Sites() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("letsencrypt", "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("letsencrypt", "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("letsencrypt", "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("letsencrypt", "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("letsencrypt", "users"), storage.Users(); actual != expected {
t.Errorf("Expected Users() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("letsencrypt", "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("letsencrypt", "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("letsencrypt", "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("letsencrypt", "users", emptyEmail), storage.User(emptyEmail); actual != expected {
t.Errorf("Expected User(\"\") to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("letsencrypt", "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("letsencrypt", "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)
}
}
}
+196
View File
@@ -0,0 +1,196 @@
package letsencrypt
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.
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.
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.
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.)
func getEmail(cfg server.Config) 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()) {
mostRecent = dir
}
}
if mostRecent != nil {
leEmail = mostRecent.Name()
}
}
}
if leEmail == "" {
// 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("Your 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 ""
}
DefaultEmail = leEmail
Agreed = true
}
return strings.TrimSpace(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"
+192
View File
@@ -0,0 +1,192 @@
package letsencrypt
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) {
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)
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{})
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{})
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{})
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)
}
}
@@ -119,6 +119,12 @@ func (d *Dispenser) NextBlock() bool {
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 {
@@ -203,9 +209,9 @@ func (d *Dispenser) SyntaxErr(expected string) error {
return errors.New(msg)
}
// EofErr returns an EOF error, meaning that end of input
// was found when another token was expected.
func (d *Dispenser) EofErr() error {
// 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")
}
@@ -5,9 +5,12 @@ import "io"
// ServerBlocks parses the input just enough to organize tokens,
// in order, by server block. No further parsing is performed.
// Server blocks are returned in the order in which they appear.
func ServerBlocks(filename string, input io.Reader) ([]serverBlock, error) {
// 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
}
@@ -24,6 +27,6 @@ func allTokens(input io.Reader) (tokens []token) {
return
}
// Set of directives that are valid (unordered). Populated
// ValidDirectives is a set of directives that are valid (unordered). Populated
// by config package's init function.
var ValidDirectives = make(map[string]struct{})
@@ -9,8 +9,9 @@ import (
type parser struct {
Dispenser
block multiServerBlock // current server block being parsed
eof bool // if we encounter a valid EOF in a hard place
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) {
@@ -21,14 +22,8 @@ func (p *parser) parseAll() ([]serverBlock, error) {
if err != nil {
return blocks, err
}
// explode the multiServerBlock into multiple serverBlocks
for _, addr := range p.block.addresses {
blocks = append(blocks, serverBlock{
Host: addr.host,
Port: addr.port,
Tokens: p.block.tokens,
})
if len(p.block.Addresses) > 0 {
blocks = append(blocks, p.block)
}
}
@@ -36,7 +31,7 @@ func (p *parser) parseAll() ([]serverBlock, error) {
}
func (p *parser) parseOne() error {
p.block = multiServerBlock{tokens: make(map[string][]token)}
p.block = serverBlock{Tokens: make(map[string][]token)}
err := p.begin()
if err != nil {
@@ -74,7 +69,7 @@ func (p *parser) addresses() error {
var expectingAnother bool
for {
tkn := p.Val()
tkn := replaceEnvVars(p.Val())
// special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() {
@@ -93,26 +88,28 @@ func (p *parser) addresses() error {
break
}
// 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
}
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
host, port, err := standardAddress(tkn)
if err != nil {
return err
// Parse and save this address
host, port, err := standardAddress(tkn)
if err != nil {
return err
}
p.block.Addresses = append(p.block.Addresses, address{host, port})
}
p.block.addresses = append(p.block.addresses, address{host, port})
// Advance token and possibly break out of loop or return error
hasNext := p.Next()
if expectingAnother && !hasNext {
return p.EofErr()
return p.EOFErr()
}
if !hasNext {
p.eof = true
@@ -224,12 +221,14 @@ func (p *parser) directive() error {
dir := p.Val()
nesting := 0
if _, ok := ValidDirectives[dir]; !ok {
return p.Errf("Unknown directive '%s'", dir)
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])
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
for p.Next() {
if p.Val() == "{" {
@@ -242,11 +241,12 @@ func (p *parser) directive() error {
} else if p.Val() == "}" && nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace")
}
p.block.tokens[dir] = append(p.block.tokens[dir], p.tokens[p.cursor])
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 p.EOFErr()
}
return nil
}
@@ -304,22 +304,52 @@ func standardAddress(str string) (host, port string, err error) {
return
}
type (
// serverBlock stores tokens by directive name for a
// single host:port (address)
serverBlock struct {
Host, Port string
Tokens map[string][]token // directive name to tokens (including directive)
}
// replaceEnvVars replaces environment variables that appear in the token
// and understands both the Unix $SYNTAX and Windows %SYNTAX%.
func replaceEnvVars(s string) string {
s = replaceEnvReferences(s, "{%", "%}")
s = replaceEnvReferences(s, "{$", "}")
return s
}
// multiServerBlock is the same as serverBlock but for
// multiple addresses that share the same tokens
multiServerBlock struct {
addresses []address
tokens map[string][]token
// 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 {
host, port string
Host, Port string
}
)
// HostList converts the list of addresses (hosts)
// that are associated with this server block into
// a slice of strings. Each string is a host:port
// combination.
func (sb serverBlock) HostList() []string {
sbHosts := make([]string, len(sb.Addresses))
for j, addr := range sb.Addresses {
sbHosts[j] = net.JoinHostPort(addr.Host, addr.Port)
}
return sbHosts
}
@@ -1,7 +1,7 @@
package parse
import (
"reflect"
"os"
"strings"
"testing"
)
@@ -60,9 +60,9 @@ func TestStandardAddress(t *testing.T) {
func TestParseOneAndImport(t *testing.T) {
setupParseTests()
testParseOne := func(input string) (multiServerBlock, error) {
testParseOne := func(input string) (serverBlock, error) {
p := testParser(input)
p.Next()
p.Next() // parseOne doesn't call Next() to start, so we must
err := p.parseOne()
return p.block, err
}
@@ -204,6 +204,14 @@ func TestParseOneAndImport(t *testing.T) {
"dir1": 3,
}},
{`localhost
dir1 {
} }`, true, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
nested {
@@ -235,6 +243,14 @@ func TestParseOneAndImport(t *testing.T) {
"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)
@@ -245,28 +261,28 @@ func TestParseOneAndImport(t *testing.T) {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(result.addresses) != len(test.addresses) {
if len(result.Addresses) != len(test.addresses) {
t.Errorf("Test %d: Expected %d addresses, got %d",
i, len(test.addresses), len(result.addresses))
i, len(test.addresses), len(result.Addresses))
continue
}
for j, addr := range result.addresses {
if addr.host != test.addresses[j].host {
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)
i, j, test.addresses[j].Host, addr.Host)
}
if addr.port != test.addresses[j].port {
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)
i, j, test.addresses[j].Port, addr.Port)
}
}
if len(result.tokens) != len(test.tokens) {
if len(result.Tokens) != len(test.tokens) {
t.Errorf("Test %d: Expected %d directives, had %d",
i, len(test.tokens), len(result.tokens))
i, len(test.tokens), len(result.Tokens))
continue
}
for directive, tokens := range result.tokens {
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))
@@ -282,39 +298,36 @@ func TestParseAll(t *testing.T) {
for i, test := range []struct {
input string
shouldErr bool
addresses []address // one per expected server block, in order
addresses [][]address // addresses per server block, in order
}{
{`localhost`, false, []address{
{"localhost", ""},
{`localhost`, false, [][]address{
{{"localhost", ""}},
}},
{`localhost:1234`, false, []address{
{"localhost", "1234"},
{`localhost:1234`, false, [][]address{
[]address{{"localhost", "1234"}},
}},
{`localhost:1234 {
}
localhost:2015 {
}`, false, []address{
{"localhost", "1234"},
{"localhost", "2015"},
}`, false, [][]address{
[]address{{"localhost", "1234"}},
[]address{{"localhost", "2015"}},
}},
{`localhost:1234, http://host2`, false, []address{
{"localhost", "1234"},
{"host2", "http"},
{`localhost:1234, http://host2`, false, [][]address{
[]address{{"localhost", "1234"}, {"host2", "http"}},
}},
{`localhost:1234, http://host2,`, true, []address{}},
{`localhost:1234, http://host2,`, true, [][]address{}},
{`http://host1.com, http://host2.com {
}
https://host3.com, https://host4.com {
}`, false, []address{
{"host1.com", "http"},
{"host2.com", "http"},
{"host3.com", "https"},
{"host4.com", "https"},
}`, false, [][]address{
[]address{{"host1.com", "http"}, {"host2.com", "http"}},
[]address{{"host3.com", "https"}, {"host4.com", "https"}},
}},
} {
p := testParser(test.input)
@@ -333,50 +346,112 @@ func TestParseAll(t *testing.T) {
continue
}
for j, block := range blocks {
if block.Host != test.addresses[j].host {
t.Errorf("Test %d, block %d: Expected host to be '%s', but was '%s'",
i, j, test.addresses[j].host, block.Host)
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
}
if block.Port != test.addresses[j].port {
t.Errorf("Test %d, block %d: Expected port to be '%s', but was '%s'",
i, j, test.addresses[j].port, block.Port)
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)
}
}
}
}
}
// Exploding the server blocks that have more than one address should replicate/share tokens
p := testParser(`host1 {
dir1 foo bar
}
func TestEnvironmentReplacement(t *testing.T) {
setupParseTests()
host2, host3 {
dir2 foo bar
dir3 foo {
bar
}
}`)
blocks, err := p.parseAll()
if err != nil {
t.Fatalf("Expected there to not be an error, but there was: %v", err)
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)
}
if !reflect.DeepEqual(blocks[1].Tokens, blocks[2].Tokens) {
t.Errorf("Expected host2 and host3 to have same tokens, but they didn't.\nhost2 Block: %v\nhost3 Block: %v",
blocks[1].Tokens, blocks[2].Tokens)
// 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)
}
}
func setupParseTests() {
// Set up some bogus directives for testing
ValidDirectives = map[string]struct{}{
"dir1": struct{}{},
"dir2": struct{}{},
"dir3": struct{}{},
"dir1": {},
"dir2": {},
"dir3": {},
}
}
func testParser(input string) parser {
buf := strings.NewReader(input)
p := parser{Dispenser: NewDispenser("Test", buf)}
p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true}
return p
}
+112
View File
@@ -0,0 +1,112 @@
// +build !windows
package caddy
import (
"encoding/gob"
"io/ioutil"
"log"
"os"
"os/exec"
)
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 {
if newCaddyfile == nil {
caddyfileMu.Lock()
newCaddyfile = caddyfile
caddyfileMu.Unlock()
}
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,
}
// 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}
// 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()
}
+27
View File
@@ -0,0 +1,27 @@
package caddy
// Restart restarts Caddy forcefully using newCaddyfile,
// or, if nil, the current/existing Caddyfile is reused.
func Restart(newCaddyfile Input) error {
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
}
@@ -1,12 +1,16 @@
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
@@ -16,6 +20,7 @@ func BasicAuth(c *Controller) (middleware.Middleware, error) {
return func(next middleware.Handler) middleware.Handler {
basic.Next = next
basic.SiteRoot = root
return basic
}, nil
}
@@ -23,6 +28,7 @@ func BasicAuth(c *Controller) (middleware.Middleware, error) {
func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
var rules []basicauth.Rule
var err error
for c.Next() {
var rule basicauth.Rule
@@ -31,7 +37,10 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
switch len(args) {
case 2:
rule.Username = args[0]
rule.Password = args[1]
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() {
@@ -41,7 +50,9 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
case 3:
rule.Resources = append(rule.Resources, args[0])
rule.Username = args[1]
rule.Password = args[2]
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()
}
@@ -51,3 +62,11 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
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)
}
@@ -2,6 +2,9 @@ package setup
import (
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/mholt/caddy/middleware/basicauth"
@@ -30,35 +33,57 @@ func TestBasicAuth(t *testing.T) {
}
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, []basicauth.Rule{
{Username: "user", Password: "pwd"},
{`basicauth user pwd`, false, "pwd", []basicauth.Rule{
{Username: "user"},
}},
{`basicauth user pwd {
}`, false, []basicauth.Rule{
{Username: "user", Password: "pwd"},
}`, false, "pwd", []basicauth.Rule{
{Username: "user"},
}},
{`basicauth user pwd {
/resource1
/resource2
}`, false, []basicauth.Rule{
{Username: "user", Password: "pwd", Resources: []string{"/resource1", "/resource2"}},
}`, false, "pwd", []basicauth.Rule{
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
}},
{`basicauth /resource user pwd`, false, []basicauth.Rule{
{Username: "user", Password: "pwd", Resources: []string{"/resource"}},
{`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{
{Username: "user", Resources: []string{"/resource"}},
}},
{`basicauth /res1 user1 pwd1
basicauth /res2 user2 pwd2`, false, []basicauth.Rule{
{Username: "user1", Password: "pwd1", Resources: []string{"/res1"}},
{Username: "user2", Password: "pwd2", Resources: []string{"/res2"}},
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"},
}},
{`basicauth user`, true, []basicauth.Rule{}},
{`basicauth`, true, []basicauth.Rule{}},
{`basicauth /resource user pwd asdf`, true, []basicauth.Rule{}},
}
for i, test := range tests {
@@ -84,9 +109,16 @@ func TestBasicAuthParse(t *testing.T) {
i, j, expectedRule.Username, actualRule.Username)
}
if actualRule.Password != expectedRule.Password {
t.Errorf("Test %d, rule %d: Expected password '%s', got '%s'",
i, j, expectedRule.Password, actualRule.Password)
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)
@@ -2,6 +2,7 @@ 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) {
@@ -2,8 +2,8 @@ package setup
import (
"fmt"
"html/template"
"io/ioutil"
"text/template"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/browse"
@@ -17,8 +17,9 @@ func Browse(c *Controller) (middleware.Middleware, error) {
}
browse := browse.Browse{
Root: c.Root,
Configs: configs,
Root: c.Root,
Configs: configs,
IgnoreIndexes: false,
}
return func(next middleware.Handler) middleware.Handler {
@@ -192,6 +193,10 @@ th a {
margin-top: 70px;
}
}
.name {
white-space: pre;
}
</style>
</head>
<body>
@@ -202,7 +207,7 @@ th a {
<div class="up">&nbsp;</div>
{{end}}
<h1>{{.Path}}</h1>
<h1 class="name">{{.Path}}</h1>
</header>
<main>
<table>
@@ -239,7 +244,7 @@ th a {
<tr>
<td>
{{if .IsDir}}&#128194;{{else}}&#128196;{{end}}
<a href="{{.URL}}">{{.Name}}</a>
<a href="{{.URL}}" class="name">{{.Name}}</a>
</td>
<td>{{.HumanSize}}</td>
<td class="hideable">{{.HumanModTime "01/02/2006 3:04:05 PM -0700"}}</td>
+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-existant 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)
}
@@ -1,12 +1,13 @@
package setup
import (
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"strconv"
"github.com/hashicorp/go-syslog"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/errors"
)
@@ -21,20 +22,43 @@ func Errors(c *Controller) (middleware.Middleware, error) {
// Open the log file for writing when the server starts
c.Startup = append(c.Startup, func() error {
var err error
var file *os.File
var writer io.Writer
if handler.LogFile == "stdout" {
file = os.Stdout
} else if handler.LogFile == "stderr" {
file = os.Stderr
} else if handler.LogFile != "" {
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(file, "", 0)
handler.Log = log.New(writer, "", 0)
return nil
})
@@ -63,13 +87,27 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
where := c.Val()
if what == "log" {
handler.LogFile = where
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 = path.Join(c.Root, where)
where = filepath.Join(c.Root, where)
f, err := os.Open(where)
if err != nil {
fmt.Println("Warning: Unable to open error page '" + where + "': " + err.Error())
log.Printf("[WARNING] Unable to open error page '%s': %v", where, err)
}
f.Close()
@@ -84,18 +122,24 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
}
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
// Otherwise, the only argument would be an error log file name or 'visible'
if !hadBlock {
if c.NextArg() {
handler.LogFile = c.Val()
} else {
handler.LogFile = errors.DefaultLogFilename
if c.Val() == "visible" {
handler.Debug = true
} else {
handler.LogFile = c.Val()
}
}
}
}
+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)
}
}
}
}
+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)
}
}
}
}
+2 -2
View File
@@ -27,8 +27,8 @@ func gzipParse(c *Controller) ([]gzip.Config, error) {
for c.Next() {
config := gzip.Config{}
pathFilter := gzip.PathFilter{make(gzip.Set)}
extFilter := gzip.ExtFilter{make(gzip.Set)}
pathFilter := gzip.PathFilter{IgnoredPaths: make(gzip.Set)}
extFilter := gzip.ExtFilter{Exts: make(gzip.Set)}
// No extra args expected
if len(c.RemainingArgs()) > 0 {
+42 -4
View File
@@ -1,9 +1,11 @@
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"
@@ -20,20 +22,33 @@ func Log(c *Controller) (middleware.Middleware, error) {
c.Startup = append(c.Startup, func() error {
for i := 0; i < len(rules); i++ {
var err error
var file *os.File
var writer io.Writer
if rules[i].OutputFile == "stdout" {
file = os.Stdout
writer = os.Stdout
} else if rules[i].OutputFile == "stderr" {
file = os.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(file, "", 0)
rules[i].Log = log.New(writer, "", 0)
}
return nil
@@ -50,12 +65,33 @@ func logParse(c *Controller) ([]caddylog.Rule, error) {
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
@@ -63,6 +99,7 @@ func logParse(c *Controller) ([]caddylog.Rule, error) {
PathScope: "/",
OutputFile: args[0],
Format: caddylog.DefaultLogFormat,
Roller: logRoller,
})
} else {
// Path scope, output file, and maybe a format specified
@@ -84,6 +121,7 @@ func logParse(c *Controller) ([]caddylog.Rule, error) {
PathScope: args[0],
OutputFile: args[1],
Format: format,
Roller: logRoller,
})
}
}
@@ -3,6 +3,7 @@ package setup
import (
"testing"
"github.com/mholt/caddy/middleware"
caddylog "github.com/mholt/caddy/middleware/log"
)
@@ -36,6 +37,9 @@ func TestLog(t *testing.T) {
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")
}
@@ -78,7 +82,7 @@ func TestLogParse(t *testing.T) {
OutputFile: "accesslog.txt",
Format: caddylog.CombinedLogFormat,
}}},
{`log /api1 log.txt
{`log /api1 log.txt
log /api2 accesslog.txt {combined}`, false, []caddylog.Rule{{
PathScope: "/api1",
OutputFile: "log.txt",
@@ -98,6 +102,17 @@ func TestLogParse(t *testing.T) {
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)
@@ -128,6 +143,32 @@ func TestLogParse(t *testing.T) {
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)
}
}
}
@@ -7,11 +7,11 @@ import (
// Proxy configures a new Proxy middleware instance.
func Proxy(c *Controller) (middleware.Middleware, error) {
if upstreams, err := proxy.NewStaticUpstreams(c.Dispenser); err == nil {
return func(next middleware.Handler) middleware.Handler {
return proxy.Proxy{Next: next, Upstreams: upstreams}
}, nil
} else {
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{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{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{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{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{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{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{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{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{redirect.Rule{FromPath: "/", To: "/foo", Code: 304}, redirect.Rule{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{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)
}
}
}
}
@@ -42,17 +42,17 @@ func TestRewriteParse(t *testing.T) {
expected []rewrite.Rule
}{
{`rewrite /from /to`, false, []rewrite.Rule{
rewrite.SimpleRule{"/from", "/to"},
rewrite.SimpleRule{From: "/from", To: "/to"},
}},
{`rewrite /from /to
rewrite a b`, false, []rewrite.Rule{
rewrite.SimpleRule{"/from", "/to"},
rewrite.SimpleRule{"a", "b"},
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`, true, []rewrite.Rule{
rewrite.SimpleRule{"a", "b"},
rewrite.SimpleRule{From: "a", To: "b"},
}},
}
@@ -98,14 +98,14 @@ func TestRewriteParse(t *testing.T) {
r .*
to /to
}`, false, []rewrite.Rule{
&rewrite.RegexpRule{"/", "/to", nil, regexp.MustCompile(".*")},
&rewrite.RegexpRule{Base: "/", To: "/to", Regexp: regexp.MustCompile(".*")},
}},
{`rewrite {
regexp .*
to /to
ext / html txt
}`, false, []rewrite.Rule{
&rewrite.RegexpRule{"/", "/to", []string{"/", "html", "txt"}, regexp.MustCompile(".*")},
&rewrite.RegexpRule{Base: "/", To: "/to", Exts: []string{"/", "html", "txt"}, Regexp: regexp.MustCompile(".*")},
}},
{`rewrite /path {
r rr
@@ -116,8 +116,8 @@ func TestRewriteParse(t *testing.T) {
to /to
}
`, false, []rewrite.Rule{
&rewrite.RegexpRule{"/path", "/dest", nil, regexp.MustCompile("rr")},
&rewrite.RegexpRule{"/", "/to", nil, regexp.MustCompile("[a-z]+")},
&rewrite.RegexpRule{Base: "/path", To: "/dest", Regexp: regexp.MustCompile("rr")},
&rewrite.RegexpRule{Base: "/", To: "/to", Regexp: regexp.MustCompile("[a-z]+")},
}},
{`rewrite {
to /to
+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
}
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"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() {
@@ -21,7 +22,7 @@ func Root(c *Controller) (middleware.Middleware, error) {
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)
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)
}
+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")
}
@@ -8,10 +8,12 @@ import (
"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.Startup)
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)
}
@@ -20,6 +22,8 @@ func Shutdown(c *Controller) (middleware.Middleware, error) {
// 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 {
@@ -46,13 +50,15 @@ func registerCallback(c *Controller, list *[]func() error) error {
if nonblock {
return cmd.Start()
} else {
return cmd.Run()
}
return cmd.Run()
}
*list = append(*list, fn)
funcs = append(funcs, fn)
}
return nil
return c.OncePerServerBlock(func() error {
*list = append(*list, funcs...)
return nil
})
}
+58
View File
@@ -0,0 +1,58 @@
package setup
import (
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"
"time"
)
// The Startup function's tests are symmetrical to Shutdown tests,
// because the Startup and Shutdown functions share virtually the
// same functionality
func TestStartup(t *testing.T) {
tempDirPath, err := getTempDirPath()
if err != nil {
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
}
testDir := filepath.Join(tempDirPath, "temp_dir_for_testing_startupshutdown.go")
osSenitiveTestDir := filepath.FromSlash(testDir)
exec.Command("rm", "-r", osSenitiveTestDir).Run() // removes osSenitiveTestDir from the OS's temp directory, if the osSenitiveTestDir already exists
tests := []struct {
input string
shouldExecutionErr bool
shouldRemoveErr bool
}{
// test case #0 tests proper functionality blocking commands
{"startup mkdir " + osSenitiveTestDir, false, false},
// test case #1 tests proper functionality of non-blocking commands
{"startup mkdir " + osSenitiveTestDir + " &", false, true},
// test case #2 tests handling of non-existant commands
{"startup " + strconv.Itoa(int(time.Now().UnixNano())), true, true},
}
for i, test := range tests {
c := NewTestController(test.input)
_, err = Startup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
err = c.FirstStartup[0]()
if err != nil && !test.shouldExecutionErr {
t.Errorf("Test %d recieved an error of:\n%v", i, err)
}
err = os.Remove(osSenitiveTestDir)
if err != nil && !test.shouldRemoveErr {
t.Errorf("Test %d recieved an error of:\n%v", i, err)
}
}
}
@@ -32,18 +32,48 @@ func templatesParse(c *Controller) ([]templates.Rule, error) {
for c.Next() {
var rule templates.Rule
if c.NextArg() {
rule.Path = defaultTemplatePath
rule.Extensions = defaultTemplateExtensions
args := c.RemainingArgs()
switch len(args) {
case 0:
// Optional block
for c.NextBlock() {
switch c.Val() {
case "path":
args := c.RemainingArgs()
if len(args) != 1 {
return nil, c.ArgErr()
}
rule.Path = args[0]
case "ext":
args := c.RemainingArgs()
if len(args) == 0 {
return nil, c.ArgErr()
}
rule.Extensions = args
case "between":
args := c.RemainingArgs()
if len(args) != 2 {
return nil, c.ArgErr()
}
rule.Delims[0] = args[0]
rule.Delims[1] = args[1]
}
}
default:
// First argument would be the path
rule.Path = c.Val()
rule.Path = args[0]
// Any remaining arguments are extensions
rule.Extensions = c.RemainingArgs()
rule.Extensions = args[1:]
if len(rule.Extensions) == 0 {
rule.Extensions = defaultTemplateExtensions
}
} else {
rule.Path = defaultTemplatePath
rule.Extensions = defaultTemplateExtensions
}
for _, ext := range rule.Extensions {
@@ -52,7 +82,6 @@ func templatesParse(c *Controller) ([]templates.Rule, error) {
rules = append(rules, rule)
}
return rules, nil
}
@@ -2,8 +2,9 @@ package setup
import (
"fmt"
"github.com/mholt/caddy/middleware/templates"
"testing"
"github.com/mholt/caddy/middleware/templates"
)
func TestTemplates(t *testing.T) {
@@ -40,7 +41,11 @@ func TestTemplates(t *testing.T) {
if fmt.Sprint(myHandler.Rules[0].IndexFiles) != fmt.Sprint(indexFiles) {
t.Errorf("Expected %v to be the Default Index files", indexFiles)
}
if myHandler.Rules[0].Delims != [2]string{} {
t.Errorf("Expected %v to be the Default Delims", [2]string{})
}
}
func TestTemplatesParse(t *testing.T) {
tests := []struct {
inputTemplateConfig string
@@ -50,19 +55,32 @@ func TestTemplatesParse(t *testing.T) {
{`templates /api1`, false, []templates.Rule{{
Path: "/api1",
Extensions: defaultTemplateExtensions,
Delims: [2]string{},
}}},
{`templates /api2 .txt .htm`, false, []templates.Rule{{
Path: "/api2",
Extensions: []string{".txt", ".htm"},
Delims: [2]string{},
}}},
{`templates /api3 .htm .html
{`templates /api3 .htm .html
templates /api4 .txt .tpl `, false, []templates.Rule{{
Path: "/api3",
Extensions: []string{".htm", ".html"},
Delims: [2]string{},
}, {
Path: "/api4",
Extensions: []string{".txt", ".tpl"},
Delims: [2]string{},
}}},
{`templates {
path /api5
ext .html
between {% %}
}`, false, []templates.Rule{{
Path: "/api5",
Extensions: []string{".html"},
Delims: [2]string{"{%", "%}"},
}}},
}
for i, test := range tests {
+1
View File
@@ -0,0 +1 @@
# Test h1
+1
View File
@@ -0,0 +1 @@
<h1>Header title</h1>
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Doc.title}}</title>
</head>
<body>
{{.Include "header.html"}}
{{.Doc.body}}
</body>
</html>
+32 -10
View File
@@ -6,26 +6,42 @@ import (
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/server"
)
// TLS sets up the TLS configuration (but does not activate Let's Encrypt; that is handled elsewhere).
func TLS(c *Controller) (middleware.Middleware, error) {
c.TLS.Enabled = true
if c.Port == "http" {
c.TLS.Enabled = false
log.Printf("Warning: TLS disabled for %s://%s. To force TLS over the plaintext HTTP port, "+
log.Printf("[WARNING] TLS disabled for %s://%s. To force TLS over the plaintext HTTP port, "+
"specify port 80 explicitly (https://%s:80).", c.Port, c.Host, c.Host)
} else {
c.TLS.Enabled = true // they had a tls directive, so assume it's on unless we confirm otherwise later
}
for c.Next() {
if !c.NextArg() {
return nil, c.ArgErr()
}
c.TLS.Certificate = c.Val()
args := c.RemainingArgs()
switch len(args) {
case 1:
c.TLS.LetsEncryptEmail = args[0]
if !c.NextArg() {
// user can force-disable LE activation this way
if c.TLS.LetsEncryptEmail == "off" {
c.TLS.Enabled = false
}
case 2:
c.TLS.Certificate = args[0]
c.TLS.Key = args[1]
// manual HTTPS configuration without port specified should be
// served on the HTTPS port; that is what user would expect, and
// makes it consistent with how the letsencrypt package works.
if c.Port == "" {
c.Port = "https"
}
default:
return nil, c.ArgErr()
}
c.TLS.Key = c.Val()
// Optional block
for c.NextBlock() {
@@ -64,6 +80,14 @@ func TLS(c *Controller) (middleware.Middleware, error) {
}
}
SetDefaultTLSParams(c.Config)
return nil, nil
}
// SetDefaultTLSParams sets the default TLS cipher suites, protocol versions and server preferences
// of a server.Config if they were not previously set.
func SetDefaultTLSParams(c *server.Config) {
// If no ciphers provided, use all that Caddy supports for the protocol
if len(c.TLS.Ciphers) == 0 {
c.TLS.Ciphers = supportedCiphers
@@ -82,8 +106,6 @@ func TLS(c *Controller) (middleware.Middleware, error) {
// Prefer server cipher suites
c.TLS.PreferServerCipherSuites = true
return nil, nil
}
// Map of supported protocols
@@ -70,14 +70,7 @@ func TestTLSParseIncompleteParams(t *testing.T) {
_, err := TLS(c)
if err == nil {
t.Errorf("Expected errors, but no error returned")
}
c = NewTestController(`tls cert.key`)
_, err = TLS(c)
if err == nil {
t.Errorf("Expected errors, but no error returned")
t.Errorf("Expected errors (first check), but no error returned")
}
}
@@ -2,26 +2,26 @@ package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/websockets"
"github.com/mholt/caddy/middleware/websocket"
)
// WebSocket configures a new WebSockets middleware instance.
// WebSocket configures a new WebSocket middleware instance.
func WebSocket(c *Controller) (middleware.Middleware, error) {
websocks, err := webSocketParse(c)
if err != nil {
return nil, err
}
websockets.GatewayInterface = c.AppName + "-CGI/1.1"
websockets.ServerSoftware = c.AppName + "/" + c.AppVersion
websocket.GatewayInterface = c.AppName + "-CGI/1.1"
websocket.ServerSoftware = c.AppName + "/" + c.AppVersion
return func(next middleware.Handler) middleware.Handler {
return websockets.WebSockets{Next: next, Sockets: websocks}
return websocket.WebSocket{Next: next, Sockets: websocks}
}, nil
}
func webSocketParse(c *Controller) ([]websockets.Config, error) {
var websocks []websockets.Config
func webSocketParse(c *Controller) ([]websocket.Config, error) {
var websocks []websocket.Config
var respawn bool
optionalBlock := func() (hadBlock bool, err error) {
@@ -74,7 +74,7 @@ func webSocketParse(c *Controller) ([]websockets.Config, error) {
return nil, err
}
websocks = append(websocks, websockets.Config{
websocks = append(websocks, websocket.Config{
Path: path,
Command: cmd,
Arguments: args,
@@ -1,8 +1,9 @@
package setup
import (
"github.com/mholt/caddy/middleware/websockets"
"testing"
"github.com/mholt/caddy/middleware/websocket"
)
func TestWebSocket(t *testing.T) {
@@ -20,10 +21,10 @@ func TestWebSocket(t *testing.T) {
}
handler := mid(EmptyNext)
myHandler, ok := handler.(websockets.WebSockets)
myHandler, ok := handler.(websocket.WebSocket)
if !ok {
t.Fatalf("Expected handler to be type WebSockets, got: %#v", handler)
t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler)
}
if myHandler.Sockets[0].Path != "/" {
@@ -38,21 +39,40 @@ func TestWebSocketParse(t *testing.T) {
tests := []struct {
inputWebSocketConfig string
shouldErr bool
expectedWebSocketConfig []websockets.Config
expectedWebSocketConfig []websocket.Config
}{
{`websocket /api1 cat`, false, []websockets.Config{{
{`websocket /api1 cat`, false, []websocket.Config{{
Path: "/api1",
Command: "cat",
}}},
{`websocket /api3 cat
websocket /api4 cat `, false, []websockets.Config{{
websocket /api4 cat `, false, []websocket.Config{{
Path: "/api3",
Command: "cat",
}, {
Path: "/api4",
Command: "cat",
}}},
{`websocket /api5 "cmd arg1 arg2 arg3"`, false, []websocket.Config{{
Path: "/api5",
Command: "cmd",
Arguments: []string{"arg1", "arg2", "arg3"},
}}},
// accept respawn
{`websocket /api6 cat {
respawn
}`, false, []websocket.Config{{
Path: "/api6",
Command: "cat",
}}},
// invalid configuration
{`websocket /api7 cat {
invalid
}`, true, []websocket.Config{}},
}
for i, test := range tests {
c := NewTestController(test.inputWebSocketConfig)
+63
View File
@@ -0,0 +1,63 @@
package caddy
import (
"log"
"os"
"os/signal"
"sync"
"github.com/mholt/caddy/server"
)
// TrapSignals create signal handlers for all applicable signals for this
// system. If your Go program uses signals, this is a rather invasive
// function; best to implement them yourself in that case. Signals are not
// required for the caddy package to function properly, but this is a
// convenient way to allow the user to control this package of your program.
func TrapSignals() {
trapSignalsCrossPlatform()
trapSignalsPosix()
}
// trapSignalsCrossPlatform captures SIGINT, which triggers forceful
// shutdown that executes shutdown callbacks first. A second interrupt
// signal will exit the process immediately.
func trapSignalsCrossPlatform() {
go func() {
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt)
for i := 0; true; i++ {
<-shutdown
if i > 0 {
log.Println("[INFO] SIGINT: Force quit")
os.Exit(1)
}
log.Println("[INFO] SIGINT: Shutting down")
go os.Exit(executeShutdownCallbacks("SIGINT"))
}
}()
}
// executeShutdownCallbacks executes the shutdown callbacks as initiated
// by signame. It logs any errors and returns the recommended exit status.
// This function is idempotent; subsequent invocations always return 0.
func executeShutdownCallbacks(signame string) (exitCode int) {
shutdownCallbacksOnce.Do(func() {
serversMu.Lock()
errs := server.ShutdownCallbacks(servers)
serversMu.Unlock()
if len(errs) > 0 {
for _, err := range errs {
log.Printf("[ERROR] %s shutdown: %v", signame, err)
}
exitCode = 1
}
})
return
}
var shutdownCallbacksOnce sync.Once
+73
View File
@@ -0,0 +1,73 @@
// +build !windows
package caddy
import (
"io/ioutil"
"log"
"os"
"os/signal"
"syscall"
)
// trapSignalsPosix captures POSIX-only signals.
func trapSignalsPosix() {
go func() {
sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1)
for sig := range sigchan {
switch sig {
case syscall.SIGTERM:
log.Println("[INFO] SIGTERM: Terminating process")
os.Exit(0)
case syscall.SIGQUIT:
log.Println("[INFO] SIGQUIT: Shutting down")
exitCode := executeShutdownCallbacks("SIGQUIT")
err := Stop()
if err != nil {
log.Printf("[ERROR] SIGQUIT stop: %v", err)
exitCode = 1
}
os.Exit(exitCode)
case syscall.SIGHUP:
log.Println("[INFO] SIGHUP: Hanging up")
err := Stop()
if err != nil {
log.Printf("[ERROR] SIGHUP stop: %v", err)
}
case syscall.SIGUSR1:
log.Println("[INFO] SIGUSR1: Reloading")
var updatedCaddyfile Input
caddyfileMu.Lock()
if caddyfile == nil {
// Hmm, did spawing process forget to close stdin? Anyhow, this is unusual.
log.Println("[ERROR] SIGUSR1: no Caddyfile to reload (was stdin left open?)")
caddyfileMu.Unlock()
continue
}
if caddyfile.IsFile() {
body, err := ioutil.ReadFile(caddyfile.Path())
if err == nil {
updatedCaddyfile = CaddyfileInput{
Filepath: caddyfile.Path(),
Contents: body,
RealFile: true,
}
}
}
caddyfileMu.Unlock()
err := Restart(updatedCaddyfile)
if err != nil {
log.Printf("[ERROR] SIGUSR1: %v", err)
}
}
}
}()
}
+3
View File
@@ -0,0 +1,3 @@
package caddy
func trapSignalsPosix() {}
-225
View File
@@ -1,225 +0,0 @@
package config
import (
"fmt"
"io"
"log"
"net"
"github.com/mholt/caddy/app"
"github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/config/setup"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/server"
)
const (
DefaultHost = "0.0.0.0"
DefaultPort = "2015"
DefaultRoot = "."
// DefaultConfigFile is the name of the configuration file that is loaded
// by default if no other file is specified.
DefaultConfigFile = "Caddyfile"
)
func Load(filename string, input io.Reader) ([]server.Config, error) {
var configs []server.Config
// turn off timestamp for parsing
flags := log.Flags()
log.SetFlags(0)
serverBlocks, err := parse.ServerBlocks(filename, input)
if err != nil {
return configs, err
}
// Each server block represents a single server/address.
// Iterate each server block and make a config for each one,
// executing the directives that were parsed.
for _, sb := range serverBlocks {
config := server.Config{
Host: sb.Host,
Port: sb.Port,
Root: Root,
Middleware: make(map[string][]middleware.Middleware),
ConfigFile: filename,
AppName: app.Name,
AppVersion: app.Version,
}
// It is crucial that directives are executed in the proper order.
for _, 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, which is the
// server config and the dispenser containing only
// this directive's tokens.
controller := &setup.Controller{
Config: &config,
Dispenser: parse.NewDispenserTokens(filename, tokens),
}
midware, err := dir.setup(controller)
if err != nil {
return configs, err
}
if midware != nil {
// TODO: For now, we only support the default path scope /
config.Middleware["/"] = append(config.Middleware["/"], midware)
}
}
}
if config.Port == "" {
config.Port = Port
}
configs = append(configs, config)
}
// restore logging settings
log.SetFlags(flags)
return configs, nil
}
// 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) (map[*net.TCPAddr][]server.Config, error) {
addresses := make(map[*net.TCPAddr][]server.Config)
// Group configs by bind address
for _, conf := range allConfigs {
newAddr, warnErr, fatalErr := resolveAddr(conf)
if fatalErr != nil {
return addresses, fatalErr
}
if warnErr != nil {
log.Println("[Warning]", 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 addr := range addresses {
if addr.String() == newAddr.String() {
addresses[addr] = append(addresses[addr], conf)
existing = true
break
}
}
if !existing {
addresses[newAddr] = append(addresses[newAddr], conf)
}
}
// Don't allow HTTP and HTTPS to be served on the same address
for _, configs := range addresses {
isTLS := configs[0].TLS.Enabled
for _, config := range configs {
if config.TLS.Enabled != isTLS {
thisConfigProto, otherConfigProto := "HTTP", "HTTP"
if config.TLS.Enabled {
thisConfigProto = "HTTPS"
}
if configs[0].TLS.Enabled {
otherConfigProto = "HTTPS"
}
return addresses, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address",
configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto)
}
}
}
return addresses, 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 handles edge cases gracefully. If a port name like
// "http" or "https" is unknown to the system, this function will
// change them to 80 or 443 respectively. If a hostname fails to
// resolve, that host can still be served but will be listening on
// the wildcard host instead. This function takes care of this for you.
func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr error, fatalErr error) {
// The host to bind to may be different from the (virtual)host to serve
bindHost := conf.BindHost
if bindHost == "" {
bindHost = conf.Host
}
resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(bindHost, conf.Port))
if warnErr != nil {
// Most likely the host lookup failed or the port is unknown
tryPort := conf.Port
switch errVal := warnErr.(type) {
case *net.AddrError:
if errVal.Err == "unknown port" {
// some odd Linux machines don't support these port names; see issue #136
switch conf.Port {
case "http":
tryPort = "80"
case "https":
tryPort = "443"
}
}
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(bindHost, tryPort))
if fatalErr != nil {
return
}
default:
// the hostname probably couldn't be resolved, just bind to wildcard then
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("0.0.0.0", tryPort))
if fatalErr != nil {
return
}
}
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
}
// Default makes a default configuration which
// is empty except for root, host, and port,
// which are essentials for serving the cwd.
func Default() server.Config {
return server.Config{
Root: Root,
Host: Host,
Port: Port,
}
}
// These three defaults are configurable through the command line
var (
Root = DefaultRoot
Host = DefaultHost
Port = DefaultPort
)
-62
View File
@@ -1,62 +0,0 @@
package config
import (
"testing"
"github.com/mholt/caddy/server"
)
func TestReolveAddr(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: "localhost", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{Host: "127.0.0.1", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{Host: "should-not-resolve", Port: "1234"}, true, false, "0.0.0.0", 1234},
{server.Config{Host: "localhost", Port: "http"}, false, false, "127.0.0.1", 80},
{server.Config{Host: "localhost", Port: "https"}, false, false, "127.0.0.1", 443},
{server.Config{Host: "", Port: "1234"}, false, false, "<nil>", 1234},
{server.Config{Host: "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, "0.0.0.0", 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)
}
}
}
-11
View File
@@ -1,11 +0,0 @@
package setup
import (
"github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/server"
)
type Controller struct {
*server.Config
parse.Dispenser
}
-32
View File
@@ -1,32 +0,0 @@
package setup
import (
"fmt"
"net/http"
"strings"
"github.com/mholt/caddy/config/parse"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/server"
)
// NewTestController creates a new *Controller for
// the input specified, with a filename of "Testfile"
func NewTestController(input string) *Controller {
return &Controller{
Config: &server.Config{},
Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)),
}
}
// 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.
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.
func SameNext(next1, next2 middleware.Handler) bool {
return fmt.Sprintf("%p", next1) == fmt.Sprintf("%p", next2)
}
-163
View File
@@ -1,163 +0,0 @@
package setup
import (
"io/ioutil"
"net/http"
"os"
"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"},
}
// For any configs that enabled static site gen, sweep the whole path at startup
c.Startup = append(c.Startup, func() error {
for _, cfg := range mdconfigs {
if cfg.StaticDir == "" {
continue
}
// If generated site already exists, clear it out
_, err := os.Stat(cfg.StaticDir)
if err == nil {
err := os.RemoveAll(cfg.StaticDir)
if err != nil {
return err
}
}
fp := filepath.Join(md.Root, cfg.PathScope)
filepath.Walk(fp, func(path string, info os.FileInfo, err error) error {
for _, ext := range cfg.Extensions {
if !info.IsDir() && strings.HasSuffix(info.Name(), ext) {
// Load the file
body, err := ioutil.ReadFile(path)
if err != nil {
return err
}
// Get the relative path as if it were a HTTP request,
// then prepend with "/" (like a real HTTP request)
reqPath, err := filepath.Rel(md.Root, path)
if err != nil {
return err
}
reqPath = "/" + reqPath
// Generate the static file
_, err = md.Process(cfg, reqPath, body)
if err != nil {
return err
}
break // don't try other file extensions
}
}
return nil
})
}
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
if !c.NextArg() || c.Val() == "{" {
return mdconfigs, c.ArgErr()
}
md.PathScope = c.Val()
// Load any other configuration parameters
for c.NextBlock() {
switch c.Val() {
case "ext":
exts := c.RemainingArgs()
if len(exts) == 0 {
return mdconfigs, c.ArgErr()
}
md.Extensions = append(md.Extensions, exts...)
case "css":
if !c.NextArg() {
return mdconfigs, c.ArgErr()
}
md.Styles = append(md.Styles, c.Val())
case "js":
if !c.NextArg() {
return mdconfigs, c.ArgErr()
}
md.Scripts = append(md.Scripts, c.Val())
case "template":
tArgs := c.RemainingArgs()
switch len(tArgs) {
case 0:
return mdconfigs, c.ArgErr()
case 1:
if _, ok := md.Templates[markdown.DefaultTemplate]; ok {
return mdconfigs, c.Err("only one default template is allowed, use alias.")
}
fpath := filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0])
md.Templates[markdown.DefaultTemplate] = fpath
case 2:
fpath := filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1])
md.Templates[tArgs[0]] = fpath
default:
return mdconfigs, c.ArgErr()
}
case "sitegen":
if c.NextArg() {
md.StaticDir = path.Join(c.Root, c.Val())
} else {
md.StaticDir = path.Join(c.Root, markdown.DefaultStaticDir)
}
if c.NextArg() {
// only 1 argument allowed
return mdconfigs, c.ArgErr()
}
default:
return mdconfigs, c.Err("Expected valid markdown configuration property")
}
}
// If no extensions were specified, assume .md
if len(md.Extensions) == 0 {
md.Extensions = []string{".md"}
}
mdconfigs = append(mdconfigs, md)
}
return mdconfigs, nil
}
-88
View File
@@ -1,88 +0,0 @@
package setup
import (
"fmt"
"github.com/mholt/caddy/middleware/markdown"
"testing"
)
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"}) {
t.Errorf("Expected .md as the Default Extension")
}
}
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"},
}}},
}
for i, test := range tests {
c := NewTestController(test.inputMarkdownConfig)
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))
}
}
}
}
-83
View File
@@ -1,83 +0,0 @@
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
for c.Next() {
var rule redirect.Rule
args := c.RemainingArgs()
// Always set the default Code, then overwrite
rule.Code = http.StatusMovedPermanently
switch len(args) {
case 1:
// To specified
rule.From = "/"
rule.To = args[0]
case 2:
// To and Code specified
rule.From = "/"
rule.To = args[0]
if "meta" == args[1] {
rule.Meta = true
} else if code, ok := httpRedirs[args[1]]; !ok {
return redirects, c.Err("Invalid redirect code '" + args[1] + "'")
} else {
rule.Code = code
}
case 3:
// From, To, and Code specified
rule.From = args[0]
rule.To = args[1]
if "meta" == args[2] {
rule.Meta = true
} else if code, ok := httpRedirs[args[2]]; !ok {
return redirects, c.Err("Invalid redirect code '" + args[2] + "'")
} else {
rule.Code = code
}
default:
return redirects, c.ArgErr()
}
if rule.From == rule.To {
return redirects, c.Err("Redirect rule cannot allow From and To arguments to be the same.")
}
redirects = append(redirects, rule)
}
return redirects, nil
}
// httpRedirs is a list of supported HTTP redirect codes.
var httpRedirs = map[string]int{
"300": 300,
"301": 301,
"302": 302,
"303": 303,
"304": 304,
"305": 305,
"307": 307,
"308": 308,
}
+68
View File
@@ -1,5 +1,73 @@
CHANGES
0.8.0 (December 4, 2015)
- HTTPS by default via Let's Encrypt (certs & keys are fully managed)
- Graceful restarts (on POSIX-compliant systems)
- Major internal refactoring to allow use of Caddy as library
- New directive 'mime' to customize Content-Type based on file extension
- New -accept flag to accept Let's Encrypt SA without prompt
- New -email flag to customize default email used for ACME transactions
- New -ca flag to customize ACME CA server URL
- New -revoke flag to revoke a certificate
- New -log flag to enable process log
- New -pidfile flag to enable writing pidfile
- New -grace flag to customize the graceful shutdown timeout
- New support for SIGHUP, SIGTERM, and SIGQUIT signals
- browse: Render filenames with multiple whitespace properly
- core: Use environment variables in Caddyfile
- markdown: Include Last-Modified header in response
- markdown: Render tables, strikethrough, and fenced code blocks
- proxy: Ability to exclude/ignore paths from proxying
- startup, shutdown: Better Windows support
- templates: Bug fix for .Host when port is absent
- templates: Include Last-Modified header in response
- templates: Support for custom delimiters
- tls: For non-local hosts, default port is now 443 unless specified
- tls: Force-disable HTTPS
- tls: Specify Let's Encrypt email address
- Many, many more tests and numerous bug fixes and improvements
0.7.6 (September 28, 2015)
- Pass in simple Caddyfile as command line arguments
- basicauth: Support for legacy htpasswd files
- browse: JSON response with file listing
- core: Caddyfile as command line argument
- errors: Can write full stack trace to HTTP response for debugging
- errors, log: Roll log files after certain size or age
- proxy: Fix for 32-bit architectures
- rewrite: Better compatibility with fastcgi and PHP apps
- templates: Added .StripExt and .StripHTML methods
- Internal improvements and minor bug fixes
0.7.5 (August 5, 2015)
- core: All listeners bind to 0.0.0.0 unless 'bind' directive is used
- fastcgi: Set HTTPS env variable if connection is secure
- log: Output to system log (except Windows)
- markdown: Added dev command to disable caching during development
- markdown: Fixed error reporting during initial site generation
- markdown: Fixed crash if path does not exist when server starts
- markdown: Fixed site generation and link indexing when files change
- templates: Added .NowDate for use in date-related functions
- Several bug fixes related to startup and shutdown functions
0.7.4 (July 30, 2015)
- browse: Sorting preference persisted in cookie
- browse: Added index.txt and default.txt to list of default files
- browse: Template files may now use Caddy template actions
- markdown: Template files may now use Caddy template actions
- markdown: Several bug fixes, especially for large and empty Markdown files
- markdown: Generate index pages to link to markdown pages (sitegen only)
- markdown: Flatten structure of front matter, changed template variables
- redir: Can use variables (placeholders) like log formats can
- redir: Catch-all redirects no longer preserve path; use {uri} instead
- redir: Syntax supports redirect tables by opening a block
- templates: Renamed .Date to .Now and added .Truncate, .Replace actions
- Other minor internal improvements and more tests
0.7.3 (July 15, 2015)
- errors: Error log now shows timestamp with each entry
- gzip: Fixed; Default filtering is by extension; removed MIME type filter
+2 -1
View File
@@ -1,7 +1,8 @@
CADDY 0.7.3
CADDY 0.8
Website
https://caddyserver.com
@caddyserver
Source Code
https://github.com/mholt/caddy
+17 -8
View File
@@ -19,28 +19,37 @@ ReleaseDir=$DistDir/release
# Compile binaries
mkdir -p $BuildDir
cd $BuildDir
rm -f *
rm -f caddy*
gox $Package
# Zip them up with release notes and stuff
mkdir -p $ReleaseDir
cd $ReleaseDir
rm -f *
rm -f caddy*
for f in $BuildDir/*
do
# Name .zip file same as binary, but strip .exe from end
zipname=$(basename ${f%".exe"}).zip
zipname=$(basename ${f%".exe"})
if [[ $f == *"linux"* ]] || [[ $f == *"bsd"* ]]; then
zipname=${zipname}.tar.gz
else
zipname=${zipname}.zip
fi
# Binary inside the zip file is simply the project name
bin=$BuildDir/$(basename $Package)
if [[ $f == *.exe ]]
then
bin=$bin.exe
binbase=$(basename $Package)
if [[ $f == *.exe ]]; then
binbase=$binbase.exe
fi
bin=$BuildDir/$binbase
mv $f $bin
# Compress distributable
zip -j $zipname $bin $DistDir/CHANGES.txt $DistDir/LICENSES.txt $DistDir/README.txt
if [[ $zipname == *.zip ]]; then
zip -j $zipname $bin $DistDir/CHANGES.txt $DistDir/LICENSES.txt $DistDir/README.txt
else
tar -cvzf $zipname -C $BuildDir $binbase -C $DistDir CHANGES.txt LICENSES.txt README.txt
fi
# Put binary filename back to original
mv $bin $f
+140 -118
View File
@@ -1,175 +1,197 @@
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"runtime"
"strconv"
"strings"
"time"
"github.com/mholt/caddy/app"
"github.com/mholt/caddy/config"
"github.com/mholt/caddy/server"
"github.com/mholt/caddy/caddy"
"github.com/mholt/caddy/caddy/letsencrypt"
)
var (
conf string
cpu string
logfile string
revoke string
version bool
)
const (
appName = "Caddy"
appVersion = "0.8"
)
func init() {
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+config.DefaultConfigFile+")")
flag.BoolVar(&app.Http2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib
flag.BoolVar(&app.Quiet, "quiet", false, "Quiet mode (no initialization output)")
caddy.TrapSignals()
flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement")
flag.StringVar(&letsencrypt.CAUrl, "ca", "https://acme-v01.api.letsencrypt.org/directory", "Certificate authority ACME server")
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")")
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site")
flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host")
flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port")
flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default Let's Encrypt account email address")
flag.DurationVar(&caddy.GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown")
flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host")
flag.BoolVar(&caddy.HTTP2, "http2", true, "HTTP/2 support") // TODO: temporary flag until http2 merged into std lib
flag.StringVar(&logfile, "log", "", "Process log file")
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port")
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site")
flag.BoolVar(&version, "version", false, "Show version")
}
func main() {
flag.Parse()
flag.Parse() // called here in main() to allow other packages to set flags in their inits
caddy.AppName = appName
caddy.AppVersion = appVersion
// set up process log before anything bad happens
switch logfile {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "":
log.SetOutput(ioutil.Discard)
default:
file, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("Error opening process log file: %v", err)
}
log.SetOutput(file)
}
if revoke != "" {
err := letsencrypt.Revoke(revoke)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Revoked certificate for %s\n", revoke)
os.Exit(0)
}
if version {
fmt.Printf("%s %s\n", app.Name, app.Version)
fmt.Printf("%s %s\n", caddy.AppName, caddy.AppVersion)
os.Exit(0)
}
// Set CPU cap
err := app.SetCPU(cpu)
err := setCPU(cpu)
if err != nil {
log.Fatal(err)
mustLogFatal(err)
}
// Load config from file
allConfigs, err := loadConfigs()
// Get Caddyfile input
caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile)
if err != nil {
log.Fatal(err)
mustLogFatal(err)
}
// Group by address (virtual hosts)
addresses, err := config.ArrangeBindings(allConfigs)
// Start your engines
err = caddy.Start(caddyfile)
if err != nil {
log.Fatal(err)
mustLogFatal(err)
}
// Start each server with its one or more configurations
for addr, configs := range addresses {
s, err := server.New(addr.String(), configs)
if err != nil {
log.Fatal(err)
}
s.HTTP2 = app.Http2 // TODO: This setting is temporary
app.Wg.Add(1)
go func(s *server.Server) {
defer app.Wg.Done()
err := s.Serve()
if err != nil {
log.Fatal(err) // kill whole process to avoid a half-alive zombie server
}
}(s)
app.Servers = append(app.Servers, s)
}
// Show initialization output
if !app.Quiet {
var checkedFdLimit bool
for addr, configs := range addresses {
for _, conf := range configs {
// Print address of site
fmt.Println(conf.Address())
// Note if non-localhost site resolves to loopback interface
if addr.IP.IsLoopback() && !isLocalhost(conf.Host) {
fmt.Printf("Notice: %s is only accessible on this machine (%s)\n",
conf.Host, addr.IP.String())
}
}
if !checkedFdLimit && !addr.IP.IsLoopback() {
checkFdlimit()
checkedFdLimit = true
}
}
}
// Wait for all listeners to stop
app.Wg.Wait()
// Twiddle your thumbs
caddy.Wait()
}
// 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.\nAt least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min)
}
}
// mustLogFatal just wraps log.Fatal() in a way that ensures the
// output is always printed to stderr so the user can see it
// if the user is still there, even if the process log was not
// enabled. If this process is a restart, however, and the user
// might not be there anymore, this just logs to the process log
// and exits.
func mustLogFatal(args ...interface{}) {
if !caddy.IsRestart() {
log.SetOutput(os.Stderr)
}
log.Fatal(args...)
}
// isLocalhost returns true if the string looks explicitly like a localhost address.
func isLocalhost(s string) bool {
return s == "localhost" || s == "::1" || strings.HasPrefix(s, "127.")
}
// loadConfigs loads configuration from a file or stdin (piped).
// Configuration is obtained from one of three sources, tried
// in this order: 1. -conf flag, 2. stdin, 3. Caddyfile.
// If none of those are available, a default configuration is
// loaded.
func loadConfigs() ([]server.Config, error) {
// -conf flag
func loadCaddyfile() (caddy.Input, error) {
// Try -conf flag
if conf != "" {
file, err := os.Open(conf)
if err != nil {
return []server.Config{}, err
if conf == "stdin" {
return caddy.CaddyfileFromPipe(os.Stdin)
}
defer file.Close()
return config.Load(path.Base(conf), file)
contents, err := ioutil.ReadFile(conf)
if err != nil {
return nil, err
}
return caddy.CaddyfileInput{
Contents: contents,
Filepath: conf,
RealFile: true,
}, nil
}
// stdin
fi, err := os.Stdin.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.
confBody, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
}
if len(confBody) > 0 {
return config.Load("stdin", bytes.NewReader(confBody))
}
// command line args
if flag.NArg() > 0 {
confBody := caddy.Host + ":" + caddy.Port + "\n" + strings.Join(flag.Args(), "\n")
return caddy.CaddyfileInput{
Contents: []byte(confBody),
Filepath: "args",
}, nil
}
// Caddyfile
file, err := os.Open(config.DefaultConfigFile)
// Caddyfile in cwd
contents, err := ioutil.ReadFile(caddy.DefaultConfigFile)
if err != nil {
if os.IsNotExist(err) {
return []server.Config{config.Default()}, nil
return caddy.DefaultInput(), nil
}
return []server.Config{}, err
return nil, err
}
defer file.Close()
return config.Load(config.DefaultConfigFile, file)
return caddy.CaddyfileInput{
Contents: contents,
Filepath: caddy.DefaultConfigFile,
RealFile: true,
}, nil
}
// setCPU parses string cpu and sets GOMAXPROCS
// according to its value. It accepts either
// a number (e.g. 3) or a percent (e.g. 50%).
func setCPU(cpu string) error {
var numCPU int
availCPU := runtime.NumCPU()
if strings.HasSuffix(cpu, "%") {
// Percent
var percent float32
pctStr := cpu[:len(cpu)-1]
pctInt, err := strconv.Atoi(pctStr)
if err != nil || pctInt < 1 || pctInt > 100 {
return errors.New("invalid CPU value: percentage must be between 1-100")
}
percent = float32(pctInt) / 100
numCPU = int(float32(availCPU) * percent)
} else {
// Number
num, err := strconv.Atoi(cpu)
if err != nil || num < 1 {
return errors.New("invalid CPU value: provide a number or percent greater than 0")
}
numCPU = num
}
if numCPU > availCPU {
numCPU = availCPU
}
runtime.GOMAXPROCS(numCPU)
return nil
}
+44
View File
@@ -0,0 +1,44 @@
package main
import (
"runtime"
"testing"
)
func TestSetCPU(t *testing.T) {
currentCPU := runtime.GOMAXPROCS(-1)
maxCPU := runtime.NumCPU()
halfCPU := int(0.5 * float32(maxCPU))
if halfCPU < 1 {
halfCPU = 1
}
for i, test := range []struct {
input string
output int
shouldErr bool
}{
{"1", 1, false},
{"-1", currentCPU, true},
{"0", currentCPU, true},
{"100%", maxCPU, false},
{"50%", halfCPU, false},
{"110%", currentCPU, true},
{"-10%", currentCPU, true},
{"invalid input", currentCPU, true},
{"invalid input%", currentCPU, true},
{"9999", maxCPU, false}, // over available CPU
} {
err := setCPU(test.input)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error, but there wasn't any", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but there was one: %v", i, err)
}
if actual, expected := runtime.GOMAXPROCS(-1), test.output; actual != expected {
t.Errorf("Test %d: GOMAXPROCS was %d but expected %d", i, actual, expected)
}
// teardown
runtime.GOMAXPROCS(currentCPU)
}
}
+83 -4
View File
@@ -2,9 +2,17 @@
package basicauth
import (
"bufio"
"crypto/subtle"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/jimstudt/http-authentication/basic"
"github.com/mholt/caddy/middleware"
)
@@ -14,8 +22,9 @@ import (
// security of HTTP Basic Auth is disputed. Use discretion when deciding
// what to protect with BasicAuth.
type BasicAuth struct {
Next middleware.Handler
Rules []Rule
Next middleware.Handler
SiteRoot string
Rules []Rule
}
// ServeHTTP implements the middleware.Handler interface.
@@ -37,7 +46,8 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// Check credentials
if !ok ||
username != rule.Username ||
subtle.ConstantTimeCompare([]byte(password), []byte(rule.Password)) != 1 {
!rule.Password(password) {
//subtle.ConstantTimeCompare([]byte(password), []byte(rule.Password)) != 1 {
continue
}
@@ -64,6 +74,75 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// file or directory paths.
type Rule struct {
Username string
Password string
Password func(string) bool
Resources []string
}
// PasswordMatcher determines whether a password matches a rule.
type PasswordMatcher func(pw string) bool
var (
htpasswords map[string]map[string]PasswordMatcher
htpasswordsMu sync.Mutex
)
// GetHtpasswdMatcher matches password rules.
func GetHtpasswdMatcher(filename, username, siteRoot string) (PasswordMatcher, error) {
filename = filepath.Join(siteRoot, filename)
htpasswordsMu.Lock()
if htpasswords == nil {
htpasswords = make(map[string]map[string]PasswordMatcher)
}
pm := htpasswords[filename]
if pm == nil {
fh, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("open %q: %v", filename, err)
}
defer fh.Close()
pm = make(map[string]PasswordMatcher)
if err = parseHtpasswd(pm, fh); err != nil {
return nil, fmt.Errorf("parsing htpasswd %q: %v", fh.Name(), err)
}
htpasswords[filename] = pm
}
htpasswordsMu.Unlock()
if pm[username] == nil {
return nil, fmt.Errorf("username %q not found in %q", username, filename)
}
return pm[username], nil
}
func parseHtpasswd(pm map[string]PasswordMatcher, r io.Reader) error {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.IndexByte(line, '#') == 0 {
continue
}
i := strings.IndexByte(line, ':')
if i <= 0 {
return fmt.Errorf("malformed line, no color: %q", line)
}
user, encoded := line[:i], line[i+1:]
for _, p := range basic.DefaultSystems {
matcher, err := p(encoded)
if err != nil {
return err
}
if matcher != nil {
pm[user] = matcher.MatchesPassword
break
}
}
}
return scanner.Err()
}
// PlainMatcher returns a PasswordMatcher that does a constant-time
// byte-wise comparison.
func PlainMatcher(passw string) PasswordMatcher {
return func(pw string) bool {
return subtle.ConstantTimeCompare([]byte(pw), []byte(passw)) == 1
}
}
+37 -3
View File
@@ -3,8 +3,11 @@ package basicauth
import (
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/mholt/caddy/middleware"
@@ -15,7 +18,7 @@ func TestBasicAuth(t *testing.T) {
rw := BasicAuth{
Next: middleware.HandlerFunc(contentHandler),
Rules: []Rule{
{Username: "test", Password: "ttest", Resources: []string{"/testing"}},
{Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}},
},
}
@@ -66,8 +69,8 @@ func TestMultipleOverlappingRules(t *testing.T) {
rw := BasicAuth{
Next: middleware.HandlerFunc(contentHandler),
Rules: []Rule{
{Username: "t", Password: "p1", Resources: []string{"/t"}},
{Username: "t1", Password: "p2", Resources: []string{"/t/t"}},
{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}},
{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}},
},
}
@@ -111,3 +114,34 @@ func contentHandler(w http.ResponseWriter, r *http.Request) (int, error) {
fmt.Fprintf(w, r.URL.String())
return http.StatusOK, nil
}
func TestHtpasswd(t *testing.T) {
htpasswdPasswd := "IedFOuGmTpT8"
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
htfh, err := ioutil.TempFile("", "basicauth-")
if err != nil {
t.Skipf("Error creating temp file (%v), will skip htpassword test")
return
}
defer os.Remove(htfh.Name())
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
}
htfh.Close()
for i, username := range []string{"sha1", "md5"} {
rule := Rule{Username: username, Resources: []string{"/testing"}}
siteRoot := filepath.Dir(htfh.Name())
filename := filepath.Base(htfh.Name())
if rule.Password, err = GetHtpasswdMatcher(filename, rule.Username, siteRoot); err != nil {
t.Fatalf("GetHtpasswdMatcher(%q, %q): %v", htfh.Name(), rule.Username, err)
}
t.Logf("%d. username=%q password=%v", i, rule.Username, rule.Password)
if !rule.Password(htpasswdPasswd) || rule.Password(htpasswdPasswd+"!") {
t.Errorf("%d (%s) password does not match.", i, rule.Username)
}
}
}
+93 -27
View File
@@ -4,14 +4,16 @@ package browse
import (
"bytes"
"encoding/json"
"errors"
"html/template"
"net/http"
"net/url"
"os"
"path"
"sort"
"strconv"
"strings"
"text/template"
"time"
"github.com/dustin/go-humanize"
@@ -21,14 +23,16 @@ import (
// Browse is an http.Handler that can show a file listing when
// directories in the given paths are specified.
type Browse struct {
Next middleware.Handler
Root string
Configs []Config
Next middleware.Handler
Root string
Configs []Config
IgnoreIndexes bool
}
// Config is a configuration for browsing in a particular path.
type Config struct {
PathScope string
Variables interface{}
Template *template.Template
}
@@ -51,6 +55,11 @@ type Listing struct {
// And which order
Order string
// Optional custom variables for use in browse templates
User interface{}
middleware.Context
}
// FileInfo is the info about a particular file or directory
@@ -85,7 +94,7 @@ func (l bySize) Less(i, j int) bool { return l.Items[i].Size < l.Items[j].Size }
// By Time
func (l byTime) Len() int { return len(l.Items) }
func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Unix() < l.Items[j].ModTime.Unix() }
func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) }
// Add sorting method to "Listing"
// it will apply what's in ".Sort" and ".Order"
@@ -118,9 +127,10 @@ func (l Listing) applySort() {
}
}
// HumanSize returns the size of the file as a human-readable string.
// HumanSize returns the size of the file as a human-readable string
// in IEC format (i.e. power of 2 or base 1024).
func (fi FileInfo) HumanSize() string {
return humanize.Bytes(uint64(fi.Size))
return humanize.IBytes(uint64(fi.Size))
}
// HumanModTime returns the modified time of the file as a human-readable string.
@@ -128,22 +138,18 @@ func (fi FileInfo) HumanModTime(format string) string {
return fi.ModTime.Format(format)
}
var IndexPages = []string{
"index.html",
"index.htm",
"default.html",
"default.htm",
}
func directoryListing(files []os.FileInfo, urlPath string, canGoUp bool) (Listing, error) {
func directoryListing(files []os.FileInfo, r *http.Request, canGoUp bool, root string, ignoreIndexes bool, vars interface{}) (Listing, error) {
var fileinfos []FileInfo
var urlPath = r.URL.Path
for _, f := range files {
name := f.Name()
// Directory is not browsable if it contains index file
for _, indexName := range IndexPages {
if name == indexName {
return Listing{}, errors.New("Directory contains index file, not browsable!")
if !ignoreIndexes {
for _, indexName := range middleware.IndexPages {
if name == indexName {
return Listing{}, errors.New("Directory contains index file, not browsable!")
}
}
}
@@ -168,13 +174,18 @@ func directoryListing(files []os.FileInfo, urlPath string, canGoUp bool) (Listin
Path: urlPath,
CanGoUp: canGoUp,
Items: fileinfos,
Context: middleware.Context{
Root: http.Dir(root),
Req: r,
URL: r.URL,
},
User: vars,
}, nil
}
// ServeHTTP implements the middleware.Handler interface.
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
filename := b.Root + r.URL.Path
info, err := os.Stat(filename)
if err != nil {
return b.Next.ServeHTTP(w, r)
@@ -222,7 +233,7 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
}
}
// Assemble listing of directory contents
listing, err := directoryListing(files, r.URL.Path, canGoUp)
listing, err := directoryListing(files, r, canGoUp, b.Root, b.IgnoreIndexes, bc.Variables)
if err != nil { // directory isn't browsable
continue
}
@@ -230,22 +241,77 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
// Get the query vales and store them in the Listing struct
listing.Sort, listing.Order = r.URL.Query().Get("sort"), r.URL.Query().Get("order")
// If the query 'sort' is empty, default to "name" and "asc"
// If the query 'sort' or 'order' is empty, check the cookies
if listing.Sort == "" {
listing.Sort = "name"
listing.Order = "asc"
sortCookie, sortErr := r.Cookie("sort")
// if there's no sorting values in the cookies, default to "name" and "asc"
if sortErr != nil {
listing.Sort = "name"
} else { // if we have values in the cookies, use them
listing.Sort = sortCookie.Value
}
} else { // save the query value of 'sort' and 'order' as cookies
http.SetCookie(w, &http.Cookie{Name: "sort", Value: listing.Sort, Path: "/"})
http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, Path: "/"})
}
if listing.Order == "" {
orderCookie, orderErr := r.Cookie("order")
// if there's no sorting values in the cookies, default to "name" and "asc"
if orderErr != nil {
listing.Order = "asc"
} else { // if we have values in the cookies, use them
listing.Order = orderCookie.Value
}
} else { // save the query value of 'sort' and 'order' as cookies
http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, Path: "/"})
}
// Apply the sorting
listing.applySort()
var buf bytes.Buffer
err = bc.Template.Execute(&buf, listing)
if err != nil {
return http.StatusInternalServerError, err
// check if we should provide json
acceptHeader := strings.Join(r.Header["Accept"], ",")
if strings.Contains(strings.ToLower(acceptHeader), "application/json") {
var marsh []byte
// check if we are limited
if limitQuery := r.URL.Query().Get("limit"); limitQuery != "" {
limit, err := strconv.Atoi(limitQuery)
if err != nil { // if the 'limit' query can't be interpreted as a number, return err
return http.StatusBadRequest, err
}
// if `limit` is equal or less than len(listing.Items) and bigger than 0, list them
if limit <= len(listing.Items) && limit > 0 {
marsh, err = json.Marshal(listing.Items[:limit])
} else { // if the 'limit' query is empty, or has the wrong value, list everything
marsh, err = json.Marshal(listing.Items)
}
if err != nil {
return http.StatusInternalServerError, err
}
} else { // there's no 'limit' query, list them all
marsh, err = json.Marshal(listing.Items)
if err != nil {
return http.StatusInternalServerError, err
}
}
// write the marshaled json to buf
if _, err = buf.Write(marsh); err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
} else { // there's no 'application/json' in the 'Accept' header, browse normally
err = bc.Template.Execute(&buf, listing)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
buf.WriteTo(w)
return http.StatusOK, nil

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