Compare commits

..

584 Commits

Author SHA1 Message Date
Matthew Holt 7dc1dc1c78 Version 0.9.4 2016-12-21 13:47:30 -07:00
Leonard Hecker a3aa414ff3 Fixed HTTP/2 support for the proxy middleware (#1300)
* Fixed HTTP/2 support for the proxy middleware

http.Transport instances whose TLSClientConfig, Dial, or DialTLS field
is non-nil will be configured without HTTP/2 support by default.

This commit adds the proper calls to http2.ConfigureTransport()
everywhere a http.Transport is created and thus fixes HTTP/2 in the
proxy middleware whenever insecure_skip_verify or keepalive is provided.

* Added HTTP/2 support check to TestReverseProxyInsecureSkipVerify
2016-12-21 12:44:07 -07:00
Mateusz Gajewski 54c63002cc Feature #1282 - Support serving statically compressed .gz and .br files (#1289)
* Feature #1282 - Support pre-gzipped files

* Fix broken test cases

* Support brotli encoding as well

* Fix for #1276 - support integers and floats as metadata in markdown (#1278)

* Fix for #1276

* Use strconv.Format

* Use map[string]interface{} as variables

* One more file

* Always run all tests before commit

* Get rid of DocFlags

* Fix syntax in caddy.conf

* Update to Go 1.7.4

* Add send_timeout property to fastcgi directive.

* Convert rwc field on FCGIClient from type io.ReadWriteCloser to net.Conn.
* Return HTTP 504 to the client when a timeout occurs.
* In Handler.ServeHTTP(), close the connection before returning an HTTP
502/504.
* Refactor tests and add coverage.

* Return HTTP 504 when FastCGI connect times out.

* test: add unit test for #1283 (#1288)

* After review fixes

* Limit the number of restarts with systemd

* Prevent fd leak

* Prevent fd leak

* Refactor loops

* gofmt
2016-12-19 09:51:09 -07:00
Mateusz Gajewski c555e95366 Fix for issue #1287 - don't list hidden files in directory listing (#1290)
* Fix for issue #1287 - hide hidden files

* Reuse IsHidden

* Fix failing tests
2016-12-17 11:30:08 -07:00
Matthew Holt 466269fd10 Limit the number of restarts with systemd 2016-12-12 21:58:35 -07:00
Guiheux Steven 8653b70c32 test: add unit test for #1283 (#1288) 2016-12-07 18:59:02 -07:00
Matt Holt e363491a28 Merge pull request #1284 from mholt/fastcgi-send-timeout
Add send_timeout property to fastcgi directive
2016-12-07 18:58:05 -07:00
Matt Holt b0685a64be Merge pull request #1286 from jbub/goupgrade
Update to Go 1.7.4
2016-12-04 13:04:20 -07:00
jbub ff98aa3dd0 Update to Go 1.7.4 2016-12-04 18:22:22 +01:00
Matt Holt c212365a3a Merge pull request #1285 from lbischof/patch-1
Fix syntax in caddy.conf
2016-12-04 07:42:55 -07:00
Lorenz Bischof fea0d5ac3a Fix syntax in caddy.conf 2016-12-04 12:52:34 +01:00
ericdreeves 9f16ac84a0 Return HTTP 504 when FastCGI connect times out. 2016-12-03 16:31:29 -06:00
ericdreeves 5874fbeb7e Add send_timeout property to fastcgi directive.
* Convert rwc field on FCGIClient from type io.ReadWriteCloser to net.Conn.
* Return HTTP 504 to the client when a timeout occurs.
* In Handler.ServeHTTP(), close the connection before returning an HTTP
502/504.
* Refactor tests and add coverage.
2016-12-03 16:15:41 -06:00
Mateusz Gajewski 17e7e6076a Fix for #1276 - support integers and floats as metadata in markdown (#1278)
* Fix for #1276

* Use strconv.Format

* Use map[string]interface{} as variables

* One more file

* Always run all tests before commit

* Get rid of DocFlags
2016-12-02 23:35:33 -07:00
Mateusz Gajewski 9e98d6cd52 Fix for #1164 - allow only one header per line (#1280)
* Fix for #1164 - allow only one header per line

* Include original reporter case
2016-11-29 21:24:12 -07:00
ericdreeves 76f12f475b Merge pull request #1274 from mholt/fastcgi-timeout-defaults
Fix read timeout and add default timeout values.
2016-11-29 19:13:12 -06:00
Mateusz Gajewski 32fa0ce6a0 Merge branch 'master' into fastcgi-timeout-defaults 2016-11-29 19:06:43 +01:00
Matthew Holt 80eb45fcfb tls: Improve flaky test depending on CPU scheduling (I think) 2016-11-28 23:37:22 -07:00
Matthew Holt 36f8759a7b Ensure some tests remove temporary directories they created 2016-11-28 22:26:54 -07:00
Matt Holt e2917784d0 Merge pull request #1275 from mholt/fup-1273
Increase code coverage (caddy.go)
2016-11-27 05:35:54 -07:00
Eldin Hadzic fec840a861 Increase code coverage 2016-11-27 08:55:17 +00:00
elcore d0bf3e1647 Fix network listener address comparison, fixes #1258 (#1273)
* Fix issue #1258

* address comments

* add a test

* address comments 2
2016-11-26 23:44:15 -07:00
ericdreeves b8722d9af3 Fix read timeout and add default timeout values.
By setting the read deadline in streamReader.Read(), the deadline was
extended by the read timeout on each subsequent call. To avoid this, the
deadline is set in FCGIClient.Request(), before the first read occurs.

See #1094.
2016-11-25 10:30:51 -06:00
Geno 7dc23b18ae Init switch from HOME to Caddy (#1272)
* INIT-systemd use CADDYPATH instatt of HOME

* INIT-upstart use CADDYPATH instatt of HOME

* INIT-upstart use CADDYPATH instatt of HOME

* INIT-upstart use CADDYPATH instatt of HOME
2016-11-23 14:12:19 -07:00
Matthew Holt 9d398adf5d dist: Give more slack to numProcs test (was failing on Travis CI) 2016-11-20 21:50:46 -07:00
Matthew Holt 22a266a259 templates: Add arguments to .Include 2016-11-20 21:40:06 -07:00
ericdreeves 5a6b765673 Add connect_timeout and read_timeout to fastcgi. (#1257) 2016-11-19 09:05:29 -07:00
Matt Holt 8acf043297 Merge pull request #1268 from mholt/fix-files-test
Sort the resulting slice before the comparison.
2016-11-19 08:47:53 -07:00
ericdreeves 98c17bcdf2 Sort the resulting slice before the comparison. 2016-11-19 08:37:36 -06:00
Matt Holt 8c392a02c8 Merge pull request #1267 from DenBeke/launchd
launchd service file for Mac
2016-11-18 15:00:09 -07:00
MathiasB 30337ac33f launchd service file for Mac 2016-11-18 17:58:40 +01:00
Gyula Voros b783caaaed Filter empty headers (#1239)
* Filter empty headers

Some web servers (e.g. Jetty 9.3) don’t like HTTP headers with empty values. This commit filters header replacements with zero length.

* Extend tests to verify removal of empty headers

* Handle add-header case

* Change - Use short variable assignment
2016-11-16 21:41:53 -07:00
Mateusz Gajewski c972ea39c8 Fastcgi upstreams (#1264)
* Make fastcgi load balanceable too

* Address one more corner case - invalid configuration fastcgi /

* After review fixes

* Simplify conditions

* Error message

* New fastcgi syntax

* golint will be happy

* Change syntax
2016-11-16 21:29:43 -07:00
Tw 12fd349916 Merge pull request #1232 from mholt/fix-1229
proxy: record request Body for retry (fixes #1229)
2016-11-08 19:24:38 +08:00
Benny Ng dd4c4d7eb6 proxy: record request Body for retry (fixes #1229) 2016-11-04 19:15:36 +08:00
Ngo The Trung 0cdaaba4b8 Add maxrequestbody directive (#1163) 2016-11-04 08:25:49 +08:00
Mateusz Gajewski 63f749112b Use http.Header instead of custom type (#1214)
* Use http.Header

* This initialization was just stupid
2016-11-03 12:24:26 -06:00
Matt Holt e19a007b38 Merge pull request #1238 from tw4452852/1234
proxy: make sure value is optional when removing a header
2016-11-03 12:13:39 -06:00
Tw e85ba0d4db proxy: make value is optional when removing a header
fix issue #1234

Signed-off-by: Tw <tw19881113@gmail.com>
2016-11-03 22:50:51 +00:00
Matthew Holt b89cbe18e2 Move header up above errors in directive order (fix #1183) 2016-11-02 08:13:58 -06:00
Matthew Holt 14500d8204 header: Implement Flusher and CloseNotifier 2016-11-02 08:13:58 -06:00
Tw a2900e46f4 header: only register deletion operation (#1212)
fix issue #1183

Signed-off-by: Tw <tw19881113@gmail.com>
2016-11-01 22:08:02 -06:00
ericdreeves 08c17c7c31 Add Files action to template context. (closes #1198) (#1226)
* Add Files action to template context. (#1198)

* Fixes to testFiles().

- Set os.ModePerm on directories created during test.
- Use filepath.Join() to create directory path.
- Use Fatalf, not Fatal.

* Make additional fixes to test cases.

* Fix test cases to use correct path format.

Dir.Open() in net/http requires '/'-separated paths while
filepath.Join() may produce paths with different separator.

* Remove directory created by test at end of loop.

* Close the FileSystem before returning.

* Initialize names slice to the number of entries.

Also, do not call os.RemoveAll() unless the path to the directory
is a valid one.
2016-11-01 22:04:53 -06:00
Matthew Holt 49cb225cbd Ensure user always sees fatal errors at startup 2016-10-31 21:45:12 -06:00
elcore 53e117802f Add support for OCSP Must-Staple for Let's Encrypt certs (#1221)
* Fix Caddytls

* Let the user decide

* Address comments
2016-10-29 08:44:49 -06:00
Matthew Holt 23f89f30e9 Add sourcegraph link to readme 2016-10-27 19:27:39 -06:00
Matt Holt ff32ade1d8 Merge pull request #1216 from chris-bandce/patch-1
Punctuation
2016-10-26 11:23:00 -06:00
chris-bandce 3ce9075d3d Punctuation 2016-10-26 16:27:35 +01:00
Matt Holt f2e999aab2 Merge pull request #1213 from mholt/simplify_code
Remove dead code, do struct alignment, simplify code
2016-10-26 00:05:08 -06:00
Mateusz Gajewski 8cc3416bbc Remove dead code, do struct alignment, simplify code 2016-10-25 19:19:54 +02:00
Toby Allen c4d64a418b Log site info output at start. Fix for #1205 (#1210)
* Log Site start to -log. Fix for #1205

* Removed Comment
2016-10-25 09:31:21 -06:00
Matt Holt f3108bb7bf Merge pull request #1207 from tw4452852/1206
proxy: set request's body to nil explicitly
2016-10-25 09:30:10 -06:00
Mateusz Gajewski c2853ea64b Use proper Request (#1197)
* Use proper Request

* Fixes
2016-10-25 09:28:53 -06:00
Matt Holt 561069fdb6 Merge pull request #1211 from mholt/LogStartupCmd
Log Startup and Shutdown Commands
2016-10-24 14:26:54 -06:00
Toby Allen 66a07773ab Log Startup and Shutdown Commands 2016-10-24 20:48:36 +01:00
Tw a1dd6f0b34 proxy: set request's body to nil explicitly
fix issue #1206

Signed-off-by: Tw <tw19881113@gmail.com>
2016-10-24 11:10:12 +08:00
Henrique Dias 2b9bbc5236 Moving File Manager higher in the directive list (#1199)
* Moving File Manager higher in the directive list

See hacdias/caddy-filemanager#35.

* gofmt
2016-10-20 14:52:43 -06:00
Matt Holt 2f5e840ea9 Merge pull request #1193 from mholt/Go-1.7.3
Update to Go 1.7.3
2016-10-19 18:00:00 -06:00
Eldin Hadzic ab3cc8f961 Update to Go 1.7.3 2016-10-19 21:26:04 +00:00
Matt Holt efb1c54e13 Merge pull request #1189 from rndmh3ro/more_upstart_scripts
add separate upstart scripts for centos 6 and ubuntu 12.04
2016-10-19 08:13:44 -06:00
Matt Holt 3c1957a612 Merge pull request #1190 from tw4452852/1188
errors: don't join the absolute file path
2016-10-17 20:48:45 -06:00
Tw 2bd6fd0aea errors: don't join the absolute file path
fix issue #1188

Signed-off-by: Tw <tw19881113@gmail.com>
2016-10-18 09:55:50 +08:00
Sebastian Gumprich f1342e37ed add separate upstart scripts for centos 6 and ubuntu 12.04 2016-10-17 22:04:26 +02:00
Toby Allen 94af37087b Fix for fastcgi deletion of Caddy-Rewrite-Original-URI header #1153 (#1184)
* Very simple fix for #1153

* Prevent  Caddy-Rewrite-Original-URI being added as  an HTTP ENV variable passed to FastCGI

part of fix for #1153

* Changes to Markdown to fix travis CI build.

#1955.2

* Revert "Changes to Markdown to fix travis CI build."

This reverts commit 4a01888839.

* fail fast and fmt changes

* Create test for existance of Caddy-Rewrite-Original-URI header value #1153

* updated test comment

* const moved outside function so available to tests
2016-10-16 12:11:52 -06:00
Matthew Holt 5fcfdab6c7 Reorder basicauth directive; it should also protect redirects 2016-10-15 09:41:03 -06:00
Gregor Noczinski 016384abef * Added directive "filter" to whitelist for support of github.com/echocat/caddy-filter (#1167) 2016-10-15 09:31:22 -06:00
Matt Holt 036633b64a Merge pull request #1174 from tw4452852/1173
header: implement http.Hijacker for responseWriterWrapper
2016-10-13 23:09:21 -06:00
Matt Holt 550b1170bd Merge branch 'master' into 1173 2016-10-13 22:56:19 -06:00
Matt Holt d44016b937 Merge pull request #1178 from tw4452852/1177
proxy: preserve path trailing slash if it was there
2016-10-11 18:26:51 +02:00
Tw 4baca884c5 proxy: preserve path trailing slash if it was there
fix issue #1177

Signed-off-by: Tw <tw19881113@gmail.com>
2016-10-11 17:06:59 +08:00
Tw d0455c7b9c add more descriptive errors
Signed-off-by: Tw <tw19881113@gmail.com>
2016-10-11 10:34:51 +08:00
Tw e5d33e73f3 header: implement http.Hijacker for responseWriterWrapper
fix issue #1173

Signed-off-by: Tw <tw19881113@gmail.com>
2016-10-11 08:53:47 +08:00
Matt Holt 9ced4b17e5 Merge pull request #1175 from tw4452852/fix_archiver
dist: adapt to archiver's refactor
2016-10-10 20:41:00 +02:00
Tw b48bda4a6d dist: adapt to archiver's refactor
Signed-off-by: Tw <tw19881113@gmail.com>
2016-10-10 16:39:27 +08:00
Matt Holt 5d9989405a Merge pull request #1165 from aishraj/master
Have milliseconds as the latency log time unit
2016-10-08 22:48:48 +02:00
Aish Raj Dahal 733f622f7a Add new placeholder for latency in milliseconds 2016-10-05 21:06:15 -07:00
Matt Holt 20a54d0e07 Merge pull request #1169 from bengadbois/patch-1
Adding Go Report Card badge
2016-10-05 15:38:27 -06:00
Ben Gadbois b5a07d43fa Adding Go Report Card badge 2016-10-05 20:48:43 +02:00
elcore 8561f42786 CurvePreferences (Test) (#1161)
We need consistency in code style!!!
2016-10-04 01:15:23 +02:00
elcore e1ea58b7c4 Customize curve preferences, closes #1117 (#1159)
* Feature Request: #1117

* The order of the curves matter
2016-10-03 10:52:45 -06:00
Matt Holt e9ce45ce61 Merge pull request #1158 from tw4452852/1154
proxy: handle 'without' option in encoded form of URL path
2016-10-02 13:39:55 -06:00
Tw cc638c7faa proxy: handle 'without' option in encoded form of URL path
fix issue #1154

Signed-off-by: Tw <tw19881113@gmail.com>
2016-10-02 19:32:14 +00:00
Matthew Holt b766dab9fa tls: Reorder some logic to avoid subtle, undocumented behavior
By calling SetTLSAddress, the acme package reset the challenge provider
to the default one instead of keeping the custom one we specified before
with SetChallengeProvider. Yikes. This means that Caddy would try to
open a listener on port 443 even though we should have been handling it
with our provider, causing the challenge to fail, since usually port 443
is in use.

So this change just reorders the calls so that our provider takes
precedence.

cf. https://github.com/xenolf/lego/pull/292
2016-09-28 18:29:46 -06:00
Matthew Holt c885edda24 Version 0.9.3 2016-09-28 12:43:28 -06:00
Peer Beckmann bb7787d2ee Remove the eager check in the browse middleware (#1144)
* Remove the eager check in the browse middleware, whether the root directory exists.
Caddy will start and throw a 404-error until the directory will be created.

* Add the complimentary test.
 - Tests the startup of the browse middleware if the site root is inexistent and browse is pointing to the site root.

* Some minor stylistic tweaks.
2016-09-28 12:23:44 -06:00
Matt Holt 8620581f95 Merge pull request #1145 from tw4452852/header_placeholder
replacer: evaluate header placeholder when replacing
2016-09-28 10:42:41 -06:00
Tw 99a6b2db67 replacer: evaluate header placeholder when replacing
fix issue #1137

Signed-off-by: Tw <tw19881113@gmail.com>
2016-09-28 19:32:16 +00:00
Matt Holt 8944332e13 Merge pull request #1143 from mholt/1136-fix
Fix #1136 - IP hash policy no longer changes host pool
2016-09-28 08:07:58 -06:00
Kris Hamoud be1c57acfe 1136 fix
logic change
2016-09-28 04:09:46 -07:00
Matt Holt b06b3981cf Merge pull request #1140 from tw4452852/defer_header
header: defer header operations
2016-09-27 19:02:28 -06:00
Sebastian Schmittner 8cb4e90852 Add fix and tests for FastCGI persistent connections (#1134)
* keep fastcgi connection open

* poor mans serialisation to make up for the lack of demuxing

* pointing includes to echse's repo

* Revert "pointing includes to echse's repo"

This reverts commit 281daad8d4.

* switch for persistent fcgi connections on/off added

* fixing ineffectual assignments

* camel case instead of _

* only activate persistent sockets on windows (and some naming conventions/cleanup)

* gitfm import sorting

* Revert "fixing ineffectual assignments"

This reverts commit 79760344e7.

# Conflicts:
#	caddyhttp/staticfiles/fileserver.go

* added another mutex and deleting map entries. thx to mholts QA comments!

* thinking about it, this RW lock was not a good idea here

* thread safety

* I keep learning about mutexs in go

* some cosmetics

* adding persistant fastcgi connections switch to directive

* Support for configurable connection pool.

* ensure positive integer pool size config

* abisofts pool fix + nicer logging for the fastcgi_test

* abisoft wants to have dialer comparison in _test instead of next to struct

* Do not put dead connections back into pool

* Fix fastcgi header error

* Do not put dead connections back into pool

* some code style improvements from the discussion in https://github.com/mholt/caddy/pull/1134

* abisofts naming convention
2016-09-27 18:12:22 -06:00
Matt Holt 871d11af00 Merge pull request #1135 from mholt/proxyerrs
proxy: Improve failover logic and retries
2016-09-27 17:53:44 -06:00
Matthew Holt 6397a85e50 proxy: Only wait 250ms between backend tries 2016-09-27 17:49:00 -06:00
Tw d0ddfc849d header: defer header operations
fix issue #1131

Signed-off-by: Tw <tw19881113@gmail.com>
2016-09-27 15:35:13 +08:00
Matthew Holt 617012c3fb Use time.Since() for readability 2016-09-24 21:27:57 -06:00
Matt Holt 4adbcd2565 Merge pull request #1125 from hlidotbe/master
Add expires directive
2016-09-24 21:11:30 -06:00
Matt Holt d01bcd591c Merge pull request #1112 from tw4452852/proxy_header
proxy: don't append predefined headers
2016-09-24 21:02:19 -06:00
Tw c9b022b5e0 proxy: don't append some predefined headers
fix issue #1086

Signed-off-by: Tw <tw19881113@gmail.com>
2016-09-25 09:24:27 +00:00
Matthew Holt a661007a55 proxy: Fix retry logic for when no hosts are available 2016-09-24 16:30:40 -06:00
Matthew Holt 0c0142c8cc Delete tryDuration, now unused 2016-09-24 16:05:33 -06:00
Matthew Holt 37f05e450f proxy: Add try_duration and try_interval; by default don't retry 2016-09-24 16:03:22 -06:00
Matthew Holt 9b9a77a160 proxy: Improved error reporting
We now report the actual error message rather than a generic one
2016-09-24 14:22:13 -06:00
Tw 4670d13c8c proxy: fix checking error in TestDownstreamHeadersUpdate and TestUpstreamHeadersUpdate
Signed-off-by: Tw <tw19881113@gmail.com>
2016-09-24 19:28:42 +00:00
Matthew Holt 9077cce126 Add tests for case insensitivity of keys and saving contexts 2016-09-24 13:24:33 -06:00
Matthew Holt 76d9d695be Remove use of proxy_header in test 2016-09-24 12:27:16 -06:00
Matthew Holt a4d70262aa Use strings.Contains instead of IndexOf for readability 2016-09-24 12:09:28 -06:00
Hugues Lismonde 79f2deee42 Add expires directive 2016-09-24 08:10:32 +02:00
Abiola Ibrahim bac54de9eb Fastcgi persistent fix (#1129)
* Support for configurable connection pool.

* ensure positive integer pool size config
2016-09-23 23:29:23 -06:00
Josh Aas 3f83eccfbd improvements for Linux systemd integration (#1127)
* Remove unnecessary config options from systemd service so it will work with earlier versions of systemd. Simplify the systemd service instructions and make them more complete.

* Minor systemd README improvements.

* Add back some of the optional systemd 229 stuff but commented out for compat.

* A bunch of updates to the README for linux systemd.
2016-09-23 16:50:39 -06:00
Matthew Holt d60a26ae30 import: No need to error for no matches if using glob pattern 2016-09-22 11:07:37 -06:00
Matthew Holt bbf954cbf2 Fix case sensitivity in site addresses 2016-09-20 22:44:05 -06:00
Matthew Holt 73916ccc30 Version 0.9.2 2016-09-20 16:33:50 -06:00
Matthew Holt fcad474064 Move prometheus directive higher in list (closes #1119) 2016-09-20 14:29:32 -06:00
Lars Wiegman 4449d3dcd9 Add the multipass plugin to the directives (#1120)
* Add the multipass plugin to the directives

* Fix gofmt
2016-09-20 09:06:28 -06:00
Matthew Holt c4a177bd3b Fix lint warnings and update CI tests to Go 1.7.1 2016-09-19 17:25:21 -06:00
Matthew Holt 8ecd543519 Refactor and improve TLS storage code (related to locking) 2016-09-19 17:24:34 -06:00
Matt Holt 7af499c28b Merge pull request #1115 from tw4452852/import
parser: fix broken absolute path imports (recursive case)
2016-09-19 16:28:55 -06:00
Tw 64fd281f5b parser: fix broken absolute path imports
fix issue #1026

Signed-off-by: Tw <tw19881113@gmail.com>
2016-09-18 10:16:24 +08:00
Matthew Holt bedad34b25 Clean up some significant portions of the TLS management code 2016-09-14 22:30:49 -06:00
Matt Holt 0e7635c54b Merge pull request #1102 from coopernurse/awslambda-ext-plugin
awslambda external plugin
2016-09-13 16:47:10 -06:00
James Cooper 40a3a6b24f Add awslambda to plugin.go 2016-09-10 07:52:04 -07:00
Sebastian Schmittner 09a1f02971 persistent fastcgi connections (#1087)
* keep fastcgi connection open

* poor mans serialisation to make up for the lack of demuxing

* pointing includes to echse's repo

* Revert "pointing includes to echse's repo"

This reverts commit 281daad8d4.

* switch for persistent fcgi connections on/off added

* fixing ineffectual assignments

* camel case instead of _

* only activate persistent sockets on windows (and some naming conventions/cleanup)

* gitfm import sorting

* Revert "fixing ineffectual assignments"

This reverts commit 79760344e7.

# Conflicts:
#	caddyhttp/staticfiles/fileserver.go

* added another mutex and deleting map entries. thx to mholts QA comments!

* thinking about it, this RW lock was not a good idea here

* thread safety

* I keep learning about mutexs in go

* some cosmetics
2016-09-10 06:47:47 -06:00
David Prandzioch 8e54d5cecb Updated FreeBSD init script (#1098)
* Updated FreeBSD init script to allow the server to stop properly

* Fixed FreeBSD init script permissions

* Updated FreeBSD init script to allow the server to stop properly
2016-09-08 21:02:28 -06:00
Matthew Holt 7ef405f9b2 Satisfy gofmt 2016-09-08 20:32:21 -06:00
Matthew Holt 11bf28f783 Weird, git didn't commit this closing curly brace 2016-09-08 18:54:36 -06:00
Matthew Holt 98bba33861 Lower-case server name for good measure
This already happens in the getCertificate function, but doing it here
guarantees case insensitivity across the board for this handshake.
2016-09-08 18:50:04 -06:00
Matthew Holt abdf13ea30 Improve TLS storage provider errors
We renamed caddytls.ErrStorageNotFound to caddytls.ErrNotExist to more
closely mirror the os package. We changed it to an interface wrapper
so that the custom error message can be preserved. Returning only "data
not found" was useless in debugging because we couldn't know the
concrete value of the error (like what it was trying to load).

Users can do a type assertion to determine if the error value is a "not
found" error instead of doing an equality check.
2016-09-08 18:50:04 -06:00
Matthew Holt a251831feb Fix bug renewing certs affecting Caddyfiles using wildcard addresses
A Caddyfile using *.example.com as its site address would be subject to
this bug at renewal time, as it would use the literal "*.example.com"
value instead of the name being passed in to obtain a certificate.
This change fixes the LoadSite call so that it looks in the proper
directory for the certificate resources.
2016-09-08 18:50:04 -06:00
Matt Holt 1ea96def31 Merge pull request #1091 from mikepulaski/master
Caddyfiles read from STDIN now have server types associated with them.
2016-09-05 13:14:35 -06:00
Mike Pulaski 635714fe38 Caddyfiles read from STDIN now have server types associated with them. Fixes #1090. 2016-09-05 18:34:32 +02:00
Matthew Holt 5f135a27d5 Eliminate ineffectual assignments
Most of these were fixed by handling errors that were previously
unhandled (oops).
2016-09-05 10:30:46 -06:00
Matthew Holt 45a3d0b526 Fix misspellings 2016-09-05 10:20:34 -06:00
Sebastian a122304196 Support for lego's timeout option (#1041)
* Support for lego's timeout option

* Changed IntVar to DurationVar for catimeout

* Modified catimeout according to mholt's comments
2016-08-30 13:38:44 -06:00
Matthew Holt 14a6e4b4ed More minor text fixes 2016-08-30 13:37:35 -06:00
Matt Holt 72e4ba8b5b Merge pull request #1079 from ijt/master
Use naoina/toml for MIT license. Make proxy_test work in any directory.
2016-08-30 08:10:57 -06:00
Issac Trotts 1991083322 Fix tests to not make long unix domain socketpaths
Some tests were running into this issue:
https://github.com/golang/go/issues/6895

Putting the sockets into temp dirs fixes the problem.
2016-08-29 18:09:46 -07:00
Issac Trotts 7ba804353c Use naoina/toml instead of BurntSushi/toml 2016-08-29 17:55:44 -07:00
Abiola Ibrahim ac933f1685 Merge pull request #1042 from mholt/status-middleware
Add 'status' middleware instead of 'status' directive for 'rewrite' middleware
2016-08-29 16:41:27 +01:00
Volodymyr Galkin 20ee457cae Add 'status' middleware instead of 'status' directive for 'rewrite' middleware 2016-08-29 17:17:23 +03:00
Matthew Holt 34a99598f7 Ignore conflicting settings if TLS disabled (fixes #1075) 2016-08-26 16:18:08 -06:00
Matthew Holt 191ec27c26 Clarify godoc for HTTP handler signature 2016-08-25 17:13:49 -06:00
Matthew Holt b1ae8a71f1 More tests for TLS configuration 2016-08-25 17:13:27 -06:00
Matt Holt d4f4fcdb4c Merge pull request #1070 from FiloSottile/master
Actually set tls.Config.PreferServerCipherSuites
2016-08-25 16:33:12 -06:00
Filippo Valsorda ef58536711 Actually set tls.Config.PreferServerCipherSuites
It was set by default on the caddy-internal config object, and even
checked for conflicts, but it was never actually reflected on the
tls.Config.

This will have user-visible changes: a client that prefers, say, AES-CBC
but also supports AES-GCM would have used AES-CBC befor this, and will
use AES-GCM after.

This is desirable and important behavior, because if for example the
server wanted to support 3DES, but *only if it was strictly necessary*,
it would have had no way of doing so with PreferServerCipherSuites
false, as the client preference would have won.
2016-08-25 18:28:51 +01:00
Matthew Holt 17709a7d3f Defer loading directives until needed (fix for previous commit)
This change is still experimental.
2016-08-25 00:15:18 -06:00
Matthew Holt 5a691fbaf5 httpserver: Added function to register directive at runtime (dev only)
This function should not be used outside of development. It destroys the
absolute ordering and guarantees of correctness. Multiple uses of it
may work fine, but maybe not if they overlap, causing non-deterministic
builds which is bad. However, this can be convenient when developing
a plugin by calling it from an init() function, since you don't have
to modify the Caddy source code just to try your plugin.
2016-08-24 23:12:41 -06:00
Matt Holt fd3008459e Merge pull request #1068 from tw4452852/multiple_log_test
log: add multiple log entry test
2016-08-24 22:40:32 -06:00
Tw e7af23e1e6 log: add multiple log entry test
Signed-off-by: Tw <tw19881113@gmail.com>
2016-08-25 11:21:08 +08:00
Matt Holt 536daf36be Merge pull request #1060 from tw4452852/multiple_log
log: support multiple log entries under one path scope
2016-08-23 22:55:17 -06:00
Tw 5e0f4083c4 log: support multiple log entries under one path scope
fix issue #1044

Signed-off-by: Tw <tw19881113@gmail.com>
2016-08-24 12:48:51 +08:00
Matthew Holt c3e0733406 Only move storage if actually starting a server (closes #1067) 2016-08-23 22:32:52 -06:00
Matt Holt 70cbfdc585 Merge pull request #1064 from stp-ip/quic-protocol-headers
Keep quic protocol headers only between one hop
2016-08-23 16:46:43 -06:00
Michael Grosser 3dc98c8ce3 Keep quic protocol headers only between one hop
Removing quic protocol headers from being persisted during proxy requests.
Not removing them could lead to the client attempting to connect to the wrong port.
This makes the quic headers consistent with other protocol headers.
2016-08-23 22:05:56 +00:00
Matthew Holt 151d0baa94 Minor text fixes ;) 2016-08-23 15:47:23 -06:00
Matt Holt 9d947713ff Merge pull request #1059 from PalmStoneGames/master
Add plugin capabilities for tls storage.
2016-08-23 15:31:20 -06:00
Luna Duclos 1dfe1e5ada Add plugin capabilities for tls storage.
To use a plugged in storage, specify "storage storage_name" in the tls block of the Caddyfile, by default, file storage will be used
2016-08-23 23:00:20 +02:00
Matthew Holt 628920e20e Improve logic related to error handling on SiteExists call
No need to check if SiteExists if the config is not managed or the name
does not even qualify.
2016-08-23 14:51:07 -06:00
Matt Holt 15d25f1ca4 Merge pull request #1062 from nemothekid/fix/ws-connection-close
Proxy: Set MaxIdleConnsPerHost to -1 to prevent Idle Conns
2016-08-23 10:39:16 -06:00
Nimi Wariboko Jr 2ef8905966 Proxy: Instead of setting DisableKeepAlives, set MaxIdleConnsPerHost to -1 to prevent net/http from pooling the connections. DisableKeepAlives causes net/http to send a Connection: Closed header which is bad. Fixes #1056 2016-08-22 18:00:37 -07:00
Matt Holt fdad616df7 Merge pull request #1049 from tw4452852/log_body
capture request body normally
2016-08-22 18:49:17 -06:00
Tw 590862a962 replacer: capture request body normally
fix issue #1015

Signed-off-by: Tw <tw19881113@gmail.com>
2016-08-23 08:20:49 +08:00
Tw 40c09d6789 replacer: code refactor
Signed-off-by: Tw <tw19881113@gmail.com>
2016-08-23 08:20:49 +08:00
Tw bba1059ef9 log: add log request body test
Signed-off-by: Tw <tw19881113@gmail.com>
2016-08-23 08:20:49 +08:00
Matt Holt 1d3212a598 Merge pull request #1046 from PalmStoneGames/master
Add error parameter to storage.SiteExists()
2016-08-20 16:52:18 -06:00
Matthew Holt c75ee0000e Fix edge case in stapling; do not allow certs without any names 2016-08-19 13:42:48 -06:00
Matt Holt 8cdc65edd2 Merge pull request #1048 from miekg/missingbits
Implement missing bits for an external servertype
2016-08-19 11:48:07 -06:00
Miek Gieben a609fa5f56 Implement missing bits for an external servertype
Make ServerListeners public and add two helper methods to get access
to the address they listen on. This is useful for tests (among other
things)

Also make DefaultConfigFile a var so it can be overridden by server
types.
2016-08-19 00:19:45 +00:00
Luna Duclos 78341a3a9a Add error parameter to storage.SiteExists() 2016-08-18 18:38:33 +02:00
Matthew Holt fdc62d015f log: Create log file directory before creating log file 2016-08-18 07:35:55 -06:00
Matthew Holt e8e55955f4 Report error when loading the lexer 2016-08-17 17:17:26 -06:00
Matthew Holt 8b8afd72d7 Support env vars in import; make sure it's a file 2016-08-17 17:03:21 -06:00
Matthew Holt c5524b0bab Report errors when loading Caddyfile 2016-08-17 17:02:01 -06:00
Matthew Holt c5aa5843d9 Version 0.9.1 2016-08-17 14:09:04 -06:00
Matthew Holt 745ae6ff2f Travis CI improvements 2016-08-15 23:05:46 -06:00
Matthew Holt 432a2d23a7 Use Go 1.7 for CI 2016-08-15 22:57:18 -06:00
Matt Holt 83345062d7 Merge pull request #1037 from devangels/template_env_1
Fix for invalid environment variable names used on windows that start…
2016-08-15 15:41:26 -06:00
Simon Lightfoot f372f5fce7 Fix for invalid environment variable names used on windows that start with an equals symbol. Even though this contradicts the Microsoft docs. 2016-08-15 20:42:00 +01:00
Matthew Holt 454b1e3939 Honor bind directive for ACME challenges
Fixes https://forum.caddyserver.com/t/basic-caddy-installation-failing-on-automatic-https/472?u=matt
2016-08-15 12:08:51 -06:00
Simon Lightfoot 45ac11088e Added support for environment variables to 'templates' module. (#1035)
* * Added support for environment variables to 'templates' module.

* Fixed flaw in test caused by environment variable ordering during testing on CI.

* Updated some local variables to camel-case.

* Reverted changes to replacer as environment variables are processed elsewhere.

* Removed PrintEnv functionality in favour of documenting using template range.
2016-08-15 11:15:58 -06:00
Matt Holt eb3bbc409f Merge pull request #1036 from mholt/fix-errors-setup-test
Fix error which lead to skipping tests in 'errors.TestErrorsParse'
2016-08-15 10:09:46 -06:00
Volodymyr Galkin b830667a25 Fix error which lead to skipping tests in 'errors.TestErrorsParse' 2016-08-15 16:44:34 +03:00
Matt Holt ba5aeab19d Merge pull request #1032 from mholt/errors-config-check-duplicate-status-codes
Check for duplicate status code entries in 'errors' directive
2016-08-12 09:28:51 -06:00
Volodymyr Galkin 441a8f5eff Check for duplicate status code entries in 'errors' directive 2016-08-12 16:47:00 +03:00
Matt Holt 4f6500c95b Merge pull request #1028 from evvvvr/wildcard-error-page-752
Add support for default (wildcard) error page
2016-08-11 23:33:44 -06:00
Carter 7dd385f6b4 Merge pull request #1023 from mholt/log-request-body
Now logging the request body
2016-08-11 22:32:32 -04:00
Matt Holt ac0dd303be Merge branch 'master' into log-request-body 2016-08-11 17:36:09 -06:00
Carter 676202a31e Fixed styling and byte count 2016-08-11 19:08:49 -04:00
Matthew Holt c8a99d2f81 Don't use X-Forwarded-For for {remote} placeholder (closes #1025) 2016-08-11 16:54:17 -06:00
Carter 8e8e2f596d Merge branch 'master' into log-request-body 2016-08-11 18:08:19 -04:00
Volodymyr Galkin f7003bee3f Add support for default (wildcard) error page 2016-08-11 15:51:15 +03:00
Carter 532ab661c7 Fully read and close the request body 2016-08-11 07:03:14 -04:00
Matthew Holt 68be4a9161 Don't prompt for email when user is not there to provide one
Also don't bother showing stdout output in same situation
2016-08-10 23:46:04 -06:00
Matthew Holt 46bc0d5c4e Whoops, finishing up the last commit properly
Need to add the name to namesObtaining each time we use the ACME client.
2016-08-10 23:44:43 -06:00
Matthew Holt 8e75ae2495 Only consume HTTP challenge for names we are solving for (closes #549)
If another ACME client is trying to solve a challenge for a name not
being served by Caddy on the same machine where Caddy is running, the
HTTP challenge will be consumed by Caddy rather than allowing the owner
to use the Caddyfile to proxy the challenge.

With this change, we only consume requests for HTTP challenges for
hostnames that we recognize. Before doing the challenge, we add the
name to a set, and when seeing if we should proxy the challenge, we
first check the path of course to see if it is an HTTP challenge;
if it is, we then check that set to see if the hostname is in the
set. Only if it is, do we consume it.

Otherwise, the request is treated like any other, allowing the owner
to configure a proxy for such requests to another ACME client.
2016-08-10 22:13:06 -06:00
Carter d56ac28bec Using a LimitReader and fixed test and log format. 2016-08-10 22:43:26 -04:00
Carter 3fd8218f67 refactor and added test 2016-08-10 11:04:37 -04:00
Carter d06c15cae6 Set the request body to a new ReadCloser 2016-08-10 10:36:16 -04:00
Carter 59b1e8b0bc Now logging the request body
Logging the request body if the Content-Type is application/json or
application/xml
2016-08-10 10:04:57 -04:00
Daniel van Dorp dbd76f7a57 dist/init/linux-sysvinit: process @weingart's feedback (#1008)
* dist/init/linux-sysvinit: use kill -0 to test process status

* dist/init/linux-sysvinit: use service (as root) instead of /etc/init.d/
2016-08-09 22:29:13 -06:00
Matthew Holt e081d8b5c2 Maintainence routine deletes old (expired) OCSP staple files 2016-08-09 16:46:51 -06:00
Matthew Holt 8eefeb6788 Begin improved OCSP stapling by persisting staple to disk 2016-08-09 16:12:22 -06:00
Abiola Ibrahim 5fb3c504c9 Merge pull request #1017 from shawnps/patch-2
fix typo
2016-08-09 09:18:41 +01:00
Shawn Smith 0f04f2fd44 fix typo 2016-08-09 14:57:17 +09:00
Matthew Holt ce8b1dfe94 Warn upon use of proxy_header 2016-08-08 13:48:13 -06:00
elcore 4b3c532573 Use P384 for TestUser (privateKey) (#1009) 2016-08-08 11:13:10 -06:00
Carter 4d76ccb1c4 Rounding the latency in certain scenarios (#1005)
* Rounding the latency in certain scenarios

* run gofmt
2016-08-08 10:14:53 -06:00
Simon Lightfoot de7bf4f241 Enable downloading of protected content. See issue #979 (#980)
* Fix for stripping of 'Content-Disposition' and other headers from 'X-Accel-Redirect' redirect scripts.

* Added test case for header manipulation of redirect response.
2016-08-07 23:16:33 -06:00
Stavros Korokithakis 681c95a749 Add default "Restricted" realm to HTTP Basic auth (#1007)
* Add default "Restricted" realm to HTTP Basic auth

* Add tests for the Basic auth realm
2016-08-07 07:50:36 -06:00
elcore e5a8927635 Allow just one TLS Protocol (Caddyfile) (#1004)
* Allow just one TLS Protocol

* Fix typo
2016-08-06 15:00:54 -06:00
Matthew Holt 2019eec5a5 Fix lint warnings; group methods for same type together 2016-08-06 14:46:52 -06:00
Matthew Holt 33d1033928 Add link to clean code guidelines for reference 2016-08-06 14:43:31 -06:00
Matthew Holt 0d8b95334f Use Let's Encrypt's permalink to subscriber agreement 2016-08-06 14:42:00 -06:00
Matthew Holt ee615371a8 Export staticfiles.Redirect for convenience in preserving query string 2016-08-06 14:40:58 -06:00
Nimi Wariboko Jr 4c6082df64 Merge pull request #987 from nemothekid/proxy/single-webconn
Proxy: Single WebSocket connection
2016-08-05 16:59:38 -07:00
Nimi Wariboko Jr 8898066455 Merge branch 'master' into proxy/single-webconn 2016-08-05 16:57:54 -07:00
Nimi Wariboko Jr fffc1bed73 Merge pull request #984 from nemothekid/proxy/keepalive-directive
Proxy: Add keepalive directive to proxy to set MaxIdleConnsPerHost on transport
2016-08-05 16:57:44 -07:00
Nimi Wariboko Jr 824ec6cb95 Merge branch 'master' into proxy/keepalive-directive 2016-08-05 16:20:37 -07:00
Nimi Wariboko Jr 5b5e365295 Instead of treating 0 is a default value, use http.DefaultMaxIdleConnsPerHost 2016-08-05 15:41:32 -07:00
Matt Holt c6c221b8db Merge pull request #996 from tw4452852/host_header
proxy: add Host header checking
2016-08-05 16:20:06 -06:00
Daniel van Dorp 985049e0c2 Merge pull request #1003 from mholt/sysvinit-fix-setcap
dist/init/linux-sysvinit: execute setcap directly
2016-08-05 16:49:24 +02:00
Daniel van Dorp 3a4f8e8d0c dist/init/linux-sysvinit: execute setcap directly
`$(which setcap)` might evaluate to nothing,
and this way the error thrown will be more clear.
If setcap is not available on Debian/Ubuntu,
you can install the package `libcap2-bin`
2016-08-05 16:33:47 +02:00
Daniel van Dorp f3a3bf6204 dist/init/linux-sysvinit: improve legacy compatibility (#1002)
* dist/init/linux-sysvinit: pass --oknodo for --start as well

* dist/init/linux-sysvinit: manually rm PIDFILE

Since start-stop-daemon --remove-pidfile is new and not present
everywhere.
2016-08-05 08:15:32 -06:00
Daniel van Dorp 81a3101efe Merge pull request #1001 from mholt/sysvinit-typo
dist/init/linux-sysvinit: fix minor typo in DAEMONOPTS
2016-08-05 14:13:33 +02:00
Daniel van Dorp 22a4b6cde2 dist/init/linux-sysvinit: fix minor typo in DAEMONOPTS 2016-08-05 14:04:30 +02:00
Tw 94c63e42d6 proxy: add Host header checking
fix issue #993

Signed-off-by: Tw <tw19881113@gmail.com>
2016-08-04 13:07:20 +08:00
s7v7nislands c110b27ef5 improve rlimit usage (#982)
* improve rlimit usage

* fix windows build

* fix code style
2016-08-02 21:01:36 -06:00
Nimi Wariboko Jr 6e9439d22e Proxy: Fix data race in test. 2016-08-02 12:39:15 -07:00
Nimi Wariboko Jr f4cdf53761 Proxy: Fix transport defn; cleanup connection. 2016-08-02 12:31:17 -07:00
Matt Holt 89f5b646c3 Merge pull request #978 from krishamoud/master
added ip_hash load balancing
2016-08-02 11:25:52 -06:00
Matthew Holt a24e361761 Enable cgo for CI tests so race detector can run 2016-08-02 10:59:16 -06:00
Matthew Holt 5ac04b91bb Add -race to CI tests; use Go 1.6.3 2016-08-02 10:55:38 -06:00
elcore 1b1aecb1e6 Merge pull request #989 from tw4452852/tls_race
tls: fix TestStandaloneTLSTicketKeyRotation data race
2016-08-02 14:03:14 +02:00
Tw 3d43c5b697 tls: fix TestStandaloneTLSTicketKeyRotation data race
==================
WARNING: DATA RACE
Write at 0x00c42049d300 by goroutine 26:
  github.com/mholt/caddy/caddytls.standaloneTLSTicketKeyRotation()
      /home/tw/golib/src/github.com/mholt/caddy/caddytls/crypto.go:230 +0x698

Previous read at 0x00c42049d300 by goroutine 25:
  github.com/mholt/caddy/caddytls.TestStandaloneTLSTicketKeyRotation()
      /home/tw/golib/src/github.com/mholt/caddy/caddytls/crypto_test.go:113 +0x413
  testing.tRunner()
      /home/tw/goroot/src/testing/testing.go:610 +0xc9

Goroutine 26 (running) created at:
  github.com/mholt/caddy/caddytls.TestStandaloneTLSTicketKeyRotation()
      /home/tw/golib/src/github.com/mholt/caddy/caddytls/crypto_test.go:101 +0x2a4
  testing.tRunner()
      /home/tw/goroot/src/testing/testing.go:610 +0xc9

Goroutine 25 (running) created at:
  testing.(*T).Run()
      /home/tw/goroot/src/testing/testing.go:646 +0x52f
  testing.RunTests.func1()
      /home/tw/goroot/src/testing/testing.go:793 +0xb9
  testing.tRunner()
      /home/tw/goroot/src/testing/testing.go:610 +0xc9
  testing.RunTests()
      /home/tw/goroot/src/testing/testing.go:799 +0x4b5
  testing.(*M).Run()
      /home/tw/goroot/src/testing/testing.go:743 +0x12f
  github.com/mholt/caddy/caddytls.TestMain()
      /home/tw/golib/src/github.com/mholt/caddy/caddytls/setup_test.go:27 +0x133
  main.main()
      github.com/mholt/caddy/caddytls/_test/_testmain.go:116 +0x1b1
==================
==================
WARNING: DATA RACE
Write at 0x00c4204aa6c0 by goroutine 26:
  github.com/mholt/caddy/caddytls.TestStandaloneTLSTicketKeyRotation.func2()
      /home/tw/golib/src/github.com/mholt/caddy/caddytls/crypto_test.go:93 +0x56
  github.com/mholt/caddy/caddytls.standaloneTLSTicketKeyRotation()
      /home/tw/golib/src/github.com/mholt/caddy/caddytls/crypto.go:233 +0x638

Previous read at 0x00c4204aa6c0 by goroutine 25:
  github.com/mholt/caddy/caddytls.TestStandaloneTLSTicketKeyRotation()
      /home/tw/golib/src/github.com/mholt/caddy/caddytls/crypto_test.go:108 +0x391
  testing.tRunner()
      /home/tw/goroot/src/testing/testing.go:610 +0xc9

Goroutine 26 (running) created at:
  github.com/mholt/caddy/caddytls.TestStandaloneTLSTicketKeyRotation()
      /home/tw/golib/src/github.com/mholt/caddy/caddytls/crypto_test.go:101 +0x2a4
  testing.tRunner()
      /home/tw/goroot/src/testing/testing.go:610 +0xc9

Goroutine 25 (running) created at:
  testing.(*T).Run()
      /home/tw/goroot/src/testing/testing.go:646 +0x52f
  testing.RunTests.func1()
      /home/tw/goroot/src/testing/testing.go:793 +0xb9
  testing.tRunner()
      /home/tw/goroot/src/testing/testing.go:610 +0xc9
  testing.RunTests()
      /home/tw/goroot/src/testing/testing.go:799 +0x4b5
  testing.(*M).Run()
      /home/tw/goroot/src/testing/testing.go:743 +0x12f
  github.com/mholt/caddy/caddytls.TestMain()
      /home/tw/golib/src/github.com/mholt/caddy/caddytls/setup_test.go:27 +0x133
  main.main()
      github.com/mholt/caddy/caddytls/_test/_testmain.go:116 +0x1b1
==================

Signed-off-by: Tw <tw19881113@gmail.com>
2016-08-02 15:28:12 +08:00
Nimi Wariboko Jr d534a2139f Proxy: When connecting to websocket backend, reuse the connection isntead of starting a new one. 2016-08-01 19:11:31 -07:00
Eric Drechsel c4e65df262 Proxy: Add a failing test which replicates #763
2 websocket connections are made instead of one
2016-08-01 19:09:02 -07:00
Kris Hamoud 88d3dcae42 added ip_hash load balancing
updated tests

fixed comment format

fixed formatting, minor logic fix

added newline to EOF

updated logic, fixed tests

added comment

updated formatting

updated test output

fixed typo
2016-08-01 18:50:53 -07:00
Nimi Wariboko Jr db4cd8ee2d Proxy: Add keepalive directive to proxy to set MaxIdleConnsPerHost on transport. Fixes #938 2016-08-01 15:54:07 -07:00
Matt Holt da5b3cfc50 Merge pull request #976 from wjkohnen/h2
Re-enable HTTP/2 for Go 1.7.
2016-08-01 15:06:44 -06:00
Matt Holt 372c77da3a Merge pull request #983 from djvdorp/sysvinit
dist/init/linux-sysvinit: caddy for SysVinit
2016-08-01 13:34:07 -06:00
Daniel van Dorp 251c38bfb2 dist/init/linux-sysvinit: caddy for SysVinit
In addition to `linux-upstart` and `linux-systemd`, I think this one
might be very useful too.

The script is based on [this script](https://git.devuan.org/fredg/stuffs/blob/master/caddy/init.d/caddy)
by @fredg, found via [Installation du serveur Caddy sous Devuan &middot; Frédéric Galusik](http://galusik.xyz/installation-caddy-server-devuan/#démon:d7570338f345f168f3c50f22e7f8c47c).
I have modified it into an extended version myself, since I had the need for this.
2016-08-01 20:51:22 +02:00
Matt Holt ba1bee2b8f Merge pull request #981 from tw4452852/redir
redir: loading block arguments before parsing matcher
2016-08-01 12:36:06 -06:00
Tw b64894c31e redir: loading block arguments before parsing matcher
fix issue #977

Signed-off-by: Tw <tw19881113@gmail.com>
2016-08-01 14:38:18 +08:00
Wolfgang Johannes Kohnen d88dd74dec Re-enable HTTP/2 for Go 1.7.
* Since Go 1.7 HTTP/2 support is enabled only if TLSConfig.NextProtos
   includes the string "h2".
 * see mholt/caddy#975
2016-07-30 18:18:53 +00:00
Matt Holt 7157bdc79d Merge pull request #970 from ponychicken/reload
Specify the reload signal in the upstart script
2016-07-29 15:24:27 -06:00
Leo 72af3f8256 Specify the reload signal in the upstart script 2016-07-29 22:56:25 +02:00
Matthew Holt c8daaba4be Update link to SA 1.1.1 (and other minor tweaks) 2016-07-28 11:11:14 -06:00
Matthew Holt af48bbd234 Scope TLS max_certs to site config instead of global 2016-07-28 11:08:18 -06:00
Matthew Holt 1e1e69b90f Discard byte order mark (fix #962) 2016-07-27 12:48:49 -06:00
Matt Holt cf1b355d30 Merge pull request #960 from phifty/patch-1
Change position of locale directive
2016-07-25 08:53:54 -06:00
Philipp Brüll 1dd413bd69 Change position of locale directive
First, great job on the 0.9 release! It seems caddy's path lead into a bright future. Thanks also for including the locale plugin.

Trying it, I've figured out, that there might be a problem with the order of the directives. In the typical use case, the result of the locale detection might be used in the `rewrite` and `log` plugin. If I'm not mistaken, it makes sense to put the `locale` directive before those.
2016-07-24 11:55:25 +02:00
Matt Holt 1bbad72ff1 Merge pull request #956 from xuqingfeng/ratelimit
Register ratelimit
2016-07-23 10:39:16 -06:00
xuqingfeng b2aed643f4 Register ratelimit 2016-07-23 10:50:07 +08:00
Viacheslav Chimishuk 62e8c4b76b Use authentification credentials from proxy's configuration as a default. (#951) 2016-07-22 11:33:50 -06:00
Matthew Holt 6490ff6224 Adjust proxy headers properly (fixes #916) 2016-07-21 18:18:01 -06:00
Matthew Holt 57710e8b0d Revert "Merge pull request #931 from pedronasser/master"
This reverts commit 9ea0591951, reversing
changes made to 2125ae5f99.
2016-07-21 13:31:43 -06:00
Matt Holt 4678471fe0 Merge pull request #952 from abiosoft/condition-patch
minor condition keyword check refactor
2016-07-21 12:03:33 -06:00
Matt Holt d746b95906 Merge pull request #950 from tw4452852/proxy_parse
proxy: fix hyphen issue when parsing target
2016-07-21 12:01:10 -06:00
Abiola Ibrahim 3c8b2b5954 minor condition keyword check refactor 2016-07-21 15:42:38 +01:00
Matt Holt cf3ce49104 Merge pull request #949 from gsquire/headers-update
HTTP Headers
2016-07-21 00:28:29 -06:00
Tw ca3d23bc70 proxy: fix hyphen issue when parsing target
fix issue #948

Signed-off-by: Tw <tw19881113@gmail.com>
2016-07-21 13:56:35 +08:00
Garrett Squire e7c842215e Allow multiple values for an HTTP header and
add a test to ensure this works.
2016-07-20 22:17:13 -07:00
Tw beae16f07c Proxy performance (#946)
* proxy: add benchmark

Signed-off-by: Tw <tw19881113@gmail.com>

* replacer: prepare lazily

update issue#939

benchmark            old ns/op     new ns/op     delta
BenchmarkProxy-4     83865         72544         -13.50%

Signed-off-by: Tw <tw19881113@gmail.com>

* proxy: use buffer pool to avoid temporary allocation

Signed-off-by: Tw <tw19881113@gmail.com>
2016-07-20 19:06:14 -06:00
Matthew Holt 1240690973 Avoid deadlock (fixes #941) 2016-07-19 12:05:40 -06:00
Matthew Holt b35d19d78e Set protocol version properly (fixes #943) 2016-07-19 11:48:44 -06:00
Matt Holt cf4e0c9c9c Merge pull request #940 from mmlkrx/update-readme-contributing-section
Update readme contributing section
2016-07-19 07:50:31 -06:00
Matthias Loker ac97cf426f Update readme contributing section 2016-07-19 15:41:54 +02:00
Matthew Holt f28af63732 Version 0.9 2016-07-18 21:50:45 -06:00
Matthew Holt 38c2463416 Fix ACME asset migration when renaming folders 2016-07-18 21:50:27 -06:00
Matthew Holt df018ea64a Properly handle path-only addresses (also fix godoc typos) 2016-07-18 18:45:20 -06:00
Matthew Holt 4ff46ad447 Refactor Server into TCPServer and UDPServer 2016-07-18 16:28:26 -06:00
Matthew Holt 59c6513b31 Clarify some godoc 2016-07-18 16:21:19 -06:00
Matthew Holt aede4ccbce Small changes; mostly comments 2016-07-18 14:32:28 -06:00
Miek Gieben 9315738dab Allow for UDP servers (#935)
* Allow for UDP servers

Extend the Server interface with ServePacket and ListenPacket - this is
in the same vein as the net package.

Plumb the packetconn through the start and restart phases.

Rename RestartPair to RestartTriple as it now also contains a Packet.
Not that these can now be nil, so we need to check for that when
restarting.

* Update the documentation
2016-07-18 14:24:09 -06:00
Matthew Holt 502a8979a8 Propagate DNS provider plugins to caddy package so -plugins shows them 2016-07-15 21:29:06 -06:00
Pedro Nasser d6110f8e9e Merge pull request #932 from pedronasser/fix-import
fix: import should always be relative to current file directory
2016-07-14 23:30:28 -03:00
Pedro Nasser d7698ecf13 fix: import should always be relative to current file directory 2016-07-14 21:48:56 -03:00
Pedro Nasser 9ea0591951 Merge pull request #931 from pedronasser/master
fix transparent host header #916
2016-07-14 21:46:24 -03:00
Pedro Nasser ffafb2eca8 Merge branch 'master' of github.com:pedronasser/caddy 2016-07-14 18:17:05 -03:00
Pedro Nasser 6bb1e0c674 improve transparent mode 2016-07-14 18:16:58 -03:00
Pedro Nasser 6f37e9d31b Merge branch 'master' into master 2016-07-14 15:48:46 -03:00
Pedro Nasser b58872925a fixed transparent host and added test case 2016-07-14 15:43:06 -03:00
Pedro Nasser 8d7136fc06 fix transparent host header #916 2016-07-14 15:30:00 -03:00
Pedro Nasser 2125ae5f99 import should get absolute path before glob (#929)
* import should get absolute path before glob

* fix test: import should get absolute path before glob

* try to fix test on windows

* use complete path as the dispenser filename

* fix caddyfile test
2016-07-13 10:58:42 -06:00
Matthew Holt 3fd3feeffe Add Ext action to template context (closes #844) 2016-07-11 08:37:19 -06:00
Matthew Holt 62622eb853 proxy: 'transparent' also sets X-Forwarded-For (closes #924) 2016-07-09 17:33:40 -06:00
Abiola Ibrahim 87c389f73d Proposal: Middleware Config (#908)
* Prototype middleware Config

* Refactors

* Minor refactors
2016-07-08 18:12:52 -06:00
Abiola Ibrahim cf03c9a6c8 Merge pull request #928 from abiosoft/master
discard remaining args for if block in redir.
2016-07-08 19:52:19 +01:00
Abiola Ibrahim 48abb41135 discard remaining args in if block 2016-07-08 19:39:31 +01:00
Pedro Nasser 7eb4bb8e1c Merge pull request #927 from pedronasser/master
fix rewrite if problem (skip remaining args)
2016-07-08 14:52:45 -03:00
Pedro Nasser 39e55072d7 fix rewrite if problem (skip remaining args) 2016-07-08 14:11:15 -03:00
Chad Retz 88a2811e2a Pluggable TLS Storage (#913)
* Initial concept for pluggable storage (sans tests and docs)

* Add TLS storage docs, test harness, and minor clean up from code review

* Fix issue with caddymain's temporary moveStorage

* Formatting improvement on struct array literal by removing struct name

* Pluggable storage changes:

* Change storage interface to persist all site or user data in one call
* Add lock/unlock calls for renewal and cert obtaining

* Key fields on composite literals
2016-07-08 07:32:31 -06:00
Matthew Holt 065eeb42c3 Move rewrite and ext middlewares to before gzip (fixes #914) 2016-07-06 00:04:53 -06:00
Matt Holt d4b10b69a7 Merge pull request #920 from ianwalter/master
Changing refs from /usr/bin to /usr/local/bin
2016-07-05 23:32:01 -06:00
Matthew Holt f77264b776 Improve basicauth password comparison
Thanks to @jaredfolkins for the feedback
2016-07-05 12:49:25 -06:00
Ian Walter ad2ed5b0ae Changing refs from /usr/bin to /usr/local/bin 2016-07-05 13:39:04 -04:00
Matthew Holt fdb6d64f9d Add locale plugin directive, update changelog 2016-07-02 18:07:24 -06:00
Matthew Holt 227664336e Misc. changes: {hostonly} placeholder, self_signed port fix 2016-07-02 14:11:17 -06:00
Matt Holt 32329a473d Merge pull request #909 from evermax/master
Test that the host header forwarding on proxy
2016-06-29 18:53:15 -06:00
Maxime e5bf8cab24 Test that the host header forwarding on the proxy middleware 2016-06-29 16:52:31 -07:00
Matt Holt 6db4771aa8 Merge pull request #907 from abiosoft/fastcgi_env_placeholder
Support for placeholders in fastcgi env vars.
2016-06-29 07:09:37 -06:00
Abiola Ibrahim b1cd0bfeff Support for placeholders in fastcgi env vars. 2016-06-29 13:41:52 +01:00
Matthew Holt 2e84fe4504 Replace auto-HTTPS info message and move a method to proper file 2016-06-28 23:01:06 -06:00
Matthew Holt d2be213e10 Import paths now relative to Caddyfile (closes #867)
This is inconsistent with the other directives, but import is a special
case and frankly the behavior of import shouldn't change depending
on the directory from which you run caddy. Breaking change but I think
it's for the better, and best to do it now before 1.0.
2016-06-28 22:39:29 -06:00
Matthew Holt a1bc94e409 Working on a fix for proxy related to setting Host header (cf. #874)
Also see
https://forum.caddyserver.com/t/caddy-0-9-beta-version-available-updated-beta-2/146/29?u=matt
which has another account of strange proxy behavior; I think this
resolves that.
2016-06-28 18:40:07 -06:00
Matthew Holt 80dd95a495 Change outreq.Host instead of r.Host (possibly related to #874)
Also a few little formatting changes and comments.
2016-06-28 18:19:35 -06:00
Matthew Holt 5a45719227 Don't change port when TLS is managed manually 2016-06-28 18:16:10 -06:00
Viacheslav Biriukov 345ece3850 add multi proxy supprot based on urls 2016-06-28 16:35:35 -06:00
Matthew Holt 2b44a7d052 Link to instructions if storage migration fails (#902) 2016-06-27 15:13:01 -06:00
Matthew Holt 58085edc16 Don't treat localhost specially when assigning bind address
If we listen on 127.0.0.1:80 for `localhost` but :80 for everything else,
then a hostname in the hosts file that resolves to 127.0.0.1 will be
served on :80 (unless the bind directive is used) but the OS will use
the socket listening at 127.0.0.1:80, thus giving a "No such site" error
even though the site is there, but it's on the other listener at :80.

Two ways to fix this: 1) Leave as-is and require the user to set "bind
127.0.0.1" in their Caddyfile for all sites that are resolved in the
hosts file, or 2) Take out this special case and let localhost sites
listen on :80 (unless the user changes that with the bind directive, of
course). Having localhost bind to any interface is a little annoying
(unsettling?) but probably best in the long run.

https://forum.caddyserver.com/t/wildcard-virtual-domains-with-wildcard-roots/221/9?u=matt
2016-06-27 13:14:28 -06:00
Matt Holt 6f05faa670 Merge pull request #900 from hacdias/patch-2
Add filemanager directive
2016-06-27 08:32:54 -06:00
Matt Holt eddb6f0a79 Merge pull request #905 from nesl247/hotfix/go-get-url
Fix go get url
2016-06-27 08:32:27 -06:00
Harrison Heck 70b75d1433 Fix go get url
Fixes #904
2016-06-26 14:40:13 -04:00
Matthew Holt 15fa5cf2da OnFirstStartup and OnFinalShutdown callbacks added
OnStartup and OnShutdown callbacks now run as part of restarts, too.
The startup and shutdown directives only run their commands NOT as part
of restarts, as before. Some middleware that use OnStartup may need to
switch to OnFirstStartup and implement OnFinalShutdown to do any cleanup
as needed.
2016-06-23 18:02:12 -06:00
Henrique Dias e74678ed43 Change hugo and filemanager order 2016-06-23 16:55:56 +01:00
Henrique Dias d84c823855 Add filemanager directive 2016-06-23 16:03:16 +01:00
Matthew Holt b49f65d5de 0.9 beta 2 2016-06-22 07:09:27 -06:00
Abiola Ibrahim fd8fe24bcb Merge pull request #899 from abiosoft/master
Minor refactor for rewrite.
2016-06-22 05:49:46 +01:00
Abiola Ibrahim 281603895b Minor refactor. 2016-06-22 05:36:29 +01:00
Matthew Holt fbad4e15c2 Fix browse template row hover effect for first row 2016-06-21 19:22:31 -06:00
Matthew Holt ab301fec00 Fix typo, formatting 2016-06-21 19:22:31 -06:00
Pedro Nasser deec149891 fix for new rewrite test case and add table test to replacer (#890)
* rewrite: fix new case
- added new test case and solution
- fix test case on rewrite_test

* replacer: change to table test
2016-06-21 18:44:16 -06:00
Abiola Ibrahim 9ca87cd139 Merge pull request #898 from abiosoft/master
Fix missed if_op refactor. Ensure with tests.
2016-06-21 20:52:51 +01:00
Abiola Ibrahim cad9b3f62f Fix missed if_op refactor. Ensure with tests. 2016-06-21 20:41:09 +01:00
Matthew Holt e585a74115 Revamped readme 2016-06-21 11:28:38 -06:00
Abiola Ibrahim d9b6563d88 Condition upgrades (if, if_op) for rewrite, redir (#889)
* checkpoint

* Added RequestMatcher interface. Extract 'if' condition into a RequestMatcher.

* Added tests for IfMatcher

* Minor refactors

* Refactors

* Use if_op

* conform with new 0.9 beta function changes.
2016-06-21 08:59:29 -06:00
Matthew Holt 0a3f68f0d7 Fix test on Windows (with 1 CPU) 2016-06-21 00:23:18 -06:00
Matthew Holt e625c7c051 Every package has a test 2016-06-21 00:11:55 -06:00
Matthew Holt 937654d1e0 Set host and port on address if specified via flag (fixes #888)
Also fixed a few typos and renamed caddyfile.ServerBlocks() to
caddyfile.Parse().
2016-06-20 18:25:42 -06:00
Matt Holt 33aba7eb91 Merge pull request #894 from RobbieMcKinstry/master
Refactoring to remove lint warnings
2016-06-20 18:02:27 -06:00
Robbie McKinstry d252d40681 Refactoring to remove lint 2016-06-20 19:11:29 -04:00
Mateusz Gajewski 81c4ea6be7 Add support for Alt-Svc headers (#892) 2016-06-20 13:50:25 -06:00
Matthew Holt 1fdc46e571 Fix tests after controller refactor
The search-and-replace was a little too aggressive and I accidentally
ran tests recursively in a subdirectory instead of repo's top folder.
2016-06-20 12:29:19 -06:00
Matthew Holt a798e0c951 Refactor how caddy.Context is stored and used
- Server types no longer need to store their own contexts; they are
  stored on the caddy.Instance, which means each context will be
  properly GC'ed when the instance is stopped. Server types should use
  type assertions to convert from caddy.Context to their concrete
  context type when they need to use it.
- Pass the entire context into httpserver.GetConfig instead of only the
  Key field.
- caddy.NewTestController now requires a server type string so it can
  create a controller with the proper concrete context associated with
  that server type.

Tests still need more attention so that we can test the proper creation
of startup functions, etc.
2016-06-20 11:59:23 -06:00
David Dyke 07b7c99965 Add timeout to health_check (#887)
* Add timeout to http get on health_check

* Add new test and up the timeout

* Tests for change to default timeout

* Only call http client once and make options more inline with current caddy directives
2016-06-20 09:49:21 -06:00
Matt Holt 807617965a Merge pull request #891 from andrewhamon/policy-cleanup
Refactor and clean up policy code
2016-06-20 09:27:48 -06:00
Andrew Hamon a50462974c Refactor and clean up policy code
This commit shouldn't change any behavior. It is simply a cleanup of
the different proxy policies. It also adds some comments explaining the
sampling method used, since on first inspection it might not appear to
be a uniformly random selection.
2016-06-18 15:41:18 -05:00
Pedro Nasser 6fe5c1a69f Merge pull request #885 from pedronasser/master
Fix rewrite bug with URL query + test case (#884)
2016-06-16 11:30:34 -03:00
Pedro Nasser 54355d8fb3 replace strings.Split for SplitN 2016-06-16 10:03:31 -03:00
Pedro Nasser e486c9c6e7 fix rewrite bug with url query + test case 2016-06-15 19:55:02 -03:00
Matt Holt 0f1e5bcebf Merge pull request #876 from hacdias/patch-1
Add minify directive
2016-06-14 15:44:04 -06:00
Andrew Hamon fee4890e94 Balance round robin evenly when some hosts are down (#880)
* Balance round robin evenly when some hosts are down

Before, when load balancing across multiple hosts, if a host went down
then the next host in line would be sent a double share of requests.
This is because the round robin counter was only incremented once per
request, regardless of the health of the selection. If current
selection was unhealthy then the policy would advance to the next host,
but this would not be reflected in the policy counter. To fix this, the
counter is now incremented for every attempted host.

This commit adds a test case that identifies the issue, and a fix.

* Make robin counter private

* Use a mutex to sync round robin selection
2016-06-14 15:43:06 -06:00
David Dyke b14baf7e20 Add proxy preset: transparent (#881)
* Add reverse_proxy preset

* Update to 'transparent' preset instead of 'reverse_proxy'
2016-06-14 12:03:30 -06:00
Matthew Holt 2b06edccd3 Use challenge domain for tls-sni solver
Matches the new upstream function signature and fixes previously broken
behavior; new solver code confirmed to work during restarts
2016-06-13 17:48:59 -06:00
Henrique Dias 492d5aa37f Merge branch 'master' into patch-1 2016-06-10 07:34:42 +01:00
Henrique Dias 1e4a4109a7 Update plugin.go 2016-06-10 07:31:07 +01:00
Matthew Holt daa4de572e Ensure certificate has a non-nil config when caching (fixes #875)
Also we change the scheme of the site's address if TLS is enabled and
no other scheme is explicitly set; this makes it appear as "https" when
we print it; otherwise it would show "http" when TLS is turned on
implicitly, and that is confusing/incorrect.
2016-06-09 19:12:11 -06:00
Henrique Dias 83451ea2a0 Update plugin.go 2016-06-09 16:06:50 +01:00
Henrique Dias 06fed0db17 Add minify directive 2016-06-09 15:14:46 +01:00
Matt Holt 98cf26377c Merge pull request #873 from mholt/fix-restart
Fix restart on USR1 not loading new Caddyfile
2016-06-08 17:40:16 -06:00
Benny Ng ff82057131 Fix restart on USR1 not loading new Caddyfile 2016-06-09 07:12:01 +08:00
Matthew Holt 6492592b4a Update change list, fix build script; version 0.9 beta 1 2016-06-07 14:33:06 -06:00
Gustavo Chaín 6c847d0723 New {request} placeholder to log entire requests (sans body) (#871)
Add a {request} placeholder to the replacer.

Closes #858.
2016-06-07 11:06:24 -06:00
Matt Holt 01e05afa0c Merge pull request #870 from mholt/close-idle-conn
Close idle connections after graceful shutdown timeout
2016-06-07 09:38:58 -06:00
Matthew Holt e7fc26e3fb Improved godoc, added two missing directives, update change log 2016-06-07 09:27:14 -06:00
Benny Ng 37ae21001d Close idle connections after graceful shutdown timeout 2016-06-07 18:32:27 +08:00
Benny Ng b23eec4fac Update go get path for Caddy binary (#869) 2016-06-07 12:09:52 +08:00
Matthew Holt 727ef24306 Improve godoc for plugin facilities 2016-06-06 18:27:54 -06:00
Matthew Holt d3860f95f5 Make RegisterPlugin() more consistent, having name as first argument 2016-06-06 15:31:03 -06:00
Matt Holt 9b4134b287 Merge pull request #866 from mholt/0.9-wip
Merge 0.9 into master (warning: huge diff)
2016-06-06 07:39:16 -06:00
Leo Koppelkamm ddff08392a Make upstart script more fault tolerant and easier to debug (#824)
* Make Upstart script more fault tolerant and easier to debug

* update readme
2016-06-06 07:32:27 -06:00
Matthew Holt 71c14fa16e Make sure Root is set for all new SiteConfigs
This situation typically only arises in tests...
2016-06-05 23:34:16 -06:00
Matthew Holt ff22fbd79a Migrate existing add-on names; set default root in SiteConfig 2016-06-05 23:24:34 -06:00
Matthew Holt a762dde145 Migrate remaining middleware packages 2016-06-05 22:39:23 -06:00
Matthew Holt 416af05a00 Migrating more middleware packages 2016-06-05 21:51:56 -06:00
Matthew Holt 2f92443de7 More tests, several fixes and improvements; export caddyfile.Token
We now sneakily chain in the errors directive if gzip is present but
not errors. This change fixes #616.
2016-06-04 22:50:23 -06:00
Matthew Holt 49fdc6a20a Add errors middleware; export httpserver.WriteTextResponse 2016-06-04 22:48:27 -06:00
Matthew Holt ac4fa2c3a9 Rewrote Caddy from the ground up; initial commit of 0.9 branch
These changes span work from the last ~4 months in an effort to make
Caddy more extensible, reduce the coupling between its components, and
lay a more robust foundation of code going forward into 1.0. A bunch of
new features have been added, too, with even higher future potential.

The most significant design change is an overall inversion of
dependencies. Instead of the caddy package knowing about the server
and the notion of middleware and config, the caddy package exposes an
interface that other components plug into. This does introduce more
indirection when reading the code, but every piece is very modular and
pluggable. Even the HTTP server is pluggable.

The caddy package has been moved to the top level, and main has been
pushed into a subfolder called caddy. The actual logic of the main
file has been pushed even further into caddy/caddymain/run.go so that
custom builds of Caddy can be 'go get'able.

The HTTPS logic was surgically separated into two parts to divide the
TLS-specific code and the HTTPS-specific code. The caddytls package can
now be used by any type of server that needs TLS, not just HTTP. I also
added the ability to customize nearly every aspect of TLS at the site
level rather than all sites sharing the same TLS configuration. Not all
of this flexibility is exposed in the Caddyfile yet, but it may be in
the future. Caddy can also generate self-signed certificates in memory
for the convenience of a developer working on localhost who wants HTTPS.
And Caddy now supports the DNS challenge, assuming at least one DNS
provider is plugged in.

Dozens, if not hundreds, of other minor changes swept through the code
base as I literally started from an empty main function, copying over
functions or files as needed, then adjusting them to fit in the new
design. Most tests have been restored and adapted to the new API,
but more work is needed there.

A lot of what was "impossible" before is now possible, or can be made
possible with minimal disruption of the code. For example, it's fairly
easy to make plugins hook into another part of the code via callbacks.
Plugins can do more than just be directives; we now have plugins that
customize how the Caddyfile is loaded (useful when you need to get your
configuration from a remote store).

Site addresses no longer need be just a host and port. They can have a
path, allowing you to scope a configuration to a specific path. There is
no inheretance, however; each site configuration is distinct.

Thanks to amazing work by Lucas Clemente, this commit adds experimental
QUIC support. Turn it on using the -quic flag; your browser may have
to be configured to enable it.

Almost everything is here, but you will notice that most of the middle-
ware are missing. After those are transferred over, we'll be ready for
beta tests.

I'm very excited to get this out. Thanks for everyone's help and
patience these last few months. I hope you like it!!
2016-06-04 17:00:29 -06:00
Matt Holt e1a6b60736 Merge pull request #861 from marc-gr/upstream
Add upstream directive
2016-06-03 17:50:33 -06:00
Marc Guasch 9b5ad487d7 Add several upstreams test case 2016-06-04 01:07:53 +02:00
Marc Guasch d291b76721 Gofmt fixes 2016-06-03 22:48:24 +02:00
Marc Guasch 881da313dd Add tests for upstream directive 2016-06-03 21:36:15 +02:00
Marc Guasch 1bdbf9d6ba Add parseUpstream method 2016-06-03 21:36:15 +02:00
Matt Holt 2536ea74d9 Merge pull request #859 from jupiter/fix/issue-643
Fix for cleaned URL.Path
2016-06-02 16:43:34 -06:00
Pieter Raubenheimer 9acfec5418 Fix cleaned URL path in proxy 2016-06-02 22:08:10 +01:00
Abiola Ibrahim a0e6eb3ba9 Merge pull request #855 from abiosoft/proxy-host-patch
Fix for #854
2016-05-29 08:11:27 +01:00
Abiola Ibrahim 6e6d9e7e9e Fix for #854 2016-05-29 07:55:40 +01:00
Matthew Holt 238250e7e6 Recommend forum in issue template 2016-05-27 10:16:11 -06:00
Matthew Holt 324ec15890 Link to forum 2016-05-27 09:15:06 -06:00
Matthew Holt c64361a753 Use latest Go version (1.6.2) for CI builds 2016-05-24 18:37:44 -06:00
Matt Holt 6d9dcb1729 Merge pull request #846 from nesl247/fix-845
Strip [] from IPv6 addresses for FastCGI.
2016-05-24 13:56:55 -06:00
Harrison Heck da97ac7c63 Strip [] from IPv6 addresses for FastCGI.
Fixes #845.
2016-05-24 15:19:50 -04:00
Matt Holt 374d0a3f09 Merge pull request #840 from mholt/more-systemd
systemd, README: Edit to account for the recent spike in reports
2016-05-21 14:44:58 -06:00
W-Mark Kubacki bee9c50a71 systemd, README: needs to be version 229 or later, and how to display logs
We have had three operators within a few days which ran into the same cause
and had not been able to figure out what went wrong.

addresses #833, #822
2016-05-21 00:54:44 +02:00
Matthew Holt bac29cc20a Change image http -> https 2016-05-15 08:26:20 -04:00
Matt Holt e6b1028da9 Merge pull request #823 from ponychicken/patch-1
fix typo
2016-05-14 17:20:19 -04:00
Leo Koppelkamm 4c62397ff8 fix typo 2016-05-14 16:39:00 +02:00
W. Mark Kubacki e516aebc08 Merge pull request #817 from mholt/systemd-service-file
Provides some more guidelines to operators on how to avoid running Caddy as root.

Introduces an user www-data, which really is a placeholder. Such an user with the same UID/GID combination is created on the most popular Linux distribution. I trust any operator can spot the difference to his/her distro and adjust the unit file.

User nobody is not used here to avoid two easy pitfalls: Such an user should not be able to access private keys (for TLS), and should not write private keys (we would do that with Letsencrypt).
2016-05-12 15:15:32 +02:00
W-Mark Kubacki da8ae9e511 systemd: Run caddy with even less privileges and more confined
The exemplary unit file for systemd is intentionally redundant at times, for
example dropping privileges which an unprivileged user "www-data" did not have
in the first place: To aid as fallback in case the file gets copied and an
operator setting UID to 0 (which reportedly happened in the past).
2016-05-12 15:11:43 +02:00
W-Mark Kubacki d377c79a5d systemd, README: Edit for clarity 2016-05-12 15:08:06 +02:00
W. Mark Kubacki 389a6eb344 Merge pull request #811 from mholt/fix-browse-2
browse: Decorate external links with: noopener noreferrer
2016-05-07 20:19:22 +02:00
W-Mark Kubacki 85d793ce88 browse: Decorate external links with: noopener noreferrer
Setting these on external links prefents the target from manipulating this
page by "window.opener" with some widely deployed browsers.
2016-05-07 20:06:55 +02:00
Matt Holt 1f29c52151 Merge pull request #807 from mholt/graceful-inproc-restart
Restart gracefully with in-process restart
2016-05-07 08:58:18 -06:00
Benny Ng 9705f34970 Restart gracefully for in-process restart 2016-05-07 13:39:47 +08:00
Matt Holt db21b0319d Merge pull request #804 from kennwhite/kennwhite-readme-patch
Adding note on the rationale for Go 1.6
2016-05-05 08:29:14 -06:00
kennwhite 25b934824f Adding note on the rationale for Go 1.6 2016-05-05 10:18:00 -04:00
Matt Holt c23c6d9cb4 Merge pull request #762 from weingart/md_changes
Markdown changes
2016-05-04 23:29:48 -06:00
Tobias Weingartner 9697c47e21 Merge branch 'master' into md_changes 2016-05-03 19:14:35 -07:00
Achim Vedam 39030d9e1b browse: delete duplicate </a>-closing tags in default template 2016-05-03 20:10:38 +02:00
Tobias Weingartner 61c7a51bfa Errant commented import. 2016-05-01 16:50:25 -07:00
Tobias Weingartner 45e783c3f9 Fix headers and unexport plaintext renderer. 2016-05-01 16:45:24 -07:00
Tobias Weingartner 2bccc1466e Fixup and address most of the feedback. 2016-04-30 20:09:25 -07:00
Tobias Weingartner a3af232dc5 Use http.MethodGet instead of "GET". 2016-04-30 17:06:46 -07:00
Tobias Weingartner 04089c533b Return errors. 2016-04-30 17:04:44 -07:00
Tobias Weingartner e0bc426050 Initial re-add of markdown summary functionality. 2016-04-30 17:01:31 -07:00
Tobias Weingartner bd2a33dd14 Europeans know time. :) 2016-04-30 14:21:30 -07:00
Tobias Weingartner 20dfaab703 Nuke unused function. 2016-04-30 14:16:19 -07:00
Tobias Weingartner 249c9a17f5 Make default template more readable/clean. 2016-04-30 14:07:42 -07:00
Tobias Weingartner c431a07af5 Merge branch 'master' into md_changes 2016-04-30 13:33:47 -07:00
William Bezuidenhout e2234497b7 proxy: Add, remove, or replace upstream and downstream headers (closes #666) (PR #788)
* Overwrite proxy headers based on directive

Headers of the request sent by the proxy upstream can now be modified in
the following way:

Prefix header with `+`: Header will be added if it doesn't exist
otherwise, the values will be merge
Prefix header with `-': Header will be removed
No prefix: Header will be replaced with given value

* Add missing formating directive reported by go vet

* Overwrite up/down stream proxy headers

Add Up/DownStreamHeaders to UpstreamHost

Split `proxy_header` option in `proxy` directive into `header_upstream`
and `header_downstream`. By splitting into two, it makes it clear in
what direction the given headers must be applied.

`proxy_header` can still be used (to maintain backward compatability)
but its assumed to be `header_upstream`

Response headers received by the reverse proxy from the upstream host
are updated according the `header_downstream` rules.
The update occurs through a func given to the reverse proxy, which is
applied once a response is received.

Headers (for upstream and downstream) can now be modified in
the following way:

Prefix header with `+`: Header will be added if it doesn't exist
otherwise, the values will be merge
Prefix header with `-': Header will be removed
No prefix: Header will be replaced with given value

Updated branch with changes from master

* minor refactor to make intent clearer

* Make Up/Down stream headers naming consistent

* Fix error descriptions to be more clear

* Fix lint issue
2016-04-30 13:41:30 -06:00
Matthew Holt 96425f0f40 Fix vet failing on Go tip 2016-04-30 11:13:04 -06:00
Matthew Holt d05dac8d2e Little bit of housekeeping 2016-04-27 12:52:00 -06:00
Matt Holt 81e26970a3 proxy: Move handling of headers around to prevent memory usage spikes (#784)
* Move handling of headers around to prevent memory use spikes

While debugging #782, I noticed that using http2 and max_fails=0,
X-Forwarded-For grew infinitely when an upstream request failed after
refreshing the test page. This change ensures that headers are only
set once per request rather than appending in a time-terminated loop.

* Refactor some code into its own function
2016-04-27 12:50:18 -06:00
W. Mark Kubacki f561dc0bc1 browse: Serve a datetime format the IE11 and Firefox understand
Another new safeguard is that we check whether the datetime has been read
correctly. If not then the listing will not be localized.

closes #793
2016-04-27 19:24:42 +02:00
Matthew Holt 21382702d2 Update readme version, 0.8.3 2016-04-26 09:28:05 -06:00
Matthew Holt e97649493b Update change log; version 0.8.3 2016-04-26 08:36:59 -06:00
Matthew Holt 19d6f666aa Fix build script with default git repo path (cwd) 2016-04-25 22:10:16 -06:00
W. Mark Kubacki 6f5cff5393 tls: Prevent Go stdlib from overwriting our very first tls ticket key (#785)
[1] https://github.com/golang/go/blob/57e459e02b4b01567f92542f92cd9afde209e193/src/crypto/tls/common.go#L424
[2] https://github.com/golang/go/blob/57e459e02b4b01567f92542f92cd9afde209e193/src/crypto/tls/common.go#L392-L407

[2] has overwritten the first tls ticket key on round N=0, that has previously
been written using [1].

Go's stdlib does not use c.sessionTicketKeys≥1 as indicator if those values had
already been set; initializing that lone SessionTicketKey does the job for for
now.
    If c.serverInit() were called in round N+1 all existing tls ticket keys
would be overwritten (in round N<4 except the very first one, of course).
As member variables of tls.Config are read-only by then, we cannot keep
updating SessionTicketKey as well.

This has been escalated to Go's authors with golang/go#15421 here:
https://github.com/golang/go/issues/15421

Thanks to Matthew Holt for the initial report!
2016-04-23 17:49:48 +02:00
Matthew Holt 5c96ee1d9c Fix bug in renewing default certificate 2016-04-22 10:14:56 -06:00
W. Mark Kubacki 3c578dfbc1 Catch whitespace code style violations locally (#774)
Those settings enforce convergence on common coding style with respect to whitespace.

Do not use tabs to indent with shell scripts because those tabs most often
serve the function of triggering command completion. Which could end a
command before it is pasted completely.
  Traditionally indentation is two spaces here, not four.

Other rules will catch stray whitespace at the end of lines or files, which,
once committed, would annoy the next developer because his editor would strip
them from lines he did not intended to modify in the first place.
2016-04-20 16:56:57 +02:00
Abiola Ibrahim a093aea797 Merge pull request #773 from eliasp/patch-1
Typ (creatation → creation)
2016-04-20 14:52:14 +01:00
Elias Probst 9f1762873a Typ (creatation → creation) 2016-04-20 13:08:31 +02:00
Matt Holt c3417a0757 Merge pull request #772 from mholt/fix-browse
Make Browse Great Again ★★★
2016-04-19 10:16:08 -06:00
W-Mark Kubacki 72bc6932b0 browse: Jail the root directory using http.Dir() 2016-04-19 03:48:27 +02:00
W-Mark Kubacki a41e3d2515 browse: Use JavaScript to localize the shown datetime
The datetime format is whatever the user sets in his/her browser or OS.

This converts timezones.

Tested with Chrome 50 and Internet Explorer 11.
2016-04-19 03:48:27 +02:00
W-Mark Kubacki 7f35600b28 browse: Emit datetime in UTC instead of the server's timezone
Makes sure the view is the same no matter where a site is hosted.
2016-04-19 00:56:47 +02:00
W-Mark Kubacki cc6aa6b54b browse: Remove whitespace from template's output, annotate output
Fixes a surplus — next to "go up".

Identifies the preamble as the table's summary.

Emits filesizes in bytes, which can be consumed by any browser-side scripts
or utilized in sorting when the table is copy-and-pasted into a spreadsheet
software.

Uses <time> along with proper datetime representation, which a browser could
utilize to display the datetime rendered according to the requestor's locale.
2016-04-19 00:20:44 +02:00
W-Mark Kubacki 239f6825f7 browse: When sorting by size, offset directories
Assigns negative sizes to directories in order to have them listed reliably
before any zero-sized files. That order is what most users expect when
sorting by size.

As side effect directories will appear before files on all filesystem
implementations. To give an example: before this change directories had a size
of 4 KiB when using Linux with ext4 or tmpfs, and with ZFS a size resembling
an estimation of the number of leaves within said directory.
2016-04-19 00:20:44 +02:00
W-Mark Kubacki 1d38d113f8 browse: Move predicate 'limit' to ServeListing
This keeps the interface of all available formatters honest,
and allows for truncated listings all formats.
2016-04-19 00:20:44 +02:00
W-Mark Kubacki 6908136092 browse: Split ServeHTTP into small specialized functions 2016-04-19 00:20:44 +02:00
Matt Holt da016f8d5a Merge pull request #760 from Burmudar/hostname-placeholder
Add hostname placeholder. Headers use replacer
2016-04-18 15:54:31 -06:00
W. Mark Kubacki 2f2d357fb6 browse: Fix known bugs (#770)
* browse: Catch the case of a directory disappearing before having been read

* browse: Revert to old pass-through behaviour

PROPFIND is a request for an alternate view on a directory's contents, which
response is indeed not implemented but ideally allowed to ask for.
OPTIONS would ideally return (at least) what methods the requestor could use,
which is an allowed request method, too.

This addresses #767.
2016-04-18 11:42:36 -06:00
Matthew Holt 924b53eb3c Minor changes 2016-04-18 09:43:28 -06:00
Tobias Weingartner 2b51be7fd7 Better error message. (#768)
* Fix PrivateKeyBytes to error out and fail tests on error.

* Better error message.
2016-04-17 15:15:14 -06:00
Tobias Weingartner 376e1090a3 Fix PrivateKeyBytes to error out and fail tests on error. 2016-04-17 14:17:42 -06:00
Tobias Weingartner dd4de698cf Better search for config.
Handle LastModifiedHeader better.
Handle HEAD/GET.
2016-04-17 10:58:51 -07:00
elcore a682100c5e Merge pull request #765 from mholt/elcore-error-ecdsa
Error if we are unable to marshal the ECDSA private key
2016-04-17 18:40:02 +02:00
elcore aba3d37c88 Error if we are unable to marshal the ECDSA private key 2016-04-17 18:19:41 +02:00
W. Mark Kubacki 0890e330e2 Merge pull request #759 from mholt/paths-cleanup
Tell usage of 'path' from 'filepath' and fix *path checking
2016-04-17 14:16:57 +02:00
Tobias Weingartner 19a85d08c6 Merge branch 'master' into md_changes 2016-04-16 19:43:50 -07:00
Tobias Weingartner 5a0d373fcd Missed fixing setup. 2016-04-16 17:13:13 -07:00
Tobias Weingartner ecf91f525f Fix gofmt -s and ineffassign. 2016-04-16 17:07:06 -07:00
Tobias Weingartner b541c717ca Add ability to markdown a directory with a template. 2016-04-16 16:50:45 -07:00
W-Mark Kubacki c05c5163e2 browse: Don't leak Cookies to sessions in HTTP from HTTPS 2016-04-17 01:16:17 +02:00
W-Mark Kubacki 3513b6f2f7 fileserver: When out of filedescriptors, spread retry attempts 2016-04-17 01:16:17 +02:00
W-Mark Kubacki 4a6121f989 Tell usage of 'path' from 'filepath' and fix *path checking 2016-04-17 01:16:16 +02:00
Tobias Weingartner e652d12cfc Move Metadata load into NewMetadata function. 2016-04-16 15:36:52 -07:00
Tobias Weingartner b97a7909d8 Nuke more redundant things. 2016-04-16 15:18:17 -07:00
Tobias Weingartner 7c9867917a Fixup tests and move metadata into own package 2016-04-16 14:45:32 -07:00
William Bezuidenhout a762bec06d Add hostname placeholder. Header uses replacer
On matched header rules, replacer is used to replace any placeholders
defined in header rules iex. X-Backend {hostname} where {hostname} will
be replaced by the hostname key present in the replacer

hostname key added to replacer. The value is determined by the output of
`os.Hostname()`
2016-04-16 19:06:39 +02:00
Matthew Holt b75016e646 Fix lint warning 2016-04-15 15:13:44 -06:00
W. Mark Kubacki ddf4b1fd3b Merge pull request #757 from mholt/extend-tls-client-auth
Extend tls client auth
2016-04-15 22:57:45 +02:00
W-Mark Kubacki 69c2d78f69 Support configuring less restrictive TLS client auth requirements
Caddyfile parameter "clients" of "tls" henceforth accepts a special
first modifier. It is one of, and effects:

 * request         = tls.RequestClientCert
 * require         = tls.RequireAnyClientCert
 * verify_if_given = tls.VerifyClientCertIfGiven
 * (none)          = tls.RequireAndVerifyClientCert

The use-case for this is as follows: A middleware would serve items to the
public, but if a certificate were given the middleware would permit file
manipulation.

And, in a different plugin such as a forum or blog, not verifying a client
cert would be nice for registration: said blog would subsequently only
compare the SPKI of a client certificate.
2016-04-15 22:21:55 +02:00
W-Mark Kubacki f31875dfde Move sanitization of URL.Path to Server
No need to have this in every plugin. And, even in flat filesystems
filenames with dots and slashes are best avoided.
2016-04-15 21:02:36 +02:00
W-Mark Kubacki 4e98cc3005 browse: Return HTTP errors on unhandled HTTP methods
For example, a HTTP POST should not be answered with StatusOK,
and a response to HTTP OPTIONS should not carry any contents.
2016-04-15 18:22:51 +02:00
Matt Holt d3a77ce3c3 Use binExt 2016-04-13 15:21:18 -06:00
Tobias Weingartner 48d294a695 Better default template. 2016-04-12 18:53:15 -07:00
W. Mark Kubacki b149a86bc2 server: Rotate TLS ticket "keys" (#742) 2016-04-12 10:09:45 -06:00
Matt Holt ac80f6edc3 Merge pull request #744 from tboerger/feature/breadcrumb
browse: Dropped LinkedPath and updated browse template
2016-04-12 10:01:01 -06:00
Thomas Boerger ef95173827 Dropped LinkedPath and updated browse template
As discussed with @mholt I have dropped the old LinkedPath function and
replaced it within the browse template with the new BreadcrumbMap
function. Visually it looks exactly the same as before, now the template
functionality is just more powerful.

Signed-off-by: Thomas Boerger <tboerger@suse.de>
2016-04-12 17:18:21 +02:00
Matt Holt 36a3e204b6 Merge pull request #740 from tboerger/feature/browse-breadcrumb
Added breadcrumb map function to browse
2016-04-11 14:08:18 -06:00
Thomas Boerger e0b63d92f4 Added breadcrumb map function to browse
In order to being able to really build a custom template for the browse
directive I have added another function to build even custom breadcrumb
paths. The other function `LinkedPath` is not that easy styleable as
this map function. That way we are able to build the breadcrumb path
matching different CSS frameworks like Bootstrap.

Signed-off-by: Thomas Boerger <thomas@webhippie.de>
2016-04-11 21:47:44 +02:00
Thomas Boerger 004a7f84ef Make test case less dependent on exact error string (#741) 2016-04-11 13:08:30 -06:00
Matt Holt ed8a48e7f1 Merge pull request #739 from tboerger/feature/browse-go-up
Added "go up" link to browse template
2016-04-11 12:28:13 -06:00
Thomas Boerger c86c26a056 Added "go up" link to browse template
In order to have directly a link within the browse listing I have added
a link to the top of the table to get one level up in the tree. Added
that after a chat with @mholt.

Signed-off-by: Thomas Boerger <thomas@webhippie.de>
2016-04-11 20:10:41 +02:00
Matt Holt 0a7ca64f53 gzipResponseWriter should also be a Flusher
To be consistent with 3faad41b43 and c64cf218b0
2016-04-11 00:24:26 -06:00
Matt Holt c4e2cf96e7 Merge pull request #737 from tw4452852/closeNotifier
http.CloseNotifier implementation for http.ResponseWriter wrapper
2016-04-11 00:18:06 -06:00
Tobias Weingartner 42b7d57421 Fix more tests, and fix template parsing. 2016-04-10 23:17:01 -07:00
Tw c64cf218b0 http.CloseNotifier implementation for http.ResponseWriter wrapper
Signed-off-by: Tw <tw19881113@gmail.com>
2016-04-11 14:10:33 +08:00
Tobias Weingartner 027f697fdf Revamp markdown processing.
Nuke pre-generation.  This may come back in the form of a more general
caching layer at some later stage.

Nuke index generation.  This should likely be rethought and re-implemented.
2016-04-10 17:47:55 -07:00
Matt Holt 6a7b777f14 panic if not a Flusher
Caddy recovers panics that occur in the middleware stack so this is not a risk to process termination. This way is also preferable to hiding the error. See https://github.com/mholt/caddy/commit/3faad41b437c48cea37863123fab425169bc0c6e#commitcomment-17035158
2016-04-09 22:11:36 -06:00
Matthew Holt 67b137175e Replaced automate.sh with Go program 2016-04-09 10:02:16 -06:00
Matthew Holt dfa3b8645d Who uses 32-bit Mac anyway. :P 2016-04-09 00:40:37 -06:00
Matthew Holt 2dca50dee8 Rewrite automate.sh as Go program; add init folder to release archives
Easier parallelism and more control over platforms we build for, but
more importantly, we can do parallel builds using the build script which
properly embeds version information into the binaries. We also produce
the archive files ourselves and in parallel rather than using external
tar and zip commands.
2016-04-09 00:21:55 -06:00
Matthew Holt 3faad41b43 middleware: ResponseRecorder now is an http.Flusher (fixes #677)
Flush every 250ms. This should keep latency somewhat low but there is
still buffering; maybe in the future we can make this configurable.
2016-04-09 00:17:15 -06:00
Matthew Holt c21ff8343c Apparently vet ships with Go now 2016-04-06 17:29:53 -06:00
Matt Holt 2072eec11f Merge pull request #731 from wjkohnen/host-caseinsensitive
Handle host names case insensitively.
2016-04-06 15:56:34 -06:00
Wolfgang Johannes Kohnen 497ebb9ccb Handle host names case insensitively.
RFC 3986 3.2.2: The host subcomponent is case-insensitive.
2016-04-06 21:36:46 +00:00
Benny Ng e4e773c9ea Merge pull request #730 from tpng/restart-refactor
Extract restartInProc to its own file
2016-04-06 11:51:44 +08:00
Benny Ng 32e63e6b94 Extract restartInProc to its own file 2016-04-06 11:40:39 +08:00
Matthew Holt 86ccafbe58 Update changes
Also testing commit signing again, different email this time.
2016-04-05 19:04:08 -06:00
Matthew Holt 3ef78d3db3 Allow git repo as argument to build script
This will help build server perform builds with copy of repo in a folder
outside the original git repo folder. Also testing signed commits on
GitHub.
2016-04-05 18:57:50 -06:00
elcore 9ec1c17846 Merge pull request #727 from mholt/rename-keytype-ec-to-p
Rename EC256,EC384 to P256,P384
2016-04-05 19:59:37 +02:00
Eldin Hadzic 7ababfc909 Rename EC256,EC384 to P256,P384 2016-04-05 13:37:39 +00:00
Benny Ng 31062dd6c2 Merge pull request #725 from tpng/reload-validate-caddyfile
revert to old Caddyfile on failed in-process restart
2016-04-05 20:07:48 +08:00
Benny Ng b952fd8f8f revert to old Caddyfile on failed in-process restart 2016-04-05 19:57:52 +08:00
Benny Ng 28e0bfbbbe update README to include Docker
update README to include Docker container zzrot/alpine-caddy
2016-04-05 17:24:33 +08:00
Sean Kilgarriff 30ce73e8fb updated README to include ZZROT's docker container that runs caddy on Alpine Linux inside of a Docker Container. 2016-04-05 03:11:55 -04:00
elcore 987a5f98c4 Merge pull request #723 from mholt/fix-#721
Fix for #721
2016-04-05 04:54:49 +02:00
Eldin Hadzic 859a93d296 Fix for #721 2016-04-04 23:59:40 +00:00
Matt Holt a14fce0b1e Merge pull request #707 from jupiter/caching-headers
Add Etag header
2016-04-03 15:01:33 -06:00
Matt Holt 25b567b301 Merge pull request #715 from mholt/elcore-faster-tests
https: Faster tests
2016-04-03 14:33:03 -06:00
Matt Holt 3d066789d3 Merge pull request #713 from 5an1ty/patch-1
proxy: Allow mixed case Upgrade headers (WebSocket fix)
2016-04-03 14:30:32 -06:00
Pieter Raubenheimer 93d982a5a4 Improve readability and fully comply with RFC7232 2016-04-03 20:14:10 +01:00
Pieter Raubenheimer 572b9e4d67 Merge branch 'master' into caching-headers 2016-04-03 19:59:25 +01:00
elcore 2a82f7b520 Faster tests 2016-04-03 20:06:21 +02:00
Ruben Callewaert 1a9f700287 Allow mixed case Upgrade headers
Caddy expects websocket to be completely lowercase.
Some applications send websocket upgrade headers like the following:
`Upgrade: WebSocket`

This change allows all variations of websocket.
2016-04-03 17:48:53 +02:00
Matt Holt 462128cd80 Merge pull request #706 from elcore/patch-3
tls: Customize key type with key_type
2016-04-02 13:40:49 -06:00
elcore cf69d190a2 A new feature for the "tls" directive 2016-04-02 21:15:18 +02:00
Pieter Raubenheimer 3441cdef64 Add Etag header 2016-04-02 08:51:08 +01:00
Matt Holt 8a2f2f8d37 Merge pull request #705 from DenBeke/upstart-conf
dist/init: caddy.conf for upstart
2016-03-31 08:25:48 -06:00
MathiasB 86854dca89 dist/init: caddy.conf for upstart 2016-03-31 14:36:44 +02:00
Matt Holt b3a5b725db Merge pull request #696 from mholt/templateUtils
templates: Adding some useful utility functions
2016-03-28 21:17:11 -06:00
Craig Peterson f28d8b8601 Adding some useful utility functions for templates 2016-03-28 20:53:51 -06:00
Matt Holt 5989eb0635 Merge pull request #699 from eiszfuchs/socket-url
proxy: fix req.URL.Path for unix sockets
2016-03-28 14:31:53 -06:00
eiszfuchs cbd9b814b9 fixed req.URL.Path for unix: sockets 2016-03-28 22:18:14 +02:00
Matthew Holt 32dbbfd64c Better way of checking gofmt 2016-03-28 10:52:48 -06:00
Matt Holt 3395f6c775 Merge pull request #698 from buddhamagnet/master
Correct unused assignments
2016-03-26 17:28:46 -06:00
buddhamagnet 61cf8b79bc drop unused md variable 2016-03-26 21:14:54 +00:00
buddhamagnet bb6764fd22 drop unused len variable 2016-03-26 21:14:22 +00:00
buddhamagnet c981b08b23 correct unused assignments 2016-03-26 21:05:19 +00:00
Matthew Holt 7271b57136 Add golint and ineffassign to CI scripts
golint is not part of the tests since our Markdown dependency defines
an interface that is not lint-compliant (unfortunately).
See https://github.com/russross/blackfriday/issues/240
2016-03-26 14:26:12 -06:00
Matthew Holt 874bcff564 Fix YAML syntax. Sigh... 2016-03-26 09:23:05 -06:00
Matthew Holt eb279e7e8a Perform gofmt -s check 2016-03-26 08:53:00 -06:00
Matt Holt f4c729bd22 Merge pull request #691 from MariadeAnton/MdA-travis-test
Only bypass "bandwidth limit exceeded" errors on pushes
2016-03-22 09:27:20 -06:00
María de Antón ea35893be4 run bash dist/gitcookie.sh step only on build pushes 2016-03-22 16:15:17 +01:00
María de Antón 1efd1029dd Only bypass "bandwidth limit exceeded" errors on pushes 2016-03-22 16:08:29 +01:00
Matthew Holt 426d165254 expvar: Allow no args; publish number of goroutines 2016-03-21 22:39:57 -06:00
Matt Holt a3127bed5f Merge pull request #285 from mem/master
Add expvar middleware
2016-03-21 21:54:18 -06:00
Marcelo E. Magallon b94e513116 Add expvar middleware
Right now it has a very simple configuration:

	expvar /debug/vars

It will return a JSON object with memory statistics and the command line
used to start caddy, which are the two expvars that expvar registers by
default.
2016-03-21 21:46:41 -06:00
Matthew Holt b6e5a599fb Update change log 2016-03-21 12:36:27 -06:00
Matt Holt 8fc35edc3b Merge pull request #689 from tpng/restart-inproc
Add -restart=inproc option for in-process restart
2016-03-21 12:32:59 -06:00
Benny Ng 260c023e1e Add -restart=inproc flag for in process restart 2016-03-22 02:25:32 +08:00
Matthew Holt 27f9b58c5d Bypass "bandwidth limit exceeded" errors when cloning from googlesource
cf. golang/go#12933
2016-03-21 12:05:16 -06:00
Matthew Holt f23d8cb37f Add {upstream} placeholder when proxy middleware is used (closes #531)
Middlewares can now make their own placeholders that may be useful in
logging, on a per-request basis. Proxy is the first one to do this.
2016-03-20 21:56:13 -06:00
Matthew Holt 3f49b32086 Revert undesired changes to shell scripts 2016-03-20 14:13:50 -06:00
Matt Holt 0aacaea918 Merge pull request #686 from wmark/for-mholt
Reflow all bash scripts
2016-03-20 14:04:44 -06:00
Matt Holt 9e0b1b4216 Merge pull request #687 from abiosoft/fastcgi-except
fastcgi: Add `except` to FastCGI. Minor refactor in proxy.
2016-03-20 13:42:14 -06:00
Abiola Ibrahim e7001e6538 Add except to FastCGI. Minor refactor in proxy. 2016-03-20 08:02:17 +01:00
Matthew Holt 4d9741dda6 pprof: Only handle if path matches /debug/pprof, add tests 2016-03-19 20:02:05 -06:00
W-Mark Kubacki 74a5cb2fe3 Convert the barbarism in dist/automate.sh to proper BASH structure
When thy variables henceforth accept blessed white-space,
    guided will thy scripture be along righteous path(s).

    -- 4 BASH 3:42

Caddy's dist files sometimes ended up being owned by matt:staff or other
quite arcane and/or frightening names. If someone extracting didn't pay
attention a regular user who happened to have same uid by accident could
later tamper with the files' contents. It's 0:0 from now on.

Use all available threads when packaging distributables
Caddy binaries will be added to their archives in-place: This change
eliminates them being renamed within dist/builds one after another.
As does 'gox', dist/automate.sh will spare one available thread if possible.
2016-03-20 01:33:58 +01:00
W-Mark Kubacki ba2e9d80fd Use best practices in build.bash
Format of main.buildDate has been locale-dependent,
and is now ISO-8601 compliant.

Caddy displayed with ```-version``` something like (mind the datetime format):

    Caddy 0.8.2 (+591b209 Fri Mar 18 21:22:55 UTC 2016)
     2 files changed, 9 insertions(+), 4 deletions(-)
    build.bash
    main.go

which is now:

    Caddy 0.8.2 (+591b209 2016-03-18 21:22:55Z)
     2 files changed, 9 insertions(+), 4 deletions(-)
    build.bash,main.go

See also:

 * http://wiki.bash-hackers.org/scripting/obsolete
 * https://google.github.io/styleguide/shell.xml
 * https://xkcd.com/1179/
2016-03-19 17:04:53 +01:00
Abiola Ibrahim a05a664d56 Merge pull request #679 from abiosoft/case-insensitive-fs
Support for case insensitive paths using CASE_SENSITIVE_PATH env var.
2016-03-19 08:53:18 +01:00
Abiola Ibrahim 9f9fbf2e1b Support for case insensitive paths using CASE_SENSITIVE_PATH environment variable. 2016-03-19 08:45:23 +01:00
Matt Holt 63e4352db7 Merge pull request #612 from captncraig/pprof
pprof: Adding pprof middleware for profiling caddy.
2016-03-18 15:45:57 -06:00
Craig Peterson 640a0ef956 Adding pprof middleware for debugging purposes 2016-03-18 10:39:29 -06:00
Matt Holt 591b209024 Merge pull request #685 from wmark/for-mholt
Update unit files for systemd
2016-03-18 08:51:40 -06:00
W-Mark Kubacki f1c1ea9905 Service file for systemd starts after all networks have gotten IP addresses
Unlike network.target the network-online.target guarantees that the network
devices are online.

If you bind to 0.0.0.0, [::], [::1], and/or 127.0.0.1 only that is enough to
proceed. But in case a particular IP is needed, like ${COREOS_PUBLIC_IPV4},
we require any IP assignments to have completed before Caddy's start. That
is achieved by depending on systemd-networkd-wait-online.service (which is
scheduled before network-online.target, then, automatically).
2016-03-18 12:36:54 +01:00
Abiola Ibrahim 6b801b111b Merge pull request #684 from abiosoft/master
Fix for #659.
2016-03-18 07:08:24 +01:00
Matthew Holt 717c88ec0f Update contributing notes 2016-03-17 21:16:31 -06:00
Matt Holt 03a22aeb7e Merge pull request #683 from klingtnet/feat/systemd
systemd unit file
2016-03-17 21:05:12 -06:00
Matthew Holt 18332df358 Refactor output out of Start() 2016-03-17 17:58:40 -06:00
Matthew Holt b9f8c183fa gofmt! 2016-03-17 16:43:50 -06:00
Matthew Holt 37d050922b Fix typo, clarify readme 2016-03-17 16:42:28 -06:00
Abiola Ibrahim 04514fb791 Fix for #659. 2016-03-17 22:29:58 +01:00
Andreas Linz 6c2bf36dab Add systemd unit file and some usage instructions
Add systemd service file for caddy

Add some README with basic setup instructions

Explain how to view the service configuration

Add a note about permissions

Add a comment about run user and group

service->service unit

A systemd service can consist of different units. A unit configuration
file has the `.service` file ending which is a bit confusing, so please
be considerate if I'm confusing `service` and `unit` in the README

Fix typos/reword

Add contact information
2016-03-17 17:39:50 +01:00
Matt Holt 4f5fe2de24 Merge pull request #662 from mholt/md-include-fix
markdown: Included files in Markdown templates have access to document vars
2016-03-16 13:56:06 -06:00
Matthew Holt 90c24d2f32 Included files in Markdown templates have access to document vars (fixes #660)
Refactor how middleware.Context includes files
2016-03-16 13:42:16 -06:00
Matthew Holt d95c21ded5 Update readme with build script and link to CLI docs 2016-03-16 11:46:10 -06:00
Matthew Holt 4f4b34d481 Update changelog 2016-03-16 11:46:10 -06:00
Matthew Holt ed0342f171 Fix tests on Go tip (caused by golang/go#14827) 2016-03-16 11:46:10 -06:00
Matt Holt f14cdcc436 Merge pull request #682 from weingart/mime_fix
mime: Use a map, and error on duplicate extensions.
2016-03-16 08:40:10 -06:00
Tobias Weingartner b471b7e835 Fixup mime middleware to use a map and error on duplicate extensions.
- The mime middleware used filepath where it should arguably use path.
 - Changed the configuration to use a map instead of scanning an array
   during every request.  The map is static (after configuration), so
   should be fine for concurrent access.
 - Catch duplicate extensions within a configuration and error out.
 - Add tests for new error case.
2016-03-15 23:11:19 -07:00
Matt Holt b79ff7403f Merge pull request #664 from jupiter/max-connections
proxy: Add max_conns parameter for per-host maximum connections
2016-03-14 15:25:44 -06:00
Matt Holt 7560778602 Merge pull request #673 from ERIIX/master
Play nice when ACME used manually.
2016-03-13 14:50:33 -06:00
Matt Holt fc10951dde Merge pull request #676 from abiosoft/list-directives
Add flag to list directives.
2016-03-13 14:49:10 -06:00
Matt Holt 3e48e6a535 Merge pull request #657 from dprandzioch/feature/freebsd-service-script
Folder for init scripts within dist / FreeBSD service script
2016-03-13 14:46:06 -06:00
Abiola Ibrahim 44fc9b18a6 Print the directives in order of priority. 2016-03-13 18:29:26 +01:00
Abiola Ibrahim 3b6c387b84 Add flag to list directives. 2016-03-13 12:59:35 +01:00
Matthew Holt 35e4c1a7bf Sanity checkL this defer does not leak fds; comment added 2016-03-12 16:32:12 -07:00
Abiola Ibrahim 25bfdfe92c Merge pull request #672 from abiosoft/master
Hide only the currently used Caddyfile
2016-03-12 20:46:20 +01:00
Abiola Ibrahim 008ad398ce Hopefully, this is the final nail on the coffin. 2016-03-12 17:47:53 +01:00
Eric R. Monson 52d7379063 Play nice when ACME used manually.
Minor change to server/server.go such that /.well-known/acme-challenge
can be passed through when TLS.Manual is true on the vhost the request
came in through.
2016-03-11 22:37:54 -08:00
Abiola Ibrahim e92a911e7d Add more tests. 2016-03-11 23:44:50 +01:00
Abiola Ibrahim 84845a66ab Fix broken build. 2016-03-11 23:11:21 +01:00
Matt Holt e2f6ab3472 Merge pull request #671 from shawnps/patch-1
capitalize struct name in comment (go lint)
2016-03-11 08:21:56 -07:00
Abiola Ibrahim f3a183ecc1 Use filepath.Clean for fileserver. 2016-03-11 15:39:13 +01:00
Shawn Smith e958686ae4 capitalize struct name in comment 2016-03-11 23:16:28 +09:00
Pieter Raubenheimer 1f7d8d8ab0 Add test for UpstreamHost defaults 2016-03-10 14:45:23 +00:00
Pieter Raubenheimer a7766c9033 Add common method for checking host availability 2016-03-10 14:42:19 +00:00
Pieter Raubenheimer ce8ee831b3 Add check for per-host maximum connections 2016-03-08 16:25:05 +00:00
Matthew Holt 741d7685f1 Merge branch 'master' into fastcgi-methods
# Conflicts:
#	middleware/fastcgi/fastcgi.go
2016-03-07 16:25:23 -07:00
Matthew Holt 88e3a26c99 Full changes to contributing doc
That was weird, only half of the file got committed...
2016-03-07 12:10:26 -07:00
Matthew Holt f52b1e80f5 Update contributing doc and add issue template 2016-03-07 12:07:39 -07:00
David Prandzioch 202679efde Renamed apache24 occurance to caddy :-) 2016-03-06 10:49:29 +01:00
David Prandzioch 75915e0a25 Added a directory dist/init/ that may provide service scripts for various distributions in the future, added a experimental FreeBSD service script 2016-03-06 10:44:07 +01:00
Matt Holt 9e386fc921 Merge pull request #652 from elcore/patch-2
https: Support ECC keys
2016-03-03 08:19:20 -07:00
elcore 9099375b11 Support ECC certificates 2016-03-03 00:52:07 +00:00
Matthew Holt 36b440c04b https: Refuse start only if renewal fails on expired cert (closes #642) 2016-03-02 11:34:39 -07:00
Matthew Holt 2a46f2a14e Revert recent Content-Length-related changes and fix fastcgi return
fastcgi's ServeHTTP method originally returned the correct value (0) in
b51e8bc191. Later, I mistakenly suggested
we change that to return the status code because I forgot that status
codes aren't logged by the return value. So fastcgi broke due in
3966936bd6 due to my error.

We later had to try to make up for this with ugly Content-Length checks
like in c37ad7f677. Turns out that all we
had to do was fix the returned status here back to 0. The proxy
middleware behaves the same way, and returning 0 is correct. We should
only return a status code if the response has not been written, but with
upstream servers, we do write a response; they do not know about our
error handler.

Also clarifed this in the middleware.Handler documentation.
2016-03-02 11:33:40 -07:00
Matthew Holt 741880a38b Only obtain certificate and enable TLS if host qualifies (fixes #638) 2016-03-01 12:27:46 -07:00
Matt Holt 43c339c7e3 Merge pull request #641 from hkjn/fix-build-acme-crypt
https: Fix build after https://github.com/xenolf/lego/commit/0e26b
2016-02-27 10:21:44 -07:00
Henrik Jonsson 49c2807ba1 Fix build after https://github.com/xenolf/lego/commit/0e26b
Fix up last-second changes

Fixes #640
2016-02-27 18:06:56 +01:00
Matthew Holt da08c94a8c Implant version information with -ldflags with help of build script
Without -ldflags, the verison information needs to be updated manually,
which is never done between releases, so development builds appear
indiscernable from stable builds using `caddy -version`.

This is part of a set of changes intended to relieve the burden of
always updating version information manually and distributing binaries
that look stable but actually may not be.

A stable build is defined as one which is produced at a git tag with
a clean working directory (no uncommitted changes). A dev build is
anything else. With this build script, `caddy -version` will now reveal
whether it is a development build and, if so, the base version, the
latest commit, the date and time of build, and the names of files with
changes as well as how many changes were made.

The output of `caddy -version` for stable builds remains the same.
2016-02-26 00:26:31 -07:00
Matthew Holt 600ee9a89f fastcgi: Accept any other methods as a POST-style request 2016-01-31 21:36:39 -07:00
328 changed files with 22388 additions and 12994 deletions
+14
View File
@@ -0,0 +1,14 @@
# shell scripts should not use tabs to indent!
*.bash text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.sh text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
# files for systemd (shell-similar)
*.path text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.service text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.timer text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
# go fmt will enforce this, but in case a user has not called "go fmt" allow GIT to catch this:
*.go text eol=lf core.whitespace whitespace=indent-with-non-tab,trailing-space,tabwidth=4
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
.git* text eol=auto core.whitespace whitespace=trailing-space
+1
View File
@@ -3,6 +3,7 @@ Thumbs.db
_gitignore/
Vagrantfile
.vagrant/
/.idea
dist/builds/
dist/release/
+21 -5
View File
@@ -1,16 +1,32 @@
language: go
go:
- 1.6
- 1.7.4
- tip
env:
- CGO_ENABLED=0
matrix:
allow_failures:
- go: tip
fast_finish: true
before_install:
# Decrypts a script that installs an authenticated cookie
# for git to use when cloning from googlesource.com.
# Bypasses "bandwidth limit exceeded" errors.
# See github.com/golang/go/issues/12933
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then openssl aes-256-cbc -K $encrypted_3df18f9af81d_key -iv $encrypted_3df18f9af81d_iv -in dist/gitcookie.sh.enc -out dist/gitcookie.sh -d; fi
install:
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash dist/gitcookie.sh; fi
- go get -t ./...
- go get golang.org/x/tools/cmd/vet
- go get github.com/golang/lint/golint
- go get github.com/gordonklaus/ineffassign
script:
- diff <(echo -n) <(gofmt -s -d .)
- ineffassign .
- go vet ./...
- go test ./...
- go test -race ./...
after_script:
- golint ./...
+94 -25
View File
@@ -1,41 +1,109 @@
## Contributing to Caddy
**[Join our dev chat on Gitter](https://gitter.im/mholt/caddy)** to chat with
other Caddy developers! (Dev chat only; try our
[support room](https://gitter.im/caddyserver/support) for help or
[general](https://gitter.im/caddyserver/general) for anything else.)
This project gladly accepts contributions and we encourage interested users to
get involved!
Welcome! Our community focuses on helping others and making Caddy the best it
can be. We gladly accept contributions and encourage you to get involved!
#### For small tweaks, bug fixes, and tests
### Join us in the forum
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time.
Bug fixes should be under test to assert correct behavior. Thank you for
helping out in simple ways!
The [Caddy forum](https://forum.caddyserver.com) is the place for all discussion
that doesn't belong in issues or pull requests. Feel free to participate with us!
If you want to file a bug report or make an improvement to Caddy, however, you
should submit an issue or pull request.
#### Ideas, questions, bug reports
### Bug reports
Feel free to [open an issue](https://github.com/mholt/caddy/issues) with your
ideas, questions, and bug reports, if one does not already exist for it. Bug
reports should state expected behavior and contain clear instructions for
isolating and reproducing the problem.
See [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
Please [search this repository](https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93)
with a variety of keywords to ensure your bug is not already reported.
If unique, [open an issue](https://github.com/mholt/caddy/issues) and answer the
questions so we can understand and reproduce the problematic behavior.
The burden is on you to convince us that it is actually a bug in Caddy. This is
easiest to do when you write clear, concise instructions so we can reproduce
the behavior (even if it seems obvious). The more detailed and specific you are,
the faster we will be able to help you. Check out
[How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're
getting free help. If we helped you, please consider
[donating](https://caddyserver.com/donate) - it keeps us motivated!
#### New features
### Minor improvements and new tests
Before submitting a pull request, please open an issue first to discuss it and
claim it. This prevents overlapping efforts and keeps the project in-line with
its goals. If you prefer to discuss the feature privately, you can reach other
developers on Gitter or you may email me directly. (My email address is below.)
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time for
minor changes or new tests. Make sure to write tests to assert your change is
working properly and is thoroughly covered. We'll ask most pull requests to be
[squashed](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html),
especially with small commits.
And don't forget to write tests for new features!
Your pull request may be thoroughly reviewed. This is because if we accept the
PR, we also assume responsibility for it, although we would prefer you to
help maintain your code after it gets merged.
#### Vulnerabilities
### Proposals, suggestions, ideas, new features
First, please [search](https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93)
with a variety of keywords to ensure your suggestion/proposal is new.
If so, you may open either an issue or a pull request for discussion and
feedback.
The advantage of issues is that you don't have to spend time implementing your
idea, but you should still describe it thoroughly as if someone reading it would
implement the whole thing starting from scratch.
The advantage of pull requests is that we can immediately see the impact the
change will have on the project, what the code will look like, and how to
improve it. The disadvantage of pull requests is that they are unlikely to get
accepted without significant changes first, or it may be rejected entirely.
Don't worry, that won't happen without an open discussion first.
If you are going to spend significant time writing code for a new pull request,
best to open an issue to "claim" it and get feedback before you invest a lot of
time. Not all pull requests are merged, and that's okay,
[Read why.](https://github.com/turbolinks/turbolinks/pull/124#issuecomment-239826060)
Remember: pull requests should always be thoroughly documented both via godoc
and with at least a rough draft of documentation that might go on the website
for users to read.
### Collaborator status
If your pull request is merged, congratulations! You're technically a
collaborator. We may also grant you "Collaborator status" which means you can
push to the repository and merge other pull requests. We hope that you will
stay involved by reviewing pull requests, submitting more of your own, and
resolving issues as you are able to. Thanks for making Caddy amazing!
We ask that collaborators will conduct thorough code reviews and be nice to
new contributors. Before merging a PR, it's best to get the approval of
at least one or two other collaborators and/or the project owner. We prefer
squashed commits instead of many little, semantically-unimportant commits. Also,
CI and other post-commit hooks must pass before being merged except in certain
unusual circumstances.
Collaborator status may be removed for inactive users from time to time as
we see fit; this is not an insult, just a basic security precaution in case
the account becomes inactive or abandoned. Privileges can always be restored
later.
**Reviewing pull requests:** Please help submit and review pull requests as
you are able! We would ask that every pull request be reviewed by at least
one collaborator who did not open the pull request before merging. This will
help ensure high code quality as new collaborators are added to the project.
Read [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments)
on the Go wiki for an idea of what we look for in good, clean Go code.
### Vulnerabilities
If you've found a vulnerability that is serious, please email me: Matthew dot
Holt at Gmail. If it's not a big deal, a pull request will probably be faster.
@@ -43,4 +111,5 @@ Holt at Gmail. If it's not a big deal, a pull request will probably be faster.
## Thank you
Thanks for your help! Caddy would not be what it is today without your contributions.
Thanks for your help! Caddy would not be what it is today without your
contributions.
+24
View File
@@ -0,0 +1,24 @@
(Are you asking for help with using Caddy? Please use our forum instead: https://forum.caddyserver.com. If you are filing a bug report, please answer the following questions. If your issue is not a bug report, you do not need to use this template. Either way, please consider donating if we've helped you. Thanks!)
### 1. What version of Caddy are you running (`caddy -version`)?
### 2. What are you trying to do?
### 3. What is your entire Caddyfile?
```text
(Put Caddyfile here)
```
### 4. How did you run Caddy (give the full command and describe the execution environment)?
### 5. What did you expect to see?
### 6. What did you see instead (give full error messages and/or log)?
### 7. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
+91 -100
View File
@@ -1,164 +1,152 @@
[![Caddy](https://caddyserver.com/resources/images/caddy-boxed.png)](https://caddyserver.com)
<a href="https://caddyserver.com"><img src="https://caddyserver.com/resources/images/caddy-lower.png" alt="Caddy" width="350"></a>
[![Dev Chat](https://img.shields.io/badge/dev%20chat-gitter-ff69b4.svg?style=flat-square&label=dev+chat&color=ff69b4)](https://gitter.im/mholt/caddy)
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/mholt/caddy)
[![Linux Build Status](https://img.shields.io/travis/mholt/caddy.svg?style=flat-square&label=linux+build)](https://travis-ci.org/mholt/caddy)
[![Windows Build Status](https://img.shields.io/appveyor/ci/mholt/caddy.svg?style=flat-square&label=windows+build)](https://ci.appveyor.com/project/mholt/caddy)
Caddy is a lightweight, general-purpose web server for Windows, Mac, Linux, BSD
and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android).
It is a capable alternative to other popular and easy to use web servers.
([@caddyserver](https://twitter.com/caddyserver) on Twitter)
The most notable features are HTTP/2, [Let's Encrypt](https://letsencrypt.org)
support, Virtual Hosts, TLS + SNI, and easy configuration with a
[Caddyfile](https://caddyserver.com/docs/caddyfile). In development, you usually
put one Caddyfile with each site. In production, Caddy serves HTTPS by default
and manages all cryptographic assets for you.
[Download](https://github.com/mholt/caddy/releases) ·
[User Guide](https://caddyserver.com/docs)
[![community](https://img.shields.io/badge/community-forum-ff69b4.svg?style=flat-square)](https://forum.caddyserver.com) [![twitter](https://img.shields.io/badge/twitter-@caddyserver-55acee.svg?style=flat-square)](https://twitter.com/caddyserver) [![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)
[![Go Report Card](https://goreportcard.com/badge/github.com/mholt/caddy?style=flat-square)](https://goreportcard.com/report/mholt/caddy)
Caddy is a 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 but easier alternative to other popular web servers.
### Menu
[Releases](https://github.com/mholt/caddy/releases) ·
[User Guide](https://caddyserver.com/docs) ·
[Community](https://forum.caddyserver.com)
- [Getting Caddy](#getting-caddy)
Try browsing [the code on Sourcegraph](https://sourcegraph.com/github.com/mholt/caddy)!
## Menu
- [Features](#features)
- [Quick Start](#quick-start)
- [Running from Source](#running-from-source)
- [Running in Production](#running-in-production)
- [Contributing](#contributing)
- [About the Project](#about-the-project)
## Features
## Getting Caddy
Caddy binaries have no dependencies and are available for nearly every platform.
[Latest release](https://github.com/mholt/caddy/releases/latest)
- **Easy configuration** with Caddyfile
- **Automatic HTTPS** via [Let's Encrypt](https://letsencrypt.org); Caddy
obtains and manages all cryptographic assets for you
- **HTTP/2** enabled by default (powered by Go standard library)
- **Virtual hosting** for hundreds of sites per server instance, including TLS
SNI
- Experimental **QUIC support** for those that like speed
- TLS session ticket **key rotation** for more secure connections
- **Brilliant extensibility** so Caddy can be customized for your needs
- **Runs anywhere** with **no external dependencies** (not even libc)
## Quick Start
The website has [full documentation](https://caddyserver.com/docs) but this will
get you started in about 30 seconds:
Caddy binaries have no dependencies and are available for every platform.
Install Caddy any one of these ways:
Place a file named "Caddyfile" with your site. Paste this into it and save:
- **[Download page](https://caddyserver.com/download)** allows you to
customize your build in the browser
- **[Latest release](https://github.com/mholt/caddy/releases/latest)** for
pre-built binaries
- **curl [getcaddy.com](https://getcaddy.com)** for auto install:
`curl https://getcaddy.com | bash`
```
Once `caddy` is in your PATH, you can `cd` to your website's folder and run
`caddy` to serve it. By default, Caddy serves the current directory at
[localhost:2015](http://localhost:2015).
To customize how your site is served, create a file named Caddyfile by your
site and paste this into it:
```plain
localhost
gzip
browse
ext .html
websocket /echo cat
log ../access.log
ext .html
log /var/log/access.log
proxy /api 127.0.0.1:7005
header /api Access-Control-Allow-Origin *
```
Run `caddy` from that directory, and it will automatically use that Caddyfile to
configure itself.
When you run `caddy` in that directory, it will automatically find and use
that Caddyfile to configure itself.
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.
This simple file enables compression, allows directory browsing (for folders
without an index file), hosts a WebSocket echo server at /echo, serves clean
URLs, logs requests to access.log, proxies all API requests to a backend on
port 7005, and adds the coveted `Access-Control-Allow-Origin: *` header for
all responses from the API.
Wow! Caddy can do a lot with just a few lines.
To host multiple sites and do more with the Caddyfile, please see the
[Caddyfile documentation](https://caddyserver.com/docs/caddyfile).
#### Defining multiple sites
Note that production sites are served over
[HTTPS by default](https://caddyserver.com/docs/automatic-https).
You can run multiple sites from the same Caddyfile, too:
```
site1.com {
# ...
}
site2.com, sub.site2.com {
# ...
}
```
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.
Caddy has a command line interface. Run `caddy -h` to view basic help or see
the [CLI documentation](https://caddyserver.com/docs/cli) for details.
**Running as root:** We advise against this. You can still listen on ports
< 1024 using setcap like so: `sudo setcap cap_net_bind_service=+ep ./caddy`
## Running from Source
Note: You will need **[Go 1.6](https://golang.org/dl/)** or newer.
Note: You will need **[Go 1.7](https://golang.org/dl/)** or newer.
1. `$ go get github.com/mholt/caddy`
1. `go get github.com/mholt/caddy/caddy`
2. `cd` into your website's directory
3. Run `caddy` (assumes `$GOPATH/bin` is in your `$PATH`)
3. Run `caddy` (assuming `$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.
Caddy's `main()` is in the caddy subfolder. To recompile Caddy, use
`build.bash` found in that folder.
#### Docker Container
## Running in Production
Caddy is available as a Docker container from any of these sources:
- [abiosoft/caddy](https://hub.docker.com/r/abiosoft/caddy/)
- [darron/caddy](https://hub.docker.com/r/darron/caddy/)
- [joshix/caddy](https://hub.docker.com/r/joshix/caddy/)
- [jumanjiman/caddy](https://hub.docker.com/r/jumanjiman/caddy/)
- [zenithar/nano-caddy](https://hub.docker.com/r/zenithar/nano-caddy/)
#### 3rd-party dependencies
Although Caddy's binaries are completely static, Caddy relies on some excellent
libraries. [Godoc.org](https://godoc.org/github.com/mholt/caddy) shows the
packages that each Caddy package imports.
The Caddy project does not officially maintain any system-specific
integrations, but your download file includes
[unofficial resources](https://github.com/mholt/caddy/tree/master/dist/init)
contributed by the community that you may find helpful for running Caddy in
production.
How you choose to run Caddy is up to you. Many users are satisfied with
`nohup caddy &`. Others use `screen`. Users who need Caddy to come back up
after reboots either do so in the script that caused the reboot, add a command
to an init script, or configure a service with their OS.
## Contributing
**[Join our dev chat on Gitter](https://gitter.im/mholt/caddy)** to chat with
other Caddy developers! (Dev chat only; try our
[support room](https://gitter.im/caddyserver/support) for help or
[general](https://gitter.im/caddyserver/general) for anything else.)
**[Join our community](https://forum.caddyserver.com) where you can chat with
other Caddy users and developers!**
This project would not be what it is without your help. Please see the
[contributing guidelines](https://github.com/mholt/caddy/blob/master/CONTRIBUTING.md)
if you haven't already.
Please see our [contributing guidelines](https://github.com/mholt/caddy/blob/master/CONTRIBUTING.md)
and check out the [developer wiki](https://github.com/mholt/caddy/wiki).
We use GitHub issues and pull requests only for discussing bug reports and
the development of specific changes. We welcome all other topics on the
[forum](https://forum.caddyserver.com)!
If you want to contribute to the documentation, please submit pull requests to [caddyserver/caddyserver.com](https://github.com/caddyserver/caddyserver.com).
Thanks for making Caddy -- and the Web -- better!
Special thanks to
[![DigitalOcean](http://i.imgur.com/sfGr0eY.png)](https://www.digitalocean.com)
[![DigitalOcean](https://i.imgur.com/sfGr0eY.png)](https://www.digitalocean.com)
for hosting the Caddy project.
## About the project
## About the Project
Caddy was born out of the need for a "batteries-included" web server that runs
anywhere and doesn't have to take its configuration with it. Caddy took
@@ -168,5 +156,8 @@ inspiration from [spark](https://github.com/rif/spark),
and [Vagrant](https://www.vagrantup.com/),
which provides a pleasant mixture of features from each of them.
**The name "Caddy":** The name of the software is "Caddy", not "Caddy Server"
or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the
Caddy web server". See [brand guidelines](https://caddyserver.com/brand).
*Twitter: [@mholt6](https://twitter.com/mholt6)*
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
+10 -5
View File
@@ -6,21 +6,26 @@ clone_folder: c:\gopath\src\github.com\mholt\caddy
environment:
GOPATH: c:\gopath
CGO_ENABLED: 0
install:
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.6.windows-amd64.zip
- 7z x go1.6.windows-amd64.zip -y -oC:\ > NUL
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.7.4.windows-amd64.zip
- 7z x go1.7.4.windows-amd64.zip -y -oC:\ > NUL
- go version
- go env
- go get golang.org/x/tools/cmd/vet
- go get -t ./...
- go get github.com/golang/lint/golint
- go get github.com/gordonklaus/ineffassign
- set PATH=%GOPATH%\bin;%PATH%
build: off
test_script:
- go vet ./...
- go test ./...
- go test -race ./...
- ineffassign .
after_test:
- golint ./...
deploy: off
+10 -5
View File
@@ -1,4 +1,4 @@
package assets
package caddy
import (
"os"
@@ -6,10 +6,15 @@ import (
"runtime"
)
// Path returns the path to the folder
// where the application may store data. This
// currently resolves to ~/.caddy
func Path() string {
// AssetsPath returns the path to the folder
// where the application may store data. If
// CADDYPATH env variable is set, that value
// is used. Otherwise, the path is the result
// of evaluating "$HOME/.caddy".
func AssetsPath() string {
if caddyPath := os.Getenv("CADDYPATH"); caddyPath != "" {
return caddyPath
}
return filepath.Join(userHomeDir(), ".caddy")
}
+19
View File
@@ -0,0 +1,19 @@
package caddy
import (
"os"
"strings"
"testing"
)
func TestAssetsPath(t *testing.T) {
if actual := AssetsPath(); !strings.HasSuffix(actual, ".caddy") {
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
}
os.Setenv("CADDYPATH", "testpath")
if actual, expected := AssetsPath(), "testpath"; actual != expected {
t.Errorf("Expected path to be %v, got: %v", expected, actual)
}
os.Setenv("CADDYPATH", "")
}
+847
View File
@@ -0,0 +1,847 @@
// Package caddy implements the Caddy server manager.
//
// To use this package:
//
// 1. Set the AppName and AppVersion variables.
// 2. Call LoadCaddyfile() to get the Caddyfile.
// Pass in the name of the server type (like "http").
// 3. Call caddy.Start() to start Caddy. You get back
// an Instance, on which you can call Restart() to
// restart it or Stop() to stop it.
//
// You should call Wait() on your instance to wait for
// all servers to quit before your process exits.
package caddy
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/mholt/caddy/caddyfile"
)
// Configurable application parameters
var (
// AppName is the name of the application.
AppName string
// AppVersion is the version of the application.
AppVersion string
// Quiet mode will not show any informative output on initialization.
Quiet bool
// PidFile is the path to the pidfile to create.
PidFile string
// GracefulTimeout is the maximum duration of a graceful shutdown.
GracefulTimeout time.Duration
// isUpgrade will be set to true if this process
// was started as part of an upgrade, where a parent
// Caddy process started this one.
isUpgrade bool
// started will be set to true when the first
// instance is started; it never gets set to
// false after that.
started bool
// mu protects the variables 'isUpgrade' and 'started'.
mu sync.Mutex
)
// Instance contains the state of servers created as a result of
// calling Start and can be used to access or control those servers.
type Instance struct {
// serverType is the name of the instance's server type
serverType string
// caddyfileInput is the input configuration text used for this process
caddyfileInput Input
// wg is used to wait for all servers to shut down
wg *sync.WaitGroup
// context is the context created for this instance.
context Context
// servers is the list of servers with their listeners.
servers []ServerListener
// these callbacks execute when certain events occur
onFirstStartup []func() error // starting, not as part of a restart
onStartup []func() error // starting, even as part of a restart
onRestart []func() error // before restart commences
onShutdown []func() error // stopping, even as part of a restart
onFinalShutdown []func() error // stopping, not as part of a restart
}
// Servers returns the ServerListeners in i.
func (i *Instance) Servers() []ServerListener { return i.servers }
// Stop stops all servers contained in i. It does NOT
// execute shutdown callbacks.
func (i *Instance) Stop() error {
// stop the servers
for _, s := range i.servers {
if gs, ok := s.server.(GracefulServer); ok {
if err := gs.Stop(); err != nil {
log.Printf("[ERROR] Stopping %s: %v", gs.Address(), err)
}
}
}
// splice i out of instance list, causing it to be garbage-collected
instancesMu.Lock()
for j, other := range instances {
if other == i {
instances = append(instances[:j], instances[j+1:]...)
break
}
}
instancesMu.Unlock()
return nil
}
// ShutdownCallbacks executes all the shutdown callbacks of i,
// including ones that are scheduled only for the final shutdown
// of i. An error returned from one does not stop execution of
// the rest. All the non-nil errors will be returned.
func (i *Instance) ShutdownCallbacks() []error {
var errs []error
for _, shutdownFunc := range i.onShutdown {
err := shutdownFunc()
if err != nil {
errs = append(errs, err)
}
}
for _, finalShutdownFunc := range i.onFinalShutdown {
err := finalShutdownFunc()
if err != nil {
errs = append(errs, err)
}
}
return errs
}
// Restart replaces the servers in i with new servers created from
// executing the newCaddyfile. Upon success, it returns the new
// instance to replace i. Upon failure, i will not be replaced.
func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
log.Println("[INFO] Reloading")
i.wg.Add(1)
defer i.wg.Done()
// run restart callbacks
for _, fn := range i.onRestart {
err := fn()
if err != nil {
return i, err
}
}
if newCaddyfile == nil {
newCaddyfile = i.caddyfileInput
}
// Add file descriptors of all the sockets that are capable of it
restartFds := make(map[string]restartTriple)
for _, s := range i.servers {
gs, srvOk := s.server.(GracefulServer)
ln, lnOk := s.listener.(Listener)
pc, pcOk := s.packet.(PacketConn)
if srvOk {
if lnOk && pcOk {
restartFds[gs.Address()] = restartTriple{server: gs, listener: ln, packet: pc}
continue
}
if lnOk {
restartFds[gs.Address()] = restartTriple{server: gs, listener: ln}
continue
}
if pcOk {
restartFds[gs.Address()] = restartTriple{server: gs, packet: pc}
continue
}
}
}
// create new instance; if the restart fails, it is simply discarded
newInst := &Instance{serverType: newCaddyfile.ServerType(), wg: i.wg}
// attempt to start new instance
err := startWithListenerFds(newCaddyfile, newInst, restartFds)
if err != nil {
return i, err
}
// success! stop the old instance
for _, shutdownFunc := range i.onShutdown {
err := shutdownFunc()
if err != nil {
return i, err
}
}
i.Stop()
log.Println("[INFO] Reloading complete")
return newInst, nil
}
// SaveServer adds s and its associated listener ln to the
// internally-kept list of servers that is running. For
// saved servers, graceful restarts will be provided.
func (i *Instance) SaveServer(s Server, ln net.Listener) {
i.servers = append(i.servers, ServerListener{server: s, listener: ln})
}
// HasListenerWithAddress returns whether this package is
// tracking a server using a listener with the address
// addr.
func HasListenerWithAddress(addr string) bool {
instancesMu.Lock()
defer instancesMu.Unlock()
for _, inst := range instances {
for _, sln := range inst.servers {
if listenerAddrEqual(sln.listener, addr) {
return true
}
}
}
return false
}
// listenerAddrEqual compares a listener's address with
// addr. Extra care is taken to match addresses with an
// empty hostname portion, as listeners tend to report
// [::]:80, for example, when the matching address that
// created the listener might be simply :80.
func listenerAddrEqual(ln net.Listener, addr string) bool {
lnAddr := ln.Addr().String()
hostname, port, err := net.SplitHostPort(addr)
if err != nil {
return lnAddr == addr
}
if lnAddr == net.JoinHostPort("::", port) {
return true
}
if lnAddr == net.JoinHostPort("0.0.0.0", port) {
return true
}
return hostname != "" && lnAddr == addr
}
// TCPServer is a type that can listen and serve connections.
// A TCPServer must associate with exactly zero or one net.Listeners.
type TCPServer interface {
// Listen starts listening by creating a new listener
// and returning it. It does not start accepting
// connections. For UDP-only servers, this method
// can be a no-op that returns (nil, nil).
Listen() (net.Listener, error)
// Serve starts serving using the provided listener.
// Serve must start the server loop nearly immediately,
// or at least not return any errors before the server
// loop begins. Serve blocks indefinitely, or in other
// words, until the server is stopped. For UDP-only
// servers, this method can be a no-op that returns nil.
Serve(net.Listener) error
}
// UDPServer is a type that can listen and serve packets.
// A UDPServer must associate with exactly zero or one net.PacketConns.
type UDPServer interface {
// ListenPacket starts listening by creating a new packetconn
// and returning it. It does not start accepting connections.
// TCP-only servers may leave this method blank and return
// (nil, nil).
ListenPacket() (net.PacketConn, error)
// ServePacket starts serving using the provided packetconn.
// ServePacket must start the server loop nearly immediately,
// or at least not return any errors before the server
// loop begins. ServePacket blocks indefinitely, or in other
// words, until the server is stopped. For TCP-only servers,
// this method can be a no-op that returns nil.
ServePacket(net.PacketConn) error
}
// Server is a type that can listen and serve. It supports both
// TCP and UDP, although the UDPServer interface can be used
// for more than just UDP.
//
// If the server uses TCP, it should implement TCPServer completely.
// If it uses UDP or some other protocol, it should implement
// UDPServer completely. If it uses both, both interfaces should be
// fully implemented. Any unimplemented methods should be made as
// no-ops that simply return nil values.
type Server interface {
TCPServer
UDPServer
}
// Stopper is a type that can stop serving. The stop
// does not necessarily have to be graceful.
type Stopper interface {
// Stop stops the server. It blocks until the
// server is completely stopped.
Stop() error
}
// GracefulServer is a Server and Stopper, the stopping
// of which is graceful (whatever that means for the kind
// of server being implemented). It must be able to return
// the address it is configured to listen on so that its
// listener can be paired with it upon graceful restarts.
// The net.Listener that a GracefulServer creates must
// implement the Listener interface for restarts to be
// graceful (assuming the listener is for TCP).
type GracefulServer interface {
Server
Stopper
// Address returns the address the server should
// listen on; it is used to pair the server to
// its listener during a graceful/zero-downtime
// restart. Thus when implementing this method,
// you must not access a listener to get the
// address; you must store the address the
// server is to serve on some other way.
Address() string
}
// Listener is a net.Listener with an underlying file descriptor.
// A server's listener should implement this interface if it is
// to support zero-downtime reloads.
type Listener interface {
net.Listener
File() (*os.File, error)
}
// PacketConn is a net.PacketConn with an underlying file descriptor.
// A server's packetconn should implement this interface if it is
// to support zero-downtime reloads (in sofar this holds true for datagram
// connections).
type PacketConn interface {
net.PacketConn
File() (*os.File, error)
}
// AfterStartup is an interface that can be implemented
// by a server type that wants to run some code after all
// servers for the same Instance have started.
type AfterStartup interface {
OnStartupComplete()
}
// LoadCaddyfile loads a Caddyfile by calling the plugged in
// Caddyfile loader methods. An error is returned if more than
// one loader returns a non-nil Caddyfile input. If no loaders
// load a Caddyfile, the default loader is used. If no default
// loader is registered or it returns nil, the server type's
// default Caddyfile is loaded. If the server type does not
// specify any default Caddyfile value, then an empty Caddyfile
// is returned. Consequently, this function never returns a nil
// value as long as there are no errors.
func LoadCaddyfile(serverType string) (Input, error) {
// Ask plugged-in loaders for a Caddyfile
cdyfile, err := loadCaddyfileInput(serverType)
if err != nil {
return nil, err
}
// Otherwise revert to default
if cdyfile == nil {
cdyfile = DefaultInput(serverType)
}
// Still nil? Geez.
if cdyfile == nil {
cdyfile = CaddyfileInput{ServerTypeName: serverType}
}
return cdyfile, nil
}
// Wait blocks until all of i's servers have stopped.
func (i *Instance) Wait() {
i.wg.Wait()
}
// 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, serverType string) (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.
// NOTE: 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(),
ServerTypeName: serverType,
}, nil
}
// not having input from the pipe is not itself an error,
// just means no input to return.
return nil, nil
}
// Caddyfile returns the Caddyfile used to create i.
func (i *Instance) Caddyfile() Input {
return i.caddyfileInput
}
// Start starts Caddy with the given Caddyfile.
//
// This function blocks until all the servers are listening.
func Start(cdyfile Input) (*Instance, error) {
writePidFile()
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup)}
return inst, startWithListenerFds(cdyfile, inst, nil)
}
func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartTriple) error {
if cdyfile == nil {
cdyfile = CaddyfileInput{}
}
stypeName := cdyfile.ServerType()
stype, err := getServerType(stypeName)
if err != nil {
return err
}
inst.caddyfileInput = cdyfile
sblocks, err := loadServerBlocks(stypeName, cdyfile.Path(), bytes.NewReader(cdyfile.Body()))
if err != nil {
return err
}
inst.context = stype.NewContext()
if inst.context == nil {
return fmt.Errorf("server type %s produced a nil Context", stypeName)
}
sblocks, err = inst.context.InspectServerBlocks(cdyfile.Path(), sblocks)
if err != nil {
return err
}
err = executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks)
if err != nil {
return err
}
slist, err := inst.context.MakeServers()
if err != nil {
return err
}
// run startup callbacks
if restartFds == nil {
for _, firstStartupFunc := range inst.onFirstStartup {
err := firstStartupFunc()
if err != nil {
return err
}
}
}
for _, startupFunc := range inst.onStartup {
err := startupFunc()
if err != nil {
return err
}
}
err = startServers(slist, inst, restartFds)
if err != nil {
return err
}
instancesMu.Lock()
instances = append(instances, inst)
instancesMu.Unlock()
// run any AfterStartup callbacks if this is not
// part of a restart; then show file descriptor notice
if restartFds == nil {
for _, srvln := range inst.servers {
if srv, ok := srvln.server.(AfterStartup); ok {
srv.OnStartupComplete()
}
}
if !Quiet {
for _, srvln := range inst.servers {
if !IsLoopback(srvln.listener.Addr().String()) {
checkFdlimit()
break
}
}
}
}
mu.Lock()
started = true
mu.Unlock()
return nil
}
func executeDirectives(inst *Instance, filename string,
directives []string, sblocks []caddyfile.ServerBlock) error {
// map of server block ID to map of directive name to whatever.
storages := make(map[int]map[string]interface{})
// It is crucial that directives are executed in the proper order.
// We loop with the directives on the outer loop so we execute
// a directive for all server blocks before going to the next directive.
// This is important mainly due to the parsing callbacks (below).
for _, dir := range directives {
for i, sb := range sblocks {
var once sync.Once
if _, ok := storages[i]; !ok {
storages[i] = make(map[string]interface{})
}
for j, key := range sb.Keys {
// Execute directive if it is in the server block
if tokens, ok := sb.Tokens[dir]; ok {
controller := &Controller{
instance: inst,
Key: key,
Dispenser: caddyfile.NewDispenserTokens(filename, tokens),
OncePerServerBlock: func(f func() error) error {
var err error
once.Do(func() {
err = f()
})
return err
},
ServerBlockIndex: i,
ServerBlockKeyIndex: j,
ServerBlockKeys: sb.Keys,
ServerBlockStorage: storages[i][dir],
}
setup, err := DirectiveAction(inst.serverType, dir)
if err != nil {
return err
}
err = setup(controller)
if err != nil {
return err
}
storages[i][dir] = controller.ServerBlockStorage // persist for this server block
}
}
}
// See if there are any callbacks to execute after this directive
if allCallbacks, ok := parsingCallbacks[inst.serverType]; ok {
callbacks := allCallbacks[dir]
for _, callback := range callbacks {
if err := callback(inst.context); err != nil {
return err
}
}
}
}
return nil
}
func startServers(serverList []Server, inst *Instance, restartFds map[string]restartTriple) error {
errChan := make(chan error, len(serverList))
for _, s := range serverList {
var (
ln net.Listener
pc net.PacketConn
err error
)
// If this is a reload and s is a GracefulServer,
// reuse the listener for a graceful restart.
if gs, ok := s.(GracefulServer); ok && restartFds != nil {
addr := gs.Address()
if old, ok := restartFds[addr]; ok {
// listener
if old.listener != nil {
file, err := old.listener.File()
if err != nil {
return err
}
ln, err = net.FileListener(file)
if err != nil {
return err
}
file.Close()
}
// packetconn
if old.packet != nil {
file, err := old.packet.File()
if err != nil {
return err
}
pc, err = net.FilePacketConn(file)
if err != nil {
return err
}
file.Close()
}
}
}
if ln == nil {
ln, err = s.Listen()
if err != nil {
return err
}
}
if pc == nil {
pc, err = s.ListenPacket()
if err != nil {
return err
}
}
inst.wg.Add(2)
go func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
defer inst.wg.Done()
go func() {
errChan <- s.Serve(ln)
defer inst.wg.Done()
}()
errChan <- s.ServePacket(pc)
}(s, ln, pc, inst)
inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc})
}
// Log errors that may be returned from Serve() calls,
// these errors should only be occurring in the server loop.
go func() {
for err := range errChan {
if err == nil {
continue
}
if strings.Contains(err.Error(), "use of closed network connection") {
// this error is normal when closing the listener
continue
}
log.Println(err)
}
}()
return nil
}
func getServerType(serverType string) (ServerType, error) {
stype, ok := serverTypes[serverType]
if ok {
return stype, nil
}
if len(serverTypes) == 0 {
return ServerType{}, fmt.Errorf("no server types plugged in")
}
if serverType == "" {
if len(serverTypes) == 1 {
for _, stype := range serverTypes {
return stype, nil
}
}
return ServerType{}, fmt.Errorf("multiple server types available; must choose one")
}
return ServerType{}, fmt.Errorf("unknown server type '%s'", serverType)
}
func loadServerBlocks(serverType, filename string, input io.Reader) ([]caddyfile.ServerBlock, error) {
validDirectives := ValidDirectives(serverType)
serverBlocks, err := caddyfile.Parse(filename, input, validDirectives)
if err != nil {
return nil, err
}
if len(serverBlocks) == 0 && serverTypes[serverType].DefaultInput != nil {
newInput := serverTypes[serverType].DefaultInput()
serverBlocks, err = caddyfile.Parse(newInput.Path(),
bytes.NewReader(newInput.Body()), validDirectives)
if err != nil {
return nil, err
}
}
return serverBlocks, nil
}
// Stop stops ALL servers. It blocks until they are all stopped.
// It does NOT execute shutdown callbacks, and it deletes all
// instances after stopping is completed. Do not re-use any
// references to old instances after calling Stop.
func Stop() error {
// This awkward for loop is to avoid a deadlock since
// inst.Stop() also acquires the instancesMu lock.
for {
instancesMu.Lock()
if len(instances) == 0 {
break
}
inst := instances[0]
instancesMu.Unlock()
if err := inst.Stop(); err != nil {
log.Printf("[ERROR] Stopping %s: %v", inst.serverType, err)
}
}
return nil
}
// IsLoopback returns true if the hostname of addr looks
// explicitly like a common local hostname. addr must only
// be a host or a host:port combination.
func IsLoopback(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr // happens if the addr is just a hostname
}
return host == "localhost" ||
strings.Trim(host, "[]") == "::1" ||
strings.HasPrefix(host, "127.")
}
// Upgrade re-launches the process, preserving the listeners
// for a graceful restart. It does NOT load new configuration;
// it only starts the process anew with a fresh binary.
//
// TODO: This is not yet implemented
func Upgrade() error {
return fmt.Errorf("not implemented")
// TODO: have child process set isUpgrade = true
}
// IsUpgrade returns true if this process is part of an upgrade
// where a parent caddy process spawned this one to ugprade
// the binary.
func IsUpgrade() bool {
mu.Lock()
defer mu.Unlock()
return isUpgrade
}
// Started returns true if at least one instance has been
// started by this package. It never gets reset to false
// once it is set to true.
func Started() bool {
mu.Lock()
defer mu.Unlock()
return started
}
// CaddyfileInput represents a Caddyfile as input
// and is simply a convenient way to implement
// the Input interface.
type CaddyfileInput struct {
Filepath string
Contents []byte
ServerTypeName string
}
// Body returns c.Contents.
func (c CaddyfileInput) Body() []byte { return c.Contents }
// Path returns c.Filepath.
func (c CaddyfileInput) Path() string { return c.Filepath }
// ServerType returns c.ServerType.
func (c CaddyfileInput) ServerType() string { return c.ServerTypeName }
// 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
// The type of server this input is intended for
ServerType() string
}
// DefaultInput returns the default Caddyfile input
// to use when it is otherwise empty or missing.
// It uses the default host and port (depends on
// host, e.g. localhost is 2015, otherwise 443) and
// root.
func DefaultInput(serverType string) Input {
if _, ok := serverTypes[serverType]; !ok {
return nil
}
if serverTypes[serverType].DefaultInput == nil {
return nil
}
return serverTypes[serverType].DefaultInput()
}
// writePidFile writes the process ID to the file at PidFile.
// It does nothing if PidFile is not set.
func writePidFile() error {
if PidFile == "" {
return nil
}
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
return ioutil.WriteFile(PidFile, pid, 0644)
}
type restartTriple struct {
server GracefulServer
listener Listener
packet PacketConn
}
var (
// instances is the list of running Instances.
instances []*Instance
// instancesMu protects instances.
instancesMu sync.Mutex
)
var (
// DefaultConfigFile is the name of the configuration file that is loaded
// by default if no other file is specified.
DefaultConfigFile = "Caddyfile"
)
-12
View File
@@ -1,12 +0,0 @@
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)
}
}
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
#
# Caddy build script. Automates proper versioning.
#
# Usage:
#
# $ ./build.bash [output_filename] [git_repo]
#
# Outputs compiled program in current directory.
# Default git repo is current directory.
# Builds always take place from current directory.
set -euo pipefail
: ${output_filename:="${1:-}"}
: ${output_filename:=""}
: ${git_repo:="${2:-}"}
: ${git_repo:="."}
pkg=github.com/mholt/caddy/caddy/caddymain
ldflags=()
# Timestamp of build
name="${pkg}.buildDate"
value=$(date -u +"%a %b %d %H:%M:%S %Z %Y")
ldflags+=("-X" "\"${name}=${value}\"")
# Current tag, if HEAD is on a tag
name="${pkg}.gitTag"
set +e
value="$(git -C "${git_repo}" describe --exact-match HEAD 2>/dev/null)"
set -e
ldflags+=("-X" "\"${name}=${value}\"")
# Nearest tag on branch
name="${pkg}.gitNearestTag"
value="$(git -C "${git_repo}" describe --abbrev=0 --tags HEAD)"
ldflags+=("-X" "\"${name}=${value}\"")
# Commit SHA
name="${pkg}.gitCommit"
value="$(git -C "${git_repo}" rev-parse --short HEAD)"
ldflags+=("-X" "\"${name}=${value}\"")
# Summary of uncommitted changes
name="${pkg}.gitShortStat"
value="$(git -C "${git_repo}" diff-index --shortstat HEAD)"
ldflags+=("-X" "\"${name}=${value}\"")
# List of modified files
name="${pkg}.gitFilesModified"
value="$(git -C "${git_repo}" diff-index --name-only HEAD)"
ldflags+=("-X" "\"${name}=${value}\"")
go build -ldflags "${ldflags[*]}" -o "${output_filename}"
-389
View File
@@ -1,389 +0,0 @@
// Package caddy implements the Caddy web server as a service
// in your own Go programs.
//
// To use this package, follow a few simple steps:
//
// 1. Set the AppName and AppVersion variables.
// 2. Call LoadCaddyfile() to get the Caddyfile (it
// might have been piped in as part of a restart).
// You should pass in your own Caddyfile loader.
// 3. Call caddy.Start() to start Caddy, caddy.Stop()
// to stop it, or caddy.Restart() to restart it.
//
// You should use caddy.Wait() to wait for all Caddy servers
// to quit before your process exits.
package caddy
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"path"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/mholt/caddy/caddy/https"
"github.com/mholt/caddy/server"
)
// Configurable application parameters
var (
// AppName is the name of the application.
AppName string
// AppVersion is the version of the application.
AppVersion string
// Quiet when set to true, will not show any informative output on initialization.
Quiet bool
// HTTP2 indicates whether HTTP2 is enabled or not.
HTTP2 bool
// PidFile is the path to the pidfile to create.
PidFile string
// GracefulTimeout is the maximum duration of a graceful shutdown.
GracefulTimeout time.Duration
)
var (
// caddyfile is the input configuration text used for this process
caddyfile Input
// caddyfileMu protects caddyfile during changes
caddyfileMu sync.Mutex
// errIncompleteRestart occurs if this process is a fork
// of the parent but no Caddyfile was piped in
errIncompleteRestart = errors.New("incomplete restart")
// servers is a list of all the currently-listening servers
servers []*server.Server
// serversMu protects the servers slice during changes
serversMu sync.Mutex
// wg is used to wait for all servers to shut down
wg sync.WaitGroup
// loadedGob is used if this is a child process as part of
// a graceful restart; it is used to map listeners to their
// index in the list of inherited file descriptors. This
// variable is not safe for concurrent access.
loadedGob caddyfileGob
// startedBefore should be set to true if caddy has been started
// at least once (does not indicate whether currently running).
startedBefore bool
)
const (
// DefaultHost is the default host.
DefaultHost = ""
// DefaultPort is the default port.
DefaultPort = "2015"
// DefaultRoot is the default root folder.
DefaultRoot = "."
)
// Start starts Caddy with the given Caddyfile. If cdyfile
// is nil, the LoadCaddyfile function will be called to get
// one.
//
// This function blocks until all the servers are listening.
//
// Note (POSIX): If Start is called in the child process of a
// restart more than once within the duration of the graceful
// cutoff (i.e. the child process called Start a first time,
// then called Stop, then Start again within the first 5 seconds
// or however long GracefulTimeout is) and the Caddyfiles have
// at least one listener address in common, the second Start
// may fail with "address already in use" as there's no
// guarantee that the parent process has relinquished the
// address before the grace period ends.
func Start(cdyfile Input) (err error) {
// If we return with no errors, we must do two things: tell the
// parent that we succeeded and write to the pidfile.
defer func() {
if err == nil {
signalSuccessToParent() // TODO: Is doing this more than once per process a bad idea? Start could get called more than once in other apps.
if PidFile != "" {
err := writePidFile()
if err != nil {
log.Printf("[ERROR] Could not write pidfile: %v", err)
}
}
}
}()
// Input must never be nil; try to load something
if cdyfile == nil {
cdyfile, err = LoadCaddyfile(nil)
if err != nil {
return err
}
}
caddyfileMu.Lock()
caddyfile = cdyfile
caddyfileMu.Unlock()
// load the server configs (activates Let's Encrypt)
configs, err := loadConfigs(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
if err != nil {
return err
}
// group virtualhosts by address
groupings, err := arrangeBindings(configs)
if err != nil {
return err
}
// Start each server with its one or more configurations
err = startServers(groupings)
if err != nil {
return err
}
startedBefore = true
// Show initialization output
if !Quiet && !IsRestart() {
var checkedFdLimit bool
for _, group := range groupings {
for _, conf := range group.Configs {
// Print address of site
fmt.Println(conf.Address())
// Note if non-localhost site resolves to loopback interface
if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
fmt.Printf("Notice: %s is only accessible on this machine (%s)\n",
conf.Host, group.BindAddr.IP.String())
}
if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
checkFdlimit()
checkedFdLimit = true
}
}
}
}
return nil
}
// startServers starts all the servers in groupings,
// taking into account whether or not this process is
// a child from a graceful restart or not. It blocks
// until the servers are listening.
func startServers(groupings bindingGroup) error {
var startupWg sync.WaitGroup
errChan := make(chan error, len(groupings)) // must be buffered to allow Serve functions below to return if stopped later
for _, group := range groupings {
s, err := server.New(group.BindAddr.String(), group.Configs, GracefulTimeout)
if err != nil {
return err
}
s.HTTP2 = HTTP2
s.ReqCallback = https.RequestCallback // ensures we can solve ACME challenges while running
if s.OnDemandTLS {
s.TLSConfig.GetCertificate = https.GetOrObtainCertificate // TLS on demand -- awesome!
} else {
s.TLSConfig.GetCertificate = https.GetCertificate
}
var ln server.ListenerFile
if IsRestart() {
// Look up this server's listener in the map of inherited file descriptors;
// if we don't have one, we must make a new one (later).
if fdIndex, ok := loadedGob.ListenerFds[s.Addr]; ok {
file := os.NewFile(fdIndex, "")
fln, err := net.FileListener(file)
if err != nil {
return err
}
ln, ok = fln.(server.ListenerFile)
if !ok {
return errors.New("listener for " + s.Addr + " was not a ListenerFile")
}
file.Close()
delete(loadedGob.ListenerFds, s.Addr)
}
}
wg.Add(1)
go func(s *server.Server, ln server.ListenerFile) {
defer wg.Done()
// run startup functions that should only execute when
// the original parent process is starting.
if !IsRestart() && !startedBefore {
err := s.RunFirstStartupFuncs()
if err != nil {
errChan <- err
return
}
}
// start the server
if ln != nil {
errChan <- s.Serve(ln)
} else {
errChan <- s.ListenAndServe()
}
}(s, ln)
startupWg.Add(1)
go func(s *server.Server) {
defer startupWg.Done()
s.WaitUntilStarted()
}(s)
serversMu.Lock()
servers = append(servers, s)
serversMu.Unlock()
}
// Close the remaining (unused) file descriptors to free up resources
if IsRestart() {
for key, fdIndex := range loadedGob.ListenerFds {
os.NewFile(fdIndex, "").Close()
delete(loadedGob.ListenerFds, key)
}
}
// Wait for all servers to finish starting
startupWg.Wait()
// Return the first error, if any
select {
case err := <-errChan:
// "use of closed network connection" is normal if it was a graceful shutdown
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
return err
}
default:
}
return nil
}
// Stop stops all servers. It blocks until they are all stopped.
// It does NOT execute shutdown callbacks that may have been
// configured by middleware (they must be executed separately).
func Stop() error {
https.Deactivate()
serversMu.Lock()
for _, s := range servers {
if err := s.Stop(); err != nil {
log.Printf("[ERROR] Stopping %s: %v", s.Addr, err)
}
}
servers = []*server.Server{} // don't reuse servers
serversMu.Unlock()
return nil
}
// Wait blocks until all servers are stopped.
func Wait() {
wg.Wait()
}
// LoadCaddyfile loads a Caddyfile, prioritizing a Caddyfile
// piped from stdin as part of a restart (only happens on first call
// to LoadCaddyfile). If it is not a restart, this function tries
// calling the user's loader function, and if that returns nil, then
// this function resorts to the default configuration. Thus, if there
// are no other errors, this function always returns at least the
// default Caddyfile.
func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) {
// If we are a fork, finishing the restart is highest priority;
// piped input is required in this case.
if IsRestart() {
err := gob.NewDecoder(os.Stdin).Decode(&loadedGob)
if err != nil {
return nil, err
}
cdyfile = loadedGob.Caddyfile
atomic.StoreInt32(https.OnDemandIssuedCount, loadedGob.OnDemandTLSCertsIssued)
}
// Try user's loader
if cdyfile == nil && loader != nil {
cdyfile, err = loader()
}
// Otherwise revert to default
if cdyfile == nil {
cdyfile = DefaultInput()
}
return
}
// CaddyfileFromPipe loads the Caddyfile input from f if f is
// not interactive input. f is assumed to be a pipe or stream,
// such as os.Stdin. If f is not a pipe, no error is returned
// but the Input value will be nil. An error is only returned
// if there was an error reading the pipe, even if the length
// of what was read is 0.
func CaddyfileFromPipe(f *os.File) (Input, error) {
fi, err := f.Stat()
if err == nil && fi.Mode()&os.ModeCharDevice == 0 {
// Note that a non-nil error is not a problem. Windows
// will not create a stdin if there is no pipe, which
// produces an error when calling Stat(). But Unix will
// make one either way, which is why we also check that
// bitmask.
// BUG: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X)
confBody, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
return CaddyfileInput{
Contents: confBody,
Filepath: f.Name(),
}, nil
}
// not having input from the pipe is not itself an error,
// just means no input to return.
return nil, nil
}
// Caddyfile returns the current Caddyfile
func Caddyfile() Input {
caddyfileMu.Lock()
defer caddyfileMu.Unlock()
return caddyfile
}
// Input represents a Caddyfile; its contents and file path
// (which should include the file name at the end of the path).
// If path does not apply (e.g. piped input) you may use
// any understandable value. The path is mainly used for logging,
// error messages, and debugging.
type Input interface {
// Gets the Caddyfile contents
Body() []byte
// Gets the path to the origin file
Path() string
// IsFile returns true if the original input was a file on the file system
// that could be loaded again later if requested.
IsFile() bool
}
-32
View File
@@ -1,32 +0,0 @@
package caddy
import (
"net/http"
"testing"
"time"
)
func TestCaddyStartStop(t *testing.T) {
caddyfile := "localhost:1984"
for i := 0; i < 2; i++ {
err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
if err != nil {
t.Fatalf("Error starting, iteration %d: %v", i, err)
}
client := http.Client{
Timeout: time.Duration(2 * time.Second),
}
resp, err := client.Get("http://localhost:1984")
if err != nil {
t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err)
}
resp.Body.Close()
err = Stop()
if err != nil {
t.Fatalf("Error stopping, iteration %d: %v", i, err)
}
}
}
+290
View File
@@ -0,0 +1,290 @@
package caddymain
import (
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"gopkg.in/natefinch/lumberjack.v2"
"github.com/xenolf/lego/acme"
"github.com/mholt/caddy"
// plug in the HTTP server type
_ "github.com/mholt/caddy/caddyhttp"
"github.com/mholt/caddy/caddytls"
// This is where other plugins get plugged in (imported)
)
func init() {
caddy.TrapSignals()
setVersion()
flag.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
flag.StringVar(&caddytls.DefaultCAUrl, "ca", "https://acme-v01.api.letsencrypt.org/directory", "URL to certificate authority's ACME server directory")
flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
flag.StringVar(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address")
flag.DurationVar(&acme.HTTPClient.Timeout, "catimeout", acme.HTTPClient.Timeout, "Default ACME CA HTTP timeout")
flag.StringVar(&logfile, "log", "", "Process log file")
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
flag.StringVar(&serverType, "type", "http", "Type of server to run")
flag.BoolVar(&version, "version", false, "Show version")
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
}
// Run is Caddy's main() function.
func Run() {
flag.Parse()
caddy.AppName = appName
caddy.AppVersion = appVersion
acme.UserAgent = appName + "/" + appVersion
// Set up process log before anything bad happens
switch logfile {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "":
log.SetOutput(ioutil.Discard)
default:
log.SetOutput(&lumberjack.Logger{
Filename: logfile,
MaxSize: 100,
MaxAge: 14,
MaxBackups: 10,
})
}
// Check for one-time actions
if revoke != "" {
err := caddytls.Revoke(revoke)
if err != nil {
mustLogFatalf(err.Error())
}
fmt.Printf("Revoked certificate for %s\n", revoke)
os.Exit(0)
}
if version {
fmt.Printf("%s %s\n", appName, appVersion)
if devBuild && gitShortStat != "" {
fmt.Printf("%s\n%s\n", gitShortStat, gitFilesModified)
}
os.Exit(0)
}
if plugins {
fmt.Println(caddy.DescribePlugins())
os.Exit(0)
}
moveStorage() // TODO: This is temporary for the 0.9 release, or until most users upgrade to 0.9+
// Set CPU cap
err := setCPU(cpu)
if err != nil {
mustLogFatalf(err.Error())
}
// Get Caddyfile input
caddyfile, err := caddy.LoadCaddyfile(serverType)
if err != nil {
mustLogFatalf(err.Error())
}
// Start your engines
instance, err := caddy.Start(caddyfile)
if err != nil {
mustLogFatalf(err.Error())
}
// Twiddle your thumbs
instance.Wait()
}
// mustLogFatalf wraps log.Fatalf() in a way that ensures the
// output is always printed to stderr so the user can see it
// if the user is still there, even if the process log was not
// enabled. If this process is an upgrade, however, and the user
// might not be there anymore, this just logs to the process
// log and exits.
func mustLogFatalf(format string, args ...interface{}) {
if !caddy.IsUpgrade() {
log.SetOutput(os.Stderr)
}
log.Fatalf(format, args...)
}
// confLoader loads the Caddyfile using the -conf flag.
func confLoader(serverType string) (caddy.Input, error) {
if conf == "" {
return nil, nil
}
if conf == "stdin" {
return caddy.CaddyfileFromPipe(os.Stdin, serverType)
}
contents, err := ioutil.ReadFile(conf)
if err != nil {
return nil, err
}
return caddy.CaddyfileInput{
Contents: contents,
Filepath: conf,
ServerTypeName: serverType,
}, nil
}
// defaultLoader loads the Caddyfile from the current working directory.
func defaultLoader(serverType string) (caddy.Input, error) {
contents, err := ioutil.ReadFile(caddy.DefaultConfigFile)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
return caddy.CaddyfileInput{
Contents: contents,
Filepath: caddy.DefaultConfigFile,
ServerTypeName: serverType,
}, nil
}
// moveStorage moves the old certificate storage location by
// renaming the "letsencrypt" folder to the hostname of the
// CA URL. This is TEMPORARY until most users have upgraded to 0.9+.
func moveStorage() {
oldPath := filepath.Join(caddy.AssetsPath(), "letsencrypt")
_, err := os.Stat(oldPath)
if os.IsNotExist(err) {
return
}
// Just use a default config to get default (file) storage
fileStorage, err := new(caddytls.Config).StorageFor(caddytls.DefaultCAUrl)
if err != nil {
mustLogFatalf("[ERROR] Unable to get new path for certificate storage: %v", err)
}
newPath := fileStorage.(*caddytls.FileStorage).Path
err = os.MkdirAll(string(newPath), 0700)
if err != nil {
mustLogFatalf("[ERROR] Unable to make new certificate storage path: %v\n\nPlease follow instructions at:\nhttps://github.com/mholt/caddy/issues/902#issuecomment-228876011", err)
}
err = os.Rename(oldPath, string(newPath))
if err != nil {
mustLogFatalf("[ERROR] Unable to migrate certificate storage: %v\n\nPlease follow instructions at:\nhttps://github.com/mholt/caddy/issues/902#issuecomment-228876011", err)
}
// convert mixed case folder and file names to lowercase
var done bool // walking is recursive and preloads the file names, so we must restart walk after a change until no changes
for !done {
done = true
filepath.Walk(string(newPath), func(path string, info os.FileInfo, err error) error {
// must be careful to only lowercase the base of the path, not the whole thing!!
base := filepath.Base(path)
if lowerBase := strings.ToLower(base); base != lowerBase {
lowerPath := filepath.Join(filepath.Dir(path), lowerBase)
err = os.Rename(path, lowerPath)
if err != nil {
mustLogFatalf("[ERROR] Unable to lower-case: %v\n\nPlease follow instructions at:\nhttps://github.com/mholt/caddy/issues/902#issuecomment-228876011", err)
}
// terminate traversal and restart since Walk needs the updated file list with new file names
done = false
return errors.New("start over")
}
return nil
})
}
}
// setVersion figures out the version information
// based on variables set by -ldflags.
func setVersion() {
// A development build is one that's not at a tag or has uncommitted changes
devBuild = gitTag == "" || gitShortStat != ""
// Only set the appVersion if -ldflags was used
if gitNearestTag != "" || gitTag != "" {
if devBuild && gitNearestTag != "" {
appVersion = fmt.Sprintf("%s (+%s %s)",
strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate)
} else if gitTag != "" {
appVersion = strings.TrimPrefix(gitTag, "v")
}
}
}
// 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
}
const appName = "Caddy"
// Flags that control program flow or startup
var (
serverType string
conf string
cpu string
logfile string
revoke string
version bool
plugins bool
)
// Build information obtained with the help of -ldflags
var (
appVersion = "(untracked dev build)" // inferred at startup
devBuild = true // inferred at startup
buildDate string // date -u
gitTag string // git describe --exact-match HEAD 2> /dev/null
gitNearestTag string // git describe --abbrev=0 --tags HEAD
gitCommit string // git rev-parse HEAD
gitShortStat string // git diff-index --shortstat
gitFilesModified string // git diff-index --name-only HEAD
)
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package caddymain
import (
"runtime"
-348
View File
@@ -1,348 +0,0 @@
package caddy
import (
"bytes"
"fmt"
"io"
"log"
"net"
"sync"
"github.com/mholt/caddy/caddy/https"
"github.com/mholt/caddy/caddy/parse"
"github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/server"
)
const (
// DefaultConfigFile is the name of the configuration file that is loaded
// by default if no other file is specified.
DefaultConfigFile = "Caddyfile"
)
// loadConfigsUpToIncludingTLS loads the configs from input with name filename and returns them,
// the parsed server blocks, the index of the last directive it processed, and an error (if any).
func loadConfigsUpToIncludingTLS(filename string, input io.Reader) ([]server.Config, []parse.ServerBlock, int, error) {
var configs []server.Config
// Each server block represents similar hosts/addresses, since they
// were grouped together in the Caddyfile.
serverBlocks, err := parse.ServerBlocks(filename, input, true)
if err != nil {
return nil, nil, 0, err
}
if len(serverBlocks) == 0 {
newInput := DefaultInput()
serverBlocks, err = parse.ServerBlocks(newInput.Path(), bytes.NewReader(newInput.Body()), true)
if err != nil {
return nil, nil, 0, err
}
}
var lastDirectiveIndex int // we set up directives in two parts; this stores where we left off
// Iterate each server block and make a config for each one,
// executing the directives that were parsed in order up to the tls
// directive; this is because we must activate Let's Encrypt.
for i, sb := range serverBlocks {
onces := makeOnces()
storages := makeStorages()
for j, addr := range sb.Addresses {
config := server.Config{
Host: addr.Host,
Port: addr.Port,
Scheme: addr.Scheme,
Root: Root,
ConfigFile: filename,
AppName: AppName,
AppVersion: AppVersion,
}
// It is crucial that directives are executed in the proper order.
for k, dir := range directiveOrder {
// Execute directive if it is in the server block
if tokens, ok := sb.Tokens[dir.name]; ok {
// Each setup function gets a controller, from which setup functions
// get access to the config, tokens, and other state information useful
// to set up its own host only.
controller := &setup.Controller{
Config: &config,
Dispenser: parse.NewDispenserTokens(filename, tokens),
OncePerServerBlock: func(f func() error) error {
var err error
onces[dir.name].Do(func() {
err = f()
})
return err
},
ServerBlockIndex: i,
ServerBlockHostIndex: j,
ServerBlockHosts: sb.HostList(),
ServerBlockStorage: storages[dir.name],
}
// execute setup function and append middleware handler, if any
midware, err := dir.setup(controller)
if err != nil {
return nil, nil, lastDirectiveIndex, err
}
if midware != nil {
config.Middleware = append(config.Middleware, midware)
}
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
}
// Stop after TLS setup, since we need to activate Let's Encrypt before continuing;
// it makes some changes to the configs that middlewares might want to know about.
if dir.name == "tls" {
lastDirectiveIndex = k
break
}
}
configs = append(configs, config)
}
}
return configs, serverBlocks, lastDirectiveIndex, nil
}
// loadConfigs reads input (named filename) and parses it, returning the
// server configurations in the order they appeared in the input. As part
// of this, it activates Let's Encrypt for the configs that are produced.
// Thus, the returned configs are already optimally configured for HTTPS.
func loadConfigs(filename string, input io.Reader) ([]server.Config, error) {
configs, serverBlocks, lastDirectiveIndex, err := loadConfigsUpToIncludingTLS(filename, input)
if err != nil {
return nil, err
}
// Now we have all the configs, but they have only been set up to the
// point of tls. We need to activate Let's Encrypt before setting up
// the rest of the middlewares so they have correct information regarding
// TLS configuration, if necessary. (this only appends, so our iterations
// over server blocks below shouldn't be affected)
if !IsRestart() && !Quiet {
fmt.Print("Activating privacy features...")
}
configs, err = https.Activate(configs)
if err != nil {
return nil, err
} else if !IsRestart() && !Quiet {
fmt.Println(" done.")
}
// Finish setting up the rest of the directives, now that TLS is
// optimally configured. These loops are similar to above except
// we don't iterate all the directives from the beginning and we
// don't create new configs.
configIndex := -1
for i, sb := range serverBlocks {
onces := makeOnces()
storages := makeStorages()
for j := range sb.Addresses {
configIndex++
for k := lastDirectiveIndex + 1; k < len(directiveOrder); k++ {
dir := directiveOrder[k]
if tokens, ok := sb.Tokens[dir.name]; ok {
controller := &setup.Controller{
Config: &configs[configIndex],
Dispenser: parse.NewDispenserTokens(filename, tokens),
OncePerServerBlock: func(f func() error) error {
var err error
onces[dir.name].Do(func() {
err = f()
})
return err
},
ServerBlockIndex: i,
ServerBlockHostIndex: j,
ServerBlockHosts: sb.HostList(),
ServerBlockStorage: storages[dir.name],
}
midware, err := dir.setup(controller)
if err != nil {
return nil, err
}
if midware != nil {
configs[configIndex].Middleware = append(configs[configIndex].Middleware, midware)
}
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
}
}
}
}
return configs, nil
}
// makeOnces makes a map of directive name to sync.Once
// instance. This is intended to be called once per server
// block when setting up configs so that Setup functions
// for each directive can perform a task just once per
// server block, even if there are multiple hosts on the block.
//
// We need one Once per directive, otherwise the first
// directive to use it would exclude other directives from
// using it at all, which would be a bug.
func makeOnces() map[string]*sync.Once {
onces := make(map[string]*sync.Once)
for _, dir := range directiveOrder {
onces[dir.name] = new(sync.Once)
}
return onces
}
// makeStorages makes a map of directive name to interface{}
// so that directives' setup functions can persist state
// between different hosts on the same server block during the
// setup phase.
func makeStorages() map[string]interface{} {
storages := make(map[string]interface{})
for _, dir := range directiveOrder {
storages[dir.name] = nil
}
return storages
}
// arrangeBindings groups configurations by their bind address. For example,
// a server that should listen on localhost and another on 127.0.0.1 will
// be grouped into the same address: 127.0.0.1. It will return an error
// if an address is malformed or a TLS listener is configured on the
// same address as a plaintext HTTP listener. The return value is a map of
// bind address to list of configs that would become VirtualHosts on that
// server. Use the keys of the returned map to create listeners, and use
// the associated values to set up the virtualhosts.
func arrangeBindings(allConfigs []server.Config) (bindingGroup, error) {
var groupings bindingGroup
// Group configs by bind address
for _, conf := range allConfigs {
// use default port if none is specified
if conf.Port == "" {
conf.Port = Port
}
bindAddr, warnErr, fatalErr := resolveAddr(conf)
if fatalErr != nil {
return groupings, fatalErr
}
if warnErr != nil {
log.Printf("[WARNING] Resolving bind address for %s: %v", conf.Address(), warnErr)
}
// Make sure to compare the string representation of the address,
// not the pointer, since a new *TCPAddr is created each time.
var existing bool
for i := 0; i < len(groupings); i++ {
if groupings[i].BindAddr.String() == bindAddr.String() {
groupings[i].Configs = append(groupings[i].Configs, conf)
existing = true
break
}
}
if !existing {
groupings = append(groupings, bindingMapping{
BindAddr: bindAddr,
Configs: []server.Config{conf},
})
}
}
// Don't allow HTTP and HTTPS to be served on the same address
for _, group := range groupings {
isTLS := group.Configs[0].TLS.Enabled
for _, config := range group.Configs {
if config.TLS.Enabled != isTLS {
thisConfigProto, otherConfigProto := "HTTP", "HTTP"
if config.TLS.Enabled {
thisConfigProto = "HTTPS"
}
if group.Configs[0].TLS.Enabled {
otherConfigProto = "HTTPS"
}
return groupings, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address",
group.Configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto)
}
}
}
return groupings, nil
}
// resolveAddr determines the address (host and port) that a config will
// bind to. The returned address, resolvAddr, should be used to bind the
// listener or group the config with other configs using the same address.
// The first error, if not nil, is just a warning and should be reported
// but execution may continue. The second error, if not nil, is a real
// problem and the server should not be started.
//
// This function does not handle edge cases like port "http" or "https" if
// they are not known to the system. It does, however, serve on the wildcard
// host if resolving the address of the specific hostname fails.
func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr, fatalErr error) {
resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.BindHost, conf.Port))
if warnErr != nil {
// the hostname probably couldn't be resolved, just bind to wildcard then
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("", conf.Port))
if fatalErr != nil {
return
}
}
return
}
// validDirective returns true if d is a valid
// directive; false otherwise.
func validDirective(d string) bool {
for _, dir := range directiveOrder {
if dir.name == d {
return true
}
}
return false
}
// DefaultInput returns the default Caddyfile input
// to use when it is otherwise empty or missing.
// It uses the default host and port (depends on
// host, e.g. localhost is 2015, otherwise 443) and
// root.
func DefaultInput() CaddyfileInput {
port := Port
if https.HostQualifies(Host) && port == DefaultPort {
port = "443"
}
return CaddyfileInput{
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, port, Root)),
}
}
// These defaults are configurable through the command line
var (
// Root is the site root
Root = DefaultRoot
// Host is the site host
Host = DefaultHost
// Port is the site port
Port = DefaultPort
)
// bindingMapping maps a network address to configurations
// that will bind to it. The order of the configs is important.
type bindingMapping struct {
BindAddr *net.TCPAddr
Configs []server.Config
}
// bindingGroup maps network addresses to their configurations.
// Preserving the order of the groupings is important
// (related to graceful shutdown and restart)
// so this is a slice, not a literal map.
type bindingGroup []bindingMapping
-159
View File
@@ -1,159 +0,0 @@
package caddy
import (
"reflect"
"sync"
"testing"
"github.com/mholt/caddy/server"
)
func TestDefaultInput(t *testing.T) {
if actual, expected := string(DefaultInput().Body()), ":2015\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
// next few tests simulate user providing -host and/or -port flags
Host = "not-localhost.com"
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:443\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
Host = "[::1]"
if actual, expected := string(DefaultInput().Body()), "[::1]:2015\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
Host = "127.0.1.1"
if actual, expected := string(DefaultInput().Body()), "127.0.1.1:2015\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
Host = "not-localhost.com"
Port = "1234"
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:1234\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
Host = DefaultHost
Port = "1234"
if actual, expected := string(DefaultInput().Body()), ":1234\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
}
func TestResolveAddr(t *testing.T) {
// NOTE: If tests fail due to comparing to string "127.0.0.1",
// it's possible that system env resolves with IPv6, or ::1.
// If that happens, maybe we should use actualAddr.IP.IsLoopback()
// for the assertion, rather than a direct string comparison.
// NOTE: Tests with {Host: "", Port: ""} and {Host: "localhost", Port: ""}
// will not behave the same cross-platform, so they have been omitted.
for i, test := range []struct {
config server.Config
shouldWarnErr bool
shouldFatalErr bool
expectedIP string
expectedPort int
}{
{server.Config{Host: "127.0.0.1", Port: "1234"}, false, false, "<nil>", 1234},
{server.Config{Host: "localhost", Port: "80"}, false, false, "<nil>", 80},
{server.Config{BindHost: "localhost", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{BindHost: "127.0.0.1", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{BindHost: "should-not-resolve", Port: "1234"}, true, false, "<nil>", 1234},
{server.Config{BindHost: "localhost", Port: "http"}, false, false, "127.0.0.1", 80},
{server.Config{BindHost: "localhost", Port: "https"}, false, false, "127.0.0.1", 443},
{server.Config{BindHost: "", Port: "1234"}, false, false, "<nil>", 1234},
{server.Config{BindHost: "localhost", Port: "abcd"}, false, true, "", 0},
{server.Config{BindHost: "127.0.0.1", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{BindHost: "localhost", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
{server.Config{BindHost: "should-not-resolve", Host: "localhost", Port: "1234"}, true, false, "<nil>", 1234},
} {
actualAddr, warnErr, fatalErr := resolveAddr(test.config)
if test.shouldFatalErr && fatalErr == nil {
t.Errorf("Test %d: Expected error, but there wasn't any", i)
}
if !test.shouldFatalErr && fatalErr != nil {
t.Errorf("Test %d: Expected no error, but there was one: %v", i, fatalErr)
}
if fatalErr != nil {
continue
}
if test.shouldWarnErr && warnErr == nil {
t.Errorf("Test %d: Expected warning, but there wasn't any", i)
}
if !test.shouldWarnErr && warnErr != nil {
t.Errorf("Test %d: Expected no warning, but there was one: %v", i, warnErr)
}
if actual, expected := actualAddr.IP.String(), test.expectedIP; actual != expected {
t.Errorf("Test %d: IP was %s but expected %s", i, actual, expected)
}
if actual, expected := actualAddr.Port, test.expectedPort; actual != expected {
t.Errorf("Test %d: Port was %d but expected %d", i, actual, expected)
}
}
}
func TestMakeOnces(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
onces := makeOnces()
if len(onces) != len(directives) {
t.Errorf("onces had len %d , expected %d", len(onces), len(directives))
}
expected := map[string]*sync.Once{
"dummy": new(sync.Once),
"dummy2": new(sync.Once),
}
if !reflect.DeepEqual(onces, expected) {
t.Errorf("onces was %v, expected %v", onces, expected)
}
}
func TestMakeStorages(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
storages := makeStorages()
if len(storages) != len(directives) {
t.Errorf("storages had len %d , expected %d", len(storages), len(directives))
}
expected := map[string]interface{}{
"dummy": nil,
"dummy2": nil,
}
if !reflect.DeepEqual(storages, expected) {
t.Errorf("storages was %v, expected %v", storages, expected)
}
}
func TestValidDirective(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
for i, test := range []struct {
directive string
valid bool
}{
{"dummy", true},
{"dummy2", true},
{"dummy3", false},
} {
if actual, expected := validDirective(test.directive), test.valid; actual != expected {
t.Errorf("Test %d: valid was %t, expected %t", i, actual, expected)
}
}
}
-98
View File
@@ -1,98 +0,0 @@
package caddy
import (
"github.com/mholt/caddy/caddy/https"
"github.com/mholt/caddy/caddy/parse"
"github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/middleware"
)
func init() {
// The parse package must know which directives
// are valid, but it must not import the setup
// or config package. To solve this problem, we
// fill up this map in our init function here.
// The parse package does not need to know the
// ordering of the directives.
for _, dir := range directiveOrder {
parse.ValidDirectives[dir.name] = struct{}{}
}
}
// Directives are registered in the order they should be
// executed. Middleware (directives that inject a handler)
// are executed in the order A-B-C-*-C-B-A, assuming
// they all call the Next handler in the chain.
//
// Ordering is VERY important. Every middleware will
// feel the effects of all other middleware below
// (after) them during a request, but they must not
// care what middleware above them are doing.
//
// For example, log needs to know the status code and
// exactly how many bytes were written to the client,
// which every other middleware can affect, so it gets
// registered first. The errors middleware does not
// care if gzip or log modifies its response, so it
// gets registered below them. Gzip, on the other hand,
// DOES care what errors does to the response since it
// must compress every output to the client, even error
// pages, so it must be registered before the errors
// middleware and any others that would write to the
// response.
var directiveOrder = []directive{
// Essential directives that initialize vital configuration settings
{"root", setup.Root},
{"bind", setup.BindHost},
{"tls", https.Setup},
// Other directives that don't create HTTP handlers
{"startup", setup.Startup},
{"shutdown", setup.Shutdown},
// Directives that inject handlers (middleware)
{"log", setup.Log},
{"gzip", setup.Gzip},
{"errors", setup.Errors},
{"header", setup.Headers},
{"rewrite", setup.Rewrite},
{"redir", setup.Redir},
{"ext", setup.Ext},
{"mime", setup.Mime},
{"basicauth", setup.BasicAuth},
{"internal", setup.Internal},
{"proxy", setup.Proxy},
{"fastcgi", setup.FastCGI},
{"websocket", setup.WebSocket},
{"markdown", setup.Markdown},
{"templates", setup.Templates},
{"browse", setup.Browse},
}
// RegisterDirective adds the given directive to caddy's list of directives.
// Pass the name of a directive you want it to be placed after,
// otherwise it will be placed at the bottom of the stack.
func RegisterDirective(name string, setup SetupFunc, after string) {
dir := directive{name: name, setup: setup}
idx := len(directiveOrder)
for i := range directiveOrder {
if directiveOrder[i].name == after {
idx = i + 1
break
}
}
newDirectives := append(directiveOrder[:idx], append([]directive{dir}, directiveOrder[idx:]...)...)
directiveOrder = newDirectives
parse.ValidDirectives[name] = struct{}{}
}
// directive ties together a directive name with its setup function.
type directive struct {
name string
setup SetupFunc
}
// SetupFunc takes a controller and may optionally return a middleware.
// If the resulting middleware is not nil, it will be chained into
// the HTTP handlers in the order specified in this package.
type SetupFunc func(c *setup.Controller) (middleware.Middleware, error)
-31
View File
@@ -1,31 +0,0 @@
package caddy
import (
"reflect"
"testing"
)
func TestRegister(t *testing.T) {
directives := []directive{
{"dummy", nil},
{"dummy2", nil},
}
directiveOrder = directives
RegisterDirective("foo", nil, "dummy")
if len(directiveOrder) != 3 {
t.Fatal("Should have 3 directives now")
}
getNames := func() (s []string) {
for _, d := range directiveOrder {
s = append(s, d.name)
}
return s
}
if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2"}) {
t.Fatalf("directive order doesn't match: %s", getNames())
}
RegisterDirective("bar", nil, "ASDASD")
if !reflect.DeepEqual(getNames(), []string{"dummy", "foo", "dummy2", "bar"}) {
t.Fatalf("directive order doesn't match: %s", getNames())
}
}
-102
View File
@@ -1,102 +0,0 @@
package caddy
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
)
// isLocalhost returns true if host looks explicitly like a localhost address.
func isLocalhost(host string) bool {
return host == "localhost" || host == "::1" || strings.HasPrefix(host, "127.")
}
// checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum.
func checkFdlimit() {
const min = 4096
// Warn if ulimit is too low for production sites
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH
if err == nil {
// Note that an error here need not be reported
lim, err := strconv.Atoi(string(bytes.TrimSpace(out)))
if err == nil && lim < min {
fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min)
}
}
}
}
// signalSuccessToParent tells the parent our status using pipe at index 3.
// If this process is not a restart, this function does nothing.
// Calling this function once this process has successfully initialized
// is vital so that the parent process can unblock and kill itself.
// This function is idempotent; it executes at most once per process.
func signalSuccessToParent() {
signalParentOnce.Do(func() {
if IsRestart() {
ppipe := os.NewFile(3, "") // parent is reading from pipe at index 3
_, err := ppipe.Write([]byte("success")) // we must send some bytes to the parent
if err != nil {
log.Printf("[ERROR] Communicating successful init to parent: %v", err)
}
ppipe.Close()
}
})
}
// signalParentOnce is used to make sure that the parent is only
// signaled once; doing so more than once breaks whatever socket is
// at fd 4 (the reason for this is still unclear - to reproduce,
// call Stop() and Start() in succession at least once after a
// restart, then try loading first host of Caddyfile in the browser).
// Do not use this directly - call signalSuccessToParent instead.
var signalParentOnce sync.Once
// caddyfileGob maps bind address to index of the file descriptor
// in the Files array passed to the child process. It also contains
// the caddyfile contents and other state needed by the new process.
// Used only during graceful restarts where a new process is spawned.
type caddyfileGob struct {
ListenerFds map[string]uintptr
Caddyfile Input
OnDemandTLSCertsIssued int32
}
// IsRestart returns whether this process is, according
// to env variables, a fork as part of a graceful restart.
func IsRestart() bool {
return os.Getenv("CADDY_RESTART") == "true"
}
// writePidFile writes the process ID to the file at PidFile, if specified.
func writePidFile() error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
return ioutil.WriteFile(PidFile, pid, 0644)
}
// CaddyfileInput represents a Caddyfile as input
// and is simply a convenient way to implement
// the Input interface.
type CaddyfileInput struct {
Filepath string
Contents []byte
RealFile bool
}
// Body returns c.Contents.
func (c CaddyfileInput) Body() []byte { return c.Contents }
// Path returns c.Filepath.
func (c CaddyfileInput) Path() string { return c.Filepath }
// IsFile returns true if the original input was a real file on the file system.
func (c CaddyfileInput) IsFile() bool { return c.RealFile }
-215
View File
@@ -1,215 +0,0 @@
package https
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"sync"
"time"
"github.com/mholt/caddy/server"
"github.com/xenolf/lego/acme"
)
// acmeMu ensures that only one ACME challenge occurs at a time.
var acmeMu sync.Mutex
// ACMEClient is an acme.Client with custom state attached.
type ACMEClient struct {
*acme.Client
AllowPrompts bool // if false, we assume AlternatePort must be used
}
// NewACMEClient creates a new ACMEClient given an email and whether
// prompting the user is allowed. Clients should not be kept and
// re-used over long periods of time, but immediate re-use is more
// efficient than re-creating on every iteration.
var NewACMEClient = func(email string, allowPrompts bool) (*ACMEClient, error) {
// Look up or create the LE user account
leUser, err := getUser(email)
if err != nil {
return nil, err
}
// The client facilitates our communication with the CA server.
client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse)
if err != nil {
return nil, err
}
// If not registered, the user must register an account with the CA
// and agree to terms
if leUser.Registration == nil {
reg, err := client.Register()
if err != nil {
return nil, errors.New("registration error: " + err.Error())
}
leUser.Registration = reg
if allowPrompts { // can't prompt a user who isn't there
if !Agreed && reg.TosURL == "" {
Agreed = promptUserAgreement(saURL, false) // TODO - latest URL
}
if !Agreed && reg.TosURL == "" {
return nil, errors.New("user must agree to terms")
}
}
err = client.AgreeToTOS()
if err != nil {
saveUser(leUser) // Might as well try, right?
return nil, errors.New("error agreeing to terms: " + err.Error())
}
// save user to the file system
err = saveUser(leUser)
if err != nil {
return nil, errors.New("could not save user: " + err.Error())
}
}
return &ACMEClient{
Client: client,
AllowPrompts: allowPrompts,
}, nil
}
// NewACMEClientGetEmail creates a new ACMEClient and gets an email
// address at the same time (a server config is required, since it
// may contain an email address in it).
func NewACMEClientGetEmail(config server.Config, allowPrompts bool) (*ACMEClient, error) {
return NewACMEClient(getEmail(config, allowPrompts), allowPrompts)
}
// Configure configures c according to bindHost, which is the host (not
// whole address) to bind the listener to in solving the http and tls-sni
// challenges.
func (c *ACMEClient) Configure(bindHost string) {
// If we allow prompts, operator must be present. In our case,
// that is synonymous with saying the server is not already
// started. So if the user is still there, we don't use
// AlternatePort because we don't need to proxy the challenges.
// Conversely, if the operator is not there, the server has
// already started and we need to proxy the challenge.
if c.AllowPrompts {
// Operator is present; server is not already listening
c.SetHTTPAddress(net.JoinHostPort(bindHost, ""))
c.SetTLSAddress(net.JoinHostPort(bindHost, ""))
//c.ExcludeChallenges([]acme.Challenge{acme.DNS01})
} else {
// Operator is not present; server is started, so proxy challenges
c.SetHTTPAddress(net.JoinHostPort(bindHost, AlternatePort))
c.SetTLSAddress(net.JoinHostPort(bindHost, AlternatePort))
//c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
}
c.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01}) // TODO: can we proxy TLS challenges? and we should support DNS...
}
// Obtain obtains a single certificate for names. It stores the certificate
// on the disk if successful.
func (c *ACMEClient) Obtain(names []string) error {
Attempts:
for attempts := 0; attempts < 2; attempts++ {
acmeMu.Lock()
certificate, failures := c.ObtainCertificate(names, true, nil)
acmeMu.Unlock()
if len(failures) > 0 {
// Error - try to fix it or report it to the user and abort
var errMsg string // we'll combine all the failures into a single error message
var promptedForAgreement bool // only prompt user for agreement at most once
for errDomain, obtainErr := range failures {
// TODO: Double-check, will obtainErr ever be nil?
if tosErr, ok := obtainErr.(acme.TOSError); ok {
// Terms of Service agreement error; we can probably deal with this
if !Agreed && !promptedForAgreement && c.AllowPrompts {
Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
promptedForAgreement = true
}
if Agreed || !c.AllowPrompts {
err := c.AgreeToTOS()
if err != nil {
return errors.New("error agreeing to updated terms: " + err.Error())
}
continue Attempts
}
}
// If user did not agree or it was any other kind of error, just append to the list of errors
errMsg += "[" + errDomain + "] failed to get certificate: " + obtainErr.Error() + "\n"
}
return errors.New(errMsg)
}
// Success - immediately save the certificate resource
err := saveCertResource(certificate)
if err != nil {
return fmt.Errorf("error saving assets for %v: %v", names, err)
}
break
}
return nil
}
// Renew renews the managed certificate for name. Right now our storage
// mechanism only supports one name per certificate, so this function only
// accepts one domain as input. It can be easily modified to support SAN
// certificates if, one day, they become desperately needed enough that our
// storage mechanism is upgraded to be more complex to support SAN certs.
//
// Anyway, this function is safe for concurrent use.
func (c *ACMEClient) Renew(name string) error {
// Prepare for renewal (load PEM cert, key, and meta)
certBytes, err := ioutil.ReadFile(storage.SiteCertFile(name))
if err != nil {
return err
}
keyBytes, err := ioutil.ReadFile(storage.SiteKeyFile(name))
if err != nil {
return err
}
metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(name))
if err != nil {
return err
}
var certMeta acme.CertificateResource
err = json.Unmarshal(metaBytes, &certMeta)
certMeta.Certificate = certBytes
certMeta.PrivateKey = keyBytes
// Perform renewal and retry if necessary, but not too many times.
var newCertMeta acme.CertificateResource
var success bool
for attempts := 0; attempts < 2; attempts++ {
acmeMu.Lock()
newCertMeta, err = c.RenewCertificate(certMeta, true)
acmeMu.Unlock()
if err == nil {
success = true
break
}
// If the legal terms changed and need to be agreed to again,
// we can handle that.
if _, ok := err.(acme.TOSError); ok {
err := c.AgreeToTOS()
if err != nil {
return err
}
continue
}
// For any other kind of error, wait 10s and try again.
time.Sleep(10 * time.Second)
}
if !success {
return errors.New("too many renewal attempts; last error: " + err.Error())
}
return saveCertResource(newCertMeta)
}
-31
View File
@@ -1,31 +0,0 @@
package https
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"os"
)
// loadRSAPrivateKey loads a PEM-encoded RSA private key from file.
func loadRSAPrivateKey(file string) (*rsa.PrivateKey, error) {
keyBytes, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
keyBlock, _ := pem.Decode(keyBytes)
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
}
// saveRSAPrivateKey saves a PEM-encoded RSA private key to file.
func saveRSAPrivateKey(key *rsa.PrivateKey, file string) error {
pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
keyOut, err := os.Create(file)
if err != nil {
return err
}
keyOut.Chmod(0600)
defer keyOut.Close()
return pem.Encode(keyOut, &pemKey)
}
-65
View File
@@ -1,65 +0,0 @@
package https
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"os"
"runtime"
"testing"
)
func init() {
rsaKeySizeToUse = 128 // make tests faster; small key size OK for testing
}
func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
keyFile := "test.key"
defer os.Remove(keyFile)
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse)
if err != nil {
t.Fatal(err)
}
// test save
err = saveRSAPrivateKey(privateKey, keyFile)
if err != nil {
t.Fatal("error saving private key:", err)
}
// it doesn't make sense to test file permission on windows
if runtime.GOOS != "windows" {
// get info of the key file
info, err := os.Stat(keyFile)
if err != nil {
t.Fatal("error stating private key:", err)
}
// verify permission of key file is correct
if info.Mode().Perm() != 0600 {
t.Error("Expected key file to have permission 0600, but it wasn't")
}
}
// test load
loadedKey, err := loadRSAPrivateKey(keyFile)
if err != nil {
t.Error("error loading private key:", err)
}
// verify loaded key is correct
if !rsaPrivateKeysSame(privateKey, loadedKey) {
t.Error("Expected key bytes to be the same, but they weren't")
}
}
// rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same.
func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool {
return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b))
}
// rsaPrivateKeyBytes returns the bytes of DER-encoded key.
func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte {
return x509.MarshalPKCS1PrivateKey(key)
}
-42
View File
@@ -1,42 +0,0 @@
package https
import (
"crypto/tls"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
const challengeBasePath = "/.well-known/acme-challenge"
// RequestCallback proxies challenge requests to ACME client if the
// request path starts with challengeBasePath. It returns true if it
// handled the request and no more needs to be done; it returns false
// if this call was a no-op and the request still needs handling.
func RequestCallback(w http.ResponseWriter, r *http.Request) bool {
if strings.HasPrefix(r.URL.Path, challengeBasePath) {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
upstream, err := url.Parse(scheme + "://localhost:" + AlternatePort)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("[ERROR] ACME proxy handler: %v", err)
return true
}
proxy := httputil.NewSingleHostReverseProxy(upstream)
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // solver uses self-signed certs
}
proxy.ServeHTTP(w, r)
return true
}
return false
}
-422
View File
@@ -1,422 +0,0 @@
// Package https facilitates the management of TLS assets and integrates
// Let's Encrypt functionality into Caddy with first-class support for
// creating and renewing certificates automatically. It is designed to
// configure sites for HTTPS by default.
package https
import (
"encoding/json"
"errors"
"io/ioutil"
"net"
"net/http"
"os"
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/redirect"
"github.com/mholt/caddy/server"
"github.com/xenolf/lego/acme"
)
// Activate sets up TLS for each server config in configs
// as needed; this consists of acquiring and maintaining
// certificates and keys for qualifying configs and enabling
// OCSP stapling for all TLS-enabled configs.
//
// This function may prompt the user to provide an email
// address if none is available through other means. It
// prefers the email address specified in the config, but
// if that is not available it will check the command line
// argument. If absent, it will use the most recent email
// address from last time. If there isn't one, the user
// will be prompted and shown SA link.
//
// Also note that calling this function activates asset
// management automatically, which keeps certificates
// renewed and OCSP stapling updated.
//
// Activate returns the updated list of configs, since
// some may have been appended, for example, to redirect
// plaintext HTTP requests to their HTTPS counterpart.
// This function only appends; it does not splice.
func Activate(configs []server.Config) ([]server.Config, error) {
// just in case previous caller forgot...
Deactivate()
// pre-screen each config and earmark the ones that qualify for managed TLS
MarkQualified(configs)
// place certificates and keys on disk
err := ObtainCerts(configs, true, false)
if err != nil {
return configs, err
}
// update TLS configurations
err = EnableTLS(configs, true)
if err != nil {
return configs, err
}
// set up redirects
configs = MakePlaintextRedirects(configs)
// renew all relevant certificates that need renewal. this is important
// to do right away for a couple reasons, mainly because each restart,
// the renewal ticker is reset, so if restarts happen more often than
// the ticker interval, renewals would never happen. but doing
// it right away at start guarantees that renewals aren't missed.
err = renewManagedCertificates(true)
if err != nil {
return configs, err
}
// keep certificates renewed and OCSP stapling updated
go maintainAssets(stopChan)
return configs, nil
}
// Deactivate cleans up long-term, in-memory resources
// allocated by calling Activate(). Essentially, it stops
// the asset maintainer from running, meaning that certificates
// will not be renewed, OCSP staples will not be updated, etc.
func Deactivate() (err error) {
defer func() {
if rec := recover(); rec != nil {
err = errors.New("already deactivated")
}
}()
close(stopChan)
stopChan = make(chan struct{})
return
}
// MarkQualified scans each config and, if it qualifies for managed
// TLS, it sets the Managed field of the TLSConfig to true.
func MarkQualified(configs []server.Config) {
for i := 0; i < len(configs); i++ {
if ConfigQualifies(configs[i]) {
configs[i].TLS.Managed = true
}
}
}
// ObtainCerts obtains certificates for all these configs as long as a
// certificate does not already exist on disk. It does not modify the
// configs at all; it only obtains and stores certificates and keys to
// the disk. If allowPrompts is true, the user may be shown a prompt.
// If proxyACME is true, the ACME challenges will be proxied to our alt port.
func ObtainCerts(configs []server.Config, allowPrompts, proxyACME bool) error {
// We group configs by email so we don't make the same clients over and
// over. This has the potential to prompt the user for an email, but we
// prevent that by assuming that if we already have a listener that can
// proxy ACME challenge requests, then the server is already running and
// the operator is no longer present.
groupedConfigs := groupConfigsByEmail(configs, allowPrompts)
for email, group := range groupedConfigs {
// Wait as long as we can before creating the client, because it
// may not be needed, for example, if we already have what we
// need on disk. Creating a client involves the network and
// potentially prompting the user, etc., so only do if necessary.
var client *ACMEClient
for _, cfg := range group {
if cfg.Host == "" || existingCertAndKey(cfg.Host) {
continue
}
// Now we definitely do need a client
if client == nil {
var err error
client, err = NewACMEClient(email, allowPrompts)
if err != nil {
return errors.New("error creating client: " + err.Error())
}
}
// c.Configure assumes that allowPrompts == !proxyACME,
// but that's not always true. For example, a restart where
// the user isn't present and we're not listening on port 80.
// TODO: This could probably be refactored better.
if proxyACME {
client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, AlternatePort))
client.ExcludeChallenges([]acme.Challenge{acme.TLSSNI01, acme.DNS01})
} else {
client.SetHTTPAddress(net.JoinHostPort(cfg.BindHost, ""))
client.SetTLSAddress(net.JoinHostPort(cfg.BindHost, ""))
client.ExcludeChallenges([]acme.Challenge{acme.DNS01})
}
err := client.Obtain([]string{cfg.Host})
if err != nil {
return err
}
}
}
return nil
}
// groupConfigsByEmail groups configs by the email address to be used by an
// ACME client. It only groups configs that have TLS enabled and that are
// marked as Managed. If userPresent is true, the operator MAY be prompted
// for an email address.
func groupConfigsByEmail(configs []server.Config, userPresent bool) map[string][]server.Config {
initMap := make(map[string][]server.Config)
for _, cfg := range configs {
if !cfg.TLS.Managed {
continue
}
leEmail := getEmail(cfg, userPresent)
initMap[leEmail] = append(initMap[leEmail], cfg)
}
return initMap
}
// EnableTLS configures each config to use TLS according to default settings.
// It will only change configs that are marked as managed, and assumes that
// certificates and keys are already on disk. If loadCertificates is true,
// the certificates will be loaded from disk into the cache for this process
// to use. If false, TLS will still be enabled and configured with default
// settings, but no certificates will be parsed loaded into the cache, and
// the returned error value will always be nil.
func EnableTLS(configs []server.Config, loadCertificates bool) error {
for i := 0; i < len(configs); i++ {
if !configs[i].TLS.Managed {
continue
}
configs[i].TLS.Enabled = true
if loadCertificates && configs[i].Host != "" {
_, err := cacheManagedCertificate(configs[i].Host, false)
if err != nil {
return err
}
}
setDefaultTLSParams(&configs[i])
}
return nil
}
// hostHasOtherPort returns true if there is another config in the list with the same
// hostname that has port otherPort, or false otherwise. All the configs are checked
// against the hostname of allConfigs[thisConfigIdx].
func hostHasOtherPort(allConfigs []server.Config, thisConfigIdx int, otherPort string) bool {
for i, otherCfg := range allConfigs {
if i == thisConfigIdx {
continue // has to be a config OTHER than the one we're comparing against
}
if otherCfg.Host == allConfigs[thisConfigIdx].Host && otherCfg.Port == otherPort {
return true
}
}
return false
}
// MakePlaintextRedirects sets up redirects from port 80 to the relevant HTTPS
// hosts. You must pass in all configs, not just configs that qualify, since
// we must know whether the same host already exists on port 80, and those would
// not be in a list of configs that qualify for automatic HTTPS. This function will
// only set up redirects for configs that qualify. It returns the updated list of
// all configs.
func MakePlaintextRedirects(allConfigs []server.Config) []server.Config {
for i, cfg := range allConfigs {
if cfg.TLS.Managed &&
!hostHasOtherPort(allConfigs, i, "80") &&
(cfg.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) {
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
}
}
return allConfigs
}
// ConfigQualifies returns true if cfg qualifies for
// fully managed TLS (but not on-demand TLS, which is
// not considered here). It does NOT check to see if a
// cert and key already exist for the config. If the
// config does qualify, you should set cfg.TLS.Managed
// to true and check that instead, because the process of
// setting up the config may make it look like it
// doesn't qualify even though it originally did.
func ConfigQualifies(cfg server.Config) bool {
return (!cfg.TLS.Manual || cfg.TLS.OnDemand) && // user might provide own cert and key
// user can force-disable automatic HTTPS for this host
cfg.Scheme != "http" &&
cfg.Port != "80" &&
cfg.TLS.LetsEncryptEmail != "off" &&
// we get can't certs for some kinds of hostnames, but
// on-demand TLS allows empty hostnames at startup
(HostQualifies(cfg.Host) || cfg.TLS.OnDemand)
}
// HostQualifies returns true if the hostname alone
// appears eligible for automatic HTTPS. For example,
// localhost, empty hostname, and IP addresses are
// not eligible because we cannot obtain certificates
// for those names.
func HostQualifies(hostname string) bool {
return hostname != "localhost" && // localhost is ineligible
// hostname must not be empty
strings.TrimSpace(hostname) != "" &&
// cannot be an IP address, see
// https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
// (also trim [] from either end, since that special case can sneak through
// for IPv6 addresses using the -host flag and with empty/no Caddyfile)
net.ParseIP(strings.Trim(hostname, "[]")) == nil
}
// existingCertAndKey returns true if the host has a certificate
// and private key in storage already, false otherwise.
func existingCertAndKey(host string) bool {
_, err := os.Stat(storage.SiteCertFile(host))
if err != nil {
return false
}
_, err = os.Stat(storage.SiteKeyFile(host))
if err != nil {
return false
}
return true
}
// saveCertResource saves the certificate resource to disk. This
// includes the certificate file itself, the private key, and the
// metadata file.
func saveCertResource(cert acme.CertificateResource) error {
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
if err != nil {
return err
}
// Save cert
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
if err != nil {
return err
}
// Save private key
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
if err != nil {
return err
}
// Save cert metadata
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
if err != nil {
return err
}
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
if err != nil {
return err
}
return nil
}
// redirPlaintextHost returns a new plaintext HTTP configuration for
// a virtualHost that simply redirects to cfg, which is assumed to
// be the HTTPS configuration. The returned configuration is set
// to listen on port 80.
func redirPlaintextHost(cfg server.Config) server.Config {
toURL := "https://{host}" // serve any host, since cfg.Host could be empty
if cfg.Port != "443" && cfg.Port != "80" {
toURL += ":" + cfg.Port
}
redirMidware := func(next middleware.Handler) middleware.Handler {
return redirect.Redirect{Next: next, Rules: []redirect.Rule{
{
FromScheme: "http",
FromPath: "/",
To: toURL + "{uri}",
Code: http.StatusMovedPermanently,
},
}}
}
return server.Config{
Host: cfg.Host,
BindHost: cfg.BindHost,
Port: "80",
Middleware: []middleware.Middleware{redirMidware},
}
}
// Revoke revokes the certificate for host via ACME protocol.
func Revoke(host string) error {
if !existingCertAndKey(host) {
return errors.New("no certificate and key for " + host)
}
email := getEmail(server.Config{Host: host}, true)
if email == "" {
return errors.New("email is required to revoke")
}
client, err := NewACMEClient(email, true)
if err != nil {
return err
}
certFile := storage.SiteCertFile(host)
certBytes, err := ioutil.ReadFile(certFile)
if err != nil {
return err
}
err = client.RevokeCertificate(certBytes)
if err != nil {
return err
}
err = os.Remove(certFile)
if err != nil {
return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error())
}
return nil
}
var (
// DefaultEmail represents the Let's Encrypt account email to use if none provided
DefaultEmail string
// Agreed indicates whether user has agreed to the Let's Encrypt SA
Agreed bool
// CAUrl represents the base URL to the CA's ACME endpoint
CAUrl string
)
// AlternatePort is the port on which the acme client will open a
// listener and solve the CA's challenges. If this alternate port
// is used instead of the default port (80 or 443), then the
// default port for the challenge must be forwarded to this one.
const AlternatePort = "5033"
// KeySize represents the length of a key in bits.
type KeySize int
// Key sizes are used to determine the strength of a key.
const (
Ecc224 KeySize = 224
Ecc256 = 256
Rsa2048 = 2048
Rsa4096 = 4096
)
// rsaKeySizeToUse is the size to use for new RSA keys.
// This shouldn't need to change except for in tests;
// the size can be drastically reduced for speed.
var rsaKeySizeToUse = Rsa2048
// stopChan is used to signal the maintenance goroutine
// to terminate.
var stopChan chan struct{}
-332
View File
@@ -1,332 +0,0 @@
package https
import (
"io/ioutil"
"net/http"
"os"
"testing"
"github.com/mholt/caddy/middleware/redirect"
"github.com/mholt/caddy/server"
"github.com/xenolf/lego/acme"
)
func TestHostQualifies(t *testing.T) {
for i, test := range []struct {
host string
expect bool
}{
{"localhost", false},
{"127.0.0.1", false},
{"127.0.1.5", false},
{"::1", false},
{"[::1]", false},
{"[::]", false},
{"::", false},
{"", false},
{" ", false},
{"0.0.0.0", false},
{"192.168.1.3", false},
{"10.0.2.1", false},
{"169.112.53.4", false},
{"foobar.com", true},
{"sub.foobar.com", true},
} {
if HostQualifies(test.host) && !test.expect {
t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
}
if !HostQualifies(test.host) && test.expect {
t.Errorf("Test %d: Expected '%s' to qualify, but it did NOT", i, test.host)
}
}
}
func TestConfigQualifies(t *testing.T) {
for i, test := range []struct {
cfg server.Config
expect bool
}{
{server.Config{Host: ""}, false},
{server.Config{Host: "localhost"}, false},
{server.Config{Host: "123.44.3.21"}, false},
{server.Config{Host: "example.com"}, true},
{server.Config{Host: "example.com", TLS: server.TLSConfig{Manual: true}}, false},
{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}}, false},
{server.Config{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}}, true},
{server.Config{Host: "example.com", Scheme: "http"}, false},
{server.Config{Host: "example.com", Port: "80"}, false},
{server.Config{Host: "example.com", Port: "1234"}, true},
{server.Config{Host: "example.com", Scheme: "https"}, true},
{server.Config{Host: "example.com", Port: "80", Scheme: "https"}, false},
} {
if test.expect && !ConfigQualifies(test.cfg) {
t.Errorf("Test %d: Expected config to qualify, but it did NOT: %#v", i, test.cfg)
}
if !test.expect && ConfigQualifies(test.cfg) {
t.Errorf("Test %d: Expected config to NOT qualify, but it did: %#v", i, test.cfg)
}
}
}
func TestRedirPlaintextHost(t *testing.T) {
cfg := redirPlaintextHost(server.Config{
Host: "example.com",
BindHost: "93.184.216.34",
Port: "1234",
})
// Check host and port
if actual, expected := cfg.Host, "example.com"; actual != expected {
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
}
if actual, expected := cfg.BindHost, "93.184.216.34"; actual != expected {
t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual)
}
if actual, expected := cfg.Port, "80"; actual != expected {
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
}
// Make sure redirect handler is set up properly
if cfg.Middleware == nil || len(cfg.Middleware) != 1 {
t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.Middleware)
}
handler, ok := cfg.Middleware[0](nil).(redirect.Redirect)
if !ok {
t.Fatalf("Expected a redirect.Redirect middleware, but got: %#v", handler)
}
if len(handler.Rules) != 1 {
t.Fatalf("Expected one redirect rule, got: %#v", handler.Rules)
}
// Check redirect rule for correctness
if actual, expected := handler.Rules[0].FromScheme, "http"; actual != expected {
t.Errorf("Expected redirect rule to be from scheme '%s' but is actually from '%s'", expected, actual)
}
if actual, expected := handler.Rules[0].FromPath, "/"; actual != expected {
t.Errorf("Expected redirect rule to be for path '%s' but is actually for '%s'", expected, actual)
}
if actual, expected := handler.Rules[0].To, "https://{host}:1234{uri}"; actual != expected {
t.Errorf("Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
}
if actual, expected := handler.Rules[0].Code, http.StatusMovedPermanently; actual != expected {
t.Errorf("Expected redirect rule to have code %d but was %d", expected, actual)
}
// browsers can infer a default port from scheme, so make sure the port
// doesn't get added in explicitly for default ports like 443 for https.
cfg = redirPlaintextHost(server.Config{Host: "example.com", Port: "443"})
handler, ok = cfg.Middleware[0](nil).(redirect.Redirect)
if actual, expected := handler.Rules[0].To, "https://{host}{uri}"; actual != expected {
t.Errorf("(Default Port) Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
}
}
func TestSaveCertResource(t *testing.T) {
storage = Storage("./le_test_save")
defer func() {
err := os.RemoveAll(string(storage))
if err != nil {
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
}
}()
domain := "example.com"
certContents := "certificate"
keyContents := "private key"
metaContents := `{
"domain": "example.com",
"certUrl": "https://example.com/cert",
"certStableUrl": "https://example.com/cert/stable"
}`
cert := acme.CertificateResource{
Domain: domain,
CertURL: "https://example.com/cert",
CertStableURL: "https://example.com/cert/stable",
PrivateKey: []byte(keyContents),
Certificate: []byte(certContents),
}
err := saveCertResource(cert)
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
certFile, err := ioutil.ReadFile(storage.SiteCertFile(domain))
if err != nil {
t.Errorf("Expected no error reading certificate file, got: %v", err)
}
if string(certFile) != certContents {
t.Errorf("Expected certificate file to contain '%s', got '%s'", certContents, string(certFile))
}
keyFile, err := ioutil.ReadFile(storage.SiteKeyFile(domain))
if err != nil {
t.Errorf("Expected no error reading private key file, got: %v", err)
}
if string(keyFile) != keyContents {
t.Errorf("Expected private key file to contain '%s', got '%s'", keyContents, string(keyFile))
}
metaFile, err := ioutil.ReadFile(storage.SiteMetaFile(domain))
if err != nil {
t.Errorf("Expected no error reading meta file, got: %v", err)
}
if string(metaFile) != metaContents {
t.Errorf("Expected meta file to contain '%s', got '%s'", metaContents, string(metaFile))
}
}
func TestExistingCertAndKey(t *testing.T) {
storage = Storage("./le_test_existing")
defer func() {
err := os.RemoveAll(string(storage))
if err != nil {
t.Fatalf("Could not remove temporary storage directory (%s): %v", storage, err)
}
}()
domain := "example.com"
if existingCertAndKey(domain) {
t.Errorf("Did NOT expect %v to have existing cert or key, but it did", domain)
}
err := saveCertResource(acme.CertificateResource{
Domain: domain,
PrivateKey: []byte("key"),
Certificate: []byte("cert"),
})
if err != nil {
t.Fatalf("Expected no error, got: %v", err)
}
if !existingCertAndKey(domain) {
t.Errorf("Expected %v to have existing cert and key, but it did NOT", domain)
}
}
func TestHostHasOtherPort(t *testing.T) {
configs := []server.Config{
{Host: "example.com", Port: "80"},
{Host: "sub1.example.com", Port: "80"},
{Host: "sub1.example.com", Port: "443"},
}
if hostHasOtherPort(configs, 0, "80") {
t.Errorf(`Expected hostHasOtherPort(configs, 0, "80") to be false, but got true`)
}
if hostHasOtherPort(configs, 0, "443") {
t.Errorf(`Expected hostHasOtherPort(configs, 0, "443") to be false, but got true`)
}
if !hostHasOtherPort(configs, 1, "443") {
t.Errorf(`Expected hostHasOtherPort(configs, 1, "443") to be true, but got false`)
}
}
func TestMakePlaintextRedirects(t *testing.T) {
configs := []server.Config{
// Happy path = standard redirect from 80 to 443
{Host: "example.com", TLS: server.TLSConfig{Managed: true}},
// Host on port 80 already defined; don't change it (no redirect)
{Host: "sub1.example.com", Port: "80", Scheme: "http"},
{Host: "sub1.example.com", TLS: server.TLSConfig{Managed: true}},
// Redirect from port 80 to port 5000 in this case
{Host: "sub2.example.com", Port: "5000", TLS: server.TLSConfig{Managed: true}},
// Can redirect from 80 to either 443 or 5001, but choose 443
{Host: "sub3.example.com", Port: "443", TLS: server.TLSConfig{Managed: true}},
{Host: "sub3.example.com", Port: "5001", Scheme: "https", TLS: server.TLSConfig{Managed: true}},
}
result := MakePlaintextRedirects(configs)
expectedRedirCount := 3
if len(result) != len(configs)+expectedRedirCount {
t.Errorf("Expected %d redirect(s) to be added, but got %d",
expectedRedirCount, len(result)-len(configs))
}
}
func TestEnableTLS(t *testing.T) {
configs := []server.Config{
{Host: "example.com", TLS: server.TLSConfig{Managed: true}},
{}, // not managed - no changes!
}
EnableTLS(configs, false)
if !configs[0].TLS.Enabled {
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
}
if configs[1].TLS.Enabled {
t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true")
}
}
func TestGroupConfigsByEmail(t *testing.T) {
if groupConfigsByEmail([]server.Config{}, false) == nil {
t.Errorf("With empty input, returned map was nil, but expected non-nil map")
}
configs := []server.Config{
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
{Host: "sub1.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
{Host: "sub2.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
{Host: "sub3.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar", Managed: true}},
{Host: "sub4.example.com", TLS: server.TLSConfig{LetsEncryptEmail: "", Managed: true}},
{Host: "sub5.example.com", TLS: server.TLSConfig{LetsEncryptEmail: ""}}, // not managed
}
DefaultEmail = "test@example.com"
groups := groupConfigsByEmail(configs, true)
if groups == nil {
t.Fatalf("Returned map was nil, but expected values")
}
if len(groups) != 2 {
t.Errorf("Expected 2 groups, got %d: %#v", len(groups), groups)
}
if len(groups["foo@bar"]) != 2 {
t.Errorf("Expected 2 configs for foo@bar, got %d: %#v", len(groups["foobar"]), groups["foobar"])
}
if len(groups[DefaultEmail]) != 3 {
t.Errorf("Expected 3 configs for %s, got %d: %#v", DefaultEmail, len(groups["foobar"]), groups["foobar"])
}
}
func TestMarkQualified(t *testing.T) {
// TODO: TestConfigQualifies and this test share the same config list...
configs := []server.Config{
{Host: ""},
{Host: "localhost"},
{Host: "123.44.3.21"},
{Host: "example.com"},
{Host: "example.com", TLS: server.TLSConfig{Manual: true}},
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "off"}},
{Host: "example.com", TLS: server.TLSConfig{LetsEncryptEmail: "foo@bar.com"}},
{Host: "example.com", Scheme: "http"},
{Host: "example.com", Port: "80"},
{Host: "example.com", Port: "1234"},
{Host: "example.com", Scheme: "https"},
{Host: "example.com", Port: "80", Scheme: "https"},
}
expectedManagedCount := 4
MarkQualified(configs)
count := 0
for _, cfg := range configs {
if cfg.TLS.Managed {
count++
}
}
if count != expectedManagedCount {
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
}
}
-206
View File
@@ -1,206 +0,0 @@
package https
import (
"log"
"time"
"github.com/mholt/caddy/server"
"golang.org/x/crypto/ocsp"
)
const (
// RenewInterval is how often to check certificates for renewal.
RenewInterval = 12 * time.Hour
// OCSPInterval is how often to check if OCSP stapling needs updating.
OCSPInterval = 1 * time.Hour
)
// maintainAssets is a permanently-blocking function
// that loops indefinitely and, on a regular schedule, checks
// certificates for expiration and initiates a renewal of certs
// that are expiring soon. It also updates OCSP stapling and
// performs other maintenance of assets.
//
// You must pass in the channel which you'll close when
// maintenance should stop, to allow this goroutine to clean up
// after itself and unblock.
func maintainAssets(stopChan chan struct{}) {
renewalTicker := time.NewTicker(RenewInterval)
ocspTicker := time.NewTicker(OCSPInterval)
for {
select {
case <-renewalTicker.C:
log.Println("[INFO] Scanning for expiring certificates")
renewManagedCertificates(false)
log.Println("[INFO] Done checking certificates")
case <-ocspTicker.C:
log.Println("[INFO] Scanning for stale OCSP staples")
updateOCSPStaples()
log.Println("[INFO] Done checking OCSP staples")
case <-stopChan:
renewalTicker.Stop()
ocspTicker.Stop()
log.Println("[INFO] Stopped background maintenance routine")
return
}
}
}
func renewManagedCertificates(allowPrompts bool) (err error) {
var renewed, deleted []Certificate
var client *ACMEClient
visitedNames := make(map[string]struct{})
certCacheMu.RLock()
for name, cert := range certCache {
if !cert.Managed {
continue
}
// the list of names on this cert should never be empty...
if cert.Names == nil || len(cert.Names) == 0 {
log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v", name, cert.Names)
deleted = append(deleted, cert)
continue
}
// skip names whose certificate we've already renewed
if _, ok := visitedNames[name]; ok {
continue
}
for _, name := range cert.Names {
visitedNames[name] = struct{}{}
}
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
if timeLeft < renewDurationBefore {
log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft)
if client == nil {
client, err = NewACMEClientGetEmail(server.Config{}, allowPrompts)
if err != nil {
return err
}
client.Configure("") // TODO: Bind address of relevant listener, yuck
}
err := client.Renew(cert.Names[0]) // managed certs better have only one name
if err != nil {
if client.AllowPrompts {
// User is present, so stop immediately and report the error
certCacheMu.RUnlock()
return err
}
log.Printf("[ERROR] %v", err)
if cert.OnDemand {
deleted = append(deleted, cert)
}
} else {
renewed = append(renewed, cert)
}
}
}
certCacheMu.RUnlock()
// Apply changes to the cache
for _, cert := range renewed {
_, err := cacheManagedCertificate(cert.Names[0], cert.OnDemand)
if err != nil {
if client.AllowPrompts {
return err // operator is present, so report error immediately
}
log.Printf("[ERROR] %v", err)
}
}
for _, cert := range deleted {
certCacheMu.Lock()
for _, name := range cert.Names {
delete(certCache, name)
}
certCacheMu.Unlock()
}
return nil
}
func updateOCSPStaples() {
// Create a temporary place to store updates
// until we release the potentially long-lived
// read lock and use a short-lived write lock.
type ocspUpdate struct {
rawBytes []byte
parsed *ocsp.Response
}
updated := make(map[string]ocspUpdate)
// A single SAN certificate maps to multiple names, so we use this
// set to make sure we don't waste cycles checking OCSP for the same
// certificate multiple times.
visited := make(map[string]struct{})
certCacheMu.RLock()
for name, cert := range certCache {
// skip this certificate if we've already visited it,
// and if not, mark all the names as visited
if _, ok := visited[name]; ok {
continue
}
for _, n := range cert.Names {
visited[n] = struct{}{}
}
// no point in updating OCSP for expired certificates
if time.Now().After(cert.NotAfter) {
continue
}
var lastNextUpdate time.Time
if cert.OCSP != nil {
// start checking OCSP staple about halfway through validity period for good measure
lastNextUpdate = cert.OCSP.NextUpdate
refreshTime := cert.OCSP.ThisUpdate.Add(lastNextUpdate.Sub(cert.OCSP.ThisUpdate) / 2)
// since OCSP is already stapled, we need only check if we're in that "refresh window"
if time.Now().Before(refreshTime) {
continue
}
}
err := stapleOCSP(&cert, nil)
if err != nil {
if cert.OCSP != nil {
// if it was no staple before, that's fine, otherwise we should log the error
log.Printf("[ERROR] Checking OCSP for %s: %v", name, err)
}
continue
}
// By this point, we've obtained the latest OCSP response.
// If there was no staple before, or if the response is updated, make
// sure we apply the update to all names on the certificate.
if lastNextUpdate.IsZero() || lastNextUpdate != cert.OCSP.NextUpdate {
log.Printf("[INFO] Advancing OCSP staple for %v from %s to %s",
cert.Names, lastNextUpdate, cert.OCSP.NextUpdate)
for _, n := range cert.Names {
updated[n] = ocspUpdate{rawBytes: cert.Certificate.OCSPStaple, parsed: cert.OCSP}
}
}
}
certCacheMu.RUnlock()
// This write lock should be brief since we have all the info we need now.
certCacheMu.Lock()
for name, update := range updated {
cert := certCache[name]
cert.OCSP = update.parsed
cert.Certificate.OCSPStaple = update.rawBytes
certCache[name] = cert
}
certCacheMu.Unlock()
}
// renewDurationBefore is how long before expiration to renew certificates.
const renewDurationBefore = (24 * time.Hour) * 30
-318
View File
@@ -1,318 +0,0 @@
package https
import (
"bytes"
"crypto/tls"
"encoding/pem"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/server"
)
// Setup sets up the TLS configuration and installs certificates that
// are specified by the user in the config file. All the automatic HTTPS
// stuff comes later outside of this function.
func Setup(c *setup.Controller) (middleware.Middleware, error) {
if c.Port == "80" || c.Scheme == "http" {
c.TLS.Enabled = false
log.Printf("[WARNING] TLS disabled for %s://%s.", c.Scheme, c.Address())
return nil, nil
}
c.TLS.Enabled = true
for c.Next() {
var certificateFile, keyFile, loadDir, maxCerts string
args := c.RemainingArgs()
switch len(args) {
case 1:
c.TLS.LetsEncryptEmail = args[0]
// user can force-disable managed TLS this way
if c.TLS.LetsEncryptEmail == "off" {
c.TLS.Enabled = false
return nil, nil
}
case 2:
certificateFile = args[0]
keyFile = args[1]
c.TLS.Manual = true
}
// Optional block with extra parameters
var hadBlock bool
for c.NextBlock() {
hadBlock = true
switch c.Val() {
case "protocols":
args := c.RemainingArgs()
if len(args) != 2 {
return nil, c.ArgErr()
}
value, ok := supportedProtocols[strings.ToLower(args[0])]
if !ok {
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
}
c.TLS.ProtocolMinVersion = value
value, ok = supportedProtocols[strings.ToLower(args[1])]
if !ok {
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
}
c.TLS.ProtocolMaxVersion = value
case "ciphers":
for c.NextArg() {
value, ok := supportedCiphersMap[strings.ToUpper(c.Val())]
if !ok {
return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val())
}
c.TLS.Ciphers = append(c.TLS.Ciphers, value)
}
case "clients":
c.TLS.ClientCerts = c.RemainingArgs()
if len(c.TLS.ClientCerts) == 0 {
return nil, c.ArgErr()
}
case "load":
c.Args(&loadDir)
c.TLS.Manual = true
case "max_certs":
c.Args(&maxCerts)
c.TLS.OnDemand = true
default:
return nil, c.Errf("Unknown keyword '%s'", c.Val())
}
}
// tls requires at least one argument if a block is not opened
if len(args) == 0 && !hadBlock {
return nil, c.ArgErr()
}
// set certificate limit if on-demand TLS is enabled
if maxCerts != "" {
maxCertsNum, err := strconv.Atoi(maxCerts)
if err != nil || maxCertsNum < 1 {
return nil, c.Err("max_certs must be a positive integer")
}
if onDemandMaxIssue == 0 || int32(maxCertsNum) < onDemandMaxIssue { // keep the minimum; TODO: We have to do this because it is global; should be per-server or per-vhost...
onDemandMaxIssue = int32(maxCertsNum)
}
}
// don't try to load certificates unless we're supposed to
if !c.TLS.Enabled || !c.TLS.Manual {
continue
}
// load a single certificate and key, if specified
if certificateFile != "" && keyFile != "" {
err := cacheUnmanagedCertificatePEMFile(certificateFile, keyFile)
if err != nil {
return nil, c.Errf("Unable to load certificate and key files for %s: %v", c.Host, err)
}
log.Printf("[INFO] Successfully loaded TLS assets from %s and %s", certificateFile, keyFile)
}
// load a directory of certificates, if specified
if loadDir != "" {
err := loadCertsInDir(c, loadDir)
if err != nil {
return nil, err
}
}
}
setDefaultTLSParams(c.Config)
return nil, nil
}
// loadCertsInDir loads all the certificates/keys in dir, as long as
// the file ends with .pem. This method of loading certificates is
// modeled after haproxy, which expects the certificate and key to
// be bundled into the same file:
// https://cbonte.github.io/haproxy-dconv/configuration-1.5.html#5.1-crt
//
// This function may write to the log as it walks the directory tree.
func loadCertsInDir(c *setup.Controller, dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Printf("[WARNING] Unable to traverse into %s; skipping", path)
return nil
}
if info.IsDir() {
return nil
}
if strings.HasSuffix(strings.ToLower(info.Name()), ".pem") {
certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer)
var foundKey bool // use only the first key in the file
bundle, err := ioutil.ReadFile(path)
if err != nil {
return err
}
for {
// Decode next block so we can see what type it is
var derBlock *pem.Block
derBlock, bundle = pem.Decode(bundle)
if derBlock == nil {
break
}
if derBlock.Type == "CERTIFICATE" {
// Re-encode certificate as PEM, appending to certificate chain
pem.Encode(certBuilder, derBlock)
} else if derBlock.Type == "EC PARAMETERS" {
// EC keys generated from openssl can be composed of two blocks:
// parameters and key (parameter block should come first)
if !foundKey {
// Encode parameters
pem.Encode(keyBuilder, derBlock)
// Key must immediately follow
derBlock, bundle = pem.Decode(bundle)
if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" {
return c.Errf("%s: expected elliptic private key to immediately follow EC parameters", path)
}
pem.Encode(keyBuilder, derBlock)
foundKey = true
}
} else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") {
// RSA key
if !foundKey {
pem.Encode(keyBuilder, derBlock)
foundKey = true
}
} else {
return c.Errf("%s: unrecognized PEM block type: %s", path, derBlock.Type)
}
}
certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes()
if len(certPEMBytes) == 0 {
return c.Errf("%s: failed to parse PEM data", path)
}
if len(keyPEMBytes) == 0 {
return c.Errf("%s: no private key block found", path)
}
err = cacheUnmanagedCertificatePEMBytes(certPEMBytes, keyPEMBytes)
if err != nil {
return c.Errf("%s: failed to load cert and key for %s: %v", path, c.Host, err)
}
log.Printf("[INFO] Successfully loaded TLS assets from %s", path)
}
return nil
})
}
// setDefaultTLSParams sets the default TLS cipher suites, protocol versions,
// and server preferences of a server.Config if they were not previously set
// (it does not overwrite; only fills in missing values). It will also set the
// port to 443 if not already set, TLS is enabled, TLS is manual, and the host
// does not equal localhost.
func setDefaultTLSParams(c *server.Config) {
// If no ciphers provided, use default list
if len(c.TLS.Ciphers) == 0 {
c.TLS.Ciphers = defaultCiphers
}
// Not a cipher suite, but still important for mitigating protocol downgrade attacks
// (prepend since having it at end breaks http2 due to non-h2-approved suites before it)
c.TLS.Ciphers = append([]uint16{tls.TLS_FALLBACK_SCSV}, c.TLS.Ciphers...)
// Set default protocol min and max versions - must balance compatibility and security
if c.TLS.ProtocolMinVersion == 0 {
c.TLS.ProtocolMinVersion = tls.VersionTLS10
}
if c.TLS.ProtocolMaxVersion == 0 {
c.TLS.ProtocolMaxVersion = tls.VersionTLS12
}
// Prefer server cipher suites
c.TLS.PreferServerCipherSuites = true
// Default TLS port is 443; only use if port is not manually specified,
// TLS is enabled, and the host is not localhost
if c.Port == "" && c.TLS.Enabled && (!c.TLS.Manual || c.TLS.OnDemand) && c.Host != "localhost" {
c.Port = "443"
}
}
// Map of supported protocols.
// SSLv3 will be not supported in future release.
// HTTP/2 only supports TLS 1.2 and higher.
var supportedProtocols = map[string]uint16{
"ssl3.0": tls.VersionSSL30,
"tls1.0": tls.VersionTLS10,
"tls1.1": tls.VersionTLS11,
"tls1.2": tls.VersionTLS12,
}
// Map of supported ciphers, used only for parsing config.
//
// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites,
// including all but two of the suites below (the two GCM suites).
// See https://http2.github.io/http2-spec/#BadCipherSuites
//
// TLS_FALLBACK_SCSV is not in this list because we manually ensure
// it is always added (even though it is not technically a cipher suite).
//
// This map, like any map, is NOT ORDERED. Do not range over this map.
var supportedCiphersMap = map[string]uint16{
"ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
"ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
"ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
"ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
"ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
"RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
"RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
"ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
"RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
}
// List of supported cipher suites in descending order of preference.
// Ordering is very important! Getting the wrong order will break
// mainstream clients, especially with HTTP/2.
//
// Note that TLS_FALLBACK_SCSV is not in this list since it is always
// added manually.
var supportedCiphers = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
}
// List of all the ciphers we want to use by default
var defaultCiphers = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
}
-232
View File
@@ -1,232 +0,0 @@
package https
import (
"crypto/tls"
"io/ioutil"
"log"
"os"
"testing"
"github.com/mholt/caddy/caddy/setup"
)
func TestMain(m *testing.M) {
// Write test certificates to disk before tests, and clean up
// when we're done.
err := ioutil.WriteFile(certFile, testCert, 0644)
if err != nil {
log.Fatal(err)
}
err = ioutil.WriteFile(keyFile, testKey, 0644)
if err != nil {
os.Remove(certFile)
log.Fatal(err)
}
result := m.Run()
os.Remove(certFile)
os.Remove(keyFile)
os.Exit(result)
}
func TestSetupParseBasic(t *testing.T) {
c := setup.NewTestController(`tls ` + certFile + ` ` + keyFile + ``)
_, err := Setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
// Basic checks
if !c.TLS.Manual {
t.Error("Expected TLS Manual=true, but was false")
}
if !c.TLS.Enabled {
t.Error("Expected TLS Enabled=true, but was false")
}
// Security defaults
if c.TLS.ProtocolMinVersion != tls.VersionTLS10 {
t.Errorf("Expected 'tls1.0 (0x0301)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion)
}
if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 {
t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %v", c.TLS.ProtocolMaxVersion)
}
// Cipher checks
expectedCiphers := []uint16{
tls.TLS_FALLBACK_SCSV,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
}
// Ensure count is correct (plus one for TLS_FALLBACK_SCSV)
if len(c.TLS.Ciphers) != len(expectedCiphers) {
t.Errorf("Expected %v Ciphers (including TLS_FALLBACK_SCSV), got %v",
len(expectedCiphers), len(c.TLS.Ciphers))
}
// Ensure ordering is correct
for i, actual := range c.TLS.Ciphers {
if actual != expectedCiphers[i] {
t.Errorf("Expected cipher in position %d to be %0x, got %0x", i, expectedCiphers[i], actual)
}
}
if !c.TLS.PreferServerCipherSuites {
t.Error("Expected PreferServerCipherSuites = true, but was false")
}
}
func TestSetupParseIncompleteParams(t *testing.T) {
// Using tls without args is an error because it's unnecessary.
c := setup.NewTestController(`tls`)
_, err := Setup(c)
if err == nil {
t.Error("Expected an error, but didn't get one")
}
}
func TestSetupParseWithOptionalParams(t *testing.T) {
params := `tls ` + certFile + ` ` + keyFile + ` {
protocols ssl3.0 tls1.2
ciphers RSA-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384
}`
c := setup.NewTestController(params)
_, err := Setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if c.TLS.ProtocolMinVersion != tls.VersionSSL30 {
t.Errorf("Expected 'ssl3.0 (0x0300)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion)
}
if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 {
t.Errorf("Expected 'tls1.2 (0x0302)' as ProtocolMaxVersion, got %#v", c.TLS.ProtocolMaxVersion)
}
if len(c.TLS.Ciphers)-1 != 3 {
t.Errorf("Expected 3 Ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers)-1)
}
}
func TestSetupDefaultWithOptionalParams(t *testing.T) {
params := `tls {
ciphers RSA-3DES-EDE-CBC-SHA
}`
c := setup.NewTestController(params)
_, err := Setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if len(c.TLS.Ciphers)-1 != 1 {
t.Errorf("Expected 1 ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers)-1)
}
}
// TODO: If we allow this... but probably not a good idea.
// func TestSetupDisableHTTPRedirect(t *testing.T) {
// c := NewTestController(`tls {
// allow_http
// }`)
// _, err := TLS(c)
// if err != nil {
// t.Errorf("Expected no error, but got %v", err)
// }
// if !c.TLS.DisableHTTPRedir {
// t.Error("Expected HTTP redirect to be disabled, but it wasn't")
// }
// }
func TestSetupParseWithWrongOptionalParams(t *testing.T) {
// Test protocols wrong params
params := `tls ` + certFile + ` ` + keyFile + ` {
protocols ssl tls
}`
c := setup.NewTestController(params)
_, err := Setup(c)
if err == nil {
t.Errorf("Expected errors, but no error returned")
}
// Test ciphers wrong params
params = `tls ` + certFile + ` ` + keyFile + ` {
ciphers not-valid-cipher
}`
c = setup.NewTestController(params)
_, err = Setup(c)
if err == nil {
t.Errorf("Expected errors, but no error returned")
}
}
func TestSetupParseWithClientAuth(t *testing.T) {
params := `tls ` + certFile + ` ` + keyFile + ` {
clients client_ca.crt client2_ca.crt
}`
c := setup.NewTestController(params)
_, err := Setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if count := len(c.TLS.ClientCerts); count != 2 {
t.Fatalf("Expected two client certs, had %d", count)
}
if actual := c.TLS.ClientCerts[0]; actual != "client_ca.crt" {
t.Errorf("Expected first client cert file to be '%s', but was '%s'", "client_ca.crt", actual)
}
if actual := c.TLS.ClientCerts[1]; actual != "client2_ca.crt" {
t.Errorf("Expected second client cert file to be '%s', but was '%s'", "client2_ca.crt", actual)
}
// Test missing client cert file
params = `tls ` + certFile + ` ` + keyFile + ` {
clients
}`
c = setup.NewTestController(params)
_, err = Setup(c)
if err == nil {
t.Errorf("Expected an error, but no error returned")
}
}
const (
certFile = "test_cert.pem"
keyFile = "test_key.pem"
)
var testCert = []byte(`-----BEGIN CERTIFICATE-----
MIIBkjCCATmgAwIBAgIJANfFCBcABL6LMAkGByqGSM49BAEwFDESMBAGA1UEAxMJ
bG9jYWxob3N0MB4XDTE2MDIxMDIyMjAyNFoXDTE4MDIwOTIyMjAyNFowFDESMBAG
A1UEAxMJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs22MtnG7
9K1mvIyjEO9GLx7BFD0tBbGnwQ0VPsuCxC6IeVuXbQDLSiVQvFZ6lUszTlczNxVk
pEfqrM6xAupB7qN1MHMwHQYDVR0OBBYEFHxYDvAxUwL4XrjPev6qZ/BiLDs5MEQG
A1UdIwQ9MDuAFHxYDvAxUwL4XrjPev6qZ/BiLDs5oRikFjAUMRIwEAYDVQQDEwls
b2NhbGhvc3SCCQDXxQgXAAS+izAMBgNVHRMEBTADAQH/MAkGByqGSM49BAEDSAAw
RQIgRvBqbyJM2JCJqhA1FmcoZjeMocmhxQHTt1c+1N2wFUgCIQDtvrivbBPA688N
Qh3sMeAKNKPsx5NxYdoWuu9KWcKz9A==
-----END CERTIFICATE-----
`)
var testKey = []byte(`-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIGLtRmwzYVcrH3J0BnzYbGPdWVF10i9p6mxkA4+b2fURoAoGCCqGSM49
AwEHoUQDQgAEs22MtnG79K1mvIyjEO9GLx7BFD0tBbGnwQ0VPsuCxC6IeVuXbQDL
SiVQvFZ6lUszTlczNxVkpEfqrM6xAupB7g==
-----END EC PRIVATE KEY-----
`)
-94
View File
@@ -1,94 +0,0 @@
package https
import (
"path/filepath"
"strings"
"github.com/mholt/caddy/caddy/assets"
)
// storage is used to get file paths in a consistent,
// cross-platform way for persisting Let's Encrypt assets
// on the file system.
var storage = Storage(filepath.Join(assets.Path(), "letsencrypt"))
// Storage is a root directory and facilitates
// forming file paths derived from it.
type Storage string
// Sites gets the directory that stores site certificate and keys.
func (s Storage) Sites() string {
return filepath.Join(string(s), "sites")
}
// Site returns the path to the folder containing assets for domain.
func (s Storage) Site(domain string) string {
return filepath.Join(s.Sites(), domain)
}
// SiteCertFile returns the path to the certificate file for domain.
func (s Storage) SiteCertFile(domain string) string {
return filepath.Join(s.Site(domain), domain+".crt")
}
// SiteKeyFile returns the path to domain's private key file.
func (s Storage) SiteKeyFile(domain string) string {
return filepath.Join(s.Site(domain), domain+".key")
}
// SiteMetaFile returns the path to the domain's asset metadata file.
func (s Storage) SiteMetaFile(domain string) string {
return filepath.Join(s.Site(domain), domain+".json")
}
// Users gets the directory that stores account folders.
func (s Storage) Users() string {
return filepath.Join(string(s), "users")
}
// User gets the account folder for the user with email.
func (s Storage) User(email string) string {
if email == "" {
email = emptyEmail
}
return filepath.Join(s.Users(), email)
}
// UserRegFile gets the path to the registration file for
// the user with the given email address.
func (s Storage) UserRegFile(email string) string {
if email == "" {
email = emptyEmail
}
fileName := emailUsername(email)
if fileName == "" {
fileName = "registration"
}
return filepath.Join(s.User(email), fileName+".json")
}
// UserKeyFile gets the path to the private key file for
// the user with the given email address.
func (s Storage) UserKeyFile(email string) string {
if email == "" {
email = emptyEmail
}
fileName := emailUsername(email)
if fileName == "" {
fileName = "private"
}
return filepath.Join(s.User(email), fileName+".key")
}
// emailUsername returns the username portion of an
// email address (part before '@') or the original
// input if it can't find the "@" symbol.
func emailUsername(email string) string {
at := strings.Index(email, "@")
if at == -1 {
return email
} else if at == 0 {
return email[1:]
}
return email[:at]
}
-88
View File
@@ -1,88 +0,0 @@
package https
import (
"path/filepath"
"testing"
)
func TestStorage(t *testing.T) {
storage = Storage("./le_test")
if expected, actual := filepath.Join("le_test", "sites"), storage.Sites(); actual != expected {
t.Errorf("Expected Sites() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com"), storage.Site("test.com"); actual != expected {
t.Errorf("Expected Site() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.crt"), storage.SiteCertFile("test.com"); actual != expected {
t.Errorf("Expected SiteCertFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.key"), storage.SiteKeyFile("test.com"); actual != expected {
t.Errorf("Expected SiteKeyFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "sites", "test.com", "test.com.json"), storage.SiteMetaFile("test.com"); actual != expected {
t.Errorf("Expected SiteMetaFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users"), storage.Users(); actual != expected {
t.Errorf("Expected Users() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", "me@example.com"), storage.User("me@example.com"); actual != expected {
t.Errorf("Expected User() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.json"), storage.UserRegFile("me@example.com"); actual != expected {
t.Errorf("Expected UserRegFile() to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", "me@example.com", "me.key"), storage.UserKeyFile("me@example.com"); actual != expected {
t.Errorf("Expected UserKeyFile() to return '%s' but got '%s'", expected, actual)
}
// Test with empty emails
if expected, actual := filepath.Join("le_test", "users", emptyEmail), storage.User(emptyEmail); actual != expected {
t.Errorf("Expected User(\"\") to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".json"), storage.UserRegFile(""); actual != expected {
t.Errorf("Expected UserRegFile(\"\") to return '%s' but got '%s'", expected, actual)
}
if expected, actual := filepath.Join("le_test", "users", emptyEmail, emptyEmail+".key"), storage.UserKeyFile(""); actual != expected {
t.Errorf("Expected UserKeyFile(\"\") to return '%s' but got '%s'", expected, actual)
}
}
func TestEmailUsername(t *testing.T) {
for i, test := range []struct {
input, expect string
}{
{
input: "username@example.com",
expect: "username",
},
{
input: "plus+addressing@example.com",
expect: "plus+addressing",
},
{
input: "me+plus-addressing@example.com",
expect: "me+plus-addressing",
},
{
input: "not-an-email",
expect: "not-an-email",
},
{
input: "@foobar.com",
expect: "foobar.com",
},
{
input: emptyEmail,
expect: emptyEmail,
},
{
input: "",
expect: "",
},
} {
if actual := emailUsername(test.input); actual != test.expect {
t.Errorf("Test %d: Expected username to be '%s' but was '%s'", i, test.expect, actual)
}
}
}
+14
View File
@@ -0,0 +1,14 @@
// By moving the application's package main logic into
// a package other than main, it becomes much easier to
// wrap caddy for custom builds that are go-gettable.
// https://forum.caddyserver.com/t/my-wish-for-0-9-go-gettable-custom-builds/59?u=matt
package main
import "github.com/mholt/caddy/caddy/caddymain"
var run = caddymain.Run // replaced for tests
func main() {
run()
}
+17
View File
@@ -0,0 +1,17 @@
package main
import "testing"
// This works because it does not have the same signature as the
// conventional "TestMain" function described in the testing package
// godoc.
func TestMain(t *testing.T) {
var ran bool
run = func() {
ran = true
}
main()
if !ran {
t.Error("Expected Run() to be called, but it wasn't")
}
}
-165
View File
@@ -1,165 +0,0 @@
package parse
import (
"strings"
"testing"
)
type lexerTestCase struct {
input string
expected []token
}
func TestLexer(t *testing.T) {
testCases := []lexerTestCase{
{
input: `host:123`,
expected: []token{
{line: 1, text: "host:123"},
},
},
{
input: `host:123
directive`,
expected: []token{
{line: 1, text: "host:123"},
{line: 3, text: "directive"},
},
},
{
input: `host:123 {
directive
}`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 2, text: "directive"},
{line: 3, text: "}"},
},
},
{
input: `host:123 { directive }`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 1, text: "directive"},
{line: 1, text: "}"},
},
},
{
input: `host:123 {
#comment
directive
# comment
foobar # another comment
}`,
expected: []token{
{line: 1, text: "host:123"},
{line: 1, text: "{"},
{line: 3, text: "directive"},
{line: 5, text: "foobar"},
{line: 6, text: "}"},
},
},
{
input: `a "quoted value" b
foobar`,
expected: []token{
{line: 1, text: "a"},
{line: 1, text: "quoted value"},
{line: 1, text: "b"},
{line: 2, text: "foobar"},
},
},
{
input: `A "quoted \"value\" inside" B`,
expected: []token{
{line: 1, text: "A"},
{line: 1, text: `quoted "value" inside`},
{line: 1, text: "B"},
},
},
{
input: `"don't\escape"`,
expected: []token{
{line: 1, text: `don't\escape`},
},
},
{
input: `"don't\\escape"`,
expected: []token{
{line: 1, text: `don't\\escape`},
},
},
{
input: `A "quoted value with line
break inside" {
foobar
}`,
expected: []token{
{line: 1, text: "A"},
{line: 1, text: "quoted value with line\n\t\t\t\t\tbreak inside"},
{line: 2, text: "{"},
{line: 3, text: "foobar"},
{line: 4, text: "}"},
},
},
{
input: `"C:\php\php-cgi.exe"`,
expected: []token{
{line: 1, text: `C:\php\php-cgi.exe`},
},
},
{
input: `empty "" string`,
expected: []token{
{line: 1, text: `empty`},
{line: 1, text: ``},
{line: 1, text: `string`},
},
},
{
input: "skip those\r\nCR characters",
expected: []token{
{line: 1, text: "skip"},
{line: 1, text: "those"},
{line: 2, text: "CR"},
{line: 2, text: "characters"},
},
},
}
for i, testCase := range testCases {
actual := tokenize(testCase.input)
lexerCompare(t, i, testCase.expected, actual)
}
}
func tokenize(input string) (tokens []token) {
l := lexer{}
l.load(strings.NewReader(input))
for l.next() {
tokens = append(tokens, l.token)
}
return
}
func lexerCompare(t *testing.T, n int, expected, actual []token) {
if len(expected) != len(actual) {
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
}
for i := 0; i < len(actual) && i < len(expected); i++ {
if actual[i].line != expected[i].line {
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
n, i, expected[i].text, expected[i].line, actual[i].line)
break
}
if actual[i].text != expected[i].text {
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
n, i, expected[i].text, actual[i].text)
break
}
}
}
-32
View File
@@ -1,32 +0,0 @@
// Package parse provides facilities for parsing configuration files.
package parse
import "io"
// ServerBlocks parses the input just enough to organize tokens,
// in order, by server block. No further parsing is performed.
// If checkDirectives is true, only valid directives will be allowed
// otherwise we consider it a parse error. Server blocks are returned
// in the order in which they appear.
func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]ServerBlock, error) {
p := parser{Dispenser: NewDispenser(filename, input)}
p.checkDirectives = checkDirectives
blocks, err := p.parseAll()
return blocks, err
}
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
func allTokens(input io.Reader) (tokens []token) {
l := new(lexer)
l.load(input)
for l.next() {
tokens = append(tokens, l.token)
}
return
}
// ValidDirectives is a set of directives that are valid (unordered). Populated
// by config package's init function.
var ValidDirectives = make(map[string]struct{})
-22
View File
@@ -1,22 +0,0 @@
package parse
import (
"strings"
"testing"
)
func TestAllTokens(t *testing.T) {
input := strings.NewReader("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"}
tokens := allTokens(input)
if len(tokens) != len(expected) {
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
}
for i, val := range expected {
if tokens[i].text != val {
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text)
}
}
}
-477
View File
@@ -1,477 +0,0 @@
package parse
import (
"os"
"strings"
"testing"
)
func TestStandardAddress(t *testing.T) {
for i, test := range []struct {
input string
scheme, host, port string
shouldErr bool
}{
{`localhost`, "", "localhost", "", false},
{`localhost:1234`, "", "localhost", "1234", false},
{`localhost:`, "", "localhost", "", false},
{`0.0.0.0`, "", "0.0.0.0", "", false},
{`127.0.0.1:1234`, "", "127.0.0.1", "1234", false},
{`:1234`, "", "", "1234", false},
{`[::1]`, "", "::1", "", false},
{`[::1]:1234`, "", "::1", "1234", false},
{`:`, "", "", "", false},
{`localhost:http`, "http", "localhost", "80", false},
{`localhost:https`, "https", "localhost", "443", false},
{`:http`, "http", "", "80", false},
{`:https`, "https", "", "443", false},
{`http://localhost:https`, "", "", "", true}, // conflict
{`http://localhost:http`, "", "", "", true}, // repeated scheme
{`http://localhost:443`, "", "", "", true}, // not conventional
{`https://localhost:80`, "", "", "", true}, // not conventional
{`http://localhost`, "http", "localhost", "80", false},
{`https://localhost`, "https", "localhost", "443", false},
{`http://127.0.0.1`, "http", "127.0.0.1", "80", false},
{`https://127.0.0.1`, "https", "127.0.0.1", "443", false},
{`http://[::1]`, "http", "::1", "80", false},
{`http://localhost:1234`, "http", "localhost", "1234", false},
{`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", false},
{`http://[::1]:1234`, "http", "::1", "1234", false},
{``, "", "", "", false},
{`::1`, "", "::1", "", true},
{`localhost::`, "", "localhost::", "", true},
{`#$%@`, "", "#$%@", "", true},
} {
actual, err := standardAddress(test.input)
if err != nil && !test.shouldErr {
t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err)
}
if err == nil && test.shouldErr {
t.Errorf("Test %d (%s): Expected error, but had none", i, test.input)
}
if actual.Scheme != test.scheme {
t.Errorf("Test %d (%s): Expected scheme '%s', got '%s'", i, test.input, test.scheme, actual.Scheme)
}
if actual.Host != test.host {
t.Errorf("Test %d (%s): Expected host '%s', got '%s'", i, test.input, test.host, actual.Host)
}
if actual.Port != test.port {
t.Errorf("Test %d (%s): Expected port '%s', got '%s'", i, test.input, test.port, actual.Port)
}
}
}
func TestParseOneAndImport(t *testing.T) {
setupParseTests()
testParseOne := func(input string) (ServerBlock, error) {
p := testParser(input)
p.Next() // parseOne doesn't call Next() to start, so we must
err := p.parseOne()
return p.block, err
}
for i, test := range []struct {
input string
shouldErr bool
addresses []address
tokens map[string]int // map of directive name to number of tokens expected
}{
{`localhost`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{}},
{`localhost
dir1`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 1,
}},
{`localhost:1234
dir1 foo bar`, false, []address{
{"localhost:1234", "", "localhost", "1234"},
}, map[string]int{
"dir1": 3,
}},
{`localhost {
dir1
}`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 1,
}},
{`localhost:1234 {
dir1 foo bar
dir2
}`, false, []address{
{"localhost:1234", "", "localhost", "1234"},
}, map[string]int{
"dir1": 3,
"dir2": 1,
}},
{`http://localhost https://localhost
dir1 foo bar`, false, []address{
{"http://localhost", "http", "localhost", "80"},
{"https://localhost", "https", "localhost", "443"},
}, map[string]int{
"dir1": 3,
}},
{`http://localhost https://localhost {
dir1 foo bar
}`, false, []address{
{"http://localhost", "http", "localhost", "80"},
{"https://localhost", "https", "localhost", "443"},
}, map[string]int{
"dir1": 3,
}},
{`http://localhost, https://localhost {
dir1 foo bar
}`, false, []address{
{"http://localhost", "http", "localhost", "80"},
{"https://localhost", "https", "localhost", "443"},
}, map[string]int{
"dir1": 3,
}},
{`http://localhost, {
}`, true, []address{
{"http://localhost", "http", "localhost", "80"},
}, map[string]int{}},
{`host1:80, http://host2.com
dir1 foo bar
dir2 baz`, false, []address{
{"host1:80", "", "host1", "80"},
{"http://host2.com", "http", "host2.com", "80"},
}, map[string]int{
"dir1": 3,
"dir2": 2,
}},
{`http://host1.com,
http://host2.com,
https://host3.com`, false, []address{
{"http://host1.com", "http", "host1.com", "80"},
{"http://host2.com", "http", "host2.com", "80"},
{"https://host3.com", "https", "host3.com", "443"},
}, map[string]int{}},
{`http://host1.com:1234, https://host2.com
dir1 foo {
bar baz
}
dir2`, false, []address{
{"http://host1.com:1234", "http", "host1.com", "1234"},
{"https://host2.com", "https", "host2.com", "443"},
}, map[string]int{
"dir1": 6,
"dir2": 1,
}},
{`127.0.0.1
dir1 {
bar baz
}
dir2 {
foo bar
}`, false, []address{
{"127.0.0.1", "", "127.0.0.1", ""},
}, map[string]int{
"dir1": 5,
"dir2": 5,
}},
{`127.0.0.1
unknown_directive`, true, []address{
{"127.0.0.1", "", "127.0.0.1", ""},
}, map[string]int{}},
{`localhost
dir1 {
foo`, true, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
}`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
} }`, true, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
nested {
foo
}
}
dir2 foo bar`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 7,
"dir2": 3,
}},
{``, false, []address{}, map[string]int{}},
{`localhost
dir1 arg1
import import_test1.txt`, false, []address{
{"localhost", "", "localhost", ""},
}, map[string]int{
"dir1": 2,
"dir2": 3,
"dir3": 1,
}},
{`import import_test2.txt`, false, []address{
{"host1", "", "host1", ""},
}, map[string]int{
"dir1": 1,
"dir2": 2,
}},
{`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}},
{`import not_found.txt`, true, []address{}, map[string]int{}},
{`""`, false, []address{}, map[string]int{}},
{``, false, []address{}, map[string]int{}},
} {
result, err := testParseOne(test.input)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(result.Addresses) != len(test.addresses) {
t.Errorf("Test %d: Expected %d addresses, got %d",
i, len(test.addresses), len(result.Addresses))
continue
}
for j, addr := range result.Addresses {
if addr.Host != test.addresses[j].Host {
t.Errorf("Test %d, address %d: Expected host to be '%s', but was '%s'",
i, j, test.addresses[j].Host, addr.Host)
}
if addr.Port != test.addresses[j].Port {
t.Errorf("Test %d, address %d: Expected port to be '%s', but was '%s'",
i, j, test.addresses[j].Port, addr.Port)
}
}
if len(result.Tokens) != len(test.tokens) {
t.Errorf("Test %d: Expected %d directives, had %d",
i, len(test.tokens), len(result.Tokens))
continue
}
for directive, tokens := range result.Tokens {
if len(tokens) != test.tokens[directive] {
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
i, directive, test.tokens[directive], len(tokens))
continue
}
}
}
}
func TestParseAll(t *testing.T) {
setupParseTests()
for i, test := range []struct {
input string
shouldErr bool
addresses [][]address // addresses per server block, in order
}{
{`localhost`, false, [][]address{
{{"localhost", "", "localhost", ""}},
}},
{`localhost:1234`, false, [][]address{
{{"localhost:1234", "", "localhost", "1234"}},
}},
{`localhost:1234 {
}
localhost:2015 {
}`, false, [][]address{
{{"localhost:1234", "", "localhost", "1234"}},
{{"localhost:2015", "", "localhost", "2015"}},
}},
{`localhost:1234, http://host2`, false, [][]address{
{{"localhost:1234", "", "localhost", "1234"}, {"http://host2", "http", "host2", "80"}},
}},
{`localhost:1234, http://host2,`, true, [][]address{}},
{`http://host1.com, http://host2.com {
}
https://host3.com, https://host4.com {
}`, false, [][]address{
{{"http://host1.com", "http", "host1.com", "80"}, {"http://host2.com", "http", "host2.com", "80"}},
{{"https://host3.com", "https", "host3.com", "443"}, {"https://host4.com", "https", "host4.com", "443"}},
}},
{`import import_glob*.txt`, false, [][]address{
{{"glob0.host0", "", "glob0.host0", ""}},
{{"glob0.host1", "", "glob0.host1", ""}},
{{"glob1.host0", "", "glob1.host0", ""}},
{{"glob2.host0", "", "glob2.host0", ""}},
}},
} {
p := testParser(test.input)
blocks, err := p.parseAll()
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(blocks) != len(test.addresses) {
t.Errorf("Test %d: Expected %d server blocks, got %d",
i, len(test.addresses), len(blocks))
continue
}
for j, block := range blocks {
if len(block.Addresses) != len(test.addresses[j]) {
t.Errorf("Test %d: Expected %d addresses in block %d, got %d",
i, len(test.addresses[j]), j, len(block.Addresses))
continue
}
for k, addr := range block.Addresses {
if addr.Host != test.addresses[j][k].Host {
t.Errorf("Test %d, block %d, address %d: Expected host to be '%s', but was '%s'",
i, j, k, test.addresses[j][k].Host, addr.Host)
}
if addr.Port != test.addresses[j][k].Port {
t.Errorf("Test %d, block %d, address %d: Expected port to be '%s', but was '%s'",
i, j, k, test.addresses[j][k].Port, addr.Port)
}
}
}
}
}
func TestEnvironmentReplacement(t *testing.T) {
setupParseTests()
os.Setenv("PORT", "8080")
os.Setenv("ADDRESS", "servername.com")
os.Setenv("FOOBAR", "foobar")
// basic test; unix-style env vars
p := testParser(`{$ADDRESS}`)
blocks, _ := p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
// multiple vars per token
p = testParser(`{$ADDRESS}:{$PORT}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// windows-style var and unix style in same token
p = testParser(`{%ADDRESS%}:{$PORT}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// reverse order
p = testParser(`{$ADDRESS}:{%PORT%}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// env var in server block body as argument
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Tokens["dir1"][1].text, "foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// combined windows env vars in argument
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].text, "servername.com/foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// malformed env var (windows)
p = testParser(":1234\ndir1 {%ADDRESS}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].text, "{%ADDRESS}"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
// malformed (non-existent) env var (unix)
p = testParser(`:{$PORT$}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Port, ""; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// in quoted field
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].text, "Test foobar test"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
}
func setupParseTests() {
// Set up some bogus directives for testing
ValidDirectives = map[string]struct{}{
"dir1": {},
"dir2": {},
"dir3": {},
}
}
func testParser(input string) parser {
buf := strings.NewReader(input)
p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true}
return p
}
-166
View File
@@ -1,166 +0,0 @@
// +build !windows
package caddy
import (
"bytes"
"encoding/gob"
"errors"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"path"
"sync/atomic"
"github.com/mholt/caddy/caddy/https"
)
func init() {
gob.Register(CaddyfileInput{})
}
// Restart restarts the entire application; gracefully with zero
// downtime if on a POSIX-compatible system, or forcefully if on
// Windows but with imperceptibly-short downtime.
//
// The restarted application will use newCaddyfile as its input
// configuration. If newCaddyfile is nil, the current (existing)
// Caddyfile configuration will be used.
//
// Note: The process must exist in the same place on the disk in
// order for this to work. Thus, multiple graceful restarts don't
// work if executing with `go run`, since the binary is cleaned up
// when `go run` sees the initial parent process exit.
func Restart(newCaddyfile Input) error {
log.Println("[INFO] Restarting")
if newCaddyfile == nil {
caddyfileMu.Lock()
newCaddyfile = caddyfile
caddyfileMu.Unlock()
}
// Get certificates for any new hosts in the new Caddyfile without causing downtime
err := getCertsForNewCaddyfile(newCaddyfile)
if err != nil {
return errors.New("TLS preload: " + err.Error())
}
if len(os.Args) == 0 { // this should never happen, but...
os.Args = []string{""}
}
// Tell the child that it's a restart
os.Setenv("CADDY_RESTART", "true")
// Prepare our payload to the child process
cdyfileGob := caddyfileGob{
ListenerFds: make(map[string]uintptr),
Caddyfile: newCaddyfile,
OnDemandTLSCertsIssued: atomic.LoadInt32(https.OnDemandIssuedCount),
}
// Prepare a pipe to the fork's stdin so it can get the Caddyfile
rpipe, wpipe, err := os.Pipe()
if err != nil {
return err
}
// Prepare a pipe that the child process will use to communicate
// its success with us by sending > 0 bytes
sigrpipe, sigwpipe, err := os.Pipe()
if err != nil {
return err
}
// Pass along relevant file descriptors to child process; ordering
// is very important since we rely on these being in certain positions.
extraFiles := []*os.File{sigwpipe} // fd 3
// Add file descriptors of all the sockets
serversMu.Lock()
for i, s := range servers {
extraFiles = append(extraFiles, s.ListenerFd())
cdyfileGob.ListenerFds[s.Addr] = uintptr(4 + i) // 4 fds come before any of the listeners
}
serversMu.Unlock()
// Set up the command
cmd := exec.Command(os.Args[0], os.Args[1:]...)
cmd.Stdin = rpipe // fd 0
cmd.Stdout = os.Stdout // fd 1
cmd.Stderr = os.Stderr // fd 2
cmd.ExtraFiles = extraFiles
// Spawn the child process
err = cmd.Start()
if err != nil {
return err
}
// Immediately close our dup'ed fds and the write end of our signal pipe
for _, f := range extraFiles {
f.Close()
}
// Feed Caddyfile to the child
err = gob.NewEncoder(wpipe).Encode(cdyfileGob)
if err != nil {
return err
}
wpipe.Close()
// Determine whether child startup succeeded
answer, readErr := ioutil.ReadAll(sigrpipe)
if answer == nil || len(answer) == 0 {
cmdErr := cmd.Wait() // get exit status
log.Printf("[ERROR] Restart: child failed to initialize (%v) - changes not applied", cmdErr)
if readErr != nil {
log.Printf("[ERROR] Restart: additionally, error communicating with child process: %v", readErr)
}
return errIncompleteRestart
}
// Looks like child is successful; we can exit gracefully.
return Stop()
}
func getCertsForNewCaddyfile(newCaddyfile Input) error {
// parse the new caddyfile only up to (and including) TLS
// so we can know what we need to get certs for.
configs, _, _, err := loadConfigsUpToIncludingTLS(path.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body()))
if err != nil {
return errors.New("loading Caddyfile: " + err.Error())
}
// first mark the configs that are qualified for managed TLS
https.MarkQualified(configs)
// since we group by bind address to obtain certs, we must call
// EnableTLS to make sure the port is set properly first
// (can ignore error since we aren't actually using the certs)
https.EnableTLS(configs, false)
// find out if we can let the acme package start its own challenge listener
// on port 80
var proxyACME bool
serversMu.Lock()
for _, s := range servers {
_, port, _ := net.SplitHostPort(s.Addr)
if port == "80" {
proxyACME = true
break
}
}
serversMu.Unlock()
// place certs on the disk
err = https.ObtainCerts(configs, false, proxyACME)
if err != nil {
return errors.New("obtaining certs: " + err.Error())
}
return nil
}
-31
View File
@@ -1,31 +0,0 @@
package caddy
import "log"
// Restart restarts Caddy forcefully using newCaddyfile,
// or, if nil, the current/existing Caddyfile is reused.
func Restart(newCaddyfile Input) error {
log.Println("[INFO] Restarting")
if newCaddyfile == nil {
caddyfileMu.Lock()
newCaddyfile = caddyfile
caddyfileMu.Unlock()
}
wg.Add(1) // barrier so Wait() doesn't unblock
err := Stop()
if err != nil {
return err
}
err = Start(newCaddyfile)
if err != nil {
return err
}
wg.Done() // take down our barrier
return nil
}
-13
View File
@@ -1,13 +0,0 @@
package setup
import "github.com/mholt/caddy/middleware"
// BindHost sets the host to bind the listener to.
func BindHost(c *Controller) (middleware.Middleware, error) {
for c.Next() {
if !c.Args(&c.BindHost) {
return nil, c.ArgErr()
}
}
return nil, nil
}
-83
View File
@@ -1,83 +0,0 @@
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)
}
-158
View File
@@ -1,158 +0,0 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/errors"
)
func TestErrors(t *testing.T) {
c := NewTestController(`errors`)
mid, err := Errors(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(*errors.ErrorHandler)
if !ok {
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
}
if myHandler.LogFile != "" {
t.Errorf("Expected '%s' as the default LogFile", "")
}
if myHandler.LogRoller != nil {
t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
// Test Startup function
if len(c.Startup) == 0 {
t.Fatal("Expected 1 startup function, had 0")
}
err = c.Startup[0]()
if myHandler.Log == nil {
t.Error("Expected Log to be non-nil after startup because Debug is not enabled")
}
}
func TestErrorsParse(t *testing.T) {
tests := []struct {
inputErrorsRules string
shouldErr bool
expectedErrorHandler errors.ErrorHandler
}{
{`errors`, false, errors.ErrorHandler{
LogFile: "",
}},
{`errors errors.txt`, false, errors.ErrorHandler{
LogFile: "errors.txt",
}},
{`errors visible`, false, errors.ErrorHandler{
LogFile: "",
Debug: true,
}},
{`errors { log visible }`, false, errors.ErrorHandler{
LogFile: "",
Debug: true,
}},
{`errors { log errors.txt
404 404.html
500 500.html
}`, false, errors.ErrorHandler{
LogFile: "errors.txt",
ErrorPages: map[int]string{
404: "404.html",
500: "500.html",
},
}},
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, errors.ErrorHandler{
LogFile: "errors.txt",
LogRoller: &middleware.LogRoller{
MaxSize: 2,
MaxAge: 10,
MaxBackups: 3,
LocalTime: true,
},
}},
{`errors { log errors.txt {
size 3
age 11
keep 5
}
404 404.html
503 503.html
}`, false, errors.ErrorHandler{
LogFile: "errors.txt",
ErrorPages: map[int]string{
404: "404.html",
503: "503.html",
},
LogRoller: &middleware.LogRoller{
MaxSize: 3,
MaxAge: 11,
MaxBackups: 5,
LocalTime: true,
},
}},
}
for i, test := range tests {
c := NewTestController(test.inputErrorsRules)
actualErrorsRule, err := errorsParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if actualErrorsRule.LogFile != test.expectedErrorHandler.LogFile {
t.Errorf("Test %d expected LogFile to be %s, but got %s",
i, test.expectedErrorHandler.LogFile, actualErrorsRule.LogFile)
}
if actualErrorsRule.Debug != test.expectedErrorHandler.Debug {
t.Errorf("Test %d expected Debug to be %v, but got %v",
i, test.expectedErrorHandler.Debug, actualErrorsRule.Debug)
}
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller == nil || actualErrorsRule.LogRoller == nil && test.expectedErrorHandler.LogRoller != nil {
t.Fatalf("Test %d expected LogRoller to be %v, but got %v",
i, test.expectedErrorHandler.LogRoller, actualErrorsRule.LogRoller)
}
if len(actualErrorsRule.ErrorPages) != len(test.expectedErrorHandler.ErrorPages) {
t.Fatalf("Test %d expected %d no of Error pages, but got %d ",
i, len(test.expectedErrorHandler.ErrorPages), len(actualErrorsRule.ErrorPages))
}
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller != nil {
if actualErrorsRule.LogRoller.Filename != test.expectedErrorHandler.LogRoller.Filename {
t.Fatalf("Test %d expected LogRoller Filename to be %s, but got %s",
i, test.expectedErrorHandler.LogRoller.Filename, actualErrorsRule.LogRoller.Filename)
}
if actualErrorsRule.LogRoller.MaxAge != test.expectedErrorHandler.LogRoller.MaxAge {
t.Fatalf("Test %d expected LogRoller MaxAge to be %d, but got %d",
i, test.expectedErrorHandler.LogRoller.MaxAge, actualErrorsRule.LogRoller.MaxAge)
}
if actualErrorsRule.LogRoller.MaxBackups != test.expectedErrorHandler.LogRoller.MaxBackups {
t.Fatalf("Test %d expected LogRoller MaxBackups to be %d, but got %d",
i, test.expectedErrorHandler.LogRoller.MaxBackups, actualErrorsRule.LogRoller.MaxBackups)
}
if actualErrorsRule.LogRoller.MaxSize != test.expectedErrorHandler.LogRoller.MaxSize {
t.Fatalf("Test %d expected LogRoller MaxSize to be %d, but got %d",
i, test.expectedErrorHandler.LogRoller.MaxSize, actualErrorsRule.LogRoller.MaxSize)
}
if actualErrorsRule.LogRoller.LocalTime != test.expectedErrorHandler.LogRoller.LocalTime {
t.Fatalf("Test %d expected LogRoller LocalTime to be %t, but got %t",
i, test.expectedErrorHandler.LogRoller.LocalTime, actualErrorsRule.LogRoller.LocalTime)
}
}
}
}
-54
View File
@@ -1,54 +0,0 @@
package setup
import (
"os"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/extensions"
)
// Ext configures a new instance of 'extensions' middleware for clean URLs.
func Ext(c *Controller) (middleware.Middleware, error) {
root := c.Root
exts, err := extParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return extensions.Ext{
Next: next,
Extensions: exts,
Root: root,
}
}, nil
}
// extParse sets up an instance of extension middleware
// from a middleware controller and returns a list of extensions.
func extParse(c *Controller) ([]string, error) {
var exts []string
for c.Next() {
// At least one extension is required
if !c.NextArg() {
return exts, c.ArgErr()
}
exts = append(exts, c.Val())
// Tack on any other extensions that may have been listed
exts = append(exts, c.RemainingArgs()...)
}
return exts, nil
}
// resourceExists returns true if the file specified at
// root + path exists; false otherwise.
func resourceExists(root, path string) bool {
_, err := os.Stat(root + path)
// technically we should use os.IsNotExist(err)
// but we don't handle any other kinds of errors anyway
return err == nil
}
-110
View File
@@ -1,110 +0,0 @@
package setup
import (
"errors"
"net/http"
"path/filepath"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/fastcgi"
)
// FastCGI configures a new FastCGI middleware instance.
func FastCGI(c *Controller) (middleware.Middleware, error) {
absRoot, err := filepath.Abs(c.Root)
if err != nil {
return nil, err
}
rules, err := fastcgiParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return fastcgi.Handler{
Next: next,
Rules: rules,
Root: c.Root,
AbsRoot: absRoot,
FileSys: http.Dir(c.Root),
SoftwareName: c.AppName,
SoftwareVersion: c.AppVersion,
ServerName: c.Host,
ServerPort: c.Port,
}
}, nil
}
func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
var rules []fastcgi.Rule
for c.Next() {
var rule fastcgi.Rule
args := c.RemainingArgs()
switch len(args) {
case 0:
return rules, c.ArgErr()
case 1:
rule.Path = "/"
rule.Address = args[0]
case 2:
rule.Path = args[0]
rule.Address = args[1]
case 3:
rule.Path = args[0]
rule.Address = args[1]
err := fastcgiPreset(args[2], &rule)
if err != nil {
return rules, c.Err("Invalid fastcgi rule preset '" + args[2] + "'")
}
}
for c.NextBlock() {
switch c.Val() {
case "ext":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.Ext = c.Val()
case "split":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.SplitPath = c.Val()
case "index":
args := c.RemainingArgs()
if len(args) == 0 {
return rules, c.ArgErr()
}
rule.IndexFiles = args
case "env":
envArgs := c.RemainingArgs()
if len(envArgs) < 2 {
return rules, c.ArgErr()
}
rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]})
}
}
rules = append(rules, rule)
}
return rules, nil
}
// fastcgiPreset configures rule according to name. It returns an error if
// name is not a recognized preset name.
func fastcgiPreset(name string, rule *fastcgi.Rule) error {
switch name {
case "php":
rule.Ext = ".php"
rule.SplitPath = ".php"
rule.IndexFiles = []string{"index.php"}
default:
return errors.New(name + " is not a valid preset name")
}
return nil
}
-107
View File
@@ -1,107 +0,0 @@
package setup
import (
"fmt"
"github.com/mholt/caddy/middleware/fastcgi"
"testing"
)
func TestFastCGI(t *testing.T) {
c := NewTestController(`fastcgi / 127.0.0.1:9000`)
mid, err := FastCGI(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(fastcgi.Handler)
if !ok {
t.Fatalf("Expected handler to be type , got: %#v", handler)
}
if myHandler.Rules[0].Path != "/" {
t.Errorf("Expected / as the Path")
}
if myHandler.Rules[0].Address != "127.0.0.1:9000" {
t.Errorf("Expected 127.0.0.1:9000 as the Address")
}
}
func TestFastcgiParse(t *testing.T) {
tests := []struct {
inputFastcgiConfig string
shouldErr bool
expectedFastcgiConfig []fastcgi.Rule
}{
{`fastcgi /blog 127.0.0.1:9000 php`,
false, []fastcgi.Rule{{
Path: "/blog",
Address: "127.0.0.1:9000",
Ext: ".php",
SplitPath: ".php",
IndexFiles: []string{"index.php"},
}}},
{`fastcgi / 127.0.0.1:9001 {
split .html
}`,
false, []fastcgi.Rule{{
Path: "/",
Address: "127.0.0.1:9001",
Ext: "",
SplitPath: ".html",
IndexFiles: []string{},
}}},
}
for i, test := range tests {
c := NewTestController(test.inputFastcgiConfig)
actualFastcgiConfigs, err := fastcgiParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualFastcgiConfigs) != len(test.expectedFastcgiConfig) {
t.Fatalf("Test %d expected %d no of FastCGI configs, but got %d ",
i, len(test.expectedFastcgiConfig), len(actualFastcgiConfigs))
}
for j, actualFastcgiConfig := range actualFastcgiConfigs {
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
}
if actualFastcgiConfig.Address != test.expectedFastcgiConfig[j].Address {
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Address, actualFastcgiConfig.Address)
}
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
t.Errorf("Test %d expected %dth FastCGI Ext to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Ext, actualFastcgiConfig.Ext)
}
if actualFastcgiConfig.SplitPath != test.expectedFastcgiConfig[j].SplitPath {
t.Errorf("Test %d expected %dth FastCGI SplitPath to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
}
if fmt.Sprint(actualFastcgiConfig.IndexFiles) != fmt.Sprint(test.expectedFastcgiConfig[j].IndexFiles) {
t.Errorf("Test %d expected %dth FastCGI IndexFiles to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].IndexFiles, actualFastcgiConfig.IndexFiles)
}
}
}
}
-84
View File
@@ -1,84 +0,0 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/headers"
)
// Headers configures a new Headers middleware instance.
func Headers(c *Controller) (middleware.Middleware, error) {
rules, err := headersParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return headers.Headers{Next: next, Rules: rules}
}, nil
}
func headersParse(c *Controller) ([]headers.Rule, error) {
var rules []headers.Rule
for c.NextLine() {
var head headers.Rule
var isNewPattern bool
if !c.NextArg() {
return rules, c.ArgErr()
}
pattern := c.Val()
// See if we already have a definition for this Path pattern...
for _, h := range rules {
if h.Path == pattern {
head = h
break
}
}
// ...otherwise, this is a new pattern
if head.Path == "" {
head.Path = pattern
isNewPattern = true
}
for c.NextBlock() {
// A block of headers was opened...
h := headers.Header{Name: c.Val()}
if c.NextArg() {
h.Value = c.Val()
}
head.Headers = append(head.Headers, h)
}
if c.NextArg() {
// ... or single header was defined as an argument instead.
h := headers.Header{Name: c.Val()}
h.Value = c.Val()
if c.NextArg() {
h.Value = c.Val()
}
head.Headers = append(head.Headers, h)
}
if isNewPattern {
rules = append(rules, head)
} else {
for i := 0; i < len(rules); i++ {
if rules[i].Path == pattern {
rules[i] = head
break
}
}
}
}
return rules, nil
}
-31
View File
@@ -1,31 +0,0 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/inner"
)
// Internal configures a new Internal middleware instance.
func Internal(c *Controller) (middleware.Middleware, error) {
paths, err := internalParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return inner.Internal{Next: next, Paths: paths}
}, nil
}
func internalParse(c *Controller) ([]string, error) {
var paths []string
for c.Next() {
if !c.NextArg() {
return paths, c.ArgErr()
}
paths = append(paths, c.Val())
}
return paths, nil
}
-130
View File
@@ -1,130 +0,0 @@
package setup
import (
"io"
"log"
"os"
"github.com/hashicorp/go-syslog"
"github.com/mholt/caddy/middleware"
caddylog "github.com/mholt/caddy/middleware/log"
"github.com/mholt/caddy/server"
)
// Log sets up the logging middleware.
func Log(c *Controller) (middleware.Middleware, error) {
rules, err := logParse(c)
if err != nil {
return nil, err
}
// Open the log files for writing when the server starts
c.Startup = append(c.Startup, func() error {
for i := 0; i < len(rules); i++ {
var err error
var writer io.Writer
if rules[i].OutputFile == "stdout" {
writer = os.Stdout
} else if rules[i].OutputFile == "stderr" {
writer = os.Stderr
} else if rules[i].OutputFile == "syslog" {
writer, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "caddy")
if err != nil {
return err
}
} else {
var file *os.File
file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return err
}
if rules[i].Roller != nil {
file.Close()
rules[i].Roller.Filename = rules[i].OutputFile
writer = rules[i].Roller.GetLogWriter()
} else {
writer = file
}
}
rules[i].Log = log.New(writer, "", 0)
}
return nil
})
return func(next middleware.Handler) middleware.Handler {
return caddylog.Logger{Next: next, Rules: rules, ErrorFunc: server.DefaultErrorFunc}
}, nil
}
func logParse(c *Controller) ([]caddylog.Rule, error) {
var rules []caddylog.Rule
for c.Next() {
args := c.RemainingArgs()
var logRoller *middleware.LogRoller
if c.NextBlock() {
if c.Val() == "rotate" {
if c.NextArg() {
if c.Val() == "{" {
var err error
logRoller, err = parseRoller(c)
if err != nil {
return nil, err
}
// This part doesn't allow having something after the rotate block
if c.Next() {
if c.Val() != "}" {
return nil, c.ArgErr()
}
}
}
}
}
}
if len(args) == 0 {
// Nothing specified; use defaults
rules = append(rules, caddylog.Rule{
PathScope: "/",
OutputFile: caddylog.DefaultLogFilename,
Format: caddylog.DefaultLogFormat,
Roller: logRoller,
})
} else if len(args) == 1 {
// Only an output file specified
rules = append(rules, caddylog.Rule{
PathScope: "/",
OutputFile: args[0],
Format: caddylog.DefaultLogFormat,
Roller: logRoller,
})
} else {
// Path scope, output file, and maybe a format specified
format := caddylog.DefaultLogFormat
if len(args) > 2 {
switch args[2] {
case "{common}":
format = caddylog.CommonLogFormat
case "{combined}":
format = caddylog.CombinedLogFormat
default:
format = args[2]
}
}
rules = append(rules, caddylog.Rule{
PathScope: args[0],
OutputFile: args[1],
Format: format,
Roller: logRoller,
})
}
}
return rules, nil
}
-175
View File
@@ -1,175 +0,0 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware"
caddylog "github.com/mholt/caddy/middleware/log"
)
func TestLog(t *testing.T) {
c := NewTestController(`log`)
mid, err := Log(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(caddylog.Logger)
if !ok {
t.Fatalf("Expected handler to be type Logger, got: %#v", handler)
}
if myHandler.Rules[0].PathScope != "/" {
t.Errorf("Expected / as the default PathScope")
}
if myHandler.Rules[0].OutputFile != caddylog.DefaultLogFilename {
t.Errorf("Expected %s as the default OutputFile", caddylog.DefaultLogFilename)
}
if myHandler.Rules[0].Format != caddylog.DefaultLogFormat {
t.Errorf("Expected %s as the default Log Format", caddylog.DefaultLogFormat)
}
if myHandler.Rules[0].Roller != nil {
t.Errorf("Expected Roller to be nil, got: %v", *myHandler.Rules[0].Roller)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestLogParse(t *testing.T) {
tests := []struct {
inputLogRules string
shouldErr bool
expectedLogRules []caddylog.Rule
}{
{`log`, false, []caddylog.Rule{{
PathScope: "/",
OutputFile: caddylog.DefaultLogFilename,
Format: caddylog.DefaultLogFormat,
}}},
{`log log.txt`, false, []caddylog.Rule{{
PathScope: "/",
OutputFile: "log.txt",
Format: caddylog.DefaultLogFormat,
}}},
{`log /api log.txt`, false, []caddylog.Rule{{
PathScope: "/api",
OutputFile: "log.txt",
Format: caddylog.DefaultLogFormat,
}}},
{`log /serve stdout`, false, []caddylog.Rule{{
PathScope: "/serve",
OutputFile: "stdout",
Format: caddylog.DefaultLogFormat,
}}},
{`log /myapi log.txt {common}`, false, []caddylog.Rule{{
PathScope: "/myapi",
OutputFile: "log.txt",
Format: caddylog.CommonLogFormat,
}}},
{`log /test accesslog.txt {combined}`, false, []caddylog.Rule{{
PathScope: "/test",
OutputFile: "accesslog.txt",
Format: caddylog.CombinedLogFormat,
}}},
{`log /api1 log.txt
log /api2 accesslog.txt {combined}`, false, []caddylog.Rule{{
PathScope: "/api1",
OutputFile: "log.txt",
Format: caddylog.DefaultLogFormat,
}, {
PathScope: "/api2",
OutputFile: "accesslog.txt",
Format: caddylog.CombinedLogFormat,
}}},
{`log /api3 stdout {host}
log /api4 log.txt {when}`, false, []caddylog.Rule{{
PathScope: "/api3",
OutputFile: "stdout",
Format: "{host}",
}, {
PathScope: "/api4",
OutputFile: "log.txt",
Format: "{when}",
}}},
{`log access.log { rotate { size 2 age 10 keep 3 } }`, false, []caddylog.Rule{{
PathScope: "/",
OutputFile: "access.log",
Format: caddylog.DefaultLogFormat,
Roller: &middleware.LogRoller{
MaxSize: 2,
MaxAge: 10,
MaxBackups: 3,
LocalTime: true,
},
}}},
}
for i, test := range tests {
c := NewTestController(test.inputLogRules)
actualLogRules, err := logParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualLogRules) != len(test.expectedLogRules) {
t.Fatalf("Test %d expected %d no of Log rules, but got %d ",
i, len(test.expectedLogRules), len(actualLogRules))
}
for j, actualLogRule := range actualLogRules {
if actualLogRule.PathScope != test.expectedLogRules[j].PathScope {
t.Errorf("Test %d expected %dth LogRule PathScope to be %s , but got %s",
i, j, test.expectedLogRules[j].PathScope, actualLogRule.PathScope)
}
if actualLogRule.OutputFile != test.expectedLogRules[j].OutputFile {
t.Errorf("Test %d expected %dth LogRule OutputFile to be %s , but got %s",
i, j, test.expectedLogRules[j].OutputFile, actualLogRule.OutputFile)
}
if actualLogRule.Format != test.expectedLogRules[j].Format {
t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s",
i, j, test.expectedLogRules[j].Format, actualLogRule.Format)
}
if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller == nil || actualLogRule.Roller == nil && test.expectedLogRules[j].Roller != nil {
t.Fatalf("Test %d expected %dth LogRule Roller to be %v, but got %v",
i, j, test.expectedLogRules[j].Roller, actualLogRule.Roller)
}
if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller != nil {
if actualLogRule.Roller.Filename != test.expectedLogRules[j].Roller.Filename {
t.Fatalf("Test %d expected %dth LogRule Roller Filename to be %s, but got %s",
i, j, test.expectedLogRules[j].Roller.Filename, actualLogRule.Roller.Filename)
}
if actualLogRule.Roller.MaxAge != test.expectedLogRules[j].Roller.MaxAge {
t.Fatalf("Test %d expected %dth LogRule Roller MaxAge to be %d, but got %d",
i, j, test.expectedLogRules[j].Roller.MaxAge, actualLogRule.Roller.MaxAge)
}
if actualLogRule.Roller.MaxBackups != test.expectedLogRules[j].Roller.MaxBackups {
t.Fatalf("Test %d expected %dth LogRule Roller MaxBackups to be %d, but got %d",
i, j, test.expectedLogRules[j].Roller.MaxBackups, actualLogRule.Roller.MaxBackups)
}
if actualLogRule.Roller.MaxSize != test.expectedLogRules[j].Roller.MaxSize {
t.Fatalf("Test %d expected %dth LogRule Roller MaxSize to be %d, but got %d",
i, j, test.expectedLogRules[j].Roller.MaxSize, actualLogRule.Roller.MaxSize)
}
if actualLogRule.Roller.LocalTime != test.expectedLogRules[j].Roller.LocalTime {
t.Fatalf("Test %d expected %dth LogRule Roller LocalTime to be %t, but got %t",
i, j, test.expectedLogRules[j].Roller.LocalTime, actualLogRule.Roller.LocalTime)
}
}
}
}
}
-157
View File
@@ -1,157 +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
args := c.RemainingArgs()
switch len(args) {
case 0:
md.PathScope = "/"
case 1:
md.PathScope = args[0]
default:
return mdconfigs, c.ArgErr()
}
// Load any other configuration parameters
for c.NextBlock() {
if err := loadParams(c, md); err != nil {
return mdconfigs, err
}
}
// If no extensions were specified, assume some defaults
if len(md.Extensions) == 0 {
md.Extensions = []string{".md", ".markdown", ".mdown"}
}
mdconfigs = append(mdconfigs, md)
}
return mdconfigs, nil
}
func loadParams(c *Controller, mdc *markdown.Config) error {
switch c.Val() {
case "ext":
exts := c.RemainingArgs()
if len(exts) == 0 {
return c.ArgErr()
}
mdc.Extensions = append(mdc.Extensions, exts...)
return nil
case "css":
if !c.NextArg() {
return c.ArgErr()
}
mdc.Styles = append(mdc.Styles, c.Val())
return nil
case "js":
if !c.NextArg() {
return c.ArgErr()
}
mdc.Scripts = append(mdc.Scripts, c.Val())
return nil
case "template":
tArgs := c.RemainingArgs()
switch len(tArgs) {
case 0:
return c.ArgErr()
case 1:
if _, ok := mdc.Templates[markdown.DefaultTemplate]; ok {
return c.Err("only one default template is allowed, use alias.")
}
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0]))
mdc.Templates[markdown.DefaultTemplate] = fpath
return nil
case 2:
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1]))
mdc.Templates[tArgs[0]] = fpath
return nil
default:
return c.ArgErr()
}
case "sitegen":
if c.NextArg() {
mdc.StaticDir = path.Join(c.Root, c.Val())
} else {
mdc.StaticDir = path.Join(c.Root, markdown.DefaultStaticDir)
}
if c.NextArg() {
// only 1 argument allowed
return c.ArgErr()
}
return nil
case "dev":
if c.NextArg() {
mdc.Development = strings.ToLower(c.Val()) == "true"
} else {
mdc.Development = true
}
if c.NextArg() {
// only 1 argument allowed
return c.ArgErr()
}
return nil
default:
return c.Err("Expected valid markdown configuration property")
}
}
-184
View File
@@ -1,184 +0,0 @@
package setup
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"testing"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/markdown"
)
func TestMarkdown(t *testing.T) {
c := NewTestController(`markdown /blog`)
mid, err := Markdown(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(markdown.Markdown)
if !ok {
t.Fatalf("Expected handler to be type Markdown, got: %#v", handler)
}
if myHandler.Configs[0].PathScope != "/blog" {
t.Errorf("Expected /blog as the Path Scope")
}
if fmt.Sprint(myHandler.Configs[0].Extensions) != fmt.Sprint([]string{".md", ".markdown", ".mdown"}) {
t.Errorf("Expected .md, .markdown, and .mdown as default extensions")
}
}
func TestMarkdownStaticGen(t *testing.T) {
c := NewTestController(`markdown /blog {
ext .md
template tpl_with_include.html
sitegen
}`)
c.Root = "./testdata"
mid, err := Markdown(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
for _, start := range c.Startup {
err := start()
if err != nil {
t.Errorf("Startup error: %v", err)
}
}
next := middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called")
return 0, nil
})
hndlr := mid(next)
mkdwn, ok := hndlr.(markdown.Markdown)
if !ok {
t.Fatalf("Was expecting a markdown.Markdown but got %T", hndlr)
}
expectedStaticFiles := map[string]string{"/blog/first_post.md": "testdata/generated_site/blog/first_post.md/index.html"}
if fmt.Sprint(expectedStaticFiles) != fmt.Sprint(mkdwn.Configs[0].StaticFiles) {
t.Fatalf("Test expected StaticFiles to be %s, but got %s",
fmt.Sprint(expectedStaticFiles), fmt.Sprint(mkdwn.Configs[0].StaticFiles))
}
filePath := "testdata/generated_site/blog/first_post.md/index.html"
if _, err := os.Stat(filePath); err != nil {
t.Fatalf("An error occured when getting the file information: %v", err)
}
html, err := ioutil.ReadFile(filePath)
if err != nil {
t.Fatalf("An error occured when getting the file content: %v", err)
}
expectedBody := []byte(`<!DOCTYPE html>
<html>
<head>
<title>first_post</title>
</head>
<body>
<h1>Header title</h1>
<h1>Test h1</h1>
</body>
</html>
`)
if !bytes.Equal(html, expectedBody) {
t.Fatalf("Expected file content: %s got: %s", string(expectedBody), string(html))
}
fp := filepath.Join(c.Root, markdown.DefaultStaticDir)
if err = os.RemoveAll(fp); err != nil {
t.Errorf("Error while removing the generated static files: %v", err)
}
}
func TestMarkdownParse(t *testing.T) {
tests := []struct {
inputMarkdownConfig string
shouldErr bool
expectedMarkdownConfig []markdown.Config
}{
{`markdown /blog {
ext .md .txt
css /resources/css/blog.css
js /resources/js/blog.js
}`, false, []markdown.Config{{
PathScope: "/blog",
Extensions: []string{".md", ".txt"},
Styles: []string{"/resources/css/blog.css"},
Scripts: []string{"/resources/js/blog.js"},
}}},
{`markdown /blog {
ext .md
template tpl_with_include.html
sitegen
}`, false, []markdown.Config{{
PathScope: "/blog",
Extensions: []string{".md"},
Templates: map[string]string{markdown.DefaultTemplate: "testdata/tpl_with_include.html"},
StaticDir: markdown.DefaultStaticDir,
}}},
}
for i, test := range tests {
c := NewTestController(test.inputMarkdownConfig)
c.Root = "./testdata"
actualMarkdownConfigs, err := markdownParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualMarkdownConfigs) != len(test.expectedMarkdownConfig) {
t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
i, len(test.expectedMarkdownConfig), len(actualMarkdownConfigs))
}
for j, actualMarkdownConfig := range actualMarkdownConfigs {
if actualMarkdownConfig.PathScope != test.expectedMarkdownConfig[j].PathScope {
t.Errorf("Test %d expected %dth Markdown PathScope to be %s , but got %s",
i, j, test.expectedMarkdownConfig[j].PathScope, actualMarkdownConfig.PathScope)
}
if fmt.Sprint(actualMarkdownConfig.Styles) != fmt.Sprint(test.expectedMarkdownConfig[j].Styles) {
t.Errorf("Test %d expected %dth Markdown Config Styles to be %s , but got %s",
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Styles), fmt.Sprint(actualMarkdownConfig.Styles))
}
if fmt.Sprint(actualMarkdownConfig.Scripts) != fmt.Sprint(test.expectedMarkdownConfig[j].Scripts) {
t.Errorf("Test %d expected %dth Markdown Config Scripts to be %s , but got %s",
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Scripts), fmt.Sprint(actualMarkdownConfig.Scripts))
}
if fmt.Sprint(actualMarkdownConfig.Templates) != fmt.Sprint(test.expectedMarkdownConfig[j].Templates) {
t.Errorf("Test %d expected %dth Markdown Config Templates to be %s , but got %s",
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Templates), fmt.Sprint(actualMarkdownConfig.Templates))
}
}
}
}
-62
View File
@@ -1,62 +0,0 @@
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
}
-17
View File
@@ -1,17 +0,0 @@
package setup
import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/proxy"
)
// Proxy configures a new Proxy middleware instance.
func Proxy(c *Controller) (middleware.Middleware, error) {
upstreams, err := proxy.NewStaticUpstreams(c.Dispenser)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return proxy.Proxy{Next: next, Upstreams: upstreams}
}, nil
}
-173
View File
@@ -1,173 +0,0 @@
package setup
import (
"net/http"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/redirect"
)
// Redir configures a new Redirect middleware instance.
func Redir(c *Controller) (middleware.Middleware, error) {
rules, err := redirParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return redirect.Redirect{Next: next, Rules: rules}
}, nil
}
func redirParse(c *Controller) ([]redirect.Rule, error) {
var redirects []redirect.Rule
// setRedirCode sets the redirect code for rule if it can, or returns an error
setRedirCode := func(code string, rule *redirect.Rule) error {
if code == "meta" {
rule.Meta = true
} else if codeNumber, ok := httpRedirs[code]; ok {
rule.Code = codeNumber
} else {
return c.Errf("Invalid redirect code '%v'", code)
}
return nil
}
// checkAndSaveRule checks the rule for validity (except the redir code)
// and saves it if it's valid, or returns an error.
checkAndSaveRule := func(rule redirect.Rule) error {
if rule.FromPath == rule.To {
return c.Err("'from' and 'to' values of redirect rule cannot be the same")
}
for _, otherRule := range redirects {
if otherRule.FromPath == rule.FromPath {
return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.FromPath, otherRule.To)
}
}
redirects = append(redirects, rule)
return nil
}
for c.Next() {
args := c.RemainingArgs()
var hadOptionalBlock bool
for c.NextBlock() {
hadOptionalBlock = true
var rule redirect.Rule
if c.Config.TLS.Enabled {
rule.FromScheme = "https"
} else {
rule.FromScheme = "http"
}
// Set initial redirect code
// BUG: If the code is specified for a whole block and that code is invalid,
// the line number will appear on the first line inside the block, even if that
// line overwrites the block-level code with a valid redirect code. The program
// still functions correctly, but the line number in the error reporting is
// misleading to the user.
if len(args) == 1 {
err := setRedirCode(args[0], &rule)
if err != nil {
return redirects, err
}
} else {
rule.Code = http.StatusMovedPermanently // default code
}
// RemainingArgs only gets the values after the current token, but in our
// case we want to include the current token to get an accurate count.
insideArgs := append([]string{c.Val()}, c.RemainingArgs()...)
switch len(insideArgs) {
case 1:
// To specified (catch-all redirect)
// Not sure why user is doing this in a table, as it causes all other redirects to be ignored.
// As such, this feature remains undocumented.
rule.FromPath = "/"
rule.To = insideArgs[0]
case 2:
// From and To specified
rule.FromPath = insideArgs[0]
rule.To = insideArgs[1]
case 3:
// From, To, and Code specified
rule.FromPath = insideArgs[0]
rule.To = insideArgs[1]
err := setRedirCode(insideArgs[2], &rule)
if err != nil {
return redirects, err
}
default:
return redirects, c.ArgErr()
}
err := checkAndSaveRule(rule)
if err != nil {
return redirects, err
}
}
if !hadOptionalBlock {
var rule redirect.Rule
if c.Config.TLS.Enabled {
rule.FromScheme = "https"
} else {
rule.FromScheme = "http"
}
rule.Code = http.StatusMovedPermanently // default
switch len(args) {
case 1:
// To specified (catch-all redirect)
rule.FromPath = "/"
rule.To = args[0]
case 2:
// To and Code specified (catch-all redirect)
rule.FromPath = "/"
rule.To = args[0]
err := setRedirCode(args[1], &rule)
if err != nil {
return redirects, err
}
case 3:
// From, To, and Code specified
rule.FromPath = args[0]
rule.To = args[1]
err := setRedirCode(args[2], &rule)
if err != nil {
return redirects, err
}
default:
return redirects, c.ArgErr()
}
err := checkAndSaveRule(rule)
if err != nil {
return redirects, err
}
}
}
return redirects, nil
}
// httpRedirs is a list of supported HTTP redirect codes.
var httpRedirs = map[string]int{
"300": http.StatusMultipleChoices,
"301": http.StatusMovedPermanently,
"302": http.StatusFound, // (NOT CORRECT for "Temporary Redirect", see 307)
"303": http.StatusSeeOther,
"304": http.StatusNotModified,
"305": http.StatusUseProxy,
"307": http.StatusTemporaryRedirect,
"308": 308, // Permanent Redirect
}
-67
View File
@@ -1,67 +0,0 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware/redirect"
)
func TestRedir(t *testing.T) {
for j, test := range []struct {
input string
shouldErr bool
expectedRules []redirect.Rule
}{
// test case #0 tests the recognition of a valid HTTP status code defined outside of block statement
{"redir 300 {\n/ /foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 300}}},
// test case #1 tests the recognition of an invalid HTTP status code defined outside of block statement
{"redir 9000 {\n/ /foo\n}", true, []redirect.Rule{{}}},
// test case #2 tests the detection of a valid HTTP status code outside of a block statement being overriden by an invalid HTTP status code inside statement of a block statement
{"redir 300 {\n/ /foo 9000\n}", true, []redirect.Rule{{}}},
// test case #3 tests the detection of an invalid HTTP status code outside of a block statement being overriden by a valid HTTP status code inside statement of a block statement
{"redir 9000 {\n/ /foo 300\n}", true, []redirect.Rule{{}}},
// test case #4 tests the recognition of a TO redirection in a block statement.The HTTP status code is set to the default of 301 - MovedPermanently
{"redir 302 {\n/foo\n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 302}}},
// test case #5 tests the recognition of a TO and From redirection in a block statement
{"redir {\n/bar /foo 303\n}", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}},
// test case #6 tests the recognition of a TO redirection in a non-block statement. The HTTP status code is set to the default of 301 - MovedPermanently
{"redir /foo", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 301}}},
// test case #7 tests the recognition of a TO and From redirection in a non-block statement
{"redir /bar /foo 303", false, []redirect.Rule{{FromPath: "/bar", To: "/foo", Code: 303}}},
// test case #8 tests the recognition of multiple redirections
{"redir {\n / /foo 304 \n} \n redir {\n /bar /foobar 305 \n}", false, []redirect.Rule{{FromPath: "/", To: "/foo", Code: 304}, {FromPath: "/bar", To: "/foobar", Code: 305}}},
// test case #9 tests the detection of duplicate redirections
{"redir {\n /bar /foo 304 \n} redir {\n /bar /foo 304 \n}", true, []redirect.Rule{{}}},
} {
recievedFunc, err := Redir(NewTestController(test.input))
if err != nil && !test.shouldErr {
t.Errorf("Test case #%d recieved an error of %v", j, err)
} else if test.shouldErr {
continue
}
recievedRules := recievedFunc(nil).(redirect.Redirect).Rules
for i, recievedRule := range recievedRules {
if recievedRule.FromPath != test.expectedRules[i].FromPath {
t.Errorf("Test case #%d.%d expected a from path of %s, but recieved a from path of %s", j, i, test.expectedRules[i].FromPath, recievedRule.FromPath)
}
if recievedRule.To != test.expectedRules[i].To {
t.Errorf("Test case #%d.%d expected a TO path of %s, but recieved a TO path of %s", j, i, test.expectedRules[i].To, recievedRule.To)
}
if recievedRule.Code != test.expectedRules[i].Code {
t.Errorf("Test case #%d.%d expected a HTTP status code of %d, but recieved a code of %d", j, i, test.expectedRules[i].Code, recievedRule.Code)
}
}
}
}
-109
View File
@@ -1,109 +0,0 @@
package setup
import (
"net/http"
"strconv"
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/rewrite"
)
// Rewrite configures a new Rewrite middleware instance.
func Rewrite(c *Controller) (middleware.Middleware, error) {
rewrites, err := rewriteParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return rewrite.Rewrite{
Next: next,
FileSys: http.Dir(c.Root),
Rules: rewrites,
}
}, nil
}
func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
var simpleRules []rewrite.Rule
var regexpRules []rewrite.Rule
for c.Next() {
var rule rewrite.Rule
var err error
var base = "/"
var pattern, to string
var status int
var ext []string
args := c.RemainingArgs()
var ifs []rewrite.If
switch len(args) {
case 1:
base = args[0]
fallthrough
case 0:
for c.NextBlock() {
switch c.Val() {
case "r", "regexp":
if !c.NextArg() {
return nil, c.ArgErr()
}
pattern = c.Val()
case "to":
args1 := c.RemainingArgs()
if len(args1) == 0 {
return nil, c.ArgErr()
}
to = strings.Join(args1, " ")
case "ext":
args1 := c.RemainingArgs()
if len(args1) == 0 {
return nil, c.ArgErr()
}
ext = args1
case "if":
args1 := c.RemainingArgs()
if len(args1) != 3 {
return nil, c.ArgErr()
}
ifCond, err := rewrite.NewIf(args1[0], args1[1], args1[2])
if err != nil {
return nil, err
}
ifs = append(ifs, ifCond)
case "status":
if !c.NextArg() {
return nil, c.ArgErr()
}
status, _ = strconv.Atoi(c.Val())
if status < 200 || (status > 299 && status < 400) || status > 499 {
return nil, c.Err("status must be 2xx or 4xx")
}
default:
return nil, c.ArgErr()
}
}
// ensure to or status is specified
if to == "" && status == 0 {
return nil, c.ArgErr()
}
if rule, err = rewrite.NewComplexRule(base, pattern, to, status, ext, ifs); err != nil {
return nil, err
}
regexpRules = append(regexpRules, rule)
// the only unhandled case is 2 and above
default:
rule = rewrite.NewSimpleRule(args[0], strings.Join(args[1:], " "))
simpleRules = append(simpleRules, rule)
}
}
// put simple rules in front to avoid regexp computation for them
return append(simpleRules, regexpRules...), nil
}
-241
View File
@@ -1,241 +0,0 @@
package setup
import (
"fmt"
"regexp"
"testing"
"github.com/mholt/caddy/middleware/rewrite"
)
func TestRewrite(t *testing.T) {
c := NewTestController(`rewrite /from /to`)
mid, err := Rewrite(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(rewrite.Rewrite)
if !ok {
t.Fatalf("Expected handler to be type Rewrite, got: %#v", handler)
}
if !SameNext(myHandler.Next, EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
if len(myHandler.Rules) != 1 {
t.Errorf("Expected handler to have %d rule, has %d instead", 1, len(myHandler.Rules))
}
}
func TestRewriteParse(t *testing.T) {
simpleTests := []struct {
input string
shouldErr bool
expected []rewrite.Rule
}{
{`rewrite /from /to`, false, []rewrite.Rule{
rewrite.SimpleRule{From: "/from", To: "/to"},
}},
{`rewrite /from /to
rewrite a b`, false, []rewrite.Rule{
rewrite.SimpleRule{From: "/from", To: "/to"},
rewrite.SimpleRule{From: "a", To: "b"},
}},
{`rewrite a`, true, []rewrite.Rule{}},
{`rewrite`, true, []rewrite.Rule{}},
{`rewrite a b c`, false, []rewrite.Rule{
rewrite.SimpleRule{From: "a", To: "b c"},
}},
}
for i, test := range simpleTests {
c := NewTestController(test.input)
actual, err := rewriteParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
} else if err != nil && test.shouldErr {
continue
}
if len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, e := range test.expected {
actualRule := actual[j].(rewrite.SimpleRule)
expectedRule := e.(rewrite.SimpleRule)
if actualRule.From != expectedRule.From {
t.Errorf("Test %d, rule %d: Expected From=%s, got %s",
i, j, expectedRule.From, actualRule.From)
}
if actualRule.To != expectedRule.To {
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
i, j, expectedRule.To, actualRule.To)
}
}
}
regexpTests := []struct {
input string
shouldErr bool
expected []rewrite.Rule
}{
{`rewrite {
r .*
to /to /index.php?
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to /index.php?", Regexp: regexp.MustCompile(".*")},
}},
{`rewrite {
regexp .*
to /to
ext / html txt
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to", Exts: []string{"/", "html", "txt"}, Regexp: regexp.MustCompile(".*")},
}},
{`rewrite /path {
r rr
to /dest
}
rewrite / {
regexp [a-z]+
to /to /to2
}
`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/path", To: "/dest", Regexp: regexp.MustCompile("rr")},
&rewrite.ComplexRule{Base: "/", To: "/to /to2", Regexp: regexp.MustCompile("[a-z]+")},
}},
{`rewrite {
r .*
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite /`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
to /to
if {path} is a
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to", Ifs: []rewrite.If{{A: "{path}", Operator: "is", B: "a"}}},
}},
{`rewrite {
status 500
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
status 400
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", Status: 400},
}},
{`rewrite {
to /to
status 400
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to", Status: 400},
}},
{`rewrite {
status 399
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
status 200
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", Status: 200},
}},
{`rewrite {
to /to
status 200
}`, false, []rewrite.Rule{
&rewrite.ComplexRule{Base: "/", To: "/to", Status: 200},
}},
{`rewrite {
status 199
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
status 0
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
{`rewrite {
to /to
status 0
}`, true, []rewrite.Rule{
&rewrite.ComplexRule{},
}},
}
for i, test := range regexpTests {
c := NewTestController(test.input)
actual, err := rewriteParse(c)
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
} else if err != nil && test.shouldErr {
continue
}
if len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, e := range test.expected {
actualRule := actual[j].(*rewrite.ComplexRule)
expectedRule := e.(*rewrite.ComplexRule)
if actualRule.Base != expectedRule.Base {
t.Errorf("Test %d, rule %d: Expected Base=%s, got %s",
i, j, expectedRule.Base, actualRule.Base)
}
if actualRule.To != expectedRule.To {
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
i, j, expectedRule.To, actualRule.To)
}
if fmt.Sprint(actualRule.Exts) != fmt.Sprint(expectedRule.Exts) {
t.Errorf("Test %d, rule %d: Expected Ext=%v, got %v",
i, j, expectedRule.To, actualRule.To)
}
if actualRule.Regexp != nil {
if actualRule.String() != expectedRule.String() {
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
i, j, expectedRule.String(), actualRule.String())
}
}
if fmt.Sprint(actualRule.Ifs) != fmt.Sprint(expectedRule.Ifs) {
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
i, j, fmt.Sprint(expectedRule.Ifs), fmt.Sprint(actualRule.Ifs))
}
}
}
}
-32
View File
@@ -1,32 +0,0 @@
package setup
import (
"log"
"os"
"github.com/mholt/caddy/middleware"
)
// Root sets up the root file path of the server.
func Root(c *Controller) (middleware.Middleware, error) {
for c.Next() {
if !c.NextArg() {
return nil, c.ArgErr()
}
c.Root = c.Val()
}
// Check if root path exists
_, err := os.Stat(c.Root)
if err != nil {
if os.IsNotExist(err) {
// Allow this, because the folder might appear later.
// But make sure the user knows!
log.Printf("[WARNING] Root path does not exist: %s", c.Root)
} else {
return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err)
}
}
return nil, nil
}
-64
View File
@@ -1,64 +0,0 @@
package setup
import (
"os"
"os/exec"
"strings"
"github.com/mholt/caddy/middleware"
)
// Startup registers a startup callback to execute during server start.
func Startup(c *Controller) (middleware.Middleware, error) {
return nil, registerCallback(c, &c.FirstStartup)
}
// Shutdown registers a shutdown callback to execute during process exit.
func Shutdown(c *Controller) (middleware.Middleware, error) {
return nil, registerCallback(c, &c.Shutdown)
}
// registerCallback registers a callback function to execute by
// using c to parse the line. It appends the callback function
// to the list of callback functions passed in by reference.
func registerCallback(c *Controller, list *[]func() error) error {
var funcs []func() error
for c.Next() {
args := c.RemainingArgs()
if len(args) == 0 {
return c.ArgErr()
}
nonblock := false
if len(args) > 1 && args[len(args)-1] == "&" {
// Run command in background; non-blocking
nonblock = true
args = args[:len(args)-1]
}
command, args, err := middleware.SplitCommandAndArgs(strings.Join(args, " "))
if err != nil {
return c.Err(err.Error())
}
fn := func() error {
cmd := exec.Command(command, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if nonblock {
return cmd.Start()
}
return cmd.Run()
}
funcs = append(funcs, fn)
}
return c.OncePerServerBlock(func() error {
*list = append(*list, funcs...)
return nil
})
}
-1
View File
@@ -1 +0,0 @@
# Test h1
-10
View File
@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.Doc.title}}</title>
</head>
<body>
{{.Include "header.html"}}
{{.Doc.body}}
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
package caddy
import (
"net"
"strconv"
"testing"
)
/*
// TODO
func TestCaddyStartStop(t *testing.T) {
caddyfile := "localhost:1984"
for i := 0; i < 2; i++ {
_, err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
if err != nil {
t.Fatalf("Error starting, iteration %d: %v", i, err)
}
client := http.Client{
Timeout: time.Duration(2 * time.Second),
}
resp, err := client.Get("http://localhost:1984")
if err != nil {
t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err)
}
resp.Body.Close()
err = Stop()
if err != nil {
t.Fatalf("Error stopping, iteration %d: %v", i, err)
}
}
}
*/
func TestIsLoopback(t *testing.T) {
for i, test := range []struct {
input string
expect bool
}{
{"example.com", false},
{"localhost", true},
{"localhost:1234", true},
{"localhost:", true},
{"127.0.0.1", true},
{"127.0.0.1:443", true},
{"127.0.1.5", true},
{"10.0.0.5", false},
{"12.7.0.1", false},
{"[::1]", true},
{"[::1]:1234", true},
{"::1", true},
{"::", false},
{"[::]", false},
{"local", false},
} {
if got, want := IsLoopback(test.input), test.expect; got != want {
t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got)
}
}
}
func TestListenerAddrEqual(t *testing.T) {
ln1, err := net.Listen("tcp", "[::]:0")
if err != nil {
t.Fatal(err)
}
defer ln1.Close()
ln1port := strconv.Itoa(ln1.Addr().(*net.TCPAddr).Port)
ln2, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln2.Close()
ln2port := strconv.Itoa(ln2.Addr().(*net.TCPAddr).Port)
for i, test := range []struct {
ln net.Listener
addr string
expect bool
}{
{ln1, ":1234", false},
{ln1, "0.0.0.0:1234", false},
{ln1, "0.0.0.0", false},
{ln1, ":" + ln1port + "", true},
{ln1, "0.0.0.0:" + ln1port + "", true},
{ln2, ":" + ln2port + "", false},
{ln2, "127.0.0.1:1234", false},
{ln2, "127.0.0.1", false},
{ln2, "127.0.0.1:" + ln2port + "", true},
} {
if got, want := listenerAddrEqual(test.ln, test.addr), test.expect; got != want {
t.Errorf("Test %d (%s == %s): expected %v but was %v", i, test.addr, test.ln.Addr().String(), want, got)
}
}
}
@@ -1,4 +1,4 @@
package parse
package caddyfile
import (
"errors"
@@ -12,22 +12,23 @@ import (
// some really convenient methods.
type Dispenser struct {
filename string
tokens []token
tokens []Token
cursor int
nesting int
}
// NewDispenser returns a Dispenser, ready to use for parsing the given input.
func NewDispenser(filename string, input io.Reader) Dispenser {
tokens, _ := allTokens(input) // ignoring error because nothing to do with it
return Dispenser{
filename: filename,
tokens: allTokens(input),
tokens: tokens,
cursor: -1,
}
}
// NewDispenserTokens returns a Dispenser filled with the given tokens.
func NewDispenserTokens(filename string, tokens []token) Dispenser {
func NewDispenserTokens(filename string, tokens []Token) Dispenser {
return Dispenser{
filename: filename,
tokens: tokens,
@@ -59,8 +60,8 @@ func (d *Dispenser) NextArg() bool {
return false
}
if d.cursor < len(d.tokens)-1 &&
d.tokens[d.cursor].file == d.tokens[d.cursor+1].file &&
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].line {
d.tokens[d.cursor].File == d.tokens[d.cursor+1].File &&
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].Line {
d.cursor++
return true
}
@@ -80,8 +81,8 @@ func (d *Dispenser) NextLine() bool {
return false
}
if d.cursor < len(d.tokens)-1 &&
(d.tokens[d.cursor].file != d.tokens[d.cursor+1].file ||
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].line) {
(d.tokens[d.cursor].File != d.tokens[d.cursor+1].File ||
d.tokens[d.cursor].Line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].Line) {
d.cursor++
return true
}
@@ -131,7 +132,7 @@ func (d *Dispenser) Val() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return ""
}
return d.tokens[d.cursor].text
return d.tokens[d.cursor].Text
}
// Line gets the line number of the current token. If there is no token
@@ -140,7 +141,7 @@ func (d *Dispenser) Line() int {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return 0
}
return d.tokens[d.cursor].line
return d.tokens[d.cursor].Line
}
// File gets the filename of the current token. If there is no token loaded,
@@ -149,7 +150,7 @@ func (d *Dispenser) File() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return d.filename
}
if tokenFilename := d.tokens[d.cursor].file; tokenFilename != "" {
if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
return tokenFilename
}
return d.filename
@@ -233,7 +234,7 @@ func (d *Dispenser) numLineBreaks(tknIdx int) int {
if tknIdx < 0 || tknIdx >= len(d.tokens) {
return 0
}
return strings.Count(d.tokens[tknIdx].text, "\n")
return strings.Count(d.tokens[tknIdx].Text, "\n")
}
// isNewLine determines whether the current token is on a different
@@ -246,6 +247,6 @@ func (d *Dispenser) isNewLine() bool {
if d.cursor > len(d.tokens)-1 {
return false
}
return d.tokens[d.cursor-1].file != d.tokens[d.cursor].file ||
d.tokens[d.cursor-1].line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].line
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
}
@@ -1,4 +1,4 @@
package parse
package caddyfile
import (
"reflect"
@@ -64,7 +64,7 @@ func TestDispenser_NextArg(t *testing.T) {
}
assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) {
if d.NextArg() != true {
if !d.NextArg() {
t.Error("NextArg(): Should load next argument but got false instead")
}
if d.cursor != expectedCursor {
@@ -74,7 +74,7 @@ func TestDispenser_NextArg(t *testing.T) {
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
}
if !loadAnother {
if d.NextArg() != false {
if d.NextArg() {
t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val())
}
if d.cursor != expectedCursor {
+21 -22
View File
@@ -4,31 +4,26 @@ import (
"bytes"
"encoding/json"
"fmt"
"net"
"sort"
"strconv"
"strings"
"github.com/mholt/caddy/caddy/parse"
)
const filename = "Caddyfile"
// ToJSON converts caddyfile to its JSON representation.
func ToJSON(caddyfile []byte) ([]byte, error) {
var j Caddyfile
var j EncodedCaddyfile
serverBlocks, err := parse.ServerBlocks(filename, bytes.NewReader(caddyfile), false)
serverBlocks, err := Parse(filename, bytes.NewReader(caddyfile), nil)
if err != nil {
return nil, err
}
for _, sb := range serverBlocks {
block := ServerBlock{Body: [][]interface{}{}}
// Fill up host list
for _, host := range sb.HostList() {
block.Hosts = append(block.Hosts, standardizeScheme(host))
block := EncodedServerBlock{
Keys: sb.Keys,
Body: [][]interface{}{},
}
// Extract directives deterministically by sorting them
@@ -40,7 +35,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
// Convert each directive's tokens into our JSON structure
for _, dir := range directives {
disp := parse.NewDispenserTokens(filename, sb.Tokens[dir])
disp := NewDispenserTokens(filename, sb.Tokens[dir])
for disp.Next() {
block.Body = append(block.Body, constructLine(&disp))
}
@@ -62,7 +57,7 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
// 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{} {
func constructLine(d *Dispenser) []interface{} {
var args []interface{}
args = append(args, d.Val())
@@ -81,7 +76,7 @@ func constructLine(d *parse.Dispenser) []interface{} {
// constructBlock recursively processes tokens into a
// JSON-encodable structure. To be used in a directive's
// block. Goes to end of block.
func constructBlock(d *parse.Dispenser) [][]interface{} {
func constructBlock(d *Dispenser) [][]interface{} {
block := [][]interface{}{}
for d.Next() {
@@ -96,7 +91,7 @@ func constructBlock(d *parse.Dispenser) [][]interface{} {
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
func FromJSON(jsonBytes []byte) ([]byte, error) {
var j Caddyfile
var j EncodedCaddyfile
var result string
err := json.Unmarshal(jsonBytes, &j)
@@ -108,11 +103,12 @@ func FromJSON(jsonBytes []byte) ([]byte, error) {
if sbPos > 0 {
result += "\n\n"
}
for i, host := range sb.Hosts {
for i, key := range sb.Keys {
if i > 0 {
result += ", "
}
result += standardizeScheme(host)
//result += standardizeScheme(key)
result += key
}
result += jsonToText(sb.Body, 1)
}
@@ -164,6 +160,8 @@ func jsonToText(scope interface{}, depth int) string {
return result
}
// TODO: Will this function come in handy somewhere else?
/*
// standardizeScheme turns an address like host:https into https://host,
// or "host:" into "host".
func standardizeScheme(addr string) string {
@@ -174,12 +172,13 @@ func standardizeScheme(addr string) string {
}
return strings.TrimSuffix(addr, ":")
}
*/
// Caddyfile encapsulates a slice of ServerBlocks.
type Caddyfile []ServerBlock
// EncodedCaddyfile encapsulates a slice of EncodedServerBlocks.
type EncodedCaddyfile []EncodedServerBlock
// ServerBlock represents a server block.
type ServerBlock struct {
Hosts []string `json:"hosts"`
Body [][]interface{} `json:"body"`
// EncodedServerBlock represents a server block ripe for encoding.
type EncodedServerBlock struct {
Keys []string `json:"keys"`
Body [][]interface{} `json:"body"`
}
@@ -9,7 +9,7 @@ var tests = []struct {
caddyfile: `foo {
root /bar
}`,
json: `[{"hosts":["foo"],"body":[["root","/bar"]]}]`,
json: `[{"keys":["foo"],"body":[["root","/bar"]]}]`,
},
{ // 1
caddyfile: `host1, host2 {
@@ -17,7 +17,7 @@ var tests = []struct {
def
}
}`,
json: `[{"hosts":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
json: `[{"keys":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
},
{ // 2
caddyfile: `host1, host2 {
@@ -26,58 +26,58 @@ var tests = []struct {
jkl
}
}`,
json: `[{"hosts":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
json: `[{"keys":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
},
{ // 3
caddyfile: `host1:1234, host2:5678 {
dir abc {
}
}`,
json: `[{"hosts":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
json: `[{"keys":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
},
{ // 4
caddyfile: `host {
foo "bar baz"
}`,
json: `[{"hosts":["host"],"body":[["foo","bar baz"]]}]`,
json: `[{"keys":["host"],"body":[["foo","bar baz"]]}]`,
},
{ // 5
caddyfile: `host, host:80 {
foo "bar \"baz\""
}`,
json: `[{"hosts":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
json: `[{"keys":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
},
{ // 6
caddyfile: `host {
foo "bar
baz"
}`,
json: `[{"hosts":["host"],"body":[["foo","bar\nbaz"]]}]`,
json: `[{"keys":["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...?
json: `[{"keys":["host"],"body":[["dir","123","4.56","true"]]}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
},
{ // 8
caddyfile: `http://host, https://host {
}`,
json: `[{"hosts":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
json: `[{"keys":["http://host","https://host"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
},
{ // 9
caddyfile: `host {
dir1 a b
dir2 c d
}`,
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
},
{ // 10
caddyfile: `host {
dir a b
dir c d
}`,
json: `[{"hosts":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
json: `[{"keys":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
},
{ // 11
caddyfile: `host {
@@ -87,7 +87,7 @@ baz"
d
}
}`,
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
},
{ // 12
caddyfile: `host1 {
@@ -97,7 +97,7 @@ baz"
host2 {
dir2
}`,
json: `[{"hosts":["host1"],"body":[["dir1"]]},{"hosts":["host2"],"body":[["dir2"]]}]`,
json: `[{"keys":["host1"],"body":[["dir1"]]},{"keys":["host2"],"body":[["dir2"]]}]`,
},
}
@@ -125,17 +125,19 @@ func TestFromJSON(t *testing.T) {
}
}
// TODO: Will these tests come in handy somewhere else?
/*
func TestStandardizeAddress(t *testing.T) {
// host:https should be converted to https://host
output, err := ToJSON([]byte(`host:https`))
if err != nil {
t.Fatal(err)
}
if expected, actual := `[{"hosts":["https://host"],"body":[]}]`, string(output); expected != actual {
if expected, actual := `[{"keys":["https://host"],"body":[]}]`, string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
output, err = FromJSON([]byte(`[{"hosts":["https://host"],"body":[]}]`))
output, err = FromJSON([]byte(`[{"keys":["https://host"],"body":[]}]`))
if err != nil {
t.Fatal(err)
}
@@ -148,10 +150,10 @@ func TestStandardizeAddress(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if expected, actual := `[{"hosts":["host"],"body":[]}]`, string(output); expected != actual {
if expected, actual := `[{"keys":["host"],"body":[]}]`, string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
output, err = FromJSON([]byte(`[{"hosts":["host:"],"body":[]}]`))
output, err = FromJSON([]byte(`[{"keys":["host:"],"body":[]}]`))
if err != nil {
t.Fatal(err)
}
@@ -159,3 +161,4 @@ func TestStandardizeAddress(t *testing.T) {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
}
*/
+23 -9
View File
@@ -1,4 +1,4 @@
package parse
package caddyfile
import (
"bufio"
@@ -13,22 +13,36 @@ type (
// in quotes if it contains whitespace.
lexer struct {
reader *bufio.Reader
token token
token Token
line int
}
// token represents a single parsable unit.
token struct {
file string
line int
text string
// Token represents a single parsable unit.
Token struct {
File string
Line int
Text string
}
)
// load prepares the lexer to scan an input for tokens.
// It discards any leading byte order mark.
func (l *lexer) load(input io.Reader) error {
l.reader = bufio.NewReader(input)
l.line = 1
// discard byte order mark, if present
firstCh, _, err := l.reader.ReadRune()
if err != nil {
return err
}
if firstCh != 0xFEFF {
err := l.reader.UnreadRune()
if err != nil {
return err
}
}
return nil
}
@@ -47,7 +61,7 @@ func (l *lexer) next() bool {
var comment, quoted, escaped bool
makeToken := func() bool {
l.token.text = string(val)
l.token.Text = string(val)
return true
}
@@ -110,7 +124,7 @@ func (l *lexer) next() bool {
}
if len(val) == 0 {
l.token = token{line: l.line}
l.token = Token{Line: l.line}
if ch == '"' {
quoted = true
continue
+171
View File
@@ -0,0 +1,171 @@
package caddyfile
import (
"strings"
"testing"
)
type lexerTestCase struct {
input string
expected []Token
}
func TestLexer(t *testing.T) {
testCases := []lexerTestCase{
{
input: `host:123`,
expected: []Token{
{Line: 1, Text: "host:123"},
},
},
{
input: `host:123
directive`,
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 3, Text: "directive"},
},
},
{
input: `host:123 {
directive
}`,
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
{Line: 2, Text: "directive"},
{Line: 3, Text: "}"},
},
},
{
input: `host:123 { directive }`,
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
{Line: 1, Text: "directive"},
{Line: 1, Text: "}"},
},
},
{
input: `host:123 {
#comment
directive
# comment
foobar # another comment
}`,
expected: []Token{
{Line: 1, Text: "host:123"},
{Line: 1, Text: "{"},
{Line: 3, Text: "directive"},
{Line: 5, Text: "foobar"},
{Line: 6, Text: "}"},
},
},
{
input: `a "quoted value" b
foobar`,
expected: []Token{
{Line: 1, Text: "a"},
{Line: 1, Text: "quoted value"},
{Line: 1, Text: "b"},
{Line: 2, Text: "foobar"},
},
},
{
input: `A "quoted \"value\" inside" B`,
expected: []Token{
{Line: 1, Text: "A"},
{Line: 1, Text: `quoted "value" inside`},
{Line: 1, Text: "B"},
},
},
{
input: `"don't\escape"`,
expected: []Token{
{Line: 1, Text: `don't\escape`},
},
},
{
input: `"don't\\escape"`,
expected: []Token{
{Line: 1, Text: `don't\\escape`},
},
},
{
input: `A "quoted value with line
break inside" {
foobar
}`,
expected: []Token{
{Line: 1, Text: "A"},
{Line: 1, Text: "quoted value with line\n\t\t\t\t\tbreak inside"},
{Line: 2, Text: "{"},
{Line: 3, Text: "foobar"},
{Line: 4, Text: "}"},
},
},
{
input: `"C:\php\php-cgi.exe"`,
expected: []Token{
{Line: 1, Text: `C:\php\php-cgi.exe`},
},
},
{
input: `empty "" string`,
expected: []Token{
{Line: 1, Text: `empty`},
{Line: 1, Text: ``},
{Line: 1, Text: `string`},
},
},
{
input: "skip those\r\nCR characters",
expected: []Token{
{Line: 1, Text: "skip"},
{Line: 1, Text: "those"},
{Line: 2, Text: "CR"},
{Line: 2, Text: "characters"},
},
},
{
input: "\xEF\xBB\xBF:8080", // test with leading byte order mark
expected: []Token{
{Line: 1, Text: ":8080"},
},
},
}
for i, testCase := range testCases {
actual := tokenize(testCase.input)
lexerCompare(t, i, testCase.expected, actual)
}
}
func tokenize(input string) (tokens []Token) {
l := lexer{}
l.load(strings.NewReader(input))
for l.next() {
tokens = append(tokens, l.token)
}
return
}
func lexerCompare(t *testing.T, n int, expected, actual []Token) {
if len(expected) != len(actual) {
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
}
for i := 0; i < len(actual) && i < len(expected); i++ {
if actual[i].Line != expected[i].Line {
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
n, i, expected[i].Text, expected[i].Line, actual[i].Line)
break
}
if actual[i].Text != expected[i].Text {
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
n, i, expected[i].Text, actual[i].Text)
break
}
}
}
+118 -111
View File
@@ -1,18 +1,45 @@
package parse
package caddyfile
import (
"fmt"
"net"
"io"
"log"
"os"
"path/filepath"
"strings"
)
// Parse parses the input just enough to group tokens, in
// order, by server block. No further parsing is performed.
// Server blocks are returned in the order in which they appear.
// Directives that do not appear in validDirectives will cause
// an error. If you do not want to check for valid directives,
// pass in nil instead.
func Parse(filename string, input io.Reader, validDirectives []string) ([]ServerBlock, error) {
p := parser{Dispenser: NewDispenser(filename, input), validDirectives: validDirectives}
return p.parseAll()
}
// allTokens lexes the entire input, but does not parse it.
// It returns all the tokens from the input, unstructured
// and in order.
func allTokens(input io.Reader) ([]Token, error) {
l := new(lexer)
err := l.load(input)
if err != nil {
return nil, err
}
var tokens []Token
for l.next() {
tokens = append(tokens, l.token)
}
return tokens, nil
}
type parser struct {
Dispenser
block ServerBlock // current server block being parsed
validDirectives []string // a directive must be valid or it's an error
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) {
@@ -23,7 +50,7 @@ func (p *parser) parseAll() ([]ServerBlock, error) {
if err != nil {
return blocks, err
}
if len(p.block.Addresses) > 0 {
if len(p.block.Keys) > 0 {
blocks = append(blocks, p.block)
}
}
@@ -32,14 +59,9 @@ 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 {
return err
}
return nil
return p.begin()
}
func (p *parser) begin() error {
@@ -48,6 +70,7 @@ func (p *parser) begin() error {
}
err := p.addresses()
if err != nil {
return err
}
@@ -58,12 +81,7 @@ func (p *parser) begin() error {
return nil
}
err = p.blockContents()
if err != nil {
return err
}
return nil
return p.blockContents()
}
func (p *parser) addresses() error {
@@ -89,7 +107,7 @@ func (p *parser) addresses() error {
break
}
if tkn != "" { // empty token possible if user typed "" in Caddyfile
if tkn != "" { // empty token possible if user typed ""
// Trailing comma indicates another address will follow, which
// may possibly be on the next line
if tkn[len(tkn)-1] == ',' {
@@ -99,12 +117,7 @@ func (p *parser) addresses() error {
expectingAnother = false // but we may still see another one on this line
}
// Parse and save this address
addr, err := standardAddress(tkn)
if err != nil {
return err
}
p.block.Addresses = append(p.block.Addresses, addr)
p.block.Keys = append(p.block.Keys, tkn)
}
// Advance token and possibly break out of loop or return error
@@ -183,22 +196,43 @@ func (p *parser) directives() error {
// other words, call Next() to access the first token that was
// imported.
func (p *parser) doImport() error {
// syntax check
// syntax checks
if !p.NextArg() {
return p.ArgErr()
}
importPattern := p.Val()
importPattern := replaceEnvVars(p.Val())
if importPattern == "" {
return p.Err("Import requires a non-empty filepath")
}
if p.NextArg() {
return p.Err("Import takes only one argument (glob pattern or file)")
}
// do glob
matches, err := filepath.Glob(importPattern)
// make path relative to Caddyfile rather than current working directory (issue #867)
// and then use glob to get list of matching filenames
absFile, err := filepath.Abs(p.Dispenser.filename)
if err != nil {
return p.Errf("Failed to get absolute path of file: %s", p.Dispenser.filename)
}
var matches []string
var globPattern string
if !filepath.IsAbs(importPattern) {
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
} else {
globPattern = importPattern
}
matches, err = filepath.Glob(globPattern)
if err != nil {
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
}
if len(matches) == 0 {
return p.Errf("No files matching import pattern %s", importPattern)
if strings.Contains(globPattern, "*") {
log.Printf("[WARNING] No files matching import pattern: %s", importPattern)
} else {
return p.Errf("File to import not found: %s", importPattern)
}
}
// splice out the import directive and its argument (2 tokens total)
@@ -206,12 +240,35 @@ func (p *parser) doImport() error {
tokensAfter := p.tokens[p.cursor+1:]
// collect all the imported tokens
var importedTokens []token
var importedTokens []Token
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
if err != nil {
return err
}
var importLine int
importDir := filepath.Dir(importFile)
for i, token := range newTokens {
if token.Text == "import" {
importLine = token.Line
continue
}
if token.Line == importLine {
var abs string
if filepath.IsAbs(token.Text) {
abs = token.Text
} else if !filepath.IsAbs(importFile) {
abs = filepath.Join(filepath.Dir(absFile), token.Text)
} else {
abs = filepath.Join(importDir, token.Text)
}
newTokens[i] = Token{
Text: abs,
Line: token.Line,
File: token.File,
}
}
}
importedTokens = append(importedTokens, newTokens...)
}
@@ -225,18 +282,28 @@ func (p *parser) doImport() error {
// doSingleImport lexes the individual file at importFile and returns
// its tokens or an error, if any.
func (p *parser) doSingleImport(importFile string) ([]token, error) {
func (p *parser) doSingleImport(importFile string) ([]Token, error) {
file, err := os.Open(importFile)
if err != nil {
return nil, p.Errf("Could not import %s: %v", importFile, err)
}
defer file.Close()
importedTokens := allTokens(file)
if info, err := file.Stat(); err != nil {
return nil, p.Errf("Could not import %s: %v", importFile, err)
} else if info.IsDir() {
return nil, p.Errf("Could not import %s: is a directory", importFile)
}
importedTokens, err := allTokens(file)
if err != nil {
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
}
// Tack the filename onto these tokens so errors show the imported file's name
filename := filepath.Base(importFile)
for i := 0; i < len(importedTokens); i++ {
importedTokens[i].file = filename
importedTokens[i].File = filename
}
return importedTokens, nil
@@ -252,10 +319,9 @@ func (p *parser) directive() error {
dir := p.Val()
nesting := 0
if p.checkDirectives {
if _, ok := ValidDirectives[dir]; !ok {
return p.Errf("Unknown directive '%s'", dir)
}
// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
if !p.validDirective(dir) {
return p.Errf("Unknown directive '%s'", dir)
}
// The directive itself is appended as a relevant token
@@ -272,7 +338,7 @@ func (p *parser) directive() error {
} else if p.Val() == "}" && nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace")
}
p.tokens[p.cursor].text = replaceEnvVars(p.tokens[p.cursor].text)
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
}
@@ -304,58 +370,17 @@ func (p *parser) closeCurlyBrace() error {
return nil
}
// standardAddress parses an address string into a structured format with separate
// scheme, host, and port portions, as well as the original input string.
func standardAddress(str string) (address, error) {
var scheme string
var err error
// first check for scheme and strip it off
input := str
if strings.HasPrefix(str, "https://") {
scheme = "https"
str = str[8:]
} else if strings.HasPrefix(str, "http://") {
scheme = "http"
str = str[7:]
// validDirective returns true if dir is in p.validDirectives.
func (p *parser) validDirective(dir string) bool {
if p.validDirectives == nil {
return true
}
// separate host and port
host, port, err := net.SplitHostPort(str)
if err != nil {
host, port, err = net.SplitHostPort(str + ":")
// no error check here; return err at end of function
}
// see if we can set port based off scheme
if port == "" {
if scheme == "http" {
port = "80"
} else if scheme == "https" {
port = "443"
for _, d := range p.validDirectives {
if d == dir {
return true
}
}
// repeated or conflicting scheme is confusing, so error
if scheme != "" && (port == "http" || port == "https") {
return address{}, fmt.Errorf("[%s] scheme specified twice in address", input)
}
// error if scheme and port combination violate convention
if (scheme == "http" && port == "443") || (scheme == "https" && port == "80") {
return address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
}
// standardize http and https ports to their respective port numbers
if port == "http" {
scheme = "http"
port = "80"
} else if port == "https" {
scheme = "https"
port = "443"
}
return address{Original: input, Scheme: scheme, Host: host, Port: port}, err
return false
}
// replaceEnvVars replaces environment variables that appear in the token
@@ -383,27 +408,9 @@ func replaceEnvReferences(s, refStart, refEnd string) string {
return s
}
type (
// ServerBlock associates tokens with a list of addresses
// and groups tokens by directive name.
ServerBlock struct {
Addresses []address
Tokens map[string][]token
}
address struct {
Original, Scheme, Host, Port string
}
)
// HostList converts the list of addresses that are
// associated with this server block into a slice of
// strings, where each address is as it was originally
// read from the input.
func (sb ServerBlock) HostList() []string {
sbHosts := make([]string, len(sb.Addresses))
for j, addr := range sb.Addresses {
sbHosts[j] = addr.Original
}
return sbHosts
// ServerBlock associates any number of keys (usually addresses
// of some sort) with tokens (grouped by directive name).
type ServerBlock struct {
Keys []string
Tokens map[string][]Token
}
+502
View File
@@ -0,0 +1,502 @@
package caddyfile
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
)
func TestAllTokens(t *testing.T) {
input := strings.NewReader("a b c\nd e")
expected := []string{"a", "b", "c", "d", "e"}
tokens, err := allTokens(input)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(tokens) != len(expected) {
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
}
for i, val := range expected {
if tokens[i].Text != val {
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text)
}
}
}
func TestParseOneAndImport(t *testing.T) {
testParseOne := func(input string) (ServerBlock, error) {
p := testParser(input)
p.Next() // parseOne doesn't call Next() to start, so we must
err := p.parseOne()
return p.block, err
}
for i, test := range []struct {
input string
shouldErr bool
keys []string
tokens map[string]int // map of directive name to number of tokens expected
}{
{`localhost`, false, []string{
"localhost",
}, map[string]int{}},
{`localhost
dir1`, false, []string{
"localhost",
}, map[string]int{
"dir1": 1,
}},
{`localhost:1234
dir1 foo bar`, false, []string{
"localhost:1234",
}, map[string]int{
"dir1": 3,
}},
{`localhost {
dir1
}`, false, []string{
"localhost",
}, map[string]int{
"dir1": 1,
}},
{`localhost:1234 {
dir1 foo bar
dir2
}`, false, []string{
"localhost:1234",
}, map[string]int{
"dir1": 3,
"dir2": 1,
}},
{`http://localhost https://localhost
dir1 foo bar`, false, []string{
"http://localhost",
"https://localhost",
}, map[string]int{
"dir1": 3,
}},
{`http://localhost https://localhost {
dir1 foo bar
}`, false, []string{
"http://localhost",
"https://localhost",
}, map[string]int{
"dir1": 3,
}},
{`http://localhost, https://localhost {
dir1 foo bar
}`, false, []string{
"http://localhost",
"https://localhost",
}, map[string]int{
"dir1": 3,
}},
{`http://localhost, {
}`, true, []string{
"http://localhost",
}, map[string]int{}},
{`host1:80, http://host2.com
dir1 foo bar
dir2 baz`, false, []string{
"host1:80",
"http://host2.com",
}, map[string]int{
"dir1": 3,
"dir2": 2,
}},
{`http://host1.com,
http://host2.com,
https://host3.com`, false, []string{
"http://host1.com",
"http://host2.com",
"https://host3.com",
}, map[string]int{}},
{`http://host1.com:1234, https://host2.com
dir1 foo {
bar baz
}
dir2`, false, []string{
"http://host1.com:1234",
"https://host2.com",
}, map[string]int{
"dir1": 6,
"dir2": 1,
}},
{`127.0.0.1
dir1 {
bar baz
}
dir2 {
foo bar
}`, false, []string{
"127.0.0.1",
}, map[string]int{
"dir1": 5,
"dir2": 5,
}},
{`localhost
dir1 {
foo`, true, []string{
"localhost",
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
}`, false, []string{
"localhost",
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
} }`, true, []string{
"localhost",
}, map[string]int{
"dir1": 3,
}},
{`localhost
dir1 {
nested {
foo
}
}
dir2 foo bar`, false, []string{
"localhost",
}, map[string]int{
"dir1": 7,
"dir2": 3,
}},
{``, false, []string{}, map[string]int{}},
{`localhost
dir1 arg1
import testdata/import_test1.txt`, false, []string{
"localhost",
}, map[string]int{
"dir1": 2,
"dir2": 3,
"dir3": 1,
}},
{`import testdata/import_test2.txt`, false, []string{
"host1",
}, map[string]int{
"dir1": 1,
"dir2": 2,
}},
{`import testdata/import_test1.txt testdata/import_test2.txt`, true, []string{}, map[string]int{}},
{`import testdata/not_found.txt`, true, []string{}, map[string]int{}},
{`""`, false, []string{}, map[string]int{}},
{``, false, []string{}, map[string]int{}},
} {
result, err := testParseOne(test.input)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(result.Keys) != len(test.keys) {
t.Errorf("Test %d: Expected %d keys, got %d",
i, len(test.keys), len(result.Keys))
continue
}
for j, addr := range result.Keys {
if addr != test.keys[j] {
t.Errorf("Test %d, key %d: Expected '%s', but was '%s'",
i, j, test.keys[j], addr)
}
}
if len(result.Tokens) != len(test.tokens) {
t.Errorf("Test %d: Expected %d directives, had %d",
i, len(test.tokens), len(result.Tokens))
continue
}
for directive, tokens := range result.Tokens {
if len(tokens) != test.tokens[directive] {
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
i, directive, test.tokens[directive], len(tokens))
continue
}
}
}
}
func TestRecursiveImport(t *testing.T) {
testParseOne := func(input string) (ServerBlock, error) {
p := testParser(input)
p.Next() // parseOne doesn't call Next() to start, so we must
err := p.parseOne()
return p.block, err
}
isExpected := func(got ServerBlock) bool {
if len(got.Keys) != 1 || got.Keys[0] != "localhost" {
t.Errorf("got keys unexpected: expect localhost, got %v", got.Keys)
return false
}
if len(got.Tokens) != 2 {
t.Errorf("got wrong number of tokens: expect 2, got %d", len(got.Tokens))
return false
}
if len(got.Tokens["dir1"]) != 1 || len(got.Tokens["dir2"]) != 2 {
t.Errorf("got unexpect tokens: %v", got.Tokens)
return false
}
return true
}
recursiveFile1, err := filepath.Abs("testdata/recursive_import_test1")
if err != nil {
t.Fatal(err)
}
recursiveFile2, err := filepath.Abs("testdata/recursive_import_test2")
if err != nil {
t.Fatal(err)
}
// test relative recursive import
err = ioutil.WriteFile(recursiveFile1, []byte(
`localhost
dir1
import recursive_import_test2`), 0644)
if err != nil {
t.Fatal(err)
}
defer os.Remove(recursiveFile1)
err = ioutil.WriteFile(recursiveFile2, []byte("dir2 1"), 0644)
if err != nil {
t.Fatal(err)
}
defer os.Remove(recursiveFile2)
// import absolute path
result, err := testParseOne("import " + recursiveFile1)
if err != nil {
t.Fatal(err)
}
if !isExpected(result) {
t.Error("absolute+relative import failed")
}
// import relative path
result, err = testParseOne("import testdata/recursive_import_test1")
if err != nil {
t.Fatal(err)
}
if !isExpected(result) {
t.Error("relative+relative import failed")
}
// test absolute recursive import
err = ioutil.WriteFile(recursiveFile1, []byte(
`localhost
dir1
import `+recursiveFile2), 0644)
if err != nil {
t.Fatal(err)
}
// import absolute path
result, err = testParseOne("import " + recursiveFile1)
if err != nil {
t.Fatal(err)
}
if !isExpected(result) {
t.Error("absolute+absolute import failed")
}
// import relative path
result, err = testParseOne("import testdata/recursive_import_test1")
if err != nil {
t.Fatal(err)
}
if !isExpected(result) {
t.Error("relative+absolute import failed")
}
}
func TestParseAll(t *testing.T) {
for i, test := range []struct {
input string
shouldErr bool
keys [][]string // keys per server block, in order
}{
{`localhost`, false, [][]string{
{"localhost"},
}},
{`localhost:1234`, false, [][]string{
{"localhost:1234"},
}},
{`localhost:1234 {
}
localhost:2015 {
}`, false, [][]string{
{"localhost:1234"},
{"localhost:2015"},
}},
{`localhost:1234, http://host2`, false, [][]string{
{"localhost:1234", "http://host2"},
}},
{`localhost:1234, http://host2,`, true, [][]string{}},
{`http://host1.com, http://host2.com {
}
https://host3.com, https://host4.com {
}`, false, [][]string{
{"http://host1.com", "http://host2.com"},
{"https://host3.com", "https://host4.com"},
}},
{`import testdata/import_glob*.txt`, false, [][]string{
{"glob0.host0"},
{"glob0.host1"},
{"glob1.host0"},
{"glob2.host0"},
}},
{`import notfound/*`, false, [][]string{}}, // glob needn't error with no matches
{`import notfound/file.conf`, true, [][]string{}}, // but a specific file should
} {
p := testParser(test.input)
blocks, err := p.parseAll()
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected an error, but didn't get one", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
}
if len(blocks) != len(test.keys) {
t.Errorf("Test %d: Expected %d server blocks, got %d",
i, len(test.keys), len(blocks))
continue
}
for j, block := range blocks {
if len(block.Keys) != len(test.keys[j]) {
t.Errorf("Test %d: Expected %d keys in block %d, got %d",
i, len(test.keys[j]), j, len(block.Keys))
continue
}
for k, addr := range block.Keys {
if addr != test.keys[j][k] {
t.Errorf("Test %d, block %d, key %d: Expected '%s', but got '%s'",
i, j, k, test.keys[j][k], addr)
}
}
}
}
}
func TestEnvironmentReplacement(t *testing.T) {
os.Setenv("PORT", "8080")
os.Setenv("ADDRESS", "servername.com")
os.Setenv("FOOBAR", "foobar")
// basic test; unix-style env vars
p := testParser(`{$ADDRESS}`)
blocks, _ := p.parseAll()
if actual, expected := blocks[0].Keys[0], "servername.com"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
// multiple vars per token
p = testParser(`{$ADDRESS}:{$PORT}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
// windows-style var and unix style in same token
p = testParser(`{%ADDRESS%}:{$PORT}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
// reverse order
p = testParser(`{$ADDRESS}:{%PORT%}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], "servername.com:8080"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
// env var in server block body as argument
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], ":8080"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// combined windows env vars in argument
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "servername.com/foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// malformed env var (windows)
p = testParser(":1234\ndir1 {%ADDRESS}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "{%ADDRESS}"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
// malformed (non-existent) env var (unix)
p = testParser(`:{$PORT$}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], ":"; expected != actual {
t.Errorf("Expected key to be '%s' but was '%s'", expected, actual)
}
// in quoted field
p = testParser(":1234\ndir1 \"Test {$FOOBAR} test\"")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].Text, "Test foobar test"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
}
func testParser(input string) parser {
buf := strings.NewReader(input)
p := parser{Dispenser: NewDispenser("Caddyfile", buf)}
return p
}
@@ -1,8 +1,13 @@
// Package basicauth implements HTTP Basic Authentication.
// Package basicauth implements HTTP Basic Authentication for Caddy.
//
// This is useful for simple protections on a website, like requiring
// a password to access an admin interface. This package assumes a
// fairly small threat model.
package basicauth
import (
"bufio"
"crypto/sha1"
"crypto/subtle"
"fmt"
"io"
@@ -13,7 +18,7 @@ import (
"sync"
"github.com/jimstudt/http-authentication/basic"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// BasicAuth is middleware to protect resources with a username and password.
@@ -22,20 +27,19 @@ import (
// security of HTTP Basic Auth is disputed. Use discretion when deciding
// what to protect with BasicAuth.
type BasicAuth struct {
Next middleware.Handler
Next httpserver.Handler
SiteRoot string
Rules []Rule
}
// ServeHTTP implements the middleware.Handler interface.
// ServeHTTP implements the httpserver.Handler interface.
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var hasAuth bool
var isAuthenticated bool
for _, rule := range a.Rules {
for _, res := range rule.Resources {
if !middleware.Path(r.URL.Path).Matches(res) {
if !httpserver.Path(r.URL.Path).Matches(res) {
continue
}
@@ -47,7 +51,6 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
if !ok ||
username != rule.Username ||
!rule.Password(password) {
//subtle.ConstantTimeCompare([]byte(password), []byte(rule.Password)) != 1 {
continue
}
@@ -58,14 +61,14 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
if hasAuth {
if !isAuthenticated {
w.Header().Set("WWW-Authenticate", "Basic")
w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"")
return http.StatusUnauthorized, nil
}
// "It's an older code, sir, but it checks out. I was about to clear them."
return a.Next.ServeHTTP(w, r)
}
// Pass-thru when no paths match
// Pass-through when no paths match
return a.Next.ServeHTTP(w, r)
}
@@ -140,9 +143,17 @@ func parseHtpasswd(pm map[string]PasswordMatcher, r io.Reader) error {
}
// PlainMatcher returns a PasswordMatcher that does a constant-time
// byte-wise comparison.
// byte comparison against the password passw.
func PlainMatcher(passw string) PasswordMatcher {
// compare hashes of equal length instead of actual password
// to avoid leaking password length
passwHash := sha1.New()
passwHash.Write([]byte(passw))
passwSum := passwHash.Sum(nil)
return func(pw string) bool {
return subtle.ConstantTimeCompare([]byte(pw), []byte(passw)) == 1
pwHash := sha1.New()
pwHash.Write([]byte(pw))
pwSum := pwHash.Sum(nil)
return subtle.ConstantTimeCompare([]byte(pwSum), []byte(passwSum)) == 1
}
}
@@ -10,13 +10,12 @@ import (
"path/filepath"
"testing"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestBasicAuth(t *testing.T) {
rw := BasicAuth{
Next: middleware.HandlerFunc(contentHandler),
Next: httpserver.HandlerFunc(contentHandler),
Rules: []Rule{
{Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}},
},
@@ -53,7 +52,7 @@ func TestBasicAuth(t *testing.T) {
if result == http.StatusUnauthorized {
headers := rec.Header()
if val, ok := headers["Www-Authenticate"]; ok {
if val[0] != "Basic" {
if val[0] != "Basic realm=\"Restricted\"" {
t.Errorf("Test %d, Www-Authenticate should be %s provided %s", i, "Basic", val[0])
}
} else {
@@ -67,7 +66,7 @@ func TestBasicAuth(t *testing.T) {
func TestMultipleOverlappingRules(t *testing.T) {
rw := BasicAuth{
Next: middleware.HandlerFunc(contentHandler),
Next: httpserver.HandlerFunc(contentHandler),
Rules: []Rule{
{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}},
{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}},
@@ -1,43 +1,54 @@
package setup
package basicauth
import (
"strings"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/basicauth"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// BasicAuth configures a new BasicAuth middleware instance.
func BasicAuth(c *Controller) (middleware.Middleware, error) {
root := c.Root
func init() {
caddy.RegisterPlugin("basicauth", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new BasicAuth middleware instance.
func setup(c *caddy.Controller) error {
cfg := httpserver.GetConfig(c)
root := cfg.Root
rules, err := basicAuthParse(c)
if err != nil {
return nil, err
return err
}
basic := basicauth.BasicAuth{Rules: rules}
basic := BasicAuth{Rules: rules}
return func(next middleware.Handler) middleware.Handler {
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
basic.Next = next
basic.SiteRoot = root
return basic
}, nil
})
return nil
}
func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
var rules []basicauth.Rule
func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
var rules []Rule
cfg := httpserver.GetConfig(c)
var err error
for c.Next() {
var rule basicauth.Rule
var rule Rule
args := c.RemainingArgs()
switch len(args) {
case 2:
rule.Username = args[0]
if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil {
if rule.Password, err = passwordMatcher(rule.Username, args[1], cfg.Root); err != nil {
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
}
@@ -50,7 +61,7 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
case 3:
rule.Resources = append(rule.Resources, args[0])
rule.Username = args[1]
if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil {
if rule.Password, err = passwordMatcher(rule.Username, args[2], cfg.Root); err != nil {
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
}
default:
@@ -63,10 +74,9 @@ func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
return rules, nil
}
func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) {
func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) {
if !strings.HasPrefix(passw, "htpasswd=") {
return basicauth.PlainMatcher(passw), nil
return PlainMatcher(passw), nil
}
return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot)
return GetHtpasswdMatcher(passw[9:], username, siteRoot)
}
@@ -1,4 +1,4 @@
package setup
package basicauth
import (
"fmt"
@@ -7,27 +7,28 @@ import (
"strings"
"testing"
"github.com/mholt/caddy/middleware/basicauth"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestBasicAuth(t *testing.T) {
c := NewTestController(`basicauth user pwd`)
mid, err := BasicAuth(c)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `basicauth user pwd`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
if mid == nil {
t.Fatal("Expected middleware, was nil instead")
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, got 0 instead")
}
handler := mid(EmptyNext)
myHandler, ok := handler.(basicauth.BasicAuth)
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(BasicAuth)
if !ok {
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
}
if !SameNext(myHandler.Next, EmptyNext) {
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
@@ -54,41 +55,40 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
input string
shouldErr bool
password string
expected []basicauth.Rule
expected []Rule
}{
{`basicauth user pwd`, false, "pwd", []basicauth.Rule{
{`basicauth user pwd`, false, "pwd", []Rule{
{Username: "user"},
}},
{`basicauth user pwd {
}`, false, "pwd", []basicauth.Rule{
}`, false, "pwd", []Rule{
{Username: "user"},
}},
{`basicauth user pwd {
/resource1
/resource2
}`, false, "pwd", []basicauth.Rule{
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
}},
{`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{
{`basicauth /resource user pwd`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource"}},
}},
{`basicauth /res1 user1 pwd1
basicauth /res2 user2 pwd2`, false, "pwd", []basicauth.Rule{
basicauth /res2 user2 pwd2`, false, "pwd", []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 user`, true, "", []Rule{}},
{`basicauth`, true, "", []Rule{}},
{`basicauth /resource user pwd asdf`, true, "", []Rule{}},
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []basicauth.Rule{
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []Rule{
{Username: "sha1"},
}},
}
for i, test := range tests {
c := NewTestController(test.input)
actual, err := basicAuthParse(c)
actual, err := basicAuthParse(caddy.NewTestController("http", test.input))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
+24
View File
@@ -0,0 +1,24 @@
package bind
import (
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("bind", caddy.Plugin{
ServerType: "http",
Action: setupBind,
})
}
func setupBind(c *caddy.Controller) error {
config := httpserver.GetConfig(c)
for c.Next() {
if !c.Args(&config.ListenHost) {
return c.ArgErr()
}
config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
}
return nil
}
+24
View File
@@ -0,0 +1,24 @@
package bind
import (
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestSetupBind(t *testing.T) {
c := caddy.NewTestController("http", `bind 1.2.3.4`)
err := setupBind(c)
if err != nil {
t.Fatalf("Expected no errors, but got: %v", err)
}
cfg := httpserver.GetConfig(c)
if got, want := cfg.ListenHost, "1.2.3.4"; got != want {
t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got)
}
if got, want := cfg.TLS.ListenHost, "1.2.3.4"; got != want {
t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got)
}
}
+442
View File
@@ -0,0 +1,442 @@
// Package browse provides middleware for listing files in a directory
// when directory path is requested instead of a specific file.
package browse
import (
"bytes"
"encoding/json"
"net/http"
"net/url"
"os"
"path"
"sort"
"strconv"
"strings"
"text/template"
"time"
"github.com/dustin/go-humanize"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddyhttp/staticfiles"
)
const (
sortByName = "name"
sortBySize = "size"
sortByTime = "time"
)
// Browse is an http.Handler that can show a file listing when
// directories in the given paths are specified.
type Browse struct {
Next httpserver.Handler
Configs []Config
IgnoreIndexes bool
}
// Config is a configuration for browsing in a particular path.
type Config struct {
PathScope string
Fs staticfiles.FileServer
Variables interface{}
Template *template.Template
}
// A Listing is the context used to fill out a template.
type Listing struct {
// The name of the directory (the last element of the path)
Name string
// The full path of the request
Path string
// Whether the parent directory is browsable
CanGoUp bool
// The items (files and folders) in the path
Items []FileInfo
// The number of directories in the listing
NumDirs int
// The number of files (items that aren't directories) in the listing
NumFiles int
// Which sorting order is used
Sort string
// And which order
Order string
// If ≠0 then Items have been limited to that many elements
ItemsLimitedTo int
// Optional custom variables for use in browse templates
User interface{}
httpserver.Context
}
// BreadcrumbMap returns l.Path where every element is a map
// of URLs and path segment names.
func (l Listing) BreadcrumbMap() map[string]string {
result := map[string]string{}
if len(l.Path) == 0 {
return result
}
// skip trailing slash
lpath := l.Path
if lpath[len(lpath)-1] == '/' {
lpath = lpath[:len(lpath)-1]
}
parts := strings.Split(lpath, "/")
for i, part := range parts {
if i == 0 && part == "" {
// Leading slash (root)
result["/"] = "/"
continue
}
result[strings.Join(parts[:i+1], "/")] = part
}
return result
}
// FileInfo is the info about a particular file or directory
type FileInfo struct {
Name string
Size int64
URL string
ModTime time.Time
Mode os.FileMode
IsDir bool
}
// HumanSize returns the size of the file as a human-readable string
// in IEC format (i.e. power of 2 or base 1024).
func (fi FileInfo) HumanSize() string {
return humanize.IBytes(uint64(fi.Size))
}
// HumanModTime returns the modified time of the file as a human-readable string.
func (fi FileInfo) HumanModTime(format string) string {
return fi.ModTime.Format(format)
}
// Implement sorting for Listing
type byName Listing
type bySize Listing
type byTime Listing
// By Name
func (l byName) Len() int { return len(l.Items) }
func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
// Treat upper and lower case equally
func (l byName) Less(i, j int) bool {
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
}
// By Size
func (l bySize) Len() int { return len(l.Items) }
func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
const directoryOffset = -1 << 31 // = math.MinInt32
func (l bySize) Less(i, j int) bool {
iSize, jSize := l.Items[i].Size, l.Items[j].Size
if l.Items[i].IsDir {
iSize = directoryOffset + iSize
}
if l.Items[j].IsDir {
jSize = directoryOffset + jSize
}
return iSize < jSize
}
// By Time
func (l byTime) Len() int { return len(l.Items) }
func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) }
// Add sorting method to "Listing"
// it will apply what's in ".Sort" and ".Order"
func (l Listing) applySort() {
// Check '.Order' to know how to sort
if l.Order == "desc" {
switch l.Sort {
case sortByName:
sort.Sort(sort.Reverse(byName(l)))
case sortBySize:
sort.Sort(sort.Reverse(bySize(l)))
case sortByTime:
sort.Sort(sort.Reverse(byTime(l)))
default:
// If not one of the above, do nothing
return
}
} else { // If we had more Orderings we could add them here
switch l.Sort {
case sortByName:
sort.Sort(byName(l))
case sortBySize:
sort.Sort(bySize(l))
case sortByTime:
sort.Sort(byTime(l))
default:
// If not one of the above, do nothing
return
}
}
}
func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config *Config) (Listing, bool) {
var (
fileinfos []FileInfo
dirCount, fileCount int
hasIndexFile bool
)
for _, f := range files {
name := f.Name()
for _, indexName := range staticfiles.IndexPages {
if name == indexName {
hasIndexFile = true
break
}
}
if f.IsDir() {
name += "/"
dirCount++
} else {
fileCount++
}
url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
if config.Fs.IsHidden(f) {
continue
}
fileinfos = append(fileinfos, FileInfo{
IsDir: f.IsDir(),
Name: f.Name(),
Size: f.Size(),
URL: url.String(),
ModTime: f.ModTime().UTC(),
Mode: f.Mode(),
})
}
return Listing{
Name: path.Base(urlPath),
Path: urlPath,
CanGoUp: canGoUp,
Items: fileinfos,
NumDirs: dirCount,
NumFiles: fileCount,
}, hasIndexFile
}
// ServeHTTP determines if the request is for this plugin, and if all prerequisites are met.
// If so, control is handed over to ServeListing.
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var bc *Config
// See if there's a browse configuration to match the path
for i := range b.Configs {
if httpserver.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
bc = &b.Configs[i]
goto inScope
}
}
return b.Next.ServeHTTP(w, r)
inScope:
// Browse works on existing directories; delegate everything else
requestedFilepath, err := bc.Fs.Root.Open(r.URL.Path)
if err != nil {
switch {
case os.IsPermission(err):
return http.StatusForbidden, err
case os.IsExist(err):
return http.StatusNotFound, err
default:
return b.Next.ServeHTTP(w, r)
}
}
defer requestedFilepath.Close()
info, err := requestedFilepath.Stat()
if err != nil {
switch {
case os.IsPermission(err):
return http.StatusForbidden, err
case os.IsExist(err):
return http.StatusGone, err
default:
return b.Next.ServeHTTP(w, r)
}
}
if !info.IsDir() {
return b.Next.ServeHTTP(w, r)
}
// Do not reply to anything else because it might be nonsensical
switch r.Method {
case http.MethodGet, http.MethodHead:
// proceed, noop
case "PROPFIND", http.MethodOptions:
return http.StatusNotImplemented, nil
default:
return b.Next.ServeHTTP(w, r)
}
// Browsing navigation gets messed up if browsing a directory
// that doesn't end in "/" (which it should, anyway)
if !strings.HasSuffix(r.URL.Path, "/") {
staticfiles.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
return 0, nil
}
return b.ServeListing(w, r, requestedFilepath, bc)
}
func (b Browse) loadDirectoryContents(requestedFilepath http.File, urlPath string, config *Config) (*Listing, bool, error) {
files, err := requestedFilepath.Readdir(-1)
if err != nil {
return nil, false, err
}
// Determine if user can browse up another folder
var canGoUp bool
curPathDir := path.Dir(strings.TrimSuffix(urlPath, "/"))
for _, other := range b.Configs {
if strings.HasPrefix(curPathDir, other.PathScope) {
canGoUp = true
break
}
}
// Assemble listing of directory contents
listing, hasIndex := directoryListing(files, canGoUp, urlPath, config)
return &listing, hasIndex, nil
}
// handleSortOrder gets and stores for a Listing the 'sort' and 'order',
// and reads 'limit' if given. The latter is 0 if not given.
//
// This sets Cookies.
func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope string) (sort string, order string, limit int, err error) {
sort, order, limitQuery := r.URL.Query().Get("sort"), r.URL.Query().Get("order"), r.URL.Query().Get("limit")
// If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies
switch sort {
case "":
sort = sortByName
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
sort = sortCookie.Value
}
case sortByName, sortBySize, sortByTime:
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil})
}
switch order {
case "":
order = "asc"
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
order = orderCookie.Value
}
case "asc", "desc":
http.SetCookie(w, &http.Cookie{Name: "order", Value: order, Path: scope, Secure: r.TLS != nil})
}
if limitQuery != "" {
limit, err = strconv.Atoi(limitQuery)
if err != nil { // if the 'limit' query can't be interpreted as a number, return err
return
}
}
return
}
// ServeListing returns a formatted view of 'requestedFilepath' contents'.
func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFilepath http.File, bc *Config) (int, error) {
listing, containsIndex, err := b.loadDirectoryContents(requestedFilepath, r.URL.Path, bc)
if err != nil {
switch {
case os.IsPermission(err):
return http.StatusForbidden, err
case os.IsExist(err):
return http.StatusGone, err
default:
return http.StatusInternalServerError, err
}
}
if containsIndex && !b.IgnoreIndexes { // directory isn't browsable
return b.Next.ServeHTTP(w, r)
}
listing.Context = httpserver.Context{
Root: bc.Fs.Root,
Req: r,
URL: r.URL,
}
listing.User = bc.Variables
// Copy the query values into the Listing struct
var limit int
listing.Sort, listing.Order, limit, err = b.handleSortOrder(w, r, bc.PathScope)
if err != nil {
return http.StatusBadRequest, err
}
listing.applySort()
if limit > 0 && limit <= len(listing.Items) {
listing.Items = listing.Items[:limit]
listing.ItemsLimitedTo = limit
}
var buf *bytes.Buffer
acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
switch {
case strings.Contains(acceptHeader, "application/json"):
if buf, err = b.formatAsJSON(listing, bc); err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
default: // There's no 'application/json' in the 'Accept' header; browse normally
if buf, err = b.formatAsHTML(listing, bc); err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
}
buf.WriteTo(w)
return http.StatusOK, nil
}
func (b Browse) formatAsJSON(listing *Listing, bc *Config) (*bytes.Buffer, error) {
marsh, err := json.Marshal(listing.Items)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
_, err = buf.Write(marsh)
return buf, err
}
func (b Browse) formatAsHTML(listing *Listing, bc *Config) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
err := bc.Template.Execute(buf, listing)
return buf, err
}
@@ -12,20 +12,10 @@ import (
"text/template"
"time"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddyhttp/staticfiles"
)
// "sort" package has "IsSorted" function, but no "IsReversed";
func isReversed(data sort.Interface) bool {
n := data.Len()
for i := n - 1; i > 0; i-- {
if !data.Less(i, i-1) {
return false
}
}
return true
}
func TestSort(t *testing.T) {
// making up []fileInfo with bogus values;
// to be used to make up our "listing"
@@ -104,22 +94,65 @@ func TestSort(t *testing.T) {
}
}
func TestBrowseTemplate(t *testing.T) {
func TestBrowseHTTPMethods(t *testing.T) {
tmpl, err := template.ParseFiles("testdata/photos.tpl")
if err != nil {
t.Fatalf("An error occured while parsing the template: %v", err)
t.Fatalf("An error occurred while parsing the template: %v", err)
}
b := Browse{
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called")
return 0, nil
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
return http.StatusTeapot, nil // not t.Fatalf, or we will not see what other methods yield
}),
Root: "./testdata",
Configs: []Config{
{
PathScope: "/photos",
Template: tmpl,
Fs: staticfiles.FileServer{
Root: http.Dir("./testdata"),
},
Template: tmpl,
},
},
}
rec := httptest.NewRecorder()
for method, expected := range map[string]int{
http.MethodGet: http.StatusOK,
http.MethodHead: http.StatusOK,
http.MethodOptions: http.StatusNotImplemented,
"PROPFIND": http.StatusNotImplemented,
} {
req, err := http.NewRequest(method, "/photos/", nil)
if err != nil {
t.Fatalf("Test: Could not create HTTP request: %v", err)
}
code, _ := b.ServeHTTP(rec, req)
if code != expected {
t.Errorf("Wrong status with HTTP Method %s: expected %d, got %d", method, expected, code)
}
}
}
func TestBrowseTemplate(t *testing.T) {
tmpl, err := template.ParseFiles("testdata/photos.tpl")
if err != nil {
t.Fatalf("An error occurred while parsing the template: %v", err)
}
b := Browse{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called")
return 0, nil
}),
Configs: []Config{
{
PathScope: "/photos",
Fs: staticfiles.FileServer{
Root: http.Dir("./testdata"),
Hide: []string{"photos/hidden.html"},
},
Template: tmpl,
},
},
}
@@ -131,7 +164,7 @@ func TestBrowseTemplate(t *testing.T) {
rec := httptest.NewRecorder()
code, err := b.ServeHTTP(rec, req)
code, _ := b.ServeHTTP(rec, req)
if code != http.StatusOK {
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
}
@@ -164,22 +197,23 @@ func TestBrowseTemplate(t *testing.T) {
}
func TestBrowseJson(t *testing.T) {
b := Browse{
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
Next: httpserver.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/",
Fs: staticfiles.FileServer{
Root: http.Dir("./testdata"),
},
},
},
}
//Getting the listing from the ./testdata/photos, the listing returned will be used to validate test results
testDataPath := b.Root + "/photos/"
testDataPath := filepath.Join("./testdata", "photos")
file, err := os.Open(testDataPath)
if err != nil {
if os.IsPermission(err) {
@@ -200,7 +234,7 @@ func TestBrowseJson(t *testing.T) {
// 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))
chTime := f.ModTime().UTC().Add(-(time.Duration(i) * time.Second))
if err := os.Chtimes(filepath.Join(testDataPath, name), chTime, chTime); err != nil {
t.Fatal(err)
}
@@ -264,20 +298,21 @@ func TestBrowseJson(t *testing.T) {
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)
if err != nil && !test.shouldErr {
t.Errorf("Test %d errored when making request, 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 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 code != http.StatusOK {
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
t.Fatalf("In test %d: Wrong status, expected %d, got %d", i, 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"))
@@ -316,3 +351,14 @@ func TestBrowseJson(t *testing.T) {
}
}
}
// "sort" package has "IsSorted" function, but no "IsReversed";
func isReversed(data sort.Interface) bool {
n := data.Len()
for i := n - 1; i > 0; i-- {
if !data.Less(i, i-1) {
return false
}
}
return true
}
@@ -1,37 +1,49 @@
package setup
package browse
import (
"fmt"
"io/ioutil"
"net/http"
"text/template"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/browse"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddyhttp/staticfiles"
)
// Browse configures a new Browse middleware instance.
func Browse(c *Controller) (middleware.Middleware, error) {
func init() {
caddy.RegisterPlugin("browse", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new Browse middleware instance.
func setup(c *caddy.Controller) error {
configs, err := browseParse(c)
if err != nil {
return nil, err
return err
}
browse := browse.Browse{
Root: c.Root,
b := Browse{
Configs: configs,
IgnoreIndexes: false,
}
return func(next middleware.Handler) middleware.Handler {
browse.Next = next
return browse
}, nil
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
b.Next = next
return b
})
return nil
}
func browseParse(c *Controller) ([]browse.Config, error) {
var configs []browse.Config
func browseParse(c *caddy.Controller) ([]Config, error) {
var configs []Config
appendCfg := func(bc browse.Config) error {
cfg := httpserver.GetConfig(c)
appendCfg := func(bc Config) error {
for _, c := range configs {
if c.PathScope == bc.PathScope {
return fmt.Errorf("duplicate browsing config for %s", c.PathScope)
@@ -42,7 +54,7 @@ func browseParse(c *Controller) ([]browse.Config, error) {
}
for c.Next() {
var bc browse.Config
var bc Config
// First argument is directory to allow browsing; default is site root
if c.NextArg() {
@@ -51,6 +63,11 @@ func browseParse(c *Controller) ([]browse.Config, error) {
bc.PathScope = "/"
}
bc.Fs = staticfiles.FileServer{
Root: http.Dir(cfg.Root),
Hide: httpserver.GetConfig(c).HiddenFiles,
}
// Second argument would be the template file to use
var tplText string
if c.NextArg() {
@@ -85,7 +102,6 @@ const defaultTemplate = `<!DOCTYPE html>
<html>
<head>
<title>{{.Name}}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { padding: 0; margin: 0; }
@@ -106,7 +122,7 @@ h1 a:hover {
}
header,
.content {
#summary {
padding-left: 5%;
padding-right: 5%;
}
@@ -168,7 +184,7 @@ tr {
border-bottom: 1px dashed #dadada;
}
tr:not(:first-child):hover {
tbody tr:hover {
background-color: #ffffec;
}
@@ -210,7 +226,8 @@ td:first-child svg {
position: absolute;
}
td .name {
td .name,
td .goup {
margin-left: 1.75em;
word-break: break-all;
overflow-wrap: break-word;
@@ -263,7 +280,6 @@ footer {
</g>
</g>
<!-- File -->
<linearGradient id="a">
<stop stop-color="#cbcbcb" offset="0"/>
@@ -299,70 +315,109 @@ footer {
</defs>
</svg>
<header>
<h1>{{.LinkedPath}}</h1>
<h1>
{{range $url, $name := .BreadcrumbMap}}<a href="{{$url}}">{{$name}}</a>{{if ne $url "/"}}/{{end}}{{end}}
</h1>
</header>
<main>
<div class="meta">
<div class="content">
<div id="summary">
<span class="meta-item"><b>{{.NumDirs}}</b> director{{if eq 1 .NumDirs}}y{{else}}ies{{end}}</span>
<span class="meta-item"><b>{{.NumFiles}}</b> file{{if ne 1 .NumFiles}}s{{end}}</span>
{{- if ne 0 .ItemsLimitedTo}}
<span class="meta-item">(of which only <b>{{.ItemsLimitedTo}}</b> are displayed)</span>
{{- end}}
</div>
</div>
<div class="listing">
<table>
<table aria-describedby="summary">
<thead>
<tr>
<th>
{{if and (eq .Sort "name") (ne .Order "desc")}}
<a href="?sort=name&order=desc">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{else if and (eq .Sort "name") (ne .Order "asc")}}
<a href="?sort=name&order=asc">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{else}}
<a href="?sort=name&order=asc">Name</a>
{{end}}
{{- if and (eq .Sort "name") (ne .Order "desc")}}
<a href="?sort=name&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{- else if and (eq .Sort "name") (ne .Order "asc")}}
<a href="?sort=name&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{- else}}
<a href="?sort=name&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Name</a>
{{- end}}
</th>
<th>
{{if and (eq .Sort "size") (ne .Order "desc")}}
<a href="?sort=size&order=desc">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a></a>
{{else if and (eq .Sort "size") (ne .Order "asc")}}
<a href="?sort=size&order=asc">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a></a>
{{else}}
<a href="?sort=size&order=asc">Size</a>
{{end}}
{{- if and (eq .Sort "size") (ne .Order "desc")}}
<a href="?sort=size&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{- else if and (eq .Sort "size") (ne .Order "asc")}}
<a href="?sort=size&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{- else}}
<a href="?sort=size&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size</a>
{{- end}}
</th>
<th class="hideable">
{{if and (eq .Sort "time") (ne .Order "desc")}}
<a href="?sort=time&order=desc">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a></a>
{{else if and (eq .Sort "time") (ne .Order "asc")}}
<a href="?sort=time&order=asc">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a></a>
{{else}}
<a href="?sort=time&order=asc">Modified</a>
{{end}}
{{- if and (eq .Sort "time") (ne .Order "desc")}}
<a href="?sort=time&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{- else if and (eq .Sort "time") (ne .Order "asc")}}
<a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{- else}}
<a href="?sort=time&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified</a>
{{- end}}
</th>
</tr>
{{range .Items}}
</thead>
<tbody>
{{- if .CanGoUp}}
<tr>
<td>
<a href="..">
<span class="goup">Go up</span>
</a>
</td>
<td>&mdash;</td>
<td class="hideable">&mdash;</td>
</tr>
{{- end}}
{{- range .Items}}
<tr>
<td>
<a href="{{.URL}}">
{{if .IsDir}}
{{- if .IsDir}}
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 35.678803 28.527945"><use xlink:href="#folder"></use></svg>
{{else}}
{{- else}}
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 26.604381 29.144726"><use xlink:href="#file"></use></svg>
{{end}}
{{- end}}
<span class="name">{{.Name}}</span>
</a>
</td>
<td>{{.HumanSize}}</td>
<td class="hideable">{{.HumanModTime "01/02/2006 03:04:05 PM"}}</td>
{{- if .IsDir}}
<td data-order="-1">&mdash;</td>
{{- else}}
<td data-order="{{.Size}}">{{.HumanSize}}</td>
{{- end}}
<td class="hideable"><time datetime="{{.HumanModTime "2006-01-02T15:04:05Z"}}">{{.HumanModTime "01/02/2006 03:04:05 PM -07:00"}}</time></td>
</tr>
{{end}}
{{- end}}
</tbody>
</table>
</div>
</main>
<footer>
Served with <a href="https://caddyserver.com">Caddy</a>
Served with <a rel="noopener noreferrer" href="https://caddyserver.com">Caddy</a>.
</footer>
<script type="text/javascript">
function localizeDatetime(e, index, ar) {
if (e.textContent === undefined) {
return;
}
var d = new Date(e.getAttribute('datetime'));
if (isNaN(d)) {
d = new Date(e.textContent);
if (isNaN(d)) {
return;
}
}
e.textContent = d.toLocaleString();
}
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
timeList.forEach(localizeDatetime);
</script>
</body>
</html>`
@@ -1,4 +1,4 @@
package setup
package browse
import (
"io/ioutil"
@@ -8,16 +8,17 @@ import (
"testing"
"time"
"github.com/mholt/caddy/middleware/browse"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestBrowse(t *testing.T) {
tempDirPath, err := getTempDirPath()
func TestSetup(t *testing.T) {
tempDirPath := os.TempDir()
_, err := os.Stat(tempDirPath)
if err != nil {
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
}
nonExistantDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano())))
nonExistentDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano())))
tempTemplate, err := ioutil.TempFile(".", "tempTemplate")
if err != nil {
@@ -35,31 +36,46 @@ func TestBrowse(t *testing.T) {
// test case #0 tests handling of multiple pathscopes
{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false},
// test case #1 tests instantiation of browse.Config with default values
// test case #1 tests instantiation of Config with default values
{"browse /", []string{"/"}, false},
// test case #2 tests detectaction of custom template
{"browse . " + tempTemplatePath, []string{"."}, false},
// test case #3 tests detection of non-existent template
{"browse . " + nonExistantDirPath, nil, true},
{"browse . " + nonExistentDirPath, nil, true},
// test case #4 tests detection of duplicate pathscopes
{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true},
} {
recievedFunc, err := Browse(NewTestController(test.input))
c := caddy.NewTestController("http", test.input)
err := setup(c)
if err != nil && !test.shouldErr {
t.Errorf("Test case #%d recieved an error of %v", i, err)
t.Errorf("Test case #%d received an error of %v", i, err)
}
if test.expectedPathScope == nil {
continue
}
recievedConfigs := recievedFunc(nil).(browse.Browse).Configs
for j, config := range recievedConfigs {
mids := httpserver.GetConfig(c).Middleware()
mid := mids[len(mids)-1]
receivedConfigs := mid(nil).(Browse).Configs
for j, config := range receivedConfigs {
if config.PathScope != test.expectedPathScope[j] {
t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope)
}
}
}
// test case #6 tests startup with missing root directory in combination with default browse settings
controller := caddy.NewTestController("http", "browse")
cfg := httpserver.GetConfig(controller)
// Make sure non-existent root path doesn't return error
cfg.Root = nonExistentDirPath
err = setup(controller)
if err != nil {
t.Errorf("Test for non-existent browse path received an error, but shouldn't have: %v", err)
}
}
+1
View File
@@ -0,0 +1 @@
Should be hidden
+31
View File
@@ -0,0 +1,31 @@
package caddyhttp
import (
// plug in the server
_ "github.com/mholt/caddy/caddyhttp/httpserver"
// plug in the standard directives
_ "github.com/mholt/caddy/caddyhttp/basicauth"
_ "github.com/mholt/caddy/caddyhttp/bind"
_ "github.com/mholt/caddy/caddyhttp/browse"
_ "github.com/mholt/caddy/caddyhttp/errors"
_ "github.com/mholt/caddy/caddyhttp/expvar"
_ "github.com/mholt/caddy/caddyhttp/extensions"
_ "github.com/mholt/caddy/caddyhttp/fastcgi"
_ "github.com/mholt/caddy/caddyhttp/gzip"
_ "github.com/mholt/caddy/caddyhttp/header"
_ "github.com/mholt/caddy/caddyhttp/internalsrv"
_ "github.com/mholt/caddy/caddyhttp/log"
_ "github.com/mholt/caddy/caddyhttp/markdown"
_ "github.com/mholt/caddy/caddyhttp/maxrequestbody"
_ "github.com/mholt/caddy/caddyhttp/mime"
_ "github.com/mholt/caddy/caddyhttp/pprof"
_ "github.com/mholt/caddy/caddyhttp/proxy"
_ "github.com/mholt/caddy/caddyhttp/redirect"
_ "github.com/mholt/caddy/caddyhttp/rewrite"
_ "github.com/mholt/caddy/caddyhttp/root"
_ "github.com/mholt/caddy/caddyhttp/status"
_ "github.com/mholt/caddy/caddyhttp/templates"
_ "github.com/mholt/caddy/caddyhttp/websocket"
_ "github.com/mholt/caddy/startupshutdown"
)
+19
View File
@@ -0,0 +1,19 @@
package caddyhttp
import (
"strings"
"testing"
"github.com/mholt/caddy"
)
// TODO: this test could be improved; the purpose is to
// ensure that the standard plugins are in fact plugged in
// and registered properly; this is a quick/naive way to do it.
func TestStandardPlugins(t *testing.T) {
numStandardPlugins := 28 // importing caddyhttp plugs in this many plugins
s := caddy.DescribePlugins()
if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want {
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
}
}
@@ -11,17 +11,27 @@ import (
"strings"
"time"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("errors", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// 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
Next httpserver.Handler
GenericErrorPage string // default error page filename
ErrorPages map[int]string // map of status code to filename
LogFile string
Log *log.Logger
LogRoller *httpserver.LogRoller
Debug bool // if true, errors are written out to client rather than to a log
file *os.File // a log file to close when done
}
func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
@@ -31,21 +41,18 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
if err != nil {
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.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(status)
fmt.Fprintln(w, errMsg)
return 0, err // returning < 400 signals that a response has been written
return 0, err // returning 0 signals that a response has been written
}
h.Log.Println(errMsg)
}
if status >= 400 {
if w.Header().Get("Content-Length") == "" {
h.errorPage(w, r, status)
}
h.errorPage(w, r, status)
return 0, err
}
@@ -56,18 +63,15 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
// 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, 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
if pagePath, ok := h.ErrorPages[code]; ok {
if pagePath, ok := h.findErrorPage(code); ok {
// Try to open it
errorPage, err := os.Open(pagePath)
if err != nil {
// 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)
httpserver.DefaultErrorFunc(w, r, code)
return
}
defer errorPage.Close()
@@ -81,14 +85,26 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int
// Epic fail... sigh.
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)
httpserver.DefaultErrorFunc(w, r, code)
}
return
}
// Default error response
http.Error(w, defaultBody, code)
httpserver.DefaultErrorFunc(w, r, code)
}
func (h ErrorHandler) findErrorPage(code int) (string, bool) {
if pagePath, ok := h.ErrorPages[code]; ok {
return pagePath, true
}
if h.GenericErrorPage != "" {
return h.GenericErrorPage, true
}
return "", false
}
func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
@@ -127,9 +143,7 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
// Write error and stack trace to the response rather than to a log
var stackBuf [4096]byte
stack := stackBuf[:runtime.Stack(stackBuf[:], false)]
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "%s\n\n%s", panicMsg, stack)
httpserver.WriteTextResponse(w, http.StatusInternalServerError, fmt.Sprintf("%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)
@@ -13,25 +13,19 @@ import (
"strings"
"testing"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestErrors(t *testing.T) {
// create a temporary page
path := filepath.Join(os.TempDir(), "errors_test.html")
f, err := os.Create(path)
const content = "This is a error page"
path, err := createErrorPageFile("errors_test.html", content)
if err != nil {
t.Fatal(err)
}
defer os.Remove(path)
const content = "This is a error page"
_, err = f.WriteString(content)
if err != nil {
t.Fatal(err)
}
f.Close()
buf := bytes.Buffer{}
em := ErrorHandler{
ErrorPages: map[int]string{
@@ -44,7 +38,7 @@ func TestErrors(t *testing.T) {
testErr := errors.New("test error")
tests := []struct {
next middleware.Handler
next httpserver.Handler
expectedCode int
expectedBody string
expectedLog string
@@ -79,13 +73,6 @@ func TestErrors(t *testing.T) {
expectedLog: "",
expectedErr: nil,
},
{
next: genErrorHandler(http.StatusNotFound, nil, "normal"),
expectedCode: 0,
expectedBody: "normal",
expectedLog: "",
expectedErr: nil,
},
{
next: genErrorHandler(http.StatusForbidden, nil, ""),
expectedCode: 0,
@@ -131,7 +118,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
eh := ErrorHandler{
ErrorPages: make(map[int]string),
Debug: true,
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
panic(panicMsg)
}),
}
@@ -153,7 +140,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
body := rec.Body.String()
if !strings.Contains(body, "[PANIC /] middleware/errors/errors_test.go") {
if !strings.Contains(body, "[PANIC /] caddyhttp/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) {
@@ -164,12 +151,109 @@ func TestVisibleErrorWithPanic(t *testing.T) {
}
}
func genErrorHandler(status int, err error, body string) middleware.Handler {
return middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
func TestGenericErrorPage(t *testing.T) {
// create temporary generic error page
const genericErrorContent = "This is a generic error page"
genericErrorPagePath, err := createErrorPageFile("generic_error_test.html", genericErrorContent)
if err != nil {
t.Fatal(err)
}
defer os.Remove(genericErrorPagePath)
// create temporary error page
const notFoundErrorContent = "This is a error page"
notFoundErrorPagePath, err := createErrorPageFile("not_found.html", notFoundErrorContent)
if err != nil {
t.Fatal(err)
}
defer os.Remove(notFoundErrorPagePath)
buf := bytes.Buffer{}
em := ErrorHandler{
GenericErrorPage: genericErrorPagePath,
ErrorPages: map[int]string{
http.StatusNotFound: notFoundErrorPagePath,
},
Log: log.New(&buf, "", 0),
}
tests := []struct {
next httpserver.Handler
expectedCode int
expectedBody string
expectedLog string
expectedErr error
}{
{
next: genErrorHandler(http.StatusNotFound, nil, ""),
expectedCode: 0,
expectedBody: notFoundErrorContent,
expectedLog: "",
expectedErr: nil,
},
{
next: genErrorHandler(http.StatusInternalServerError, nil, ""),
expectedCode: 0,
expectedBody: genericErrorContent,
expectedLog: "",
expectedErr: nil,
},
}
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
for i, test := range tests {
em.Next = test.next
buf.Reset()
rec := httptest.NewRecorder()
code, err := em.ServeHTTP(rec, req)
if err != test.expectedErr {
t.Errorf("Test %d: Expected error %v, but got %v",
i, test.expectedErr, err)
}
if code != test.expectedCode {
t.Errorf("Test %d: Expected status code %d, but got %d",
i, test.expectedCode, code)
}
if body := rec.Body.String(); body != test.expectedBody {
t.Errorf("Test %d: Expected body %q, but got %q",
i, test.expectedBody, body)
}
if log := buf.String(); !strings.Contains(log, test.expectedLog) {
t.Errorf("Test %d: Expected log %q, but got %q",
i, test.expectedLog, log)
}
}
}
func genErrorHandler(status int, err error, body string) httpserver.Handler {
return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
if len(body) > 0 {
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
fmt.Fprint(w, body)
}
fmt.Fprint(w, body)
return status, err
})
}
func createErrorPageFile(name string, content string) (string, error) {
errorPageFilePath := filepath.Join(os.TempDir(), name)
f, err := os.Create(errorPageFilePath)
if err != nil {
return "", err
}
_, err = f.WriteString(content)
if err != nil {
return "", err
}
f.Close()
return errorPageFilePath, nil
}
@@ -1,4 +1,4 @@
package setup
package errors
import (
"io"
@@ -8,19 +8,19 @@ import (
"strconv"
"github.com/hashicorp/go-syslog"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/errors"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// Errors configures a new errors middleware instance.
func Errors(c *Controller) (middleware.Middleware, error) {
// setup configures a new errors middleware instance.
func setup(c *caddy.Controller) error {
handler, err := errorsParse(c)
if err != nil {
return nil, err
return err
}
// Open the log file for writing when the server starts
c.Startup = append(c.Startup, func() error {
c.OnStartup(func() error {
var err error
var writer io.Writer
@@ -49,11 +49,10 @@ func Errors(c *Controller) (middleware.Middleware, error) {
}
if handler.LogRoller != nil {
file.Close()
handler.LogRoller.Filename = handler.LogFile
writer = handler.LogRoller.GetLogWriter()
} else {
handler.file = file
writer = file
}
}
@@ -62,17 +61,29 @@ func Errors(c *Controller) (middleware.Middleware, error) {
return nil
})
return func(next middleware.Handler) middleware.Handler {
// When server stops, close any open log file
c.OnShutdown(func() error {
if handler.file != nil {
handler.file.Close()
}
return nil
})
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
handler.Next = next
return handler
}, nil
})
return nil
}
func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
// Very important that we make a pointer because the Startup
func errorsParse(c *caddy.Controller) (*ErrorHandler, error) {
// Very important that we make a pointer because the startup
// function that opens the log file must have access to the
// same instance of the handler, not a copy.
handler := &errors.ErrorHandler{ErrorPages: make(map[int]string)}
handler := &ErrorHandler{ErrorPages: make(map[int]string)}
cfg := httpserver.GetConfig(c)
optionalBlock := func() (bool, error) {
var hadBlock bool
@@ -94,7 +105,7 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
if c.NextArg() {
if c.Val() == "{" {
c.IncrNest()
logRoller, err := parseRoller(c)
logRoller, err := httpserver.ParseRoller(c)
if err != nil {
return hadBlock, err
}
@@ -104,18 +115,32 @@ func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
}
} else {
// Error page; ensure it exists
where = filepath.Join(c.Root, where)
if !filepath.IsAbs(where) {
where = filepath.Join(cfg.Root, where)
}
f, err := os.Open(where)
if err != nil {
log.Printf("[WARNING] Unable to open error page '%s': %v", where, err)
}
f.Close()
whatInt, err := strconv.Atoi(what)
if err != nil {
return hadBlock, c.Err("Expecting a numeric status code, got '" + what + "'")
if what == "*" {
if handler.GenericErrorPage != "" {
return hadBlock, c.Errf("Duplicate status code entry: %s", what)
}
handler.GenericErrorPage = where
} else {
whatInt, err := strconv.Atoi(what)
if err != nil {
return hadBlock, c.Err("Expecting a numeric status code or '*', got '" + what + "'")
}
if _, exists := handler.ErrorPages[whatInt]; exists {
return hadBlock, c.Errf("Duplicate status code entry: %s", what)
}
handler.ErrorPages[whatInt] = where
}
handler.ErrorPages[whatInt] = where
}
}
return hadBlock, nil
+160
View File
@@ -0,0 +1,160 @@
package errors
import (
"path/filepath"
"reflect"
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `errors`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middlewares, was nil instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(*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 !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
// Test Startup function -- TODO
// if len(c.Startup) == 0 {
// t.Fatal("Expected 1 startup function, had 0")
// }
// 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) {
testAbs, err := filepath.Abs("./404.html")
if err != nil {
t.Error(err)
}
tests := []struct {
inputErrorsRules string
shouldErr bool
expectedErrorHandler ErrorHandler
}{
{`errors`, false, ErrorHandler{
ErrorPages: map[int]string{},
}},
{`errors errors.txt`, false, ErrorHandler{
ErrorPages: map[int]string{},
LogFile: "errors.txt",
}},
{`errors visible`, false, ErrorHandler{
ErrorPages: map[int]string{},
Debug: true,
}},
{`errors { log visible }`, false, ErrorHandler{
ErrorPages: map[int]string{},
Debug: true,
}},
{`errors { log errors.txt
404 404.html
500 500.html
}`, false, 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, ErrorHandler{
LogFile: "errors.txt",
LogRoller: &httpserver.LogRoller{
MaxSize: 2,
MaxAge: 10,
MaxBackups: 3,
LocalTime: true,
},
ErrorPages: map[int]string{},
}},
{`errors { log errors.txt {
size 3
age 11
keep 5
}
404 404.html
503 503.html
}`, false, ErrorHandler{
LogFile: "errors.txt",
ErrorPages: map[int]string{
404: "404.html",
503: "503.html",
},
LogRoller: &httpserver.LogRoller{
MaxSize: 3,
MaxAge: 11,
MaxBackups: 5,
LocalTime: true,
},
}},
{`errors { log errors.txt
* generic_error.html
404 404.html
503 503.html
}`, false, ErrorHandler{
LogFile: "errors.txt",
GenericErrorPage: "generic_error.html",
ErrorPages: map[int]string{
404: "404.html",
503: "503.html",
},
}},
// test absolute file path
{`errors {
404 ` + testAbs + `
}`,
false, ErrorHandler{
ErrorPages: map[int]string{
404: testAbs,
},
}},
// Next two test cases is the detection of duplicate status codes
{`errors {
503 503.html
503 503.html
}`, true, ErrorHandler{ErrorPages: map[int]string{}}},
{`errors {
* generic_error.html
* generic_error.html
}`, true, ErrorHandler{ErrorPages: map[int]string{}}},
}
for i, test := range tests {
actualErrorsRule, err := errorsParse(caddy.NewTestController("http", test.inputErrorsRules))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
} else if err != nil && test.shouldErr {
continue
}
if !reflect.DeepEqual(actualErrorsRule, &test.expectedErrorHandler) {
t.Errorf("Test %d expect %v, but got %v", i,
actualErrorsRule, test.expectedErrorHandler)
}
}
}
+45
View File
@@ -0,0 +1,45 @@
package expvar
import (
"expvar"
"fmt"
"net/http"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// ExpVar is a simple struct to hold expvar's configuration
type ExpVar struct {
Next httpserver.Handler
Resource Resource
}
// ServeHTTP handles requests to expvar's configured entry point with
// expvar, or passes all other requests up the chain.
func (e ExpVar) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
if httpserver.Path(r.URL.Path).Matches(string(e.Resource)) {
expvarHandler(w, r)
return 0, nil
}
return e.Next.ServeHTTP(w, r)
}
// expvarHandler returns a JSON object will all the published variables.
//
// This is lifted straight from the expvar package.
func expvarHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
fmt.Fprintf(w, "{\n")
first := true
expvar.Do(func(kv expvar.KeyValue) {
if !first {
fmt.Fprintf(w, ",\n")
}
first = false
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
})
fmt.Fprintf(w, "\n}\n")
}
// Resource contains the path to the expvar entry point
type Resource string
+46
View File
@@ -0,0 +1,46 @@
package expvar
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestExpVar(t *testing.T) {
rw := ExpVar{
Next: httpserver.HandlerFunc(contentHandler),
Resource: "/d/v",
}
tests := []struct {
from string
result int
}{
{"/d/v", 0},
{"/x/y", http.StatusOK},
}
for i, test := range tests {
req, err := http.NewRequest("GET", test.from, nil)
if err != nil {
t.Fatalf("Test %d: Could not create HTTP request %v", i, err)
}
rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req)
if err != nil {
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
}
if result != test.result {
t.Errorf("Test %d: Expected Header '%d' but was '%d'",
i, test.result, result)
}
}
}
func contentHandler(w http.ResponseWriter, r *http.Request) (int, error) {
fmt.Fprintf(w, r.URL.String())
return http.StatusOK, nil
}

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