Compare commits

..

256 Commits

Author SHA1 Message Date
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
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
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
141 changed files with 7568 additions and 1486 deletions
+9 -2
View File
@@ -1,7 +1,14 @@
language: go
go:
- 1.4
- 1.4.2
- 1.5.1
- tip
script: go test ./...
install:
- go get -d ./...
- go get golang.org/x/tools/cmd/vet
script:
- go vet ./...
- go test ./...
+44 -43
View File
@@ -1,21 +1,20 @@
[![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.
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).
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,38 +28,6 @@ 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/)
- [zenithar/nano-caddy](https://registry.hub.docker.com/u/zenithar/nano-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
@@ -81,33 +48,67 @@ header /api Access-Control-Allow-Origin *
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.
## 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.
**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
-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.5"
)
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)
}
}
+358
View File
@@ -0,0 +1,358 @@
// 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.
//
// Importing this package has the side-effect of trapping
// SIGINT on all platforms and SIGUSR1 on not-Windows systems.
// It has to do this in order to perform shutdowns or reloads.
package caddy
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"path"
"strings"
"sync"
"github.com/mholt/caddy/caddy/letsencrypt"
"github.com/mholt/caddy/server"
)
// Configurable application parameters
var (
// The name and version of the application.
AppName, AppVersion string
// If true, initialization will not show any informative output.
Quiet bool
// HTTP2 indicates whether HTTP2 is enabled or not
HTTP2 bool // TODO: temporary flag until http2 is standard
)
var (
// caddyfile is the input configuration text used for this process
caddyfile Input
// caddyfileMu protects caddyfile during changes
caddyfileMu sync.Mutex
// incompleteRestartErr occurs if this process is a fork
// of the parent but no Caddyfile was piped in
incompleteRestartErr = errors.New("cannot finish restart successfully")
// 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
)
const (
DefaultHost = "0.0.0.0"
DefaultPort = "2015"
DefaultRoot = "."
)
// Start starts Caddy with the given Caddyfile. If cdyfile
// is nil or the process is forked from a parent as part of
// a graceful restart, Caddy will check to see if Caddyfile
// was piped from stdin and use that. It blocks until all the
// servers are listening.
//
// If this process is a fork and no Caddyfile was piped in,
// an error will be returned (the Restart() function does this
// for you automatically). If this process is NOT a fork and
// cdyfile is nil, a default configuration will be assumed.
// In any case, an error is returned if Caddy could not be
// started.
func Start(cdyfile Input) error {
// TODO: What if already started -- is that an error?
var err error
// 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
}
// Close remaining file descriptors we may have inherited that we don't need
if IsRestart() {
for _, fdIndex := range loadedGob.ListenerFds {
file := os.NewFile(fdIndex, "")
fln, err := net.FileListener(file)
if err == nil {
fln.Close()
}
}
}
// 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
}
}
}
}
// Tell parent process that we got this
if IsRestart() {
ppipe := os.NewFile(3, "") // parent is listening on pipe at index 3
ppipe.Write([]byte("success"))
ppipe.Close()
}
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 Group) error {
var startupWg sync.WaitGroup
errChan := make(chan error)
for _, group := range groupings {
s, err := server.New(group.BindAddr.String(), group.Configs)
if err != nil {
log.Fatal(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.
if fdIndex, ok := loadedGob.ListenerFds[s.Addr]; ok {
file := os.NewFile(fdIndex, "")
fln, err := net.FileListener(file)
if err != nil {
log.Fatal(err)
}
ln, ok = fln.(server.ListenerFile)
if !ok {
log.Fatal("listener was not a ListenerFile")
}
delete(loadedGob.ListenerFds, s.Addr) // mark it as used
}
}
wg.Add(1)
go func(s *server.Server, ln server.ListenerFile) {
defer wg.Done()
if ln != nil {
err = s.Serve(ln)
} else {
err = s.ListenAndServe()
}
// "use of closed network connection" is normal if doing graceful shutdown...
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
errChan <- err
}
}(s, ln)
startupWg.Add(1)
go func(s *server.Server) {
defer startupWg.Done()
s.WaitUntilStarted()
}(s)
serversMu.Lock()
servers = append(servers, s)
serversMu.Unlock()
}
// Wait for all servers to finish starting
startupWg.Wait()
// Return the first error, if any
select {
case err := <-errChan:
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 are executed on SIGINT).
func Stop() error {
letsencrypt.Deactivate()
serversMu.Lock()
for _, s := range servers {
s.Stop() // TODO: error checking/reporting?
}
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 in a way that prioritizes
// reading from stdin pipe; otherwise it calls loader to load
// the Caddyfile. If loader does not return a Caddyfile, the
// default one will be returned. Thus, if there are no other
// errors, this function always returns at least the default
// Caddyfile (not the previously-used 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
}
// Otherwise, we first try to get from stdin pipe
if cdyfile == nil {
cdyfile, err = CaddyfileFromPipe(os.Stdin)
if err != nil {
return nil, err
}
}
// No piped input, so try the user's loader instead
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)
}
}
}
+163
View File
@@ -0,0 +1,163 @@
package caddyfile
import (
"bytes"
"encoding/json"
"fmt"
"net"
"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: make(map[string]interface{})}
for _, host := range sb.HostList() {
block.Hosts = append(block.Hosts, strings.TrimSuffix(host, ":"))
}
for dir, tokens := range sb.Tokens {
disp := parse.NewDispenserTokens(filename, tokens)
disp.Next() // the first token is the directive; skip it
block.Body[dir] = constructLine(disp)
}
j = append(j, block)
}
result, err := json.Marshal(j)
if err != nil {
return nil, err
}
return result, nil
}
// constructLine transforms tokens into a JSON-encodable structure;
// but only one line at a time, to be used at the top-level of
// a server block only (where the first token on each line is a
// directive) - not to be used at any other nesting level.
func constructLine(d parse.Dispenser) interface{} {
var args []interface{}
all := d.RemainingArgs()
for _, arg := range all {
args = append(args, arg)
}
d.Next()
if d.Val() == "{" {
args = append(args, constructBlock(d))
}
return args
}
// constructBlock recursively processes tokens into a
// JSON-encodable structure.
func constructBlock(d parse.Dispenser) interface{} {
block := make(map[string]interface{})
for d.Next() {
if d.Val() == "}" {
break
}
dir := d.Val()
all := d.RemainingArgs()
var args []interface{}
for _, arg := range all {
args = append(args, arg)
}
if d.Val() == "{" {
args = append(args, constructBlock(d))
}
block[dir] = args
}
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 _, sb := range j {
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 map[string]interface{}:
result += " {\n"
for param, args := range val {
result += strings.Repeat("\t", depth) + param
result += jsonToText(args, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
case []interface{}:
for _, v := range val {
result += jsonToText(v, depth)
}
}
return result
}
type Caddyfile []ServerBlock
type ServerBlock struct {
Hosts []string `json:"hosts"`
Body map[string]interface{} `json:"body"`
}
+91
View File
@@ -0,0 +1,91 @@
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":null}]}}]`,
},
{ // 2
caddyfile: `host1, host2 {
dir abc {
def ghi
}
}`,
json: `[{"hosts":["host1","host2"],"body":{"dir":["abc",{"def":["ghi"]}]}}]`,
},
{ // 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
},
}
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))
}
}
}
+374
View File
@@ -0,0 +1,374 @@
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
// turn off timestamp for parsing
flags := log.Flags()
log.SetFlags(0)
// 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)
configs, err = letsencrypt.Activate(configs)
if err != nil {
return nil, err
}
// 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
}
}
}
}
// restore logging settings
log.SetFlags(flags)
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) (Group, error) {
var groupings Group
// 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.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 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 error, 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
}
// NewDefault makes a default configuration, which
// is empty except for root, host, and port,
// which are essentials for serving the cwd.
func NewDefault() server.Config {
return server.Config{
Root: Root,
Host: Host,
Port: Port,
}
}
// DefaultInput returns the default Caddyfile input
// to use when it is otherwise empty or missing.
func DefaultInput() CaddyfileInput {
return CaddyfileInput{
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)),
}
}
// These defaults are configurable through the command line
var (
// Site root
Root = DefaultRoot
// Site host
Host = DefaultHost
// 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
}
// Group 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 Group []BindingMapping
+75 -1
View File
@@ -1,11 +1,27 @@
package config
package caddy
import (
"reflect"
"sync"
"testing"
"github.com/mholt/caddy/server"
)
func TestNewDefault(t *testing.T) {
config := NewDefault()
if actual, expected := config.Root, DefaultRoot; actual != expected {
t.Errorf("Root was %s but expected %s", actual, expected)
}
if actual, expected := config.Host, DefaultHost; actual != expected {
t.Errorf("Host was %s but expected %s", actual, expected)
}
if actual, expected := config.Port, DefaultPort; actual != expected {
t.Errorf("Port was %s but expected %s", actual, expected)
}
}
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.
@@ -62,3 +78,61 @@ func TestResolveAddr(t *testing.T) {
}
}
}
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)
+71
View File
@@ -0,0 +1,71 @@
package caddy
import (
"bytes"
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"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)
}
}
}
}
// 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"
}
// 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 }
// Path 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))
}
+67
View File
@@ -0,0 +1,67 @@
package letsencrypt
import (
"crypto/tls"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"sync/atomic"
"github.com/mholt/caddy/middleware"
)
// Handler is a Caddy middleware that can proxy ACME requests
// to the real ACME endpoint. This is necessary to renew certificates
// while the server is running. Obviously, a site served on port
// 443 (HTTPS) binds to that port, so another listener created by
// our acme client can't bind successfully and solve the challenge.
// Thus, we chain this handler in so that it can, when activated,
// proxy ACME requests to an ACME client listening on an alternate
// port.
type Handler struct {
sync.Mutex // protects the ChallengePath property
Next middleware.Handler
ChallengeActive int32 // use sync/atomic for speed to set/get this flag
ChallengePath string // the exact request path to match before proxying
}
// 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) {
// Only if challenge is active
if atomic.LoadInt32(&h.ChallengeActive) == 1 {
h.Lock()
path := h.ChallengePath
h.Unlock()
// Request path must be correct; if so, proxy to ACME client
if r.URL.Path == path {
upstream, err := url.Parse("https://" + r.Host + ":" + alternatePort)
if err != nil {
return http.StatusInternalServerError, err
}
proxy := httputil.NewSingleHostReverseProxy(upstream)
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client uses self-signed cert
}
proxy.ServeHTTP(w, r)
return 0, nil
}
}
return h.Next.ServeHTTP(w, r)
}
// ChallengeOn enables h to proxy ACME requests.
func (h *Handler) ChallengeOn(challengePath string) {
h.Lock()
h.ChallengePath = challengePath
h.Unlock()
atomic.StoreInt32(&h.ChallengeActive, 1)
}
// ChallengeOff disables ACME proxying from this h.
func (h *Handler) ChallengeOff(success bool) {
atomic.StoreInt32(&h.ChallengeActive, 0)
}
+510
View File
@@ -0,0 +1,510 @@
// 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/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()
// TODO: All the output the end user should see when running caddy is something
// simple like "Setting up HTTPS..." (and maybe 'done' at the end of the line when finished).
// In other words, hide all the other logging except for on errors. Or maybe
// have a place to put those logs.
// 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
hosts := make([]string, len(cfgIndexes))
for i, idx := range cfgIndexes {
hosts[i] = 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
cfg.Host != "localhost" &&
cfg.Host != "" &&
cfg.Host != "0.0.0.0" &&
cfg.Host != "::1" &&
!strings.HasPrefix(cfg.Host, "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
// make sure another HTTPS version of this config doesn't exist in the list already
!otherHostHasScheme(allConfigs, cfgIndex, "https")
}
// 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, exposePort)
}
// 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) {
// collect all the hostnames into one slice
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 {
os.MkdirAll(storage.Site(cert.Domain), 0700)
// 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
if cfg.Port == "" {
cfg.Port = "https"
}
// Chain in ACME middleware proxy if we use up the SSL port
if cfg.Port == "https" || cfg.Port == "443" {
handler := new(Handler)
mid := func(next middleware.Handler) middleware.Handler {
handler.Next = next
return handler
}
cfg.Middleware["/"] = append(cfg.Middleware["/"], mid)
acmeHandlers[cfg.Host] = handler
}
// 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
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))
}
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 (
// Let's Encrypt account email to use if none provided
DefaultEmail string
// Whether user has agreed to the Let's Encrypt SA
Agreed bool
// The base URL to the CA's ACME endpoint
CAUrl string
)
// Some essential values related to the Let's Encrypt process
const (
// The port to expose to the CA server for Simple HTTP Challenge.
// NOTE: Let's Encrypt requires port 443. If exposePort is not 443,
// then port 443 must be forwarded to exposePort.
exposePort = "443"
// If port 443 is in use by a Caddy server instance, then this is
// port on which the acme client will solve challenges. (Whatever is
// listening on port 443 must proxy ACME requests to this port.)
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)
+51
View File
@@ -0,0 +1,51 @@
package letsencrypt
import (
"net/http"
"testing"
"github.com/mholt/caddy/middleware/redirect"
"github.com/mholt/caddy/server"
)
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)
}
}
+183
View File
@@ -0,0 +1,183 @@
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] cert renewal: %v\n", 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\n", 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\n", oldStatus, newStatus)
err := OnChange()
if err != nil {
log.Printf("[ERROR] onchange after ocsp update: %v\n", 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.Print("[INFO] Processing certificate renewals...")
var errs []error
var n int
defer func() {
// reset these so as to not interfere with other challenges
acme.OnSimpleHTTPStart = nil
acme.OnSimpleHTTPEnd = nil
}()
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] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host)
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 metadata
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
// Tell the handler to accept and proxy acme request in order to solve challenge
acme.OnSimpleHTTPStart = acmeHandlers[cfg.Host].ChallengeOn
acme.OnSimpleHTTPEnd = acmeHandlers[cfg.Host].ChallengeOff
// Renew certificate.
// TODO: revokeOld should be an option in the caddyfile
// TODO: bundle should be an option in the caddyfile as well :)
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("[WARN] There are %d days left on the certificate for %s. Will renew when 14 days remain.\n", daysLeft, cfg.Host)
}
}
return n, errs
}
// acmeHandlers is a map of host to ACME handler. These
// are used to proxy ACME requests to the ACME client
// when port 443 is in use.
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)
}
// CertFile 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]
}
+84
View File
@@ -0,0 +1,84 @@
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,
},
} {
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,11 @@ 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) {
p := parser{Dispenser: NewDispenser(filename, input)}
// 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), checkDirectives: checkDirectives}
blocks, err := p.parseAll()
return blocks, err
}
@@ -9,12 +9,13 @@ import (
type parser struct {
Dispenser
block ServerBlock // 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) {
var blocks []ServerBlock
func (p *parser) parseAll() ([]serverBlock, error) {
var blocks []serverBlock
for p.Next() {
err := p.parseOne()
@@ -30,7 +31,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
}
func (p *parser) parseOne() error {
p.block = ServerBlock{Tokens: make(map[string][]token)}
p.block = serverBlock{Tokens: make(map[string][]token)}
err := p.begin()
if err != nil {
@@ -87,7 +88,7 @@ func (p *parser) addresses() error {
break
}
if tkn != "" {
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] == ',' {
@@ -102,13 +103,13 @@ func (p *parser) addresses() error {
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
@@ -220,8 +221,10 @@ 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
@@ -242,7 +245,7 @@ func (p *parser) directive() error {
}
if nesting > 0 {
return p.EofErr()
return p.EOFErr()
}
return nil
}
@@ -301,15 +304,26 @@ func standardAddress(str string) (host, port string, err error) {
}
type (
// ServerBlock associates tokens with a list of addresses
// serverBlock associates tokens with a list of addresses
// and groups tokens by directive name.
ServerBlock struct {
Addresses []Address
serverBlock struct {
Addresses []address
Tokens map[string][]token
}
// Address represents a host and port.
Address struct {
address struct {
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
}
@@ -59,7 +59,7 @@ func TestStandardAddress(t *testing.T) {
func TestParseOneAndImport(t *testing.T) {
setupParseTests()
testParseOne := func(input string) (ServerBlock, error) {
testParseOne := func(input string) (serverBlock, error) {
p := testParser(input)
p.Next() // parseOne doesn't call Next() to start, so we must
err := p.parseOne()
@@ -69,22 +69,22 @@ func TestParseOneAndImport(t *testing.T) {
for i, test := range []struct {
input string
shouldErr bool
addresses []Address
addresses []address
tokens map[string]int // map of directive name to number of tokens expected
}{
{`localhost`, false, []Address{
{`localhost`, false, []address{
{"localhost", ""},
}, map[string]int{}},
{`localhost
dir1`, false, []Address{
dir1`, false, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 1,
}},
{`localhost:1234
dir1 foo bar`, false, []Address{
dir1 foo bar`, false, []address{
{"localhost", "1234"},
}, map[string]int{
"dir1": 3,
@@ -92,7 +92,7 @@ func TestParseOneAndImport(t *testing.T) {
{`localhost {
dir1
}`, false, []Address{
}`, false, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 1,
@@ -101,7 +101,7 @@ func TestParseOneAndImport(t *testing.T) {
{`localhost:1234 {
dir1 foo bar
dir2
}`, false, []Address{
}`, false, []address{
{"localhost", "1234"},
}, map[string]int{
"dir1": 3,
@@ -109,7 +109,7 @@ func TestParseOneAndImport(t *testing.T) {
}},
{`http://localhost https://localhost
dir1 foo bar`, false, []Address{
dir1 foo bar`, false, []address{
{"localhost", "http"},
{"localhost", "https"},
}, map[string]int{
@@ -118,7 +118,7 @@ func TestParseOneAndImport(t *testing.T) {
{`http://localhost https://localhost {
dir1 foo bar
}`, false, []Address{
}`, false, []address{
{"localhost", "http"},
{"localhost", "https"},
}, map[string]int{
@@ -127,7 +127,7 @@ func TestParseOneAndImport(t *testing.T) {
{`http://localhost, https://localhost {
dir1 foo bar
}`, false, []Address{
}`, false, []address{
{"localhost", "http"},
{"localhost", "https"},
}, map[string]int{
@@ -135,13 +135,13 @@ func TestParseOneAndImport(t *testing.T) {
}},
{`http://localhost, {
}`, true, []Address{
}`, true, []address{
{"localhost", "http"},
}, map[string]int{}},
{`host1:80, http://host2.com
dir1 foo bar
dir2 baz`, false, []Address{
dir2 baz`, false, []address{
{"host1", "80"},
{"host2.com", "http"},
}, map[string]int{
@@ -151,7 +151,7 @@ func TestParseOneAndImport(t *testing.T) {
{`http://host1.com,
http://host2.com,
https://host3.com`, false, []Address{
https://host3.com`, false, []address{
{"host1.com", "http"},
{"host2.com", "http"},
{"host3.com", "https"},
@@ -161,7 +161,7 @@ func TestParseOneAndImport(t *testing.T) {
dir1 foo {
bar baz
}
dir2`, false, []Address{
dir2`, false, []address{
{"host1.com", "1234"},
{"host2.com", "https"},
}, map[string]int{
@@ -175,7 +175,7 @@ func TestParseOneAndImport(t *testing.T) {
}
dir2 {
foo bar
}`, false, []Address{
}`, false, []address{
{"127.0.0.1", ""},
}, map[string]int{
"dir1": 5,
@@ -183,13 +183,13 @@ func TestParseOneAndImport(t *testing.T) {
}},
{`127.0.0.1
unknown_directive`, true, []Address{
unknown_directive`, true, []address{
{"127.0.0.1", ""},
}, map[string]int{}},
{`localhost
dir1 {
foo`, true, []Address{
foo`, true, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 3,
@@ -197,7 +197,15 @@ func TestParseOneAndImport(t *testing.T) {
{`localhost
dir1 {
}`, false, []Address{
}`, false, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
} }`, true, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 3,
@@ -209,18 +217,18 @@ func TestParseOneAndImport(t *testing.T) {
foo
}
}
dir2 foo bar`, false, []Address{
dir2 foo bar`, false, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 7,
"dir2": 3,
}},
{``, false, []Address{}, map[string]int{}},
{``, false, []address{}, map[string]int{}},
{`localhost
dir1 arg1
import import_test1.txt`, false, []Address{
import import_test1.txt`, false, []address{
{"localhost", ""},
}, map[string]int{
"dir1": 2,
@@ -228,16 +236,20 @@ func TestParseOneAndImport(t *testing.T) {
"dir3": 1,
}},
{`import import_test2.txt`, false, []Address{
{`import import_test2.txt`, false, []address{
{"host1", ""},
}, map[string]int{
"dir1": 1,
"dir2": 2,
}},
{``, false, []Address{}, map[string]int{}},
{`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}},
{`""`, false, []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)
@@ -282,43 +294,43 @@ func TestParseOneAndImport(t *testing.T) {
func TestParseAll(t *testing.T) {
setupParseTests()
testParseAll := func(input string) ([]ServerBlock, error) {
p := testParser(input)
return p.parseAll()
}
for i, test := range []struct {
input string
shouldErr bool
numBlocks int
addresses [][]address // addresses per server block, in order
}{
{`localhost`, false, 1},
{`localhost`, false, [][]address{
{{"localhost", ""}},
}},
{`localhost {
dir1
}`, false, 1},
{`localhost:1234`, false, [][]address{
[]address{{"localhost", "1234"}},
}},
{`http://localhost https://localhost
dir1 foo bar`, false, 1},
{`localhost:1234 {
}
localhost:2015 {
}`, false, [][]address{
[]address{{"localhost", "1234"}},
[]address{{"localhost", "2015"}},
}},
{`http://localhost, https://localhost {
dir1 foo bar
}`, false, 1},
{`localhost:1234, http://host2`, false, [][]address{
[]address{{"localhost", "1234"}, {"host2", "http"}},
}},
{`http://host1.com,
http://host2.com,
https://host3.com`, false, 1},
{`localhost:1234, http://host2,`, true, [][]address{}},
{`host1 {
}
host2 {
}`, false, 2},
{`""`, false, 0},
{``, false, 0},
{`http://host1.com, http://host2.com {
}
https://host3.com, https://host4.com {
}`, false, [][]address{
[]address{{"host1.com", "http"}, {"host2.com", "http"}},
[]address{{"host3.com", "https"}, {"host4.com", "https"}},
}},
} {
results, err := testParseAll(test.input)
p := testParser(test.input)
blocks, err := p.parseAll()
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
@@ -327,25 +339,42 @@ func TestParseAll(t *testing.T) {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(results) != test.numBlocks {
if len(blocks) != len(test.addresses) {
t.Errorf("Test %d: Expected %d server blocks, got %d",
i, test.numBlocks, len(results))
i, len(test.addresses), len(blocks))
continue
}
for j, block := range blocks {
if len(block.Addresses) != len(test.addresses[j]) {
t.Errorf("Test %d: Expected %d addresses in block %d, got %d",
i, len(test.addresses[j]), j, len(block.Addresses))
continue
}
for k, addr := range block.Addresses {
if addr.Host != test.addresses[j][k].Host {
t.Errorf("Test %d, block %d, address %d: Expected host to be '%s', but was '%s'",
i, j, k, test.addresses[j][k].Host, addr.Host)
}
if addr.Port != test.addresses[j][k].Port {
t.Errorf("Test %d, block %d, address %d: Expected port to be '%s', but was '%s'",
i, j, k, test.addresses[j][k].Port, addr.Port)
}
}
}
}
}
func 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
}
+102
View File
@@ -0,0 +1,102 @@
// +build !windows
package caddy
import (
"encoding/gob"
"io/ioutil"
"log"
"os"
"syscall"
)
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...
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 or failure with us, the parent
sigrpipe, sigwpipe, err := os.Pipe()
if err != nil {
return err
}
// Pass along current environment and file descriptors to child.
// Ordering here is very important: stdin, stdout, stderr, sigpipe,
// and then the listener file descriptors (in order).
fds := []uintptr{rpipe.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), sigwpipe.Fd()}
// Now add file descriptors of the sockets
serversMu.Lock()
for i, s := range servers {
fds = append(fds, s.ListenerFd())
cdyfileGob.ListenerFds[s.Addr] = uintptr(4 + i) // 4 fds come before any of the listeners
}
serversMu.Unlock()
// Fork the process with the current environment and file descriptors
execSpec := &syscall.ProcAttr{
Env: os.Environ(),
Files: fds,
}
_, err = syscall.ForkExec(os.Args[0], os.Args, execSpec)
if err != nil {
return err
}
// Feed it the Caddyfile
err = gob.NewEncoder(wpipe).Encode(cdyfileGob)
if err != nil {
return err
}
wpipe.Close()
// Wait for child process to signal success or fail
sigwpipe.Close() // close our copy of the write end of the pipe or we might be stuck
answer, err := ioutil.ReadAll(sigrpipe)
if err != nil || len(answer) == 0 {
log.Println("restart: child failed to initialize; changes not applied")
return incompleteRestartErr
}
// Child process is listening now; we can stop all our servers here.
return Stop()
}
+25
View File
@@ -0,0 +1,25 @@
package caddy
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)
@@ -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>
@@ -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>
+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)
}
@@ -5,7 +5,7 @@ import (
"io"
"log"
"os"
"path"
"path/filepath"
"strconv"
"github.com/hashicorp/go-syslog"
@@ -23,25 +23,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 io.Writer
var writer io.Writer
if handler.LogFile == "stdout" {
file = os.Stdout
} else if handler.LogFile == "stderr" {
file = os.Stderr
} else if handler.LogFile == "syslog" {
file, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy")
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
}
} else if handler.LogFile != "" {
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
})
@@ -70,10 +88,24 @@ 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())
@@ -91,18 +123,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)
}
}
}
}
@@ -31,7 +31,7 @@ func FastCGI(c *Controller) (middleware.Middleware, error) {
SoftwareName: c.AppName,
SoftwareVersion: c.AppVersion,
ServerName: c.Host,
ServerPort: c.Port, // BUG: This is not known until the server blocks are split up...
ServerPort: c.Port,
}
}, nil
}
@@ -51,6 +51,16 @@ func TestFastcgiParse(t *testing.T) {
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)
+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 {
+36 -5
View File
@@ -22,25 +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 io.Writer
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" {
file, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "caddy")
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
@@ -57,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
@@ -70,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
@@ -91,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)
}
}
}
}
+152
View File
@@ -0,0 +1,152 @@
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
if !c.NextArg() || c.Val() == "{" {
return mdconfigs, c.ArgErr()
}
md.PathScope = c.Val()
// Load any other configuration parameters
for c.NextBlock() {
if err := loadParams(c, md); err != nil {
return mdconfigs, err
}
}
// If no extensions were specified, assume .md
if len(md.Extensions) == 0 {
md.Extensions = []string{".md"}
}
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")
}
}
@@ -1,6 +1,7 @@
package setup
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
@@ -92,7 +93,7 @@ func TestMarkdownStaticGen(t *testing.T) {
t.Fatalf("An error occured when getting the file content: %v", err)
}
expectedBody := `<!DOCTYPE html>
expectedBody := []byte(`<!DOCTYPE html>
<html>
<head>
<title>first_post</title>
@@ -104,14 +105,15 @@ func TestMarkdownStaticGen(t *testing.T) {
</body>
</html>
`
if string(html) != expectedBody {
t.Fatalf("Expected file content: %v got: %v", expectedBody, 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: ", err)
t.Errorf("Error while removing the generated static files: %v", err)
}
}
+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
}
+29 -16
View File
@@ -37,13 +37,13 @@ func redirParse(c *Controller) ([]redirect.Rule, error) {
// 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.From == rule.To {
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.From == rule.From {
return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.From, otherRule.To)
if otherRule.FromPath == rule.FromPath {
return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.FromPath, otherRule.To)
}
}
@@ -60,6 +60,12 @@ func redirParse(c *Controller) ([]redirect.Rule, error) {
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
@@ -84,15 +90,15 @@ func redirParse(c *Controller) ([]redirect.Rule, error) {
// 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.From = "/"
rule.FromPath = "/"
rule.To = insideArgs[0]
case 2:
// From and To specified
rule.From = insideArgs[0]
rule.FromPath = insideArgs[0]
rule.To = insideArgs[1]
case 3:
// From, To, and Code specified
rule.From = insideArgs[0]
rule.FromPath = insideArgs[0]
rule.To = insideArgs[1]
err := setRedirCode(insideArgs[2], &rule)
if err != nil {
@@ -110,16 +116,23 @@ func redirParse(c *Controller) ([]redirect.Rule, error) {
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.From = "/"
rule.FromPath = "/"
rule.To = args[0]
case 2:
// To and Code specified (catch-all redirect)
rule.From = "/"
rule.FromPath = "/"
rule.To = args[0]
err := setRedirCode(args[1], &rule)
if err != nil {
@@ -127,7 +140,7 @@ func redirParse(c *Controller) ([]redirect.Rule, error) {
}
case 3:
// From, To, and Code specified
rule.From = args[0]
rule.FromPath = args[0]
rule.To = args[1]
err := setRedirCode(args[2], &rule)
if err != nil {
@@ -149,12 +162,12 @@ func redirParse(c *Controller) ([]redirect.Rule, error) {
// httpRedirs is a list of supported HTTP redirect codes.
var httpRedirs = map[string]int{
"300": 300, // Multiple Choices
"301": 301, // Moved Permanently
"302": 302, // Found (NOT CORRECT for "Temporary Redirect", see 307)
"303": 303, // See Other
"304": 304, // Not Modified
"305": 305, // Use Proxy
"307": 307, // Temporary Redirect
"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
}
@@ -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
}
+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")
}
@@ -20,6 +20,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 +48,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.Startup[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 {
+27 -7
View File
@@ -2,24 +2,44 @@ package setup
import (
"crypto/tls"
"log"
"strings"
"github.com/mholt/caddy/middleware"
)
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, "+
"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() {
@@ -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)
+33
View File
@@ -0,0 +1,33 @@
package caddy
import (
"log"
"os"
"os/signal"
"github.com/mholt/caddy/server"
)
func init() {
// Trap quit signals (cross-platform)
go func() {
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, os.Kill)
<-shutdown
var exitCode int
serversMu.Lock()
errs := server.ShutdownCallbacks(servers)
serversMu.Unlock()
if len(errs) > 0 {
for _, err := range errs {
log.Println(err)
}
exitCode = 1
}
os.Exit(exitCode)
}()
}
+43
View File
@@ -0,0 +1,43 @@
// +build !windows
package caddy
import (
"io/ioutil"
"log"
"os"
"os/signal"
"syscall"
)
func init() {
// Trap POSIX-only signals
go func() {
reload := make(chan os.Signal, 1)
signal.Notify(reload, syscall.SIGUSR1) // reload configuration
for {
<-reload
var updatedCaddyfile Input
caddyfileMu.Lock()
if caddyfile.IsFile() {
body, err := ioutil.ReadFile(caddyfile.Path())
if err == nil {
caddyfile = CaddyfileInput{
Filepath: caddyfile.Path(),
Contents: body,
RealFile: true,
}
}
}
caddyfileMu.Unlock()
err := Restart(updatedCaddyfile)
if err != nil {
log.Println("error at restart:", err)
}
}
}()
}
-259
View File
@@ -1,259 +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"
)
// Load reads input (named filename) and parses it, returning server
// configurations grouped by listening address.
func Load(filename string, input io.Reader) (Group, 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 nil, err
}
if len(serverBlocks) == 0 {
return Default()
}
// Each server block represents one or more servers/addresses.
// Iterate each server block and make a config for each one,
// executing the directives that were parsed.
for _, sb := range serverBlocks {
sharedConfig, err := serverBlockToConfig(filename, sb)
if err != nil {
return nil, err
}
// Now share the config with as many hosts as share the server block
for i, addr := range sb.Addresses {
config := sharedConfig
config.Host = addr.Host
config.Port = addr.Port
if config.Port == "" {
config.Port = Port
}
if config.Port == "http" {
config.TLS.Enabled = false
log.Printf("Warning: TLS disabled for %s://%s. To force TLS over the plaintext HTTP port, "+
"specify port 80 explicitly (https://%s:80).", config.Port, config.Host, config.Host)
}
if i == 0 {
sharedConfig.Startup = []func() error{}
sharedConfig.Shutdown = []func() error{}
}
configs = append(configs, config)
}
}
// restore logging settings
log.SetFlags(flags)
// Group by address/virtualhosts
return arrangeBindings(configs)
}
// serverBlockToConfig makes a config for the server block
// by executing the tokens that were parsed. The returned
// config is shared among all hosts/addresses for the server
// block, so Host and Port information is not filled out
// here.
func serverBlockToConfig(filename string, sb parse.ServerBlock) (server.Config, error) {
sharedConfig := server.Config{
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: &sharedConfig,
Dispenser: parse.NewDispenserTokens(filename, tokens),
}
midware, err := dir.setup(controller)
if err != nil {
return sharedConfig, err
}
if midware != nil {
// TODO: For now, we only support the default path scope /
sharedConfig.Middleware["/"] = append(sharedConfig.Middleware["/"], midware)
}
}
}
return sharedConfig, 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) (Group, error) {
addresses := make(Group)
// 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) {
bindHost := conf.BindHost
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
}
func NewDefault() server.Config {
return server.Config{
Root: Root,
Host: Host,
Port: Port,
}
}
// Default makes a default configuration which
// is empty except for root, host, and port,
// which are essentials for serving the cwd.
func Default() (Group, error) {
return arrangeBindings([]server.Config{NewDefault()})
}
// These three defaults are configurable through the command line
var (
Root = DefaultRoot
Host = DefaultHost
Port = DefaultPort
)
type Group map[*net.TCPAddr][]server.Config
-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)
}
-139
View File
@@ -1,139 +0,0 @@
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
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()
}
case "dev":
if c.NextArg() {
md.Development = strings.ToLower(c.Val()) == "true"
} else {
md.Development = true
}
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
}
+34
View File
@@ -1,5 +1,39 @@
CHANGES
0.8 beta
- Let's Encrypt (free, automatic, fully-managed HTTPS for your sites)
- Graceful restarts (for POSIX-compatible 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
- browse: Render filenames with multiple whitespace properly
- markdown: Include Last-Modified header in response
- 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
+1 -1
View File
@@ -1,4 +1,4 @@
CADDY 0.7.5
CADDY 0.8 beta 2
Website
https://caddyserver.com
+102 -107
View File
@@ -1,169 +1,164 @@
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path"
"runtime"
"strconv"
"strings"
"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
version bool
revoke string
)
const (
appName = "Caddy"
appVersion = "0.8 beta 2"
)
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)")
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")")
flag.BoolVar(&caddy.HTTP2, "http2", true, "HTTP/2 support") // TODO: temporary flag until http2 merged into std lib
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
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(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site")
flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host")
flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port")
flag.BoolVar(&version, "version", false, "Show version")
// TODO: Boulder dev URL is: http://192.168.99.100:4000
// TODO: Staging API URL is: https://acme-staging.api.letsencrypt.org
// TODO: Production endpoint is: https://acme-v01.api.letsencrypt.org
flag.StringVar(&letsencrypt.CAUrl, "ca", "https://acme-staging.api.letsencrypt.org", "Certificate authority ACME server")
flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement")
flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default Let's Encrypt account email address")
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
}
func main() {
flag.Parse()
caddy.AppName = appName
caddy.AppVersion = appVersion
if version {
fmt.Printf("%s %s\n", app.Name, app.Version)
fmt.Printf("%s %s\n", caddy.AppName, caddy.AppVersion)
os.Exit(0)
}
if revoke != "" {
err := letsencrypt.Revoke(revoke)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Revoked certificate for %s\n", revoke)
os.Exit(0)
}
// Set CPU cap
err := app.SetCPU(cpu)
err := setCPU(cpu)
if err != nil {
log.Fatal(err)
}
// Load address configurations from highest priority input
addresses, err := loadConfigs()
// Get Caddyfile input
caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile)
if err != nil {
log.Fatal(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 {
// Start your engines
err = caddy.Start(caddyfile)
if err != nil {
if caddy.IsRestart() {
log.Println("error starting servers:", err)
} else {
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() && !isLocalhost(conf.Host) {
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. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min)
}
}
}
}
// 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).
// The configurations are grouped by bind address.
// 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() (config.Group, error) {
func loadCaddyfile() (caddy.Input, error) {
// -conf flag
if conf != "" {
file, err := os.Open(conf)
contents, err := ioutil.ReadFile(conf)
if err != nil {
return nil, err
}
defer file.Close()
return config.Load(path.Base(conf), file)
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.DefaultPort + "\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 config.Default()
return caddy.DefaultInput(), nil
}
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
}
+40
View File
@@ -0,0 +1,40 @@
package main
import (
"runtime"
"testing"
)
func TestSetCPU(t *testing.T) {
currentCPU := runtime.GOMAXPROCS(-1)
maxCPU := runtime.NumCPU()
for i, test := range []struct {
input string
output int
shouldErr bool
}{
{"1", 1, false},
{"-1", currentCPU, true},
{"0", currentCPU, true},
{"100%", maxCPU, false},
{"50%", int(0.5 * float32(maxCPU)), 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)
}
}
+82 -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,74 @@ 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 mathes a rule.
type PasswordMatcher func(pw string) bool
var (
htpasswords map[string]map[string]PasswordMatcher
htpasswordsMu sync.Mutex
)
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)
}
}
}
+71 -29
View File
@@ -4,12 +4,14 @@ package browse
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/url"
"os"
"path"
"sort"
"strconv"
"strings"
"text/template"
"time"
@@ -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
}
@@ -52,6 +56,9 @@ type Listing struct {
// And which order
Order string
// Optional custom variables for use in browse templates
User interface{}
middleware.Context
}
@@ -131,25 +138,18 @@ func (fi FileInfo) HumanModTime(format string) string {
return fi.ModTime.Format(format)
}
var IndexPages = []string{
"index.html",
"index.htm",
"index.txt",
"default.html",
"default.htm",
"default.txt",
}
func directoryListing(files []os.FileInfo, r *http.Request, canGoUp bool, root string) (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!")
}
}
}
@@ -179,13 +179,13 @@ func directoryListing(files []os.FileInfo, r *http.Request, canGoUp bool, root s
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)
@@ -233,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, canGoUp, b.Root)
listing, err := directoryListing(files, r, canGoUp, b.Root, b.IgnoreIndexes, bc.Variables)
if err != nil { // directory isn't browsable
continue
}
@@ -242,34 +242,76 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
listing.Sort, listing.Order = r.URL.Query().Get("sort"), r.URL.Query().Get("order")
// If the query 'sort' or 'order' is empty, check the cookies
if listing.Sort == "" || listing.Order == "" {
if listing.Sort == "" {
sortCookie, sortErr := r.Cookie("sort")
orderCookie, orderErr := r.Cookie("order")
// if there's no sorting values in the cookies, default to "name" and "asc"
if sortErr != nil || orderErr != nil {
if sortErr != nil {
listing.Sort = "name"
listing.Order = "asc"
} else { // if we have values in the cookies, use them
listing.Sort = sortCookie.Value
listing.Order = orderCookie.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
+165 -4
View File
@@ -1,8 +1,12 @@
package browse
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"sort"
"testing"
"text/template"
@@ -113,7 +117,7 @@ func TestBrowseTemplate(t *testing.T) {
}),
Root: "./testdata",
Configs: []Config{
Config{
{
PathScope: "/photos",
Template: tmpl,
},
@@ -127,9 +131,9 @@ func TestBrowseTemplate(t *testing.T) {
rec := httptest.NewRecorder()
b.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, rec.Code)
code, err := b.ServeHTTP(rec, req)
if code != http.StatusOK {
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
}
respBody := rec.Body.String()
@@ -147,6 +151,8 @@ func TestBrowseTemplate(t *testing.T) {
<a href="test2.html">test2.html</a><br>
<a href="test3.html">test3.html</a><br>
</body>
</html>
`
@@ -154,4 +160,159 @@ func TestBrowseTemplate(t *testing.T) {
if respBody != expectedBody {
t.Fatalf("Expected body: %v got: %v", expectedBody, respBody)
}
}
func TestBrowseJson(t *testing.T) {
b := Browse{
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called")
return 0, nil
}),
Root: "./testdata",
Configs: []Config{
{
PathScope: "/photos/",
},
},
}
//Getting the listing from the ./testdata/photos, the listing returned will be used to validate test results
testDataPath := b.Root + "/photos/"
file, err := os.Open(testDataPath)
if err != nil {
if os.IsPermission(err) {
t.Fatalf("Os Permission Error")
}
}
defer file.Close()
files, err := file.Readdir(-1)
if err != nil {
t.Fatalf("Unable to Read Contents of the directory")
}
var fileinfos []FileInfo
for i, f := range files {
name := f.Name()
// Tests fail in CI environment because all file mod times are the same for
// some reason, making the sorting unpredictable. To hack around this,
// we ensure here that each file has a different mod time.
chTime := f.ModTime().Add(-(time.Duration(i) * time.Second))
if err := os.Chtimes(filepath.Join(testDataPath, name), chTime, chTime); err != nil {
t.Fatal(err)
}
if f.IsDir() {
name += "/"
}
url := url.URL{Path: name}
fileinfos = append(fileinfos, FileInfo{
IsDir: f.IsDir(),
Name: f.Name(),
Size: f.Size(),
URL: url.String(),
ModTime: chTime,
Mode: f.Mode(),
})
}
listing := Listing{Items: fileinfos} // this listing will be used for validation inside the tests
tests := []struct {
QueryUrl string
SortBy string
OrderBy string
Limit int
shouldErr bool
expectedResult []FileInfo
}{
//test case 1: testing for default sort and order and without the limit parameter, default sort is by name and the default order is ascending
//without the limit query entire listing will be produced
{"/", "", "", -1, false, listing.Items},
//test case 2: limit is set to 1, orderBy and sortBy is default
{"/?limit=1", "", "", 1, false, listing.Items[:1]},
//test case 3 : if the listing request is bigger than total size of listing then it should return everything
{"/?limit=100000000", "", "", 100000000, false, listing.Items},
//test case 4 : testing for negative limit
{"/?limit=-1", "", "", -1, false, listing.Items},
//test case 5 : testing with limit set to -1 and order set to descending
{"/?limit=-1&order=desc", "", "desc", -1, false, listing.Items},
//test case 6 : testing with limit set to 2 and order set to descending
{"/?limit=2&order=desc", "", "desc", 2, false, listing.Items},
//test case 7 : testing with limit set to 3 and order set to descending
{"/?limit=3&order=desc", "", "desc", 3, false, listing.Items},
//test case 8 : testing with limit set to 3 and order set to ascending
{"/?limit=3&order=asc", "", "asc", 3, false, listing.Items},
//test case 9 : testing with limit set to 1111111 and order set to ascending
{"/?limit=1111111&order=asc", "", "asc", 1111111, false, listing.Items},
//test case 10 : testing with limit set to default and order set to ascending and sorting by size
{"/?order=asc&sort=size", "size", "asc", -1, false, listing.Items},
//test case 11 : testing with limit set to default and order set to ascending and sorting by last modified
{"/?order=asc&sort=time", "time", "asc", -1, false, listing.Items},
//test case 12 : testing with limit set to 1 and order set to ascending and sorting by last modified
{"/?order=asc&sort=time&limit=1", "time", "asc", 1, false, listing.Items},
//test case 13 : testing with limit set to -100 and order set to ascending and sorting by last modified
{"/?order=asc&sort=time&limit=-100", "time", "asc", -100, false, listing.Items},
//test case 14 : testing with limit set to -100 and order set to ascending and sorting by size
{"/?order=asc&sort=size&limit=-100", "size", "asc", -100, false, listing.Items},
}
for i, test := range tests {
var marsh []byte
req, err := http.NewRequest("GET", "/photos"+test.QueryUrl, nil)
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)
}
req.Header.Set("Accept", "application/json")
rec := httptest.NewRecorder()
code, err := b.ServeHTTP(rec, req)
if code != http.StatusOK {
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
}
if rec.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
t.Fatalf("Expected Content type to be application/json; charset=utf-8, but got %s ", rec.HeaderMap.Get("Content-Type"))
}
actualJSONResponse := rec.Body.String()
copyOflisting := listing
if test.SortBy == "" {
copyOflisting.Sort = "name"
} else {
copyOflisting.Sort = test.SortBy
}
if test.OrderBy == "" {
copyOflisting.Order = "asc"
} else {
copyOflisting.Order = test.OrderBy
}
copyOflisting.applySort()
limit := test.Limit
if limit <= len(copyOflisting.Items) && limit > 0 {
marsh, err = json.Marshal(copyOflisting.Items[:limit])
} else { // if the 'limit' query is empty, or has the wrong value, list everything
marsh, err = json.Marshal(copyOflisting.Items)
}
if err != nil {
t.Fatalf("Unable to Marshal the listing ")
}
expectedJSON := string(marsh)
if actualJSONResponse != expectedJSON {
t.Errorf("JSON response doesn't match the expected for test number %d with sort=%s, order=%s\nExpected response %s\nActual response = %s\n",
i+1, test.SortBy, test.OrderBy, expectedJSON, actualJSONResponse)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
<!DOCTYPE html>
<html>
</html>
+98 -5
View File
@@ -2,18 +2,30 @@ package middleware
import (
"errors"
"runtime"
"unicode"
"github.com/flynn/go-shlex"
)
var runtimeGoos = runtime.GOOS
// SplitCommandAndArgs takes a command string and parses it
// shell-style into the command and its separate arguments.
func SplitCommandAndArgs(command string) (cmd string, args []string, err error) {
parts, err := shlex.Split(command)
if err != nil {
err = errors.New("error parsing command: " + err.Error())
return
} else if len(parts) == 0 {
var parts []string
if runtimeGoos == "windows" {
parts = parseWindowsCommand(command) // parse it Windows-style
} else {
parts, err = parseUnixCommand(command) // parse it Unix-style
if err != nil {
err = errors.New("error parsing command: " + err.Error())
return
}
}
if len(parts) == 0 {
err = errors.New("no command contained in '" + command + "'")
return
}
@@ -25,3 +37,84 @@ func SplitCommandAndArgs(command string) (cmd string, args []string, err error)
return
}
// parseUnixCommand parses a unix style command line and returns the
// command and its arguments or an error
func parseUnixCommand(cmd string) ([]string, error) {
return shlex.Split(cmd)
}
// parseWindowsCommand parses windows command lines and
// returns the command and the arguments as an array. It
// should be able to parse commonly used command lines.
// Only basic syntax is supported:
// - spaces in double quotes are not token delimiters
// - double quotes are escaped by either backspace or another double quote
// - except for the above case backspaces are path separators (not special)
//
// Many sources point out that escaping quotes using backslash can be unsafe.
// Use two double quotes when possible. (Source: http://stackoverflow.com/a/31413730/2616179 )
//
// This function has to be used on Windows instead
// of the shlex package because this function treats backslash
// characters properly.
func parseWindowsCommand(cmd string) []string {
const backslash = '\\'
const quote = '"'
var parts []string
var part string
var inQuotes bool
var lastRune rune
for i, ch := range cmd {
if i != 0 {
lastRune = rune(cmd[i-1])
}
if ch == backslash {
// put it in the part - for now we don't know if it's an
// escaping char or path separator
part += string(ch)
continue
}
if ch == quote {
if lastRune == backslash {
// remove the backslash from the part and add the escaped quote instead
part = part[:len(part)-1]
part += string(ch)
continue
}
if lastRune == quote {
// revert the last change of the inQuotes state
// it was an escaping quote
inQuotes = !inQuotes
part += string(ch)
continue
}
// normal escaping quotes
inQuotes = !inQuotes
continue
}
if unicode.IsSpace(ch) && !inQuotes && len(part) > 0 {
parts = append(parts, part)
part = ""
continue
}
part += string(ch)
}
if len(part) > 0 {
parts = append(parts, part)
part = ""
}
return parts
}
+291
View File
@@ -0,0 +1,291 @@
package middleware
import (
"fmt"
"runtime"
"strings"
"testing"
)
func TestParseUnixCommand(t *testing.T) {
tests := []struct {
input string
expected []string
}{
// 0 - emtpy command
{
input: ``,
expected: []string{},
},
// 1 - command without arguments
{
input: `command`,
expected: []string{`command`},
},
// 2 - command with single argument
{
input: `command arg1`,
expected: []string{`command`, `arg1`},
},
// 3 - command with multiple arguments
{
input: `command arg1 arg2`,
expected: []string{`command`, `arg1`, `arg2`},
},
// 4 - command with single argument with space character - in quotes
{
input: `command "arg1 arg1"`,
expected: []string{`command`, `arg1 arg1`},
},
// 5 - command with multiple spaces and tab character
{
input: "command arg1 arg2\targ3",
expected: []string{`command`, `arg1`, `arg2`, `arg3`},
},
// 6 - command with single argument with space character - escaped with backspace
{
input: `command arg1\ arg2`,
expected: []string{`command`, `arg1 arg2`},
},
// 7 - single quotes should escape special chars
{
input: `command 'arg1\ arg2'`,
expected: []string{`command`, `arg1\ arg2`},
},
}
for i, test := range tests {
errorPrefix := fmt.Sprintf("Test [%d]: ", i)
errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input)
actual, _ := parseUnixCommand(test.input)
if len(actual) != len(test.expected) {
t.Errorf(errorPrefix+"Expected %d parts, got %d: %#v."+errorSuffix, len(test.expected), len(actual), actual)
continue
}
for j := 0; j < len(actual); j++ {
if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart {
t.Errorf(errorPrefix+"Expected: %v Actual: %v (index %d)."+errorSuffix, expectedPart, actualPart, j)
}
}
}
}
func TestParseWindowsCommand(t *testing.T) {
tests := []struct {
input string
expected []string
}{
{ // 0 - empty command - do not fail
input: ``,
expected: []string{},
},
{ // 1 - cmd without args
input: `cmd`,
expected: []string{`cmd`},
},
{ // 2 - multiple args
input: `cmd arg1 arg2`,
expected: []string{`cmd`, `arg1`, `arg2`},
},
{ // 3 - multiple args with space
input: `cmd "combined arg" arg2`,
expected: []string{`cmd`, `combined arg`, `arg2`},
},
{ // 4 - path without spaces
input: `mkdir C:\Windows\foo\bar`,
expected: []string{`mkdir`, `C:\Windows\foo\bar`},
},
{ // 5 - command with space in quotes
input: `"command here"`,
expected: []string{`command here`},
},
{ // 6 - argument with escaped quotes (two quotes)
input: `cmd ""arg""`,
expected: []string{`cmd`, `"arg"`},
},
{ // 7 - argument with escaped quotes (backslash)
input: `cmd \"arg\"`,
expected: []string{`cmd`, `"arg"`},
},
{ // 8 - two quotes (escaped) inside an inQuote element
input: `cmd "a ""quoted value"`,
expected: []string{`cmd`, `a "quoted value`},
},
// TODO - see how many quotes are dislayed if we use "", """, """""""
{ // 9 - two quotes outside an inQuote element
input: `cmd a ""quoted value`,
expected: []string{`cmd`, `a`, `"quoted`, `value`},
},
{ // 10 - path with space in quotes
input: `mkdir "C:\directory name\foobar"`,
expected: []string{`mkdir`, `C:\directory name\foobar`},
},
{ // 11 - space without quotes
input: `mkdir C:\ space`,
expected: []string{`mkdir`, `C:\`, `space`},
},
{ // 12 - space in quotes
input: `mkdir "C:\ space"`,
expected: []string{`mkdir`, `C:\ space`},
},
{ // 13 - UNC
input: `mkdir \\?\C:\Users`,
expected: []string{`mkdir`, `\\?\C:\Users`},
},
{ // 14 - UNC with space
input: `mkdir "\\?\C:\Program Files"`,
expected: []string{`mkdir`, `\\?\C:\Program Files`},
},
{ // 15 - unclosed quotes - treat as if the path ends with quote
input: `mkdir "c:\Program files`,
expected: []string{`mkdir`, `c:\Program files`},
},
{ // 16 - quotes used inside the argument
input: `mkdir "c:\P"rogra"m f"iles`,
expected: []string{`mkdir`, `c:\Program files`},
},
}
for i, test := range tests {
errorPrefix := fmt.Sprintf("Test [%d]: ", i)
errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input)
actual := parseWindowsCommand(test.input)
if len(actual) != len(test.expected) {
t.Errorf(errorPrefix+"Expected %d parts, got %d: %#v."+errorSuffix, len(test.expected), len(actual), actual)
continue
}
for j := 0; j < len(actual); j++ {
if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart {
t.Errorf(errorPrefix+"Expected: %v Actual: %v (index %d)."+errorSuffix, expectedPart, actualPart, j)
}
}
}
}
func TestSplitCommandAndArgs(t *testing.T) {
// force linux parsing. It's more robust and covers error cases
runtimeGoos = "linux"
defer func() {
runtimeGoos = runtime.GOOS
}()
var parseErrorContent = "error parsing command:"
var noCommandErrContent = "no command contained in"
tests := []struct {
input string
expectedCommand string
expectedArgs []string
expectedErrContent string
}{
// 0 - emtpy command
{
input: ``,
expectedCommand: ``,
expectedArgs: nil,
expectedErrContent: noCommandErrContent,
},
// 1 - command without arguments
{
input: `command`,
expectedCommand: `command`,
expectedArgs: nil,
expectedErrContent: ``,
},
// 2 - command with single argument
{
input: `command arg1`,
expectedCommand: `command`,
expectedArgs: []string{`arg1`},
expectedErrContent: ``,
},
// 3 - command with multiple arguments
{
input: `command arg1 arg2`,
expectedCommand: `command`,
expectedArgs: []string{`arg1`, `arg2`},
expectedErrContent: ``,
},
// 4 - command with unclosed quotes
{
input: `command "arg1 arg2`,
expectedCommand: "",
expectedArgs: nil,
expectedErrContent: parseErrorContent,
},
// 5 - command with unclosed quotes
{
input: `command 'arg1 arg2"`,
expectedCommand: "",
expectedArgs: nil,
expectedErrContent: parseErrorContent,
},
}
for i, test := range tests {
errorPrefix := fmt.Sprintf("Test [%d]: ", i)
errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input)
actualCommand, actualArgs, actualErr := SplitCommandAndArgs(test.input)
// test if error matches expectation
if test.expectedErrContent != "" {
if actualErr == nil {
t.Errorf(errorPrefix+"Expected error with content [%s], found no error."+errorSuffix, test.expectedErrContent)
} else if !strings.Contains(actualErr.Error(), test.expectedErrContent) {
t.Errorf(errorPrefix+"Expected error with content [%s], found [%v]."+errorSuffix, test.expectedErrContent, actualErr)
}
} else if actualErr != nil {
t.Errorf(errorPrefix+"Expected no error, found [%v]."+errorSuffix, actualErr)
}
// test if command matches
if test.expectedCommand != actualCommand {
t.Errorf(errorPrefix+"Expected command: [%s], actual: [%s]."+errorSuffix, test.expectedCommand, actualCommand)
}
// test if arguments match
if len(test.expectedArgs) != len(actualArgs) {
t.Errorf(errorPrefix+"Wrong number of arguments! Expected [%v], actual [%v]."+errorSuffix, test.expectedArgs, actualArgs)
} else {
// test args only if the count matches.
for j, actualArg := range actualArgs {
expectedArg := test.expectedArgs[j]
if actualArg != expectedArg {
t.Errorf(errorPrefix+"Argument at position [%d] differ! Expected [%s], actual [%s]"+errorSuffix, j, expectedArg, actualArg)
}
}
}
}
}
func ExampleSplitCommandAndArgs() {
var commandLine string
var command string
var args []string
// just for the test - change GOOS and reset it at the end of the test
runtimeGoos = "windows"
defer func() {
runtimeGoos = runtime.GOOS
}()
commandLine = `mkdir /P "C:\Program Files"`
command, args, _ = SplitCommandAndArgs(commandLine)
fmt.Printf("Windows: %s: %s [%s]\n", commandLine, command, strings.Join(args, ","))
// set GOOS to linux
runtimeGoos = "linux"
commandLine = `mkdir -p /path/with\ space`
command, args, _ = SplitCommandAndArgs(commandLine)
fmt.Printf("Linux: %s: %s [%s]\n", commandLine, command, strings.Join(args, ","))
// Output:
// Windows: mkdir /P "C:\Program Files": mkdir [/P,C:\Program Files]
// Linux: mkdir -p /path/with\ space: mkdir [-p,/path/with space]
}
+51
View File
@@ -97,6 +97,10 @@ func (c Context) URI() string {
func (c Context) Host() (string, error) {
host, _, err := net.SplitHostPort(c.Req.Host)
if err != nil {
if !strings.Contains(c.Req.Host, ":") {
// common with sites served on the default port 80
return c.Req.Host, nil
}
return "", err
}
return host, nil
@@ -131,6 +135,53 @@ func (c Context) Truncate(input string, length int) string {
return input
}
// StripHTML returns s without HTML tags. It is fairly naive
// but works with most valid HTML inputs.
func (c Context) StripHTML(s string) string {
var buf bytes.Buffer
var inTag, inQuotes bool
var tagStart int
for i, ch := range s {
if inTag {
if ch == '>' && !inQuotes {
inTag = false
} else if ch == '<' && !inQuotes {
// false start
buf.WriteString(s[tagStart:i])
tagStart = i
} else if ch == '"' {
inQuotes = !inQuotes
}
continue
}
if ch == '<' {
inTag = true
tagStart = i
continue
}
buf.WriteRune(ch)
}
if inTag {
// false start
buf.WriteString(s[tagStart:])
inTag = false
}
return buf.String()
}
// StripExt returns the input string without the extension,
// which is the suffix starting with the final '.' character
// but not before the final path separator ('/') character.
// If there is no extension, the whole input is returned.
func (c Context) StripExt(path string) string {
for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
if path[i] == '.' {
return path[:i]
}
}
return path
}
// Replace replaces instances of find in input with replacement.
func (c Context) Replace(input, find, replacement string) string {
return strings.Replace(input, find, replacement, -1)
+545
View File
@@ -0,0 +1,545 @@
package middleware
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestInclude(t *testing.T) {
context := getContextOrFail(t)
inputFilename := "test_file"
absInFilePath := filepath.Join(fmt.Sprintf("%s", context.Root), inputFilename)
defer func() {
err := os.Remove(absInFilePath)
if err != nil && !os.IsNotExist(err) {
t.Fatalf("Failed to clean test file!")
}
}()
tests := []struct {
fileContent string
expectedContent string
shouldErr bool
expectedErrorContent string
}{
// Test 0 - all good
{
fileContent: `str1 {{ .Root }} str2`,
expectedContent: fmt.Sprintf("str1 %s str2", context.Root),
shouldErr: false,
expectedErrorContent: "",
},
// Test 1 - failure on template.Parse
{
fileContent: `str1 {{ .Root } str2`,
expectedContent: "",
shouldErr: true,
expectedErrorContent: `unexpected "}" in operand`,
},
// Test 3 - failure on template.Execute
{
fileContent: `str1 {{ .InvalidField }} str2`,
expectedContent: "",
shouldErr: true,
expectedErrorContent: `InvalidField is not a field of struct type middleware.Context`,
},
}
for i, test := range tests {
testPrefix := getTestPrefix(i)
// WriteFile truncates the contentt
err := ioutil.WriteFile(absInFilePath, []byte(test.fileContent), os.ModePerm)
if err != nil {
t.Fatal(testPrefix+"Failed to create test file. Error was: %v", err)
}
content, err := context.Include(inputFilename)
if err != nil {
if !test.shouldErr {
t.Errorf(testPrefix+"Expected no error, found [%s]", test.expectedErrorContent, err.Error())
}
if !strings.Contains(err.Error(), test.expectedErrorContent) {
t.Errorf(testPrefix+"Expected error content [%s], found [%s]", test.expectedErrorContent, err.Error())
}
}
if err == nil && test.shouldErr {
t.Errorf(testPrefix+"Expected error [%s] but found nil. Input file was: %s", test.expectedErrorContent, inputFilename)
}
if content != test.expectedContent {
t.Errorf(testPrefix+"Expected content [%s] but found [%s]. Input file was: %s", test.expectedContent, content, inputFilename)
}
}
}
func TestIncludeNotExisting(t *testing.T) {
context := getContextOrFail(t)
_, err := context.Include("not_existing")
if err == nil {
t.Errorf("Expected error but found nil!")
}
}
func TestCookie(t *testing.T) {
tests := []struct {
cookie *http.Cookie
cookieName string
expectedValue string
}{
// Test 0 - happy path
{
cookie: &http.Cookie{Name: "cookieName", Value: "cookieValue"},
cookieName: "cookieName",
expectedValue: "cookieValue",
},
// Test 1 - try to get a non-existing cookie
{
cookie: &http.Cookie{Name: "cookieName", Value: "cookieValue"},
cookieName: "notExisting",
expectedValue: "",
},
// Test 2 - partial name match
{
cookie: &http.Cookie{Name: "cookie", Value: "cookieValue"},
cookieName: "cook",
expectedValue: "",
},
// Test 3 - cookie with optional fields
{
cookie: &http.Cookie{Name: "cookie", Value: "cookieValue", Path: "/path", Domain: "https://localhost", Expires: (time.Now().Add(10 * time.Minute)), MaxAge: 120},
cookieName: "cookie",
expectedValue: "cookieValue",
},
}
for i, test := range tests {
testPrefix := getTestPrefix(i)
// reinitialize the context for each test
context := getContextOrFail(t)
context.Req.AddCookie(test.cookie)
actualCookieVal := context.Cookie(test.cookieName)
if actualCookieVal != test.expectedValue {
t.Errorf(testPrefix+"Expected cookie value [%s] but found [%s] for cookie with name %s", test.expectedValue, actualCookieVal, test.cookieName)
}
}
}
func TestCookieMultipleCookies(t *testing.T) {
context := getContextOrFail(t)
cookieNameBase, cookieValueBase := "cookieName", "cookieValue"
// make sure that there's no state and multiple requests for different cookies return the correct result
for i := 0; i < 10; i++ {
context.Req.AddCookie(&http.Cookie{Name: fmt.Sprintf("%s%d", cookieNameBase, i), Value: fmt.Sprintf("%s%d", cookieValueBase, i)})
}
for i := 0; i < 10; i++ {
expectedCookieVal := fmt.Sprintf("%s%d", cookieValueBase, i)
actualCookieVal := context.Cookie(fmt.Sprintf("%s%d", cookieNameBase, i))
if actualCookieVal != expectedCookieVal {
t.Fatalf("Expected cookie value %s, found %s", expectedCookieVal, actualCookieVal)
}
}
}
func TestHeader(t *testing.T) {
context := getContextOrFail(t)
headerKey, headerVal := "Header1", "HeaderVal1"
context.Req.Header.Add(headerKey, headerVal)
actualHeaderVal := context.Header(headerKey)
if actualHeaderVal != headerVal {
t.Errorf("Expected header %s, found %s", headerVal, actualHeaderVal)
}
missingHeaderVal := context.Header("not-existing")
if missingHeaderVal != "" {
t.Errorf("Expected empty header value, found %s", missingHeaderVal)
}
}
func TestIP(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
inputRemoteAddr string
expectedIP string
}{
// Test 0 - ipv4 with port
{"1.1.1.1:1111", "1.1.1.1"},
// Test 1 - ipv4 without port
{"1.1.1.1", "1.1.1.1"},
// Test 2 - ipv6 with port
{"[::1]:11", "::1"},
// Test 3 - ipv6 without port and brackets
{"[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]"},
// Test 4 - ipv6 with zone and port
{`[fe80:1::3%eth0]:44`, `fe80:1::3%eth0`},
}
for i, test := range tests {
testPrefix := getTestPrefix(i)
context.Req.RemoteAddr = test.inputRemoteAddr
actualIP := context.IP()
if actualIP != test.expectedIP {
t.Errorf(testPrefix+"Expected IP %s, found %s", test.expectedIP, actualIP)
}
}
}
func TestURL(t *testing.T) {
context := getContextOrFail(t)
inputURL := "http://localhost"
context.Req.RequestURI = inputURL
if inputURL != context.URI() {
t.Errorf("Expected url %s, found %s", inputURL, context.URI())
}
}
func TestHost(t *testing.T) {
tests := []struct {
input string
expectedHost string
shouldErr bool
}{
{
input: "localhost:123",
expectedHost: "localhost",
shouldErr: false,
},
{
input: "localhost",
expectedHost: "localhost",
shouldErr: false,
},
{
input: "[::]",
expectedHost: "",
shouldErr: true,
},
}
for _, test := range tests {
testHostOrPort(t, true, test.input, test.expectedHost, test.shouldErr)
}
}
func TestPort(t *testing.T) {
tests := []struct {
input string
expectedPort string
shouldErr bool
}{
{
input: "localhost:123",
expectedPort: "123",
shouldErr: false,
},
{
input: "localhost",
expectedPort: "",
shouldErr: true, // missing port in address
},
{
input: ":8080",
expectedPort: "8080",
shouldErr: false,
},
}
for _, test := range tests {
testHostOrPort(t, false, test.input, test.expectedPort, test.shouldErr)
}
}
func testHostOrPort(t *testing.T, isTestingHost bool, input, expectedResult string, shouldErr bool) {
context := getContextOrFail(t)
context.Req.Host = input
var actualResult, testedObject string
var err error
if isTestingHost {
actualResult, err = context.Host()
testedObject = "host"
} else {
actualResult, err = context.Port()
testedObject = "port"
}
if shouldErr && err == nil {
t.Errorf("Expected error, found nil!")
return
}
if !shouldErr && err != nil {
t.Errorf("Expected no error, found %s", err)
return
}
if actualResult != expectedResult {
t.Errorf("Expected %s %s, found %s", testedObject, expectedResult, actualResult)
}
}
func TestMethod(t *testing.T) {
context := getContextOrFail(t)
method := "POST"
context.Req.Method = method
if method != context.Method() {
t.Errorf("Expected method %s, found %s", method, context.Method())
}
}
func TestPathMatches(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
urlStr string
pattern string
shouldMatch bool
}{
// Test 0
{
urlStr: "http://localhost/",
pattern: "",
shouldMatch: true,
},
// Test 1
{
urlStr: "http://localhost",
pattern: "",
shouldMatch: true,
},
// Test 1
{
urlStr: "http://localhost/",
pattern: "/",
shouldMatch: true,
},
// Test 3
{
urlStr: "http://localhost/?param=val",
pattern: "/",
shouldMatch: true,
},
// Test 4
{
urlStr: "http://localhost/dir1/dir2",
pattern: "/dir2",
shouldMatch: false,
},
// Test 5
{
urlStr: "http://localhost/dir1/dir2",
pattern: "/dir1",
shouldMatch: true,
},
// Test 6
{
urlStr: "http://localhost:444/dir1/dir2",
pattern: "/dir1",
shouldMatch: true,
},
// Test 7
{
urlStr: "http://localhost/dir1/dir2",
pattern: "*/dir2",
shouldMatch: false,
},
}
for i, test := range tests {
testPrefix := getTestPrefix(i)
var err error
context.Req.URL, err = url.Parse(test.urlStr)
if err != nil {
t.Fatalf("Failed to prepare test URL from string %s! Error was: %s", test.urlStr, err)
}
matches := context.PathMatches(test.pattern)
if matches != test.shouldMatch {
t.Errorf(testPrefix+"Expected and actual result differ: expected to match [%t], actual matches [%t]", test.shouldMatch, matches)
}
}
}
func TestTruncate(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
inputString string
inputLength int
expected string
}{
// Test 0 - small length
{
inputString: "string",
inputLength: 1,
expected: "s",
},
// Test 1 - exact length
{
inputString: "string",
inputLength: 6,
expected: "string",
},
// Test 2 - bigger length
{
inputString: "string",
inputLength: 10,
expected: "string",
},
}
for i, test := range tests {
actual := context.Truncate(test.inputString, test.inputLength)
if actual != test.expected {
t.Errorf(getTestPrefix(i)+"Expected %s, found %s. Input was Truncate(%q, %d)", test.expected, actual, test.inputString, test.inputLength)
}
}
}
func TestStripHTML(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
input string
expected string
}{
// Test 0 - no tags
{
input: `h1`,
expected: `h1`,
},
// Test 1 - happy path
{
input: `<h1>h1</h1>`,
expected: `h1`,
},
// Test 2 - tag in quotes
{
input: `<h1">">h1</h1>`,
expected: `h1`,
},
// Test 3 - multiple tags
{
input: `<h1><b>h1</b></h1>`,
expected: `h1`,
},
// Test 4 - tags not closed
{
input: `<h1`,
expected: `<h1`,
},
// Test 5 - false start
{
input: `<h1<b>hi`,
expected: `<h1hi`,
},
}
for i, test := range tests {
actual := context.StripHTML(test.input)
if actual != test.expected {
t.Errorf(getTestPrefix(i)+"Expected %s, found %s. Input was StripHTML(%s)", test.expected, actual, test.input)
}
}
}
func TestStripExt(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
input string
expected string
}{
// Test 0 - empty input
{
input: "",
expected: "",
},
// Test 1 - relative file with ext
{
input: "file.ext",
expected: "file",
},
// Test 2 - relative file without ext
{
input: "file",
expected: "file",
},
// Test 3 - absolute file without ext
{
input: "/file",
expected: "/file",
},
// Test 4 - absolute file with ext
{
input: "/file.ext",
expected: "/file",
},
// Test 5 - with ext but ends with /
{
input: "/dir.ext/",
expected: "/dir.ext/",
},
// Test 6 - file with ext under dir with ext
{
input: "/dir.ext/file.ext",
expected: "/dir.ext/file",
},
}
for i, test := range tests {
actual := context.StripExt(test.input)
if actual != test.expected {
t.Errorf(getTestPrefix(i)+"Expected %s, found %s. Input was StripExt(%q)", test.expected, actual, test.input)
}
}
}
func initTestContext() (Context, error) {
body := bytes.NewBufferString("request body")
request, err := http.NewRequest("GET", "https://localhost", body)
if err != nil {
return Context{}, err
}
return Context{Root: http.Dir(os.TempDir()), Req: request}, nil
}
func getContextOrFail(t *testing.T) Context {
context, err := initTestContext()
if err != nil {
t.Fatalf("Failed to prepare test context")
}
return context
}
func getTestPrefix(testN int) string {
return fmt.Sprintf("Test [%d]: ", testN)
}
+32 -12
View File
@@ -14,12 +14,14 @@ import (
"github.com/mholt/caddy/middleware"
)
// ErrorHandler handles HTTP errors (or errors from other middleware).
// ErrorHandler handles HTTP errors (and errors from other middleware).
type ErrorHandler struct {
Next middleware.Handler
ErrorPages map[int]string // map of status code to filename
LogFile string
Log *log.Logger
LogRoller *middleware.LogRoller
Debug bool // if true, errors are written out to client rather than to a log
}
func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
@@ -28,12 +30,20 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
status, err := h.Next.ServeHTTP(w, r)
if err != nil {
h.Log.Printf("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), status, r.URL.Path, err)
errMsg := fmt.Sprintf("%s [ERROR %d %s] %v", time.Now().Format(timeFormat), status, r.URL.Path, err)
if h.Debug {
// Write error to response instead of to log
w.WriteHeader(status)
fmt.Fprintln(w, errMsg)
return 0, err // returning < 400 signals that a response has been written
}
h.Log.Println(errMsg)
}
if status >= 400 {
h.errorPage(w, status)
return 0, err // status < 400 signals that a response has been written
h.errorPage(w, r, status)
return 0, err
}
return status, err
@@ -42,7 +52,7 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
// errorPage serves a static error page to w according to the status
// code. If there is an error serving the error page, a plaintext error
// message is written instead, and the extra error is logged.
func (h ErrorHandler) errorPage(w http.ResponseWriter, code int) {
func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int) {
defaultBody := fmt.Sprintf("%d %s", code, http.StatusText(code))
// See if an error page for this status code was specified
@@ -51,8 +61,9 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, code int) {
// Try to open it
errorPage, err := os.Open(pagePath)
if err != nil {
// An error handling an error... <insert grumpy cat here>
h.Log.Printf("HTTP %d could not load error page %s: %v", code, pagePath, err)
// An additional error handling an error... <insert grumpy cat here>
h.Log.Printf("%s [NOTICE %d %s] could not load error page: %v",
time.Now().Format(timeFormat), code, r.URL.String(), err)
http.Error(w, defaultBody, code)
return
}
@@ -65,7 +76,8 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, code int) {
if err != nil {
// Epic fail... sigh.
h.Log.Printf("HTTP %d could not respond with %s: %v", code, pagePath, err)
h.Log.Printf("%s [NOTICE %d %s] could not respond with %s: %v",
time.Now().Format(timeFormat), code, r.URL.String(), pagePath, err)
http.Error(w, defaultBody, code)
}
@@ -107,10 +119,18 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
file = file[pkgPathPos+len(delim):]
}
// Currently we don't use the function name, as file:line is more conventional
h.Log.Printf("%s [PANIC %s] %s:%d - %v", time.Now().Format(timeFormat), r.URL.String(), file, line, rec)
h.errorPage(w, http.StatusInternalServerError)
panicMsg := fmt.Sprintf("%s [PANIC %s] %s:%d - %v", time.Now().Format(timeFormat), r.URL.String(), file, line, rec)
if h.Debug {
// Write error and stack trace to the response rather than to a log
var stackBuf [4096]byte
stack := stackBuf[:runtime.Stack(stackBuf[:], false)]
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "%s\n\n%s", panicMsg, stack)
} else {
// Currently we don't use the function name, since file:line is more conventional
h.Log.Printf(panicMsg)
h.errorPage(w, r, http.StatusInternalServerError)
}
}
const DefaultLogFilename = "error.log"
const timeFormat = "02/Jan/2006:15:04:05 -0700"
+45 -6
View File
@@ -33,11 +33,12 @@ func TestErrors(t *testing.T) {
buf := bytes.Buffer{}
em := ErrorHandler{
ErrorPages: make(map[int]string),
Log: log.New(&buf, "", 0),
ErrorPages: map[int]string{
http.StatusNotFound: path,
http.StatusForbidden: "not_exist_file",
},
Log: log.New(&buf, "", 0),
}
em.ErrorPages[http.StatusNotFound] = path
em.ErrorPages[http.StatusForbidden] = "not_exist_file"
_, notExistErr := os.Open("not_exist_file")
testErr := errors.New("test error")
@@ -82,8 +83,8 @@ func TestErrors(t *testing.T) {
expectedCode: 0,
expectedBody: fmt.Sprintf("%d %s\n", http.StatusForbidden,
http.StatusText(http.StatusForbidden)),
expectedLog: fmt.Sprintf("HTTP %d could not load error page %s: %v\n",
http.StatusForbidden, "not_exist_file", notExistErr),
expectedLog: fmt.Sprintf("[NOTICE %d /] could not load error page: %v\n",
http.StatusForbidden, notExistErr),
expectedErr: nil,
},
}
@@ -117,6 +118,44 @@ func TestErrors(t *testing.T) {
}
}
func TestVisibleErrorWithPanic(t *testing.T) {
const panicMsg = "I'm a panic"
eh := ErrorHandler{
ErrorPages: make(map[int]string),
Debug: true,
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
panic(panicMsg)
}),
}
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
code, err := eh.ServeHTTP(rec, req)
if code != 0 {
t.Errorf("Expected error handler to return 0 (it should write to response), got status %d", code)
}
if err != nil {
t.Errorf("Expected error handler to return nil error (it should panic!), but got '%v'", err)
}
body := rec.Body.String()
if !strings.Contains(body, "[PANIC /] middleware/errors/errors_test.go") {
t.Errorf("Expected response body to contain error log line, but it didn't:\n%s", body)
}
if !strings.Contains(body, panicMsg) {
t.Errorf("Expected response body to contain panic message, but it didn't:\n%s", body)
}
if len(body) < 500 {
t.Errorf("Expected response body to contain stack trace, but it was too short: len=%d", len(body))
}
}
func genErrorHandler(status int, err error, body string) middleware.Handler {
return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
fmt.Fprint(w, body)
Regular → Executable
+66 -35
View File
@@ -4,6 +4,7 @@
package fastcgi
import (
"errors"
"io"
"net/http"
"os"
@@ -46,10 +47,21 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
fpath := r.URL.Path
if idx, ok := middleware.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
fpath = idx
// Index file present.
// If request path cannot be split, return error.
if !h.canSplit(fpath, rule) {
return http.StatusInternalServerError, ErrIndexMissingSplit
}
} else {
// No index file present.
// If request path cannot be split, ignore request.
if !h.canSplit(fpath, rule) {
continue
}
}
// These criteria work well in this order for PHP sites
if fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) || !h.exists(fpath) {
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) {
// Create environment for CGI script
env, err := h.buildEnv(r, rule, fpath)
@@ -58,17 +70,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
}
// Connect to FastCGI gateway
var fcgi *FCGIClient
// check if unix socket or tcp
if strings.HasPrefix(rule.Address, "/") || strings.HasPrefix(rule.Address, "unix:") {
if strings.HasPrefix(rule.Address, "unix:") {
rule.Address = rule.Address[len("unix:"):]
}
fcgi, err = Dial("unix", rule.Address)
} else {
fcgi, err = Dial("tcp", rule.Address)
}
fcgi, err := getClient(&rule)
if err != nil {
return http.StatusBadGateway, err
}
@@ -102,13 +104,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
return http.StatusBadGateway, err
}
// Write the response header
for key, vals := range resp.Header {
for _, val := range vals {
w.Header().Add(key, val)
}
}
w.WriteHeader(resp.StatusCode)
writeHeader(w, resp)
// Write the response body
// TODO: If this has an error, the response will already be
@@ -126,6 +122,26 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
return h.Next.ServeHTTP(w, r)
}
func getClient(r *Rule) (*FCGIClient, error) {
// check if unix socket or TCP
if trim := strings.HasPrefix(r.Address, "unix"); strings.HasPrefix(r.Address, "/") || trim {
if trim {
r.Address = r.Address[len("unix:"):]
}
return Dial("unix", r.Address)
}
return Dial("tcp", r.Address)
}
func writeHeader(w http.ResponseWriter, r *http.Response) {
for key, vals := range r.Header {
for _, val := range vals {
w.Header().Add(key, val)
}
}
w.WriteHeader(r.StatusCode)
}
func (h Handler) exists(path string) bool {
if _, err := os.Stat(h.Root + path); err == nil {
return true
@@ -133,6 +149,10 @@ func (h Handler) exists(path string) bool {
return false
}
func (h Handler) canSplit(path string, rule Rule) bool {
return strings.Contains(path, rule.SplitPath)
}
// buildEnv returns a set of CGI environment variables for the request.
func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) {
var env map[string]string
@@ -149,21 +169,28 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
ip = r.RemoteAddr
}
// Split path in preparation for env variables
// Split path in preparation for env variables.
// Previous h.canSplit checks ensure this can never be -1.
splitPos := strings.Index(fpath, rule.SplitPath)
var docURI, scriptName, scriptFilename, pathInfo string
if splitPos == -1 {
// Request doesn't have the extension, so assume index file in root
docURI = "/" + rule.IndexFiles[0]
scriptName = "/" + rule.IndexFiles[0]
scriptFilename = filepath.Join(h.AbsRoot, rule.IndexFiles[0])
pathInfo = fpath
} else {
// Request has the extension; path was split successfully
docURI = fpath[:splitPos+len(rule.SplitPath)]
pathInfo = fpath[splitPos+len(rule.SplitPath):]
scriptName = fpath
scriptFilename = absPath
// Request has the extension; path was split successfully
docURI := fpath[:splitPos+len(rule.SplitPath)]
pathInfo := fpath[splitPos+len(rule.SplitPath):]
scriptName := fpath
scriptFilename := absPath
// Strip PATH_INFO from SCRIPT_NAME
scriptName = strings.TrimSuffix(scriptName, pathInfo)
// Get the request URI. The request URI might be as it came in over the wire,
// or it might have been rewritten internally by the rewrite middleware (see issue #256).
// If it was rewritten, there will be a header indicating the original URL,
// which is needed to get the correct RequestURI value for PHP apps.
const internalRewriteFieldName = "Caddy-Rewrite-Original-URI"
reqURI := r.URL.RequestURI()
if origURI := r.Header.Get(internalRewriteFieldName); origURI != "" {
reqURI = origURI
r.Header.Del(internalRewriteFieldName)
}
// Some variables are unused but cleared explicitly to prevent
@@ -192,7 +219,7 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
"DOCUMENT_ROOT": h.AbsRoot,
"DOCUMENT_URI": docURI,
"HTTP_HOST": r.Host, // added here, since not always part of headers
"REQUEST_URI": r.URL.RequestURI(),
"REQUEST_URI": reqURI,
"SCRIPT_FILENAME": scriptFilename,
"SCRIPT_NAME": scriptName,
}
@@ -249,4 +276,8 @@ type Rule struct {
EnvVars [][2]string
}
var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
var (
headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
ErrIndexMissingSplit = errors.New("configured index file(s) must include split value")
)
+42 -40
View File
@@ -30,45 +30,45 @@ import (
"sync"
)
const FCGI_LISTENSOCK_FILENO uint8 = 0
const FCGI_HEADER_LEN uint8 = 8
const VERSION_1 uint8 = 1
const FCGI_NULL_REQUEST_ID uint8 = 0
const FCGI_KEEP_CONN uint8 = 1
const FCGIListenSockFileno uint8 = 0
const FCGIHeaderLen uint8 = 8
const Version1 uint8 = 1
const FCGINullRequestID uint8 = 0
const FCGIKeepConn uint8 = 1
const doubleCRLF = "\r\n\r\n"
const (
FCGI_BEGIN_REQUEST uint8 = iota + 1
FCGI_ABORT_REQUEST
FCGI_END_REQUEST
FCGI_PARAMS
FCGI_STDIN
FCGI_STDOUT
FCGI_STDERR
FCGI_DATA
FCGI_GET_VALUES
FCGI_GET_VALUES_RESULT
FCGI_UNKNOWN_TYPE
FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
BeginRequest uint8 = iota + 1
AbortRequest
EndRequest
Params
Stdin
Stdout
Stderr
Data
GetValues
GetValuesResult
UnknownType
MaxType = UnknownType
)
const (
FCGI_RESPONDER uint8 = iota + 1
FCGI_AUTHORIZER
FCGI_FILTER
Responder uint8 = iota + 1
Authorizer
Filter
)
const (
FCGI_REQUEST_COMPLETE uint8 = iota
FCGI_CANT_MPX_CONN
FCGI_OVERLOADED
FCGI_UNKNOWN_ROLE
RequestComplete uint8 = iota
CantMultiplexConns
Overloaded
UnknownRole
)
const (
FCGI_MAX_CONNS string = "MAX_CONNS"
FCGI_MAX_REQS string = "MAX_REQS"
FCGI_MPXS_CONNS string = "MPXS_CONNS"
MaxConns string = "MAX_CONNS"
MaxRequests string = "MAX_REQS"
MultiplexConns string = "MPXS_CONNS"
)
const (
@@ -79,7 +79,7 @@ const (
type header struct {
Version uint8
Type uint8
Id uint16
ID uint16
ContentLength uint16
PaddingLength uint8
Reserved uint8
@@ -92,7 +92,7 @@ var pad [maxPad]byte
func (h *header) init(recType uint8, reqID uint16, contentLength int) {
h.Version = 1
h.Type = recType
h.Id = reqID
h.ID = reqID
h.ContentLength = uint16(contentLength)
h.PaddingLength = uint8(-contentLength & 7)
}
@@ -110,7 +110,7 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
err = errors.New("fcgi: invalid header version")
return
}
if rec.h.Type == FCGI_END_REQUEST {
if rec.h.Type == EndRequest {
err = io.EOF
return
}
@@ -126,13 +126,15 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
return
}
// FCGIClient implements a FastCGI client, which is a standard for
// interfacing external applications with Web servers.
type FCGIClient struct {
mutex sync.Mutex
rwc io.ReadWriteCloser
h header
buf bytes.Buffer
keepAlive bool
reqId uint16
reqID uint16
}
// Dial connects to the fcgi responder at the specified network address.
@@ -148,7 +150,7 @@ func Dial(network, address string) (fcgi *FCGIClient, err error) {
fcgi = &FCGIClient{
rwc: conn,
keepAlive: false,
reqId: 1,
reqID: 1,
}
return
@@ -163,7 +165,7 @@ func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.buf.Reset()
c.h.init(recType, c.reqId, len(content))
c.h.init(recType, c.reqID, len(content))
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
return err
}
@@ -179,14 +181,14 @@ func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error {
b := [8]byte{byte(role >> 8), byte(role), flags}
return c.writeRecord(FCGI_BEGIN_REQUEST, b[:])
return c.writeRecord(BeginRequest, b[:])
}
func (c *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error {
b := make([]byte, 8)
binary.BigEndian.PutUint32(b, uint32(appStatus))
b[4] = protocolStatus
return c.writeRecord(FCGI_END_REQUEST, b)
return c.writeRecord(EndRequest, b)
}
func (c *FCGIClient) writePairs(recType uint8, pairs map[string]string) error {
@@ -334,17 +336,17 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
// Do made the request and returns a io.Reader that translates the data read
// from fcgi responder out of fcgi packet before returning it.
func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
err = c.writeBeginRequest(uint16(FCGI_RESPONDER), 0)
err = c.writeBeginRequest(uint16(Responder), 0)
if err != nil {
return
}
err = c.writePairs(FCGI_PARAMS, p)
err = c.writePairs(Params, p)
if err != nil {
return
}
body := newWriter(c, FCGI_STDIN)
body := newWriter(c, Stdin)
if req != nil {
io.Copy(body, req)
}
@@ -381,9 +383,9 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
return
}
if len(statusParts) > 1 {
resp.Status = statusParts[1]
resp.Status = statusParts[1]
}
} else {
resp.StatusCode = http.StatusOK
}

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