Compare commits

..

2121 Commits

Author SHA1 Message Date
Matthew Holt 15fecbc161 1.0 release 2019-04-24 11:24:40 -06:00
Taufiq Rahman c32a0f5f71 fix lint warnings (issue 2541) (#2551)
* Lint: fix some errcheck #2541

* Lint: fix passing structcheck #2541

* Lint: update fix structcheck #2541

* Lint: fix errcheck for basicauth, browse, fastcgi_test #2541

* Lint: fix errcheck for browse, fastcgi_test, fcgiclient, fcgiclient_test #2541

* Lint: fix errcheck for responsefilter_test, fcgilient_test #2541

* Lint: fix errcheck for header_test #2541

* Lint: update errcheck for fcgiclient_test #2541

* Lint: fix errcheck for server, header_test, fastcgi_test, https_test, recorder_test #2541

* Lint: fix errcheck for tplcontext, vhosttrie_test, internal_test, handler_test #2541

* Lint: fix errcheck for log_test, markdown mholt#2541

* Lint: fix errcheck for policy, body_test, proxy_test #2541

* Lint: fix errcheck for on multiple packages #2541

- reverseproxy
- reverseproxy_test
- upstream
- upstream_test
- body_test

* Lint: fix errcheck in multiple packages mholt#2541
- handler_test
- redirect_test
- requestid_test
- rewrite_test
- fileserver_test

* Lint: fix errcheck in multiple packages mholt#2541

- websocket
- setup
- collection
- redirect_test
- templates_test

* Lint: fix errcheck in logger test #2541

run goimports against #2551
- lexer_test
- log_test
- markdown

* Update caddyhttp/httpserver/logger_test.go

Co-Authored-By: Inconnu08 <taufiqrx8@gmail.com>

* Update log_test.go

* Lint: fix scope in logger_test #2541

* remove redundant err check in logger_test #2541

* fix alias in logger_test #2541

* fix import for format #2541

* refactor variable names and error check #2541
2019-04-22 10:20:37 -06:00
Matthew Holt 0c3d90ed21 I just learned about go mod tidy 2019-04-20 17:34:10 -06:00
Matthew Holt fb31669261 Release beta2 2019-04-20 13:13:42 -06:00
Matt Holt 917d9bc9da tls: Update to match CertMagic refactor (#2571)
* Update to match CertMagic's refactoring

* mod: CertMagic v0.5.0
2019-04-20 12:11:27 -06:00
Matthew Holt fd6e4516dc Remove now-unnecessary build.go
Caddy can be built, even with plugins, without modifying the source
code and without special build scripts, thanks to Go modules. See
the README or wiki.
2019-04-20 11:40:22 -06:00
Abiola Ibrahim 86205efcfe Merge pull request #2570 from whalehub/patch-1
Fix instructions for building Caddy from source
2019-04-20 13:26:12 +01:00
Aaron 701e77514f Fix instructions for building Caddy from source 2019-04-20 13:42:23 +02:00
Matthew Holt 018105eec9 readme: Update build-from-source instructions for Go modules 2019-04-20 01:19:44 -06:00
Matthew Holt bf6ec2bbfd main: Use embedded version, debug.BuildInfo 2019-04-20 00:52:53 -06:00
Matthew Holt 13d0454f71 Remove now-unused gitcookie 2019-04-20 00:52:23 -06:00
Marten Seemann db2741c6e0 mod: import the right version of quic-go and tidy (#2562)
* run go mod tidy

* import the right version of quic-go
2019-04-17 12:13:44 -06:00
elcore 605787f671 readme: Update build instructions (#2565)
Fix #2560
2019-04-12 14:31:38 -06:00
Matthew Holt 657780bcdf Prepare 1.0beta1 release 2019-04-09 11:29:06 -06:00
Toby Allen 9d767e768a readme: Add link to Good First Issue (#2481)
* Add link to Good First Issue

 in Contributing Section

* Fix heading
2019-04-09 11:11:26 -06:00
johncming 15268e8cdb caddytls: sort import statement (#2552) 2019-04-07 10:39:52 -06:00
Matthew Holt 04789a2446 ci: Enable use of Go modules
Temporary until Go 1.13
2019-04-06 16:31:06 -06:00
johncming e28ee90c2a httpserver: remove unused listener field (#2527) 2019-03-29 22:29:17 -06:00
Michael Grosser 3841517ce1 Migrate to Go modules - remove vendor folder (#2504)
* Migrate to gomods

* Fix conflict

* gomod: Switch xenolf/lego to go-acme/lego
2019-03-29 20:30:48 -06:00
Matt Holt bea48b80ce readme: Replace CI badges 2019-03-29 19:16:42 -06:00
Matthew Holt 9f525af210 ci: Remove Travis and AppVeyor manifests 2019-03-29 19:04:56 -06:00
Matt Holt c00b3a520c ci: Azure Pipelines CI system (#2540)
* Set up CI with Azure Pipelines [skip ci]

* Add other platforms [skip ci]

* Oops, add vmImage back in [skip ci]

* Fix goVersion [skip ci]

* Use sudo to install Go [skip ci]

* Attempt platform-specific Go isntalls [skip ci]

* Try sharing the variable a different way [skip ci]

* Trying this again... [skip ci]

* Fix the PATH [skip ci]

* Fix GOROOT, hopefully [skip ci]

* More fixing [skip ci]

* Still fixing [skip ci]

* Add golang-ci [skip ci]

* asdfasdfasf [skip ci]

* Sigh, work around broken golangci-lint installer [skip ci]

* Try again [skip ci]

* ahhhhhh [skip ci]

* sooooo close! cleanup [skip ci]

* oh c'mon [skip ci]

* thought I had it for a sec [skip ci]

* yaaaayyyy [skip ci]

ship it
2019-03-29 18:57:02 -06:00
johncming f6e6a6be04 caddy: improve error naming (#2526) 2019-03-16 23:33:11 -06:00
johncming bc5df3b383 onevent: add missing return error (#2525) 2019-03-15 22:43:28 -06:00
Linkonoid 1a0292b830 httpserver: Register dyndns directive (#2521) 2019-03-15 17:43:01 -06:00
M. Ángel Jimeno e6a3e5e1f3 cmd: rename -env to -envfile and use -env to print the environment (#2517)
* caddy: Rename env flag to envfile

* caddy: Add env flag to print environment variables
2019-03-11 17:50:04 -06:00
Aka.Why 397d67876c caddy: Start all servers only after all listeners successful (#2508) 2019-03-10 21:01:56 -06:00
comp500 47b78714b8 proxy: Change headers using regex (#2144)
* Add upstream header replacements (TODO: tests, docs)

* Add tests, fix a few bugs

* Add more tests and comments

* Refactor header_upstream to use a fallthrough; return regex errors
2019-03-06 14:35:07 -07:00
Toby Allen fda7350a43 github: Change name to ensure PR template is seen (#2501)
as per https://help.github.com/en/articles/creating-a-pull-request-template-for-your-repository
2019-03-04 19:14:18 -07:00
Matthew Holt 80dfb8b2a7 vendor: Update lego; notes for v0.11.5 2019-03-04 12:14:25 -07:00
Toby Allen 98f160e39c httpserver: More organized startup output (#2497)
* Move SiteOutput to a seperate function sorted by port.

* Rename vars and tidy up

* Move loopback note to output loop

* Fix Typo

* Remove unneeded var

* Readability Change

* Change to other port string.

* Simplify as all sites in Server use the same port

* Ensure -quiet supresses fmt.Println calls

* Prevent double output of siteinfo to log - improve log message

* change name of log in comment

* Remove spaces

* Remove extra line output

* final tidy!

* Use caddy.LogDestination to setup log

* Ensure Log is still output if quiet.

* Correct case of functions and make function param bool

* Remove conditional check for LogDestination

* Revert output to simple blocks

* comment update
2019-03-04 12:06:04 -07:00
Toby Allen 4f8020a94c basicauth: Simplify user replacement on auth failure (#2470)
* Can simply add user to replacer before Replace call.  user is then also added to logs

* use fmt.Errorf

* fix how fmt.Errorf is called.
2019-03-04 12:02:24 -07:00
Toby Allen b295aab2d8 errors: Return parse error if more than one argument (#2472)
* Check errors directive only has 1 argument

* Added Tests
2019-03-04 12:00:28 -07:00
Matthias Schneider 448edcca8e caddytls: removed useless code in selfsigned (#2494)
removed "names": is not used
2019-03-01 08:35:05 -07:00
Wèi Cōngruì 72d0debde6 caddytls: add TLS 1.3 support and remove CBC ciphers (#2399) 2019-02-25 18:39:30 -07:00
Matt Holt 9037d3ab85 appveyor: Use Go 1.12 2019-02-25 18:31:08 -07:00
Matthew Holt 8a511989a0 vendor: Update CertMagic; fix bug related to accounts with empty emails 2019-02-24 23:28:34 -07:00
Toby Allen 44e3a97a67 Revert "Modify Startup Output (#2469)" (#2482)
This reverts commit c0190a3460.
2019-02-24 14:44:17 -07:00
Toby Allen c0190a3460 Modify Startup Output (#2469)
* Move SiteOutput to a seperate function.

* Simplify as all sites in Server use the same port

* Ensure -quiet supresses fmt.Println calls

* Prevent double output of siteinfo to log - improve log message

* Use caddy.LogDestination to setup log

* Ensure Log is still output if quiet.
2019-02-24 10:04:24 +00:00
magikstm 396d8e989f internal: add internal paths to HiddenFiles (#2133)
* Append Internal paths to Caddy config HiddenFiles

* gofmt

* Reuse a variable

* Update caddyhttp/internalsrv/setup.go

Co-Authored-By: magikstm <myskina@gmail.com>
2019-02-17 14:32:22 -07:00
Matthew Holt 33b00dc8b1 staticfiles: HEAD, not OPTIONS 2019-02-16 08:22:04 -07:00
Matthew Holt eb9857137a staticfiles: Re-allow HEAD requests 2019-02-16 07:42:44 -07:00
Matthew Holt c1d6c928e3 Merge branch 'master' of ssh://github.com/mholt/caddy 2019-02-15 12:00:02 -07:00
Matthew Holt 118f666706 Update release notes for 0.11.4 2019-02-15 11:58:56 -07:00
Matthew Fay e9641c5c7e proxy: Re-add pre-existing trailing slashes in AllowedPath (#2151)
* proxy.AllowedPath: Re-append trailing slashes to excepted path comparisons

* Substitute HasSuffix for manual check; check slash was actually removed
2019-02-15 11:53:14 -07:00
Toby Allen 495656f72b basicauth: Log error when authentication fails (#2434)
* log basicauth errors

* Fixed tests

* Fix log include

* Remove some unneeded blank lines.
2019-02-15 11:50:30 -07:00
Matthew Holt c70d4a4cf6 Update pull request and issue templates 2019-02-15 10:35:51 -07:00
Matthew Holt 39c5d6b964 caddytls: Remove repeated variable declaration 2019-02-14 17:44:58 -07:00
Matthew Holt 0c69e9ed7f Merge branch 'tlscluster' 2019-02-14 17:21:25 -07:00
Matthew Holt 0a95b5d359 caddytls: Move config of certmagic storage to NewConfig (fixes #2465)
Breaking API change for server type plugins that use caddytls package.
Now an error value is returned from NewConfig as well. Sorry about that.
2019-02-14 17:20:06 -07:00
linquize 6246d4c3ca Expose JSON <-> Caddyfile conversion via command line flags (#2374)
* Expose JSON <-> Caddyfile conversion via command line flags stdin / stdout

*  cli caddyfile-to-json and json-to-caddyfile
2019-02-13 21:30:14 +00:00
Yue Ko 4de9d64c0c core: Terminate goroutine used for logging errors (#2398)
Terminate the goroutine used for logging errors by using a
WaitGroup (stopWg) to track termination of servers.
Fixes #2358.
2019-02-13 11:51:57 -07:00
Matt Holt 1867ded14c caddytls: Change clustering to be a plugin to the caddytls package (#2459)
* caddytls: Change clustering to be a plugin to the caddytls package

Should resolve the failure in
https://github.com/coredns/coredns/pull/2541.

This change is breaking to clustering plugin developers (not Caddy
users), but logical, since only the caddytls package uses CertMagic
directly (the httpserver package also uses it, but only because it also
uses the caddytls plugin); and it is early enough that no clustering
plugins really exist yet.

This will also require a change of devportal
so that it looks for a different registration function, which has moved
to the caddytls package.

* Remove unused variable

* caddyhttp: Fix test (adjust plugin counting)

* ummmm, remove extra line break

somehow VS Code didn't fmt on save... weird.
2019-02-08 13:06:21 -07:00
Matthew Holt 22db8bcf3d ummmm, remove extra line break
somehow VS Code didn't fmt on save... weird.
2019-02-08 12:56:51 -07:00
Matthew Holt 59e7a8864a caddyhttp: Fix test (adjust plugin counting) 2019-02-08 12:43:20 -07:00
Matthew Holt 7d737427a9 Remove unused variable 2019-02-08 12:28:27 -07:00
Matthew Holt eac939e9a7 caddytls: Change clustering to be a plugin to the caddytls package
Should resolve the failure in
https://github.com/coredns/coredns/pull/2541.

This change is breaking to clustering plugin developers (not Caddy
users), but logical, since only the caddytls package uses CertMagic
directly (the httpserver package also uses it, but only because it also
uses the caddytls plugin); and it is early enough that no clustering
plugins really exist yet.

This will also require a change of devportal
so that it looks for a different registration function, which has moved
to the caddytls package.
2019-02-08 12:25:01 -07:00
Matthew Holt 2ea544e9a0 Notes for v0.11.3 2019-02-05 13:14:50 -07:00
Matthew Holt 87b645386f vendor: Update lego 2019-02-05 11:27:09 -07:00
Michael Li e3ba9ffff2 telemetry: Improve parsing of disabled-metrics flag (#2389)
* optimized parse cli's disabledMetrics flag string to initTelemetry

* add splitTrim to obtain string slice that not contain empty string

* change TestSplitTrim error output

* gofmt for run_test.go

* restore name of disabledMetrics made more sense

* optimized TestSplitTrim case

* just update splitTrim comment to force CI restart
2019-02-05 10:33:52 -07:00
Danny Navarro e0efb027da proxy: Implement own CA certificates of backends (#2454)
By using option ca_certificates in proxy block it is possible now to select
CA against which backend certificates shall be checked.

Resolves #1550

Co-authored-by: Danny Navarro <navdgo@gmail.com>
2019-02-05 10:16:08 -07:00
Matt Holt 9e4a29191c caddytls: Fix handling of IP-only TLS configs and empty-SNI handshakes (#2452)
* caddytls: Fix empty SNI handling (new -default-sni flag)

vendor: update certmagic, needed to support this

Hopefully fixes #2451, fixes #2438, and fixes #2414

* caddytls: Don't overwrite certmagic Manager (fixes #2407)

Supersedes #2447

* vendor: Update certmagic to fix nil pointer deref and TLS-ALPN cleanup

* Improve -default-sni flag help text
2019-02-05 09:30:22 -07:00
Toby Allen fa10b0275f on: Roll back additional instance_startup event from #2161 (#2453) 2019-02-04 13:59:13 -07:00
Matthew Holt 4f8ff09551 staticfiles: Require method GET
Other methods are not currently implemented in the static file server
2019-02-02 18:36:20 -07:00
Matthew Holt f2491580e0 httpserver: Fix address display and routing for IPv6 IPs/wildcards 2019-02-02 14:32:37 -07:00
Josh Soref 8369a12115 Fix spelling (#2448)
* spelling: access

* spelling: associated

* spelling: because

* spelling: characteristics

* spelling: chooses

* spelling: cleared

* spelling: clustering

* spelling: collaborators

* spelling: connection

* spelling: content

* spelling: conversion

* spelling: deferring

* spelling: detection

* spelling: displayed

* spelling: dispenser

* spelling: heuristic

* spelling: nonexistent

* spelling: oflisting

* spelling: preparses

* spelling: response

* spelling: responder

* spelling: sensitive

* spelling: specify

* spelling: simple

* spelling: spawning

* spelling: status

* spelling: unsupported

* spelling: upstream

* spelling: username

* spelling: whether
2019-01-29 10:51:10 -07:00
Matthew Holt 97e1f14dd3 httpserver: Revert misleading comment 2019-01-28 10:31:31 -07:00
Matthew Holt 930ca1cc1b main,log,errors: Option to disable log rotation ("rolling")
For log and errors directive, as well as process log.
2019-01-28 10:28:22 -07:00
Matthew Holt 23627bbf54 caddy: Improve error messages when (re)starting servers 2019-01-28 10:28:22 -07:00
Matthew Holt 2fc615b405 appveyor: Don't run checks twice with PR 2019-01-28 10:28:22 -07:00
Sebastian Hutter a36c7c7e87 Disable basic authentication for OPTIONS method (#2415)
Execute an OPTIONS call and make sure we receive a valid response
independently of the provided username or password as the
authentication step is ignored

* Do not authenticate OPTIONS calls
* Add test for OPTIONS call
2019-01-28 10:26:22 +00:00
Abiola Ibrahim fdec3c68f0 Merge pull request #2443 from maxheyer/master
Add header X-Forwarded-Port to preset transparent
2019-01-26 08:30:12 +01:00
Abiola Ibrahim 0ecc5c46bf Merge branch 'master' into master 2019-01-26 08:20:17 +01:00
Bryan Burke a947f70c56 httpserver: add extauth plugin directive (#2444) 2019-01-26 00:12:43 -07:00
Max Heyer c259381541 Add header X-Forwarded-Port with placeholder {server_port} to preset proxy preset transparent 2019-01-26 00:42:51 +01:00
Max Heyer 7f546e529e httpserver: Implement {sever_port} placeholder (#2424) 2019-01-25 20:54:33 +00:00
Matthew Holt a7aeb979be caddytls: Use IP address to find config; vendor: update certmagic
Closes #2356
2019-01-21 18:58:15 -07:00
elcore 771dcf3d40 caddy: move EmitEvent(InstanceStartupEvent, instance) (#2161)
* caddy: move EmitEvent(InstanceStartupEvent, instance)

* caddy: update SupportedEvents
2019-01-18 10:46:21 -07:00
Matthew Holt f3a4f46d78 vendor: Update certmagic; fix #2400 2019-01-18 10:39:00 -07:00
Wèi Cōngruì 78455c7cb9 caddytls: set certmagic.Config.Email when parsing config file (#2432) 2019-01-18 07:25:41 -07:00
Matthew Holt 01f2b85826 vendor: Update certmagic and lego 2019-01-17 11:12:11 -07:00
Oleg Kovalov 7fe9e13fbf caddyhttp: use strings.EqualFold (#2413) 2019-01-16 22:51:55 -07:00
Jeffrey Zhao f92a3aa0e5 gzip: avoid unnecessary allocations when not compressing (#2396) 2019-01-16 22:43:31 -07:00
Marten Seemann 917534e35e vendor: update quic-go to v0.10.1 (#2431) 2019-01-16 21:38:10 -07:00
Matthew Holt 8ab447e615 Cut release 0.11.2 2019-01-16 16:04:42 -07:00
Adam Woodbeck 0d8384a9b4 caddyfile: Support 'import' inside directives (#2428) 2019-01-14 22:08:54 -07:00
Marten Seemann e14328b71b tls: Set a GetCertificate callback in the tls.Config (#2404)
A tls.Config must have Certificates or GetCertificate set, in order to
be accepted by tls.Listen and quic.Listen.
2019-01-13 21:39:17 -07:00
Henrique Dias f5aaa471de httpserver: remove jekyll, hugo. Replace by filebrowser (#2417) 2019-01-08 06:30:18 -07:00
Matthew Holt 0b83014ff8 caddytls: Use latest certmagic package, with updated Storage interface 2018-12-19 21:53:52 -07:00
Kurtis Rader 0684cf8611 Implement {when_iso_local} placeholder (#2363)
Implement `{when_iso_local}` placeholder

This implements the `{when_iso_local}` placeholder. This is like the
`{when_iso}` placeholder but the output is in the current timezone
rather than UTC.

Resolves #2362
2018-12-18 22:42:05 +00:00
Matthew Holt 1570bc5d03 caddytls: Fix race condition in tests 2018-12-13 07:34:00 -07:00
Matthew Holt 8811853f6d caddytls: Better handle FileStorage and cleaning up locks on exit 2018-12-13 07:06:47 -07:00
Matthew Holt b7028b139f vendor: Update certmagic to include List fix 2018-12-12 14:50:25 -07:00
Matthew Holt 620f9687c8 Merge branch 'reload-ln-middleware' 2018-12-11 21:32:23 -07:00
Matthew Holt 2c43616781 readme: Add certmagic link 2018-12-11 19:46:06 -07:00
Matthew Holt d1171af679 httpserver: Don't obtain certs for unmanaged configs (fixes #2387) 2018-12-11 19:37:08 -07:00
Matthew Holt 598de9e6d9 vendor: Update certmagic 2018-12-11 19:36:46 -07:00
Matthew Holt 393bc2992e Add clustering plugin types; use latest certmagic.Storage interface 2018-12-11 12:13:48 -07:00
Matthew Holt 33f2b16a1b Merge branch 'certmagic' 2018-12-10 20:08:55 -07:00
Matthew Holt f03ad80701 Update tests after large refactor 2018-12-10 20:08:29 -07:00
Matthew Holt a68b01080c vendor: Update dependencies; add certmagic, update lego 2018-12-10 20:00:34 -07:00
Matthew Holt e0f1a02c37 Extract most of caddytls core code into external CertMagic package
All code relating to a caddytls.Config and setting it up from the
Caddyfile is still intact; only the certificate management-related
code was removed into a separate package.

I don't expect this to build in CI successfully; updating dependencies
and vendor is coming next.

I've also removed the ad-hoc, half-baked storage plugins that we need
to finish making first-class Caddy plugins (they were never documented
anyway). The new certmagic package has a much better storage interface,
and we can finally move toward making a new storage plugin type, but
it shouldn't be configurable in the Caddyfile, I think, since it doesn't
make sense for a Caddy instance to use more than one storage config...

We also have the option of eliminating DNS provider plugins and just
shipping all of lego's DNS providers by using a lego package (the
caddytls/setup.go file has a comment describing how) -- but it doubles
Caddy's binary size by 100% from about 19 MB to around 40 MB...!
2018-12-10 19:49:29 -07:00
Abiola Ibrahim 2358102c07 Merge pull request #2384 from mholt/francislavoie-patch-1
Fix `s3browser` plugin name
2018-12-08 09:10:02 +00:00
Francis Lavoie 1533652b78 Fix s3browser plugin name
Thanks to @webprofusion-chrisc for spotting this: https://github.com/mholt/caddy/pull/2383#issuecomment-445432256
2018-12-08 03:55:34 -05:00
techknowlogick c7562e46a4 httpserver: Add s3browser directive (#2383)
Fixes https://github.com/techknowlogick/caddy-s3browser/issues/2
2018-12-07 19:17:36 -07:00
Matthew Holt 8f583dcf36 vendor: Update github.com/xenolf/lego/acme to latest 2018-12-05 18:01:22 -07:00
Matt Holt 09188981c4 tls: Add support for the tls-alpn-01 challenge (#2201)
* tls: Add support for the tls-alpn-01 challenge

Also updates lego/acme to latest on master.

TODO: This implementation of the tls-alpn challenge is not yet solvable
in a distributed Caddy cluster like the http challenge is.

* build: Allow building with the race detector

* tls: Support distributed solving of the TLS-ALPN-01 challenge

* Update vendor and add a todo in MITM checker
2018-12-05 17:33:23 -07:00
linquize ae5f013a48 Fix linter warning (#2375) 2018-12-02 15:39:58 -07:00
Matthew Holt b7091650f8 Revert "bind: support multiple values (#2128)"
This reverts commit 3a810c6502.
2018-11-27 15:57:38 -07:00
zhsj 3a810c6502 bind: support multiple values (#2128)
Signed-off-by: Shengjing Zhu <i@zhsj.me>
2018-11-26 18:27:58 -07:00
Simon Legner 764c9ec956 browse: filter on document load (#2368)
When using the Browse directive, this applies the filter when using the browser history to go the
previous directory.
2018-11-25 19:35:15 +00:00
Wèi Cōngruì ce0988f48a templates: delete ETag and Last-Modified headers (#2338)
Fixes #1920
2018-11-18 14:06:54 -07:00
Christoph Blecker 1c92557c8b Fix line endings (#2351) 2018-11-18 14:04:20 -07:00
linquize 8f7a1d6a25 Prevent errors test fail if $GOPATH is something like /home/user/caddy (#2347) 2018-11-17 21:51:08 +00:00
Matthew Holt 1b085efa47 Add Relica as sponsor on readme 2018-11-17 10:22:45 -07:00
linquize d9e6e7ffa5 *.txt, *.tpl, *.htm, *.html, *.md should always use eol=lf (#2345) 2018-11-17 09:24:54 -07:00
Makeev Ivan 05d0b213a9 proxy: HTTP status 499 for 'Context canceled' errors (#2297)
* Adding {when_unix_ms} requests placeholder (unix timestamp with a milliseconds precision)

* Add an 499 HTTP status code on user's cancel request as NGINX doing (instead of 502 Bad Gateway status with 'Context canceled' message)

* 499 HTTP status code was added as constant CustomStatusContextCancelled = 499
2018-11-16 13:52:34 -07:00
Matthew Holt 6f580c6aa3 Add flags for JS/WASM builds 2018-11-12 14:46:14 -07:00
Matthew Holt 1d9a094315 Update changes/readme for v0.11.1. 2018-11-12 14:39:01 -07:00
Matt Holt f6e50890b3 caddytls: Raise TLS alert if no certificate matches SAN (closes #1303) (#2339)
* caddytls: Raise TLS alert if no certificate matches SAN (closes #1303)

I don't love this half-baked solution to the issue raised in #1303 way
more than a year after the original issue was closed (the necro comments
are about an issue separate from the original issue that started it),
but I do like TLS alerts more than wrong certificates.

* Restore test to match

* Restore another previous test
2018-11-12 14:24:07 -07:00
Jake Lucas 22dfb140d0 proxy: Add new fallback_delay sub-directive (#2309)
* Updates the existing proxy and reverse proxy tests to include a new fallback delay value

* Adds a new fallback_delay sub-directive to the proxy directive and uses it in the creation of single host reverse proxies
2018-10-30 12:02:59 -06:00
16yuki0702 15455e5a7e caddytls: Improve flaky test related to email (#2318)
Signed-off-by: Hiroyuki Sasagawa <hs19870702@gmail.com>
2018-10-30 11:59:23 -06:00
Billie Cleek f46da403d8 caddyfile: fix env var expansion after Go template (#2304) 2018-10-30 11:58:37 -06:00
Matthew Holt 4f5df39bdd caddy: Re-invoke listener middlewares on reloads and upgrades 2018-10-30 11:55:28 -06:00
Darshan Chaudhary 1f8d1df4ec caddyfile: Allow partial directive env var expansion (#2253) 2018-10-30 11:41:41 -06:00
Matthew Holt dd83687447 readme: Add AWS marketplace link 2018-10-29 21:05:19 -06:00
Ruslan Drozhdzh 3ce3f3a96a caddy: Run OnShutdown callbacks before instance Stop() calls (#2320)
- see https://github.com/coredns/coredns/issues/1666#issuecomment-380624422
2018-10-29 18:25:36 -06:00
Eugen Kleiner 86060ef9b4 caddy: Add OnRestartFailed callback (#2262)
* Add callback OnRestartFailed to caddy.Controller

* markdown: Fix 500 error (#2266)

* Addressed the comments

* Update paths for filebrowser plugins

* httpserver: update minify ordering (#2273)

* Bump required version of golang to 1.10 in README.md (#2267)

Adding TLS client cert placeholders #2217 uses features of go
v1.10.  Update README requirements accordingly.

* Update CI to use Go 1.11

* caddytls: gofmt (Go 1.11) (#2241)

* Ensure assets path exists before writing UUID file

* Adding {when_unix_ms} requests placeholder (unix timestamp with a milliseconds precision) (#2260)

* update to quic-go v0.10.0 (#2288)

quic-go now vendors all of its dependencies, so we don't need to vendor
them here.

Created by running:
gvt delete github.com/lucas-clemente/quic-go
gvt delete github.com/bifurcation/mint
gvt delete github.com/lucas-clemente/aes12
gvt delete github.com/lucas-clemente/fnv128a
gvt delete github.com/lucas-clemente/quic-go-certificates
gvt delete github.com/aead/chacha20
gvt delete github.com/hashicorp/golang-lru
gvt fetch -tag v0.10.0-no-integrationtests github.com/lucas-clemente/quic-go

* fastcgi: Add default timeouts (#2265)

Default fastcgi timeout is 60 seconds
Add tests

* Fix AppVeyor builds (#2289)

* Attempting to fix AppVeyor builds

* Trying again, 2015 image this time

* Use Appveyor's Go 1.11 stack

* Restore GOPATH\bin to PATH and delete old image config

* Add gcc to path manually

* Addressed the comments

* Fix broken link to sourcegraph in README (#2285)

* Fix deadlock, ensure instances mutex unlocked (#2296)

it's a stupid mistake

* proxy: Use DualStack=true in defaultDialer (#2305)

* ci: get golint tool from `golang.org/x/lint/golint` (#2324)

* templates: TLSVersion (#2323)

* new template action: TLS protocol version

* new template action: use caddytls.GetSupportedProtocolName

Avoids code duplication by reusing existing method to get TLS protocol
version used on connection. Also adds tests

* Don't return error on onRestartFail. Only log it.
2018-10-29 18:00:44 -06:00
Kris Kwiatkowski d3e3fc533f templates: TLSVersion (#2323)
* new template action: TLS protocol version

* new template action: use caddytls.GetSupportedProtocolName

Avoids code duplication by reusing existing method to get TLS protocol
version used on connection. Also adds tests
2018-10-19 11:51:10 -06:00
Zoe 03b10f9c8e ci: get golint tool from golang.org/x/lint/golint (#2324) 2018-10-16 11:56:41 -06:00
Jake Lucas f7757da7ed proxy: Use DualStack=true in defaultDialer (#2305) 2018-09-30 08:17:04 -06:00
yyqbuct 13f9c34d16 Fix deadlock, ensure instances mutex unlocked (#2296)
it's a stupid mistake
2018-09-18 09:00:10 -06:00
Zachary J Miller 13a54dbdda Fix broken link to sourcegraph in README (#2285) 2018-09-07 15:24:07 -06:00
Matt Holt 7ed7a95524 Fix AppVeyor builds (#2289)
* Attempting to fix AppVeyor builds

* Trying again, 2015 image this time

* Use Appveyor's Go 1.11 stack

* Restore GOPATH\bin to PATH and delete old image config

* Add gcc to path manually
2018-09-07 15:11:25 -06:00
Alexander Danilov d47b041923 fastcgi: Add default timeouts (#2265)
Default fastcgi timeout is 60 seconds
Add tests
2018-09-02 15:28:10 -06:00
Marten Seemann dfbc2e81e3 update to quic-go v0.10.0 (#2288)
quic-go now vendors all of its dependencies, so we don't need to vendor
them here.

Created by running:
gvt delete github.com/lucas-clemente/quic-go
gvt delete github.com/bifurcation/mint
gvt delete github.com/lucas-clemente/aes12
gvt delete github.com/lucas-clemente/fnv128a
gvt delete github.com/lucas-clemente/quic-go-certificates
gvt delete github.com/aead/chacha20
gvt delete github.com/hashicorp/golang-lru
gvt fetch -tag v0.10.0-no-integrationtests github.com/lucas-clemente/quic-go
2018-09-02 15:18:54 -06:00
Makeev Ivan 9edc16e4d6 Adding {when_unix_ms} requests placeholder (unix timestamp with a milliseconds precision) (#2260) 2018-08-28 11:08:55 +01:00
Matthew Holt 73273c5bf8 Ensure assets path exists before writing UUID file 2018-08-26 09:13:59 -06:00
elcore 93c5256318 caddytls: gofmt (Go 1.11) (#2241) 2018-08-24 16:43:56 -06:00
Matthew Holt 3ccad1814e Update CI to use Go 1.11 2018-08-24 16:41:15 -06:00
Brad Beveridge 35269572d7 Bump required version of golang to 1.10 in README.md (#2267)
Adding TLS client cert placeholders #2217 uses features of go
v1.10.  Update README requirements accordingly.
2018-08-24 11:09:49 -06:00
Henrique Dias a457b35750 httpserver: update minify ordering (#2273) 2018-08-22 09:19:37 -06:00
Francis Lavoie 5e5f9b0563 caddyhttp: Update paths for filebrowser plugins
Update paths for filebrowser plugins
2018-08-21 22:06:27 -04:00
cmulk 16722e4d99 Update paths for filebrowser plugins 2018-08-21 10:30:42 -05:00
Alexander Danilov 89c20f9a55 markdown: Fix 500 error (#2266) 2018-08-15 23:35:06 -06:00
Alexander Danilov d3b731e925 proxy: Fix 502 errors for requests without headers (#2188)
* Fix 502 errors for requests without headers

* Add unexported roundRobinPolicier

We have to preserve state for fallback mode of Header policy, so
it's required to save state in some variable
2018-08-07 17:01:24 -06:00
Jiri Tyr 3e0695ee31 ci: Go version bump for Windows builds (#2236) 2018-07-30 08:36:20 -06:00
Jiri Tyr 9239f3cbcc Adding TLS client cert placeholders (#2217)
* Adding TLS client cert placeholders

* Use function to get the peer certificate

* Changing SHA1 to SHA256

* Use UTC instead of GMT

* Adding tests

* Adding getters for Protocol and Cipher
2018-07-28 09:26:24 +01:00
Felix Lange b7a7fd4651 caddy/caddymain: make EnableTelemetry a variable (#2191)
This makes setting the flag easier in projects that embed package caddymain.
2018-07-26 14:16:30 -06:00
Henrique Dias 06b067b02c caddyhttp: correct import path for filemanager plugins (#2211) 2018-07-26 14:15:18 -06:00
Albert ten Napel dfb5aa6dc6 caddytls: Remove weak cipher suites from the defaults. (#2227) 2018-07-26 14:03:37 -06:00
Richard Hartmann f56696f478 rlimit_posix.go: Use backticks for shell code (#2235) 2018-07-26 13:55:42 -06:00
Silver fcbb90a9af gitignore: Don't ignore the caddyfile/ package (#2237) (#2238) 2018-07-26 13:54:10 -06:00
emersion be84b74d01 httpserver: Register 'wkd' (Web Key Directory) directive (#2239) 2018-07-26 13:50:18 -06:00
Sebastian Pipping bb5b01c911 browse: Improve table layout in Firefox (fixes #2179) (#2221)
Current Caddy code used a combination of CSS styles that
some mainstream browsers (e.g. Firefox) do not support well:
"td:first-child { width: 100%; }" together with
"td:last-child { padding-right: 5%; }".

The old approach was three columns with:
- "Name": 100% width, 5% padding left
- "Size": minimal width
- "Modified": minimal width, 5% padding right

Now the new approach is five columns with:
- <Dummy>: 5% width
- "Name": 80% width
- "Size": minimal width
- "Modified": minimal width
- <Dummy>: 5% width
2018-07-19 11:03:44 -06:00
Sebastian Pipping 3ca6bc4a66 browse: Set page background color (fixes #2214) (#2222)
Browser defaults vary and this way we prevent mix-ups
from setting only some colors.
2018-07-12 18:22:30 -06:00
Augusto Roman 053373a385 caddyfile: Fix multi-file snippets and import literals. (#2205)
* Fix a few import problems: snippets and import literals.

Two problems are fixed by this code simplification:
1. Snippets defined in one import file are strangely not available in
   another.
2. If an imported file had a directive with an argument "import", then
   the rest of the tokens on the line would be converted to absolute
   filepaths.

An example of #2 would be the following directive in an imported file:
    basicauth / import secret

In this case, the password would actually be an absolute path to the
file 'secret' (whether or not it exists) in the directory of the imported
Caddyfile.

The problem was the blind token processing to fix import paths in the
imported tokens without considering the context of the 'import' token.

My first inclination was to just add more context (detect 'import' tokens
at the beginning of lines and check the value tokens against defined
snippets), however I eventually realized that we already do all of this
in the parser, so the code was redundant. Instead we just use the current
token's File property when importing. This works fine with imported tokens
since they already have the absolute path to the imported file!

Fixes #2204

* renamed file2 -> fileName

* Fix copy/pasted comment in test.

* Change gzip example to basicauth example.

This makes it more clear how the import side effect is detrimental.
2018-06-28 10:06:52 -06:00
smlx e263566673 init: Fix configuration permissions in systemd integration. (#2130)
This fixes the permissions on /etc/caddy to match standard linux
permissions for /etc, and makes the Caddyfile read-only for the caddy
user.
2018-06-19 09:15:38 -06:00
Denis 6965075825 core: instance restart (reload) event (#2178) 2018-06-12 17:00:53 -06:00
magikstm e54dfa49c3 gzip: Add .m3u and .m3u8 (HLS playlist files) (#2182) 2018-06-07 23:38:55 -06:00
Alexander Danilov accaa378f0 Add -env-file flag (#2176)
This adds new feature to load envs from file provided from command line argument
Implement parsing of the env file for simple KEY=VALUE format
2018-05-28 09:22:21 -06:00
Abiola Ibrahim 60a0208e8d Merge pull request #2172 from SmilingNavern/add_request_scheme
Add REQUEST_SCHEME to fastcgi envs
2018-05-18 03:57:39 +01:00
Alexander Danilov 2aaaa368bb Add REQUEST_SCHEME to fastcgi envs
Fixes https://github.com/mholt/caddy/issues/2152
2018-05-18 01:20:19 +03:00
Abiola Ibrahim 4829cc6aaf Merge pull request #2158 from caldwell/script_filename-fix
fastcgi: strip PATH_INFO from SCRIPT_FILENAME (mirroring SCRIPT_NAME)
2018-05-17 20:25:37 +01:00
Abiola Ibrahim 553acf93e2 Merge branch 'master' into script_filename-fix 2018-05-17 20:06:48 +01:00
Matthew Holt f058419042 Change UUID file with CADDY_UUID_FILE environment variable 2018-05-15 19:39:15 -06:00
Matthew Holt 13268db536 Update readme with regards to telemetry 2018-05-10 11:31:31 -06:00
Matthew Holt 1f7b5abc80 Version 0.11 2018-05-10 09:45:05 -06:00
Matthew Holt c667f81866 telemetry: Use int64 constant for duration interval
Otherwise it overflows int type on 32-bit builds
2018-05-10 09:41:57 -06:00
Matthew Holt b321c00a8f telemetry: Use production endpoint 2018-05-10 09:27:03 -06:00
Matthew Holt 9160789b42 telemetry: Make http_user_agent a normalized field
This way we store a short 8-byte hash of the UA instead of the full
string; exactly the same way we store TLS ClientHello info.
2018-05-10 08:57:25 -06:00
Matthew Holt df7cdc3fae telemetry: Add memory and goroutine metrics, rename container
And fix a typo in a comment, sigh
2018-05-09 22:36:23 -06:00
Matthew Holt 86fd2f22fb telemetry: Add in_container metric
Knowing whether Caddy is running in a container is super-useful for
debugging and troubleshooting, as well as for making development-time
decisions, because Docker is one of the top contributors to our
user support burden.

Thanks to Eldin for helping to test it.
2018-05-09 17:20:38 -06:00
Matt Holt 148a6f4430 Merge pull request #2079 from mholt/telemetry
Caddy telemetry: a global, server-side perspective of the health of the Internet
2018-05-09 04:52:40 -06:00
Matthew Holt b05006663f telemetry: Add variance to retry interval, and disable keepalive 2018-05-08 22:54:12 -06:00
David Caldwell 5f1f8e4ee6 fastcgi: strip PATH_INFO from SCRIPT_FILENAME (mirroring SCRIPT_NAME) 2018-05-08 17:47:12 -07:00
Matthew Holt ef48e17e79 caddytls: Fix tests 2018-05-07 17:04:39 -06:00
Matthew Holt fe03c1aefa telemetry: Fix MITM tests 2018-05-07 16:42:35 -06:00
Matthew Holt 078770a5a6 telemetry: Record TLS ClientHellos by hash of key of structured data
Also improve handling of disabled metrics, and record TLS ClientHello
in association with User-Agent
2018-05-07 16:09:39 -06:00
Guilherme Bernal 294f6957f0 tls: Fix typo in error message, "incompabile" (#2147) 2018-05-01 13:45:23 -06:00
Wèi Cōngruì fe664c00ff proxy: initialize ReverseProxy.Transport earlier and fix TCP connection leak (#2134) 2018-04-28 08:32:20 -06:00
Matthew Holt 518edd3cd4 Corrected permissions for UUID file 2018-04-20 00:04:44 -06:00
Matthew Holt b019501b8b Merge branch 'master' into telemetry
# Conflicts:
#	caddy/caddymain/run.go
#	caddyhttp/httpserver/plugin.go
#	caddytls/client.go
2018-04-20 00:03:57 -06:00
Matthew Holt 2922d09bef Version 0.10.14 2018-04-19 18:11:50 -06:00
Matthew Holt 97487e6f0d vendor: Update lego to fix error handling bug (closes #2124) 2018-04-19 18:07:12 -06:00
Matthew Holt 694d2c9b2e Version 0.10.13 2018-04-18 17:09:54 -06:00
Matthew Holt a674c0051a vendor: Update quic and lego/acme dependencies 2018-04-18 15:48:08 -06:00
Tanmay Chaudhry 98de336a21 proxy: Enabled configurable timeout (#2070)
* Enabled configurable Timeout for the proxy directive

* Added Test for reverse for proxy timeout

* Removed Duplication in proxy constructors

* Remove indirection from multiple constructors and refactor into one

* Fix inconsistent error message and refactor dialer initialization
2018-04-17 08:09:22 -06:00
Abiola Ibrahim 9fe2ef417c rewrite: Regular expression support for simple rule (#2082)
* Regexp support for simple rewrite rule

* Add negate option for simplicity

* ascertain explicit regexp char
2018-04-14 19:40:55 -06:00
Theofanis Despoudis 88edca65d3 proxy: Fix transparent pass-thru of existing X-Forwarded-For headers
* Fixes #1960 Transparent proxy not appending
existing X-Forwarded-For header

* Fixes #1960 Formatting Code
2018-04-05 00:04:06 -06:00
Matt Holt 64c18a7c6c caddyfile: Fix errors caught by fuzzing (#2097)
* caddyfile: More robust parsing for 'import' (fixes #2096)

The fix for hanging involves limiting the number of wildcards in an
import pattern to just 1. Otherwise some patterns can expand to the
entire disk.

The other fix requires that the end string for an environment variable
expansion come after the start string.

* caddyfile: Fix more fuzzing errors
2018-04-03 11:54:32 -06:00
Matthew Holt d2fc045219 Update contributing instructions related to docs 2018-04-02 08:17:37 -06:00
Matthew Holt 917a604094 httpserver: Ignore ErrServerClosed when closing server 2018-04-02 08:17:21 -06:00
Lucas Lorentz b33b24fc9e httpserver: Add 'supervisor' directive (#2061) 2018-03-31 17:31:35 -06:00
Matt Holt 4d9ee000c8 httpserver: Prevent TLS client authentication bypass in 3 ways (#2099)
- Introduce StrictHostMatching mode for sites that require clientauth
- Error if QUIC is enabled whilst TLS clientauth is configured
  (Our QUIC implementation does not yet support TLS clientauth, but
  maybe it will in the future - fixes #2095)
- Error if one but not all TLS configs for the same hostname have a
  different ClientAuth CA pool
2018-03-30 14:40:04 -06:00
Matthew Holt 2966db7b78 httpserver: Fix test that relies on external DNS lookup
Apparently Cloudflare just caused 1.1.1.1 to resolve, so we have to
change our test IP, hopefully this is better
2018-03-30 06:39:46 -06:00
Matt Holt 38e65e28d4 tls: Fix tests on Windows (#2093) 2018-03-28 12:42:47 -06:00
Matthew Holt 73b61af58d tls: Prevent directory traversal via On-Demand TLS (fixes #2092) 2018-03-28 12:04:35 -06:00
Francis Lavoie 858e96f21c readme: Update instructions for contributing to docs (#2089) 2018-03-27 18:05:53 -06:00
Matthew Holt f379bf3421 Version 0.10.12 2018-03-26 22:09:10 -06:00
Toby Allen 1896b420d8 log: 'except' subdirective to skip logging certain requests (#2028)
* proof of concept

* Initial implementation with debug code

* Tidy up debug code

* remove unneeded import

* removed extra line

* Move ShouldLog function to rule entry Logger type

* add tests for ShouldLog

* Added tests for log exceptions

* Fix logic

* fix govet fail for test

* Updates requested for code clarity

* Update requested for style

* log: Minor style tweaks to logic of log exceptions
2018-03-26 17:17:43 -06:00
Matthew Holt 1580169e2b vendor: Update quic-go 2018-03-25 22:37:41 -06:00
Matt Holt 95514da91b Merge pull request #2072 from mholt/acmev2
tls: Use ACMEv2 and support automatic wildcard certificates
2018-03-25 22:09:03 -06:00
Matthew Holt 18ff8748e7 caddytls: Default to Let's Encrypt's ACMEv2 production endpoint 2018-03-25 22:00:18 -06:00
Matthew Holt 2ed1dd6afc Merge branch 'master' into acmev2
# Conflicts:
#	caddyhttp/httpserver/replacer.go
#	caddyhttp/httpserver/replacer_test.go
2018-03-25 21:56:11 -06:00
Matthew Holt 8039a7127f telemetry: Remove a metric, clarify another, and fix tests 2018-03-25 21:50:07 -06:00
Denis a8dfa9f0b7 httpserver: CaseSensitivePath applied to paths in site keys (#2034)
* different cases in path make different keys

* Respect CaseSensitivePath variable when matching paths
2018-03-25 21:32:30 -06:00
Matthew Holt 33aeb1cb5c telemetry: Add CLI option to selectively disable some metrics
Also fix a couple metrics that were named wrong or reported in excess.
2018-03-23 23:44:16 -06:00
Matthew Holt 8bdd13b594 telemetry: Honor the server's request to toggle certain metrics 2018-03-22 19:50:38 -06:00
Matthew Holt 52316952a5 Refactor diagnostics -> telemetry 2018-03-22 18:05:31 -06:00
Matthew Holt 7c868afd32 diagnostics: Specially handle HTTP 410 and 451 codes
An attempt to future-proof older Caddy instances so that they won't
keep trying to send telemetry to endpoints that just simply aren't
going to be available
2018-03-21 17:51:07 -06:00
Matthew Holt 4df8028bc3 diagnostics: Add/remove metrics 2018-03-21 17:01:14 -06:00
Matthew Fay f1eaae9b0d httpserver: Rework Replacer loop to ignore escaped braces (#2075)
* httpserver.Replacer: Rework loop to ignore escaped placeholder braces

* Fix typo and ineffectual assignment to ret

* Remove redundant idxOffset declaration, simplify escape check

* Add benchmark tests for new Replacer code
2018-03-18 20:42:43 -06:00
Matthew Holt 385ea53309 diagnostics: Use Retry-After header if decoding JSON fails
Improve error message and backoff as well
2018-03-18 15:49:17 -06:00
elcore 2716e272c1 Purge event hooks after USR1 reload, fix #2044 (#2047)
* caddy: Purge event hooks after USR1 reload

* caddy: Remove event hook purge logging

* caddy: Remove deleteEventHook

* caddy: use old event hooks in case of an unsuccessful restart

* caddy: implement restoreEventHooks
2018-03-17 18:29:22 -06:00
David Somers ca34a3e1aa httpserver: Placeholders for tls_protocol and tls_cipher (#2062)
Also add SSL_PROTOCOL and SSL_CIPHER env vars for fastcgi.

* Implement placeholders for ssl_protocol and ssl_cipher

* gofmt

* goimports

* Housekeeping and implement as {tls_protocol} and {tls_cipher}
2018-03-17 17:27:10 -06:00
Toby Allen 3ee6d30659 httpserver: Fix #2038 (query string being lost from URI) (#2039) 2018-03-17 17:17:42 -06:00
Matthew Holt ef40659c70 Merge branch 'master' into acmev2 2018-03-17 17:05:30 -06:00
Matt Holt 6e2de19d9f tls: Fall back to certificate keyed by empty name (fixes #2035) (#2037)
* tls: Fall back to certificate keyed by empty name (fixes #2035)

This should only happen for sites defined with an empty hostname (like
":8080") and which are using self-signed certificates or some other
funky self-managed certificate. But that certificate should arguably
be used for all incoming SNI names.

* tls: Revert to serving any certificate if no match, regardless of SNI

Also fix self-signed certs to include IP addresses in their name
if they are configured to serve an IP address

* Remove tests which are now irrelevant (behavior reverted)

It would be good to revisit this in the future.
2018-03-17 17:03:12 -06:00
Matthew Holt 3afb1ae380 Merge branch 'master' into acmev2 2018-03-17 11:30:21 -06:00
Matthew Holt 37c852c382 tls: Add 'wildcard' subdirective to force wildcard certificate
Should only be used when many sites are defined in the Caddyfile, and
you would run up against Let's Encrypt rate limits without a wildcard.
2018-03-17 11:29:19 -06:00
Matthew Holt 3d01f46efa Dangit, goimports imported the wrong acme package 2018-03-15 19:38:29 -06:00
Matthew Holt 3a6496c268 tls: Support distributed solving of the HTTP-01 challenge
Caddy can now obtain certificates when behind load balancers and/or in
fleet/cluster configurations, without needing any extra configuration.
The only requirement is sharing the same $CADDYPATH/acme folder.
This works with the HTTP challenge, whereas before the DNS challenge
was required. This commit allows one Caddy instance to initiate the
HTTP challenge and another to complete it.

When sharing that folder, certificate management is synchronized and
coordinated, without the Caddy instances needing to know about each
other. No load balancer reconfiguration should be required, either.

Currently, this is only supported when using FileStorage for TLS
storage (which is ~99.999% of users).
2018-03-15 19:30:45 -06:00
Andrey Blinov 64c9f20919 httpserver: Add geoip directive (closes #1819) (#2066)
* Add Geoip plugin to httpserver/plugin.go

* Move GeoIP plugin higher
2018-03-15 07:30:25 -06:00
Matthew Holt d10d8c23c4 httpserver: Add a couple test cases for the Replacer on {labelN} 2018-03-14 22:11:13 -06:00
Matthew Holt 3cd36fd47d tls: Replace '*' with 'wildcard_' in OCSP staple filenames (fix #2071)
Windows doesn't allow asterisk in file names, sigh...
2018-03-14 21:58:59 -06:00
Matthew Holt aaec7e469c httpserver: Add {labelN} placeholders for parts of hostnames
For example, {label1} would match "sub" in "sub.example.com" or whatever
value is in the wildcard spot of "*.example.com". Useful for rewrite!
2018-03-14 21:57:25 -06:00
Matthew Holt 6f78cc49d1 tls: Initial transition to ACMEv2 and support automatic wildcard certs
- Using xenolf/lego's likely-temporary acmev2 branch
- Cleaned up vendor folder a little bit (probably more to do)
- Temporarily set default CA URL to v2 staging endpoint
- Refactored user management a bit; updated tests (biggest change is
  how we get the email address, which now requires being able to make
  an ACME client with a User with a private key so that we can get the
  current ToS URL)
- Automatic HTTPS now allows specific wildcard pattern hostnames
- Commented out (but kept) the TLS-SNI code, as the challenge type
  may return in the future in a similar form
2018-03-14 21:44:08 -06:00
Chris Werner Rau 13dfffd203 tls: Change default tls minimum version to 1.2 (#2053) 2018-03-10 08:39:07 -07:00
elcore 5552dcbbc7 startup/shutdown: Remove deprecated startup/shutdown directives (#2033)
* caddy: Remove deprecated startup/shutdown directives

* caddyhttp: Remove deprecated startup/shutdown directives

Users should use 'on startup' and 'on shutdown' instead.
2018-02-21 10:56:09 -07:00
Matthew Holt 37b291f82c tls: Avoid nil pointer deref when parsing corrupt OCSP staple files
Fixes #2041
2018-02-21 10:53:12 -07:00
Matthew Holt d3f338ddab Update for version 0.10.11 2018-02-19 22:24:07 -07:00
Toby Allen 3b66865da5 httpserver: Placeholder for response header fields (#2029)
* Allow Response Headers in logs

* Remove log line

* remove unneeded log import

* Check if rr is nil.  Added test to check

* merge if statements

* remove temp file
2018-02-18 14:21:06 -07:00
Matthew Holt 637b0b47ee basicauth: Make test pass with Go 1.10 2018-02-18 00:13:11 -07:00
Matthew Holt a6521357e5 Fix bad merge conflict, make tests pass 2018-02-16 23:20:08 -07:00
Matthew Holt 269a8b5fce Merge branch 'master' into diagnostics
# Conflicts:
#	plugins.go
#	vendor/manifest
2018-02-16 22:42:14 -07:00
Amos Ng 1201492222 vendor: Updated quic-go for QUIC 39+ (#1968)
* Updated lucas-clemente/quic-go for QUIC 39+ support

* Update quic-go to latest
2018-02-16 22:29:53 -07:00
Toby Allen faa5248d1f httpserver: Leave %2f encoded when trimming path in site address Fix #1927 (#2014)
* Trim path prefix using EscapedPath()

* clarify comments

* Added Tests for trimPathPrefix

* Ensure path with trailing slash is properly trimmed

* Updated tests to match prepatch behaviour

* Updated tests to match prepatch behaviour

* call parse on url rather than instance

* add additional tests

* return unmodified url if error.  Additional tests
2018-02-16 14:18:02 -07:00
Matt Holt 986d4ffe3d Merge pull request #2015 from mholt/cert-cache
tls: Restructure and improve certificate management
2018-02-16 12:46:27 -07:00
Matthew Holt a03eba6fbc tls: In HTTP->HTTPS redirects, preserve redir port in some circumstances
Only strip the port from the Location URL value if the port is NOT the
HTTPSPort (before, we compared against DefaultHTTPSPort instead of
HTTPSPort). The HTTPSPort can be changed, but is done so for port
forwarding, since in reality you can't 'change' the standard HTTPS port,
you can only forward it.
2018-02-16 12:36:28 -07:00
Matthew Holt 8db80c4a88 tls: Fix HTTP->HTTPS redirects and HTTP challenge when using custom port 2018-02-16 12:05:34 -07:00
Toby Allen 4704a56a17 Merge branch 'master' into cert-cache 2018-02-15 20:17:46 +00:00
Matthew Holt 896dc6bc69 tls: Try empty name if no matches for getting config during handshake
See discussion on #2015; the initial change had removed this check, and
I can't remember why I removed it or if it was accidental. Anyway, it's
back now.
2018-02-15 08:48:05 -07:00
Jason Daly 6f4cf7eec7 readme: Update minimum version to build from source (#2024)
Re: #2009, 1.9 or newer is needed because of the introduction of `sync.Map`
2018-02-15 08:05:58 -07:00
Matthew Holt be96cc0e65 httpserver: Raise error when adjusted site addresses clash at startup
See discussion on #2015 for how this situation was discovered. For a
Caddyfile like this:

	localhost {
		...
	}
	:2015 {
		...
	}

Running Caddy like this:

	caddy -host localhost

Produces two sites both defined as `localhost:2015` because the flag
changes the default host value to be `localhost`. This should be an
error since the sites are not distinct and it is confusing. It can also
cause issues with TLS handshakes loading the wrong cert, as the linked
discussion shows.
2018-02-15 00:04:31 -07:00
Matthew Holt ef585ed810 tls: Ensure parent dir exists before creating lock file 2018-02-14 13:32:16 -07:00
Matthew Holt 4b2e22289d sigtrap: Ensure cleanup actions happen before too many things go wrong 2018-02-13 13:27:08 -07:00
Matthew Holt f26447e2fb Merge branch 'master' into cert-cache
# Conflicts:
#	sigtrap_posix.go
2018-02-13 13:25:29 -07:00
Matthew Holt 08028714b5 tls: Synchronize renewals between Caddy instances sharing file storage
Also introduce caddy.OnProcessExit which is a list of functions that
run before exiting the process cleanly; these do not count as shutdown
callbacks, so they do not return errors and must execute quickly.
2018-02-13 13:23:09 -07:00
Matthew Holt 2de4950015 Merge branch 'master' of ssh://github.com/mholt/caddy 2018-02-13 09:30:48 -07:00
Matthew Holt d29640699e readme: Update logo image 2018-02-13 09:30:26 -07:00
Etienne Bruines 6a9aea04b1 fastcig: GET requests send along the body (#1975)
Fixes #1961

According to RFC 7231 and RFC 7230, there's
no reason a GET-Request can't have a body
(other than it possibly not being supported
by existing software). It's use is simply not
defined, and is left to the application.
2018-02-11 14:45:45 -07:00
Matthew Holt 592d199315 staticfiles: Prevent path-based open redirects
Not a huge issue, but has security implications if OAuth tokens leaked
2018-02-11 13:30:01 -07:00
Matthew Holt 5820356cf6 diagnostics: Persist UUID in string format for convenience 2018-02-10 20:21:16 -07:00
Matthew Holt 6b3c2212a1 diagnostics: AppendUnique(), restructure sets, add metrics, fix bugs 2018-02-10 12:59:23 -07:00
elcore 703cf7bf8b vendor: delete github.com/codahale/aesnicheck in favor of cpuid (#2020) 2018-02-09 10:39:21 -07:00
Matthew Holt 3e00e18adc diagnostics: Point to staging endpoint 2018-02-08 23:37:42 -07:00
Matthew Holt 6c17e4d4c8 diagnostics: Add a few tests 2018-02-08 21:15:28 -07:00
Matthew Holt 388ff6bc0a diagnostics: Implemented collection functions and create first metrics
- Also implemented robust error handling and failovers
- Vendored klauspost/cpuid
2018-02-08 19:55:44 -07:00
Matthew Holt fc2ff9155c tls: Restructure and improve certificate management
- Expose the list of Caddy instances through caddy.Instances()

- Added arbitrary storage to caddy.Instance

- The cache of loaded certificates is no longer global; now scoped
  per-instance, meaning upon reload (like SIGUSR1) the old cert cache
  will be discarded entirely, whereas before, aggressively reloading
  config that added and removed lots of sites would cause unnecessary
  build-up in the cache over time.

- Key certificates in the cache by their SHA-256 hash instead of
  by their names. This means certificates will not be duplicated in
  memory (within each instance), making Caddy much more memory-efficient
  for large-scale deployments with thousands of sites sharing certs.

- Perform name-to-certificate lookups scoped per caddytls.Config instead
  of a single global lookup. This prevents certificates from stepping on
  each other when they overlap in their names.

- Do not allow TLS configurations keyed by the same hostname to be
  different; this now throws an error.

- Updated relevant tests, with a stark awareness that more tests are
  needed.

- Change the NewContext function signature to include an *Instance.

- Strongly recommend (basically require) use of caddytls.NewConfig()
  to create a new *caddytls.Config, to ensure pointers to the instance
  certificate cache are initialized properly.

- Update the TLS-SNI challenge solver (even though TLS-SNI is disabled
  currently on the CA side). Store temporary challenge cert in instance
  cache, but do so directly by the ACME challenge name, not the hash.
  Modified the getCertificate function to check the cache directly for
  a name match if one isn't found otherwise. This will allow any
  caddytls.Config to be able to help solve a TLS-SNI challenge, with one
  extra side-effect that might actually be kind of interesting (and
  useless): clients could send a certificate's hash as the SNI and
  Caddy would be able to serve that certificate for the handshake.

- Do not attempt to match a "default" (random) certificate when SNI
  is present but unrecognized; return no certificate so a TLS alert
  happens instead.

- Store an Instance in the list of instances even while the instance
  is still starting up (this allows access to the cert cache for
  performing renewals at startup, etc). Will be removed from list again
  if instance startup fails.

- Laid groundwork for ACMEv2 and Let's Encrypt wildcard support.

Server type plugins will need to be updated slightly to accommodate
minor adjustments to their API (like passing in an Instance). This
commit includes the changes for the HTTP server.

Certain Caddyfile configurations might error out with this change, if
they configured different TLS settings for the same hostname.

This change trades some complexity for other complexity, but ultimately
this new complexity is more correct and robust than earlier logic.

Fixes #1991
Fixes #1994
Fixes #1303
2018-02-04 00:58:27 -07:00
Toby Allen a50f3a4cfe gitignore: Ignore .bat files (#2013) 2018-02-03 14:48:02 -07:00
magikstm fd3fafa50c Disable PrivateDevices in systemd as it doesn't work for some devices (#1990) 2018-02-03 11:13:23 -07:00
Phillipp Engelke e20779e405 Update README.md (#2004)
Adding the bash command for downloading the caddy.service file from the reposetory. Because it was easy to forget where you find it.
2018-02-02 23:53:40 -07:00
Tw fc6d62286e make eventHooks thread safe (Go 1.9) (#2009)
Signed-off-by: Tw <tw19881113@gmail.com>
2018-02-02 23:52:53 -07:00
Matthew Holt e2997ac974 request_id: Allow reusing ID from header (closes #2012) 2018-02-02 19:59:28 -07:00
Matthew Holt 8f0b44b8a4 Create diagnostics package; persist UUID 2018-02-02 19:15:28 -07:00
Michael Schubert 50ab4fe11e caddy.service: fix typo, s/retrict/restrict/ (#2008) 2018-01-30 07:19:02 -07:00
Matthew Holt 106d62b067 sigtrap: Fix log messages, and ignore SIGHUP (#1993) 2018-01-26 22:24:11 -07:00
Miek Gieben a76222f607 sigtrap: allow graceful shutdown for SIGTERM on posix (#1995)
* shutdown: allow graceful shutdown for SIGTERM on posix

The signal is already trapped; make it do the same thing as SIGQUIT to
be more inline with Unix/Linux shutdown expectations.

Fixes #1993

* Implement comment feedback ideas
2018-01-16 15:55:33 -07:00
Whitestrake e9515425e0 use import to handle globbed values for -conf flag (#1973) 2018-01-16 11:37:49 -07:00
Heri Sim c80c34ef45 proxy: Turn on KeepAlive in QuicConfig of RoundTripper (#1943)
* Turn on KeepAlive in QuicConfig of RoundTripper

* Update reverseproxy.go
2018-01-15 21:00:59 -07:00
Tw 1ba5512015 ResponseBuffer: add missing header writing (#1997)
Signed-off-by: Tw <tw19881113@gmail.com>
2018-01-15 18:32:19 -07:00
Tw 55a564df6d template: add extension filter test and simplify test code (#1996)
Signed-off-by: Tw <tw19881113@gmail.com>
2018-01-15 18:27:55 -07:00
Andreas Ulm 8a326d4dc1 implemented sourcing of default file for sysvinit (#1984)
* implemented source of default file for sysvinit

Signed-off-by: root360-AndreasUlm <andreas.ulm@root360.de>

* added documentation in README

Signed-off-by: root360-AndreasUlm <andreas.ulm@root360.de>

* fixed sourcing command for sh

Signed-off-by: root360-AndreasUlm <andreas.ulm@root360.de>

* implemented source of default file for sysvinit

Signed-off-by: root360-AndreasUlm <andreas.ulm@root360.de>

* added documentation in README

Signed-off-by: root360-AndreasUlm <andreas.ulm@root360.de>

* fixed sourcing command for sh

Signed-off-by: root360-AndreasUlm <andreas.ulm@root360.de>

* implemented DAEMONOPTS overwrite

Signed-off-by: root360-AndreasUlm <andreas.ulm@root360.de>
2018-01-15 18:22:53 -07:00
magikstm d35719daed browse: Correct 'modified' date alignment (#1954)
* Correct browse modified date alignment

* New solution to adjust alignment
2018-01-15 18:18:25 -07:00
detaoin c296d7e7e0 caddymain: fix setCPU silently ignoring small percent values (#1969)
* caddymain: fix setCPU silently ignoring small percent values

the percent value is resolved in a GOMAXPROCS relative number by simple
division, thus rounding down the non-integer quotient. If zero, the call
to runtime.GOMAXPROCS is silently ignored.

We decide here to exceptionally round up the CPU cap in case of percent
values that are too small.

* caddymain: gofmt -s
2018-01-15 18:17:27 -07:00
Sean Lane fc1509eed4 Update README.md (change to ownership command) (#1970)
* Update README.md

I believe the owner and group of the `chown` command here are mixed up. As it was caused a permissions issue, with the service being unable to read the directory.

* Update README.md

* Update README.md

Revert changes back to the original suggested changes
2018-01-15 18:15:17 -07:00
Toby Allen 9619fe224c add basicauth {user} to replacer (#1979) 2018-01-07 14:44:49 +00:00
Toby Allen c0efec52d9 Allow Masking of IP address in Logfile. (#1930)
* First working mask

* IP Mask working with defaults and empty

* add tests for ipmask

* Store Mask as setup, some tidying, cleaner flow

* Prevent mask from running when directive not present

* use custom replacement to store masked ip
2017-12-23 10:52:11 +00:00
magikstm a74320bf4c Add {user} placeholder to CommonLogFormat (#1953) 2017-12-17 09:13:41 +00:00
Craig Peterson 1125a236ea Merge pull request #1921 from mholt/macros
Caddyfile snippets
2017-11-13 12:56:48 -05:00
Craig Peterson 8658e189e1 Merge branch 'master' into macros 2017-11-13 12:45:23 -05:00
Aaron Taylor 9a22cda15d httpserver: give each req context a Replacer that preserves custom values (#1937)
This allows custom replacements to be defined in a way that propagates
throughout all plugins.
2017-11-07 10:10:03 -07:00
insomniac 169ab3acda Check for nil listener before printing address (#1946)
* Checking that a server listener is not nil before printing verbose information

* Improved readability of a loop
2017-11-07 10:08:54 -07:00
Mohammad Gufran 5f39cbef94 caddytls: Extract locker into an interface (#1942) 2017-11-06 09:43:41 -07:00
Mohammad Gufran 63fd264043 proxy: Add SRV support for proxy upstream (#1915)
* Simplify parseUpstream function

* Add SRV support for proxy upstream
2017-11-05 23:01:10 -07:00
Toby Allen 345b312e00 Merge branch 'master' into macros 2017-11-05 21:03:44 +00:00
Tw 5cca9cc18e markdown: only update template when file changed (#1909)
Signed-off-by: Tw <tw19881113@gmail.com>
2017-11-04 17:36:59 +00:00
Toby Allen 9ebc11d775 Merge branch 'master' into macros 2017-11-04 11:10:27 +00:00
Kevin Stock 689591ef01 tls: Add option for backend to approve on-demand cert (#1939)
This adds the ask sub-directive to tls that defines the URL of a backend HTTP service to be queried during the TLS handshake to determine if an on-demand TLS certificate should be acquired for incoming hostnames. When the ask sub-directive is defined, Caddy will query the URL for permission to acquire a cert by making a HTTP GET request to the URL including the requested domain in the query string. If the backend service returns a 2xx response Caddy will acquire a cert. Any other response code (including 3xx redirects) are be considered a rejection and the certificate will not be acquired.
2017-11-03 23:01:30 -06:00
Sayem Chowdhury 2782553231 readme: Update build instructions (#1916)
* Update build instruction

This changes add proper an easy instruction for building.

* Update README.md
2017-11-02 06:11:18 -06:00
Craig Peterson 4ec5522a33 Merge branch 'macros' of github.com:mholt/caddy into macros 2017-10-31 23:56:35 -04:00
Craig Peterson ad2956fd1d snippets now 2017-10-31 23:56:24 -04:00
frk 34a34c565d FreeBSD init: Remove unnecessary daemon -u option (#1924)
The rc.subr framework already takes care of substituting user. So, using
daemon's -u option is double user-substitution and fails if $caddy_user
is non-root.
2017-10-31 10:31:09 -06:00
Arthur Silva 74d4fd3c29 improve error checking (#1938) 2017-10-31 10:19:51 -06:00
Arthur Silva ac1f3bfaaa a few code improvements (#1936)
caddy.go:569: could be simplified

sigtrap_posix.go:87: value of inst is never used

upgrade.go:151: should omit nil check; len() for nil slices is defined as zero
2017-10-31 10:12:05 -06:00
Mohammed Al Sahaf f7a70266ed Implement per-site index (#1906) 2017-10-29 21:13:10 +00:00
elcore fc75527eb5 onevent/startupshutdown: run command once per server block (#1934) 2017-10-23 00:27:44 +02:00
Toby Allen e5d04f9a96 Change log output for startup and shutdown (#1932)
Changes the log output of on startup
2017-10-22 13:43:40 +01:00
Craig Peterson 91a60a8d25 Merge branch 'master' into macros 2017-10-20 10:32:21 -04:00
Craig Peterson 5c9fc3a473 Merge branch 'macros' of github.com:mholt/caddy into macros 2017-10-19 19:55:14 -04:00
Craig Peterson 02ac1f61c4 retrigger build 2017-10-19 19:54:15 -04:00
elcore 59a8ada4a8 Fix CI Tests (#1929) 2017-10-19 11:02:56 -06:00
Craig Peterson 1889049ef3 Merge branch 'master' into macros 2017-10-19 10:34:13 -04:00
Craig Peterson 68a495f144 actually return error on redeclaration 2017-10-19 10:27:10 -04:00
Matthew Holt a2db340378 tls: Final check of OCSP response validity date before stapling 2017-10-16 17:25:55 -06:00
Alex Gaynor c6a2911725 tls: Handle when OCSP responder cert expires before a response it issued (#1922)
* Handle the case of an OCSP responder certificate expiring before an OCSP response it issued

* oops

* doh, gofmt
2017-10-16 17:23:21 -06:00
Matthew Holt 654f26cb91 tls: Evict existing certificates from cache when loading ones from disk 2017-10-16 16:40:43 -06:00
Craig Peterson dd4b3efa47 remove 'macro foo' syntax 2017-10-15 19:10:56 -04:00
Craig Peterson 3a969bc075 add nil check 2017-10-13 11:08:17 -04:00
Craig Peterson 425f61142f initial implementation of caddyfile macros 2017-10-13 11:04:44 -04:00
Wèi Cōngruì 79072828a5 staticfiles: remove mapFSRootOpenErr because Go stdlib has fixed the relevant issue (#1919) 2017-10-13 08:01:30 -06:00
Eugene Dementiev 0548b97701 init: Fix upstart script for Centos6 (and Amazon Linux) (#1914) 2017-10-12 17:02:46 -06:00
Guilherme Bernal 99625ae3f6 on: Allow nonblocking command with no other arguments (#1913) 2017-10-12 10:11:50 -06:00
Matthew Holt c4dfbb9956 Update readme and changelog for v0.10.10 2017-10-08 22:20:05 -06:00
Matthew Holt b0d9c058cc Change CASE_SENSITIVE_PATH default to false
A default of true is risky when protecting assets by matching base path.
It's not obvious that protecting /foo/ will allow /Foo/ through, and if
accessing static files on a case-insensitive file system... that's no
good. So the default is now to be case-INsensitive when matching paths.
2017-10-08 22:19:35 -06:00
Matthew Holt cccfe3b4ef proxy: Allow insecure certificate in QUIC tests 2017-10-05 11:11:48 -06:00
Matthew Holt f71955e89c Grammar improvements 2017-10-04 18:37:11 -06:00
elcore dd44491e13 startupshutdown: gofmt code (#1902) 2017-10-03 07:18:29 -06:00
Mohammad Gufran ac865e8910 fastcgi: Add support for SRV upstreams (#1870) 2017-10-03 07:17:54 -06:00
elcore b7167803f2 startupshutdown: is an alias for 'on' (#1880) 2017-10-01 20:41:45 -06:00
Kevin Stock 97710ced7e Add hook for instance startup (#1888)
Provides a new hook for plugins as a means to provide the current caddy.Instance when starting or restarting.
2017-10-01 20:36:23 -06:00
elcore f878247a18 Implement CertRenewEvent (#1879) 2017-10-01 11:25:30 -06:00
elcore 118cf5f240 Implement 'http.on' plugin and replace UUID lib (#1864)
* Implement 'command' plugin

* Rename 'command' to 'on'

* Split this PR
2017-10-01 11:24:50 -06:00
Matthew Holt f9cba03d25 redir: Do not count multiple rules with if statements as duplicates
This allows you to have multiple redir directives conditioned solely
upon if statements, without regard to path.
2017-09-28 11:41:11 -06:00
Matthew Holt baf6db5b57 Apply Apache license to all .go source files (closes #1865)
I am not a lawyer, but according to the appendix of the license,
these boilerplate notices should be included with every source file.
2017-09-22 23:56:58 -06:00
Matthew Holt e60400a92e caddyfile: Use full, absolute file path in token structs (fixes #1892)
When two Caddyfiles with the same name, but different paths, are
imported, it can cause a weird bug because isNewLine() returned false
when it should return true, since the files are actually different,
but it couldn't know that because only the base name was stored,
not the whole path.
2017-09-22 20:02:48 -06:00
Tw e377eeff50 proxy: websocket proxy exits immediately if backend is shutdown (#1869)
Signed-off-by: Tw <tw19881113@gmail.com>
2017-09-22 18:10:48 -06:00
Matthew Holt 84a2f8e89e Add iOS 11 stable ClientHello to MITM test corpus (issue #1890) 2017-09-22 17:41:47 -06:00
Matthew Holt 64be3e410c websocket: Avoid multiple calls to WriteHeader if Upgrade fails 2017-09-22 17:39:18 -06:00
Matthew Holt 643dac688c Clarify unofficial builds in version string 2017-09-22 17:25:43 -06:00
Daniel van Dorp 0a624f87ff Merge pull request #1884 from timothywlewis/fix-pid-error-in-linux-sysvinit
Fix pid error in linux sysvinit
2017-09-22 16:45:55 +02:00
Tim Lewis fea8f37f9d Fix linux-sysvinit script to prevent missing caddy.log
Create /var/log/caddy.log and chown prior to starting caddy.
Caddy running as DAEMONUSER does not have permission to create the /var/log/caddy.log.
2017-09-18 19:16:15 -04:00
Tim Lewis a808252079 Fix spurious .pid file error in linux-sysvinit
This change eliminates the `[ERROR] Could not write pidfile: open /var/run/caddy.pid: permission denied` from caddy.log.
The start-stop-daemon writes the file as root so the DAEMONUSER that caddy runs as cannot write to the .pid file.
2017-09-18 19:14:56 -04:00
Davor Kapsa 93bcca0ccc travis: add 1.x instead 1.9 go version (#1868) 2017-09-16 09:48:27 -06:00
Fake ID d39b95600a readme: fixed build instructions (#1875) 2017-09-16 09:35:58 -06:00
Matthew Holt 545fa844bb EULA: Remove restriction clause related to sponsors header 2017-09-14 21:45:32 -06:00
Adam Williams b6e10e3cb2 Revert "Implement Caddy-Sponsors HTTP response header" (#1866)
This reverts commit 56453e9664.
2017-09-14 21:42:22 -06:00
Matthew Holt bc56793d3b Update readme and changes for version 0.10.9 2017-09-12 11:02:53 -06:00
Matthew Holt ad973f1d12 Merge branch 'sponsors-header' 2017-09-12 10:53:21 -06:00
Matthew Holt c06941ed52 proxy: Disable QUIC test outside CI environment (see #1782) 2017-09-11 23:34:39 -06:00
Matthew Holt 54c65cb025 templates: Properly propagate response status code (fixes #1841)
Benchmarks with wrk showed no noticeable performance impact
2017-09-11 23:25:41 -06:00
twdkeule 22b835b9f4 proxy: Support QUIC for upstream connections (#1782)
* Proxy can now use QUIC for upstream connections

Add HandshakeTimeout, change h2quic syntax

* Add setup and upstream test

Test QUIC proxy with actual h2quic instance

Use different port fo QUIC test server

Add quic host to CI config

Added testdata to vendor

Revert "Added testdata to vendor"

This reverts commit 959512282deed8623168d090e5ca5e5a7933019c.

* Use local testdata
2017-09-11 19:49:02 -06:00
Matthew Holt 46ae4a6652 tls: Remove expiring certificates from cache and load renewed ones
Renewed certificates would not be reloaded into the cache because their
names conflict with names of certificates already in the cache; this
was intentional when loading new certs to avoid confusion, but is
problematic when renewing, since the old certificate doesn't get
evicted from the cache. (Oops.)

Here, I remedy this situation by explicitly deleting the old cert from
the cache before adding the renewed one back in.
2017-09-11 12:37:42 -06:00
Matthew Holt 56453e9664 Implement Caddy-Sponsors HTTP response header
(See EULA.) Personally-licensed official Caddy builds cannot remove
this header by configuration. The commercially-licensed builds of Caddy
don't have this header.
2017-09-10 19:51:57 -06:00
Matthew Holt 3b144c21d0 Change build program to use new builds package 2017-09-10 14:09:57 -06:00
Matthew Holt 9e156e0940 Update readme/changes for v0.10.8 2017-09-08 11:06:39 -06:00
Matt Holt 65191eb5ae Merge pull request #1861 from mholt/fix1859
httpserver: Fix #1859 by cleaning paths when matching them
2017-09-08 11:04:09 -06:00
Matthew Holt f6d75bb79a httpserver: Fix #1859 by cleaning paths when matching them
Signed-off-by: Matthew Holt <mholt@users.noreply.github.com>
2017-09-08 07:19:52 -06:00
Matthew Holt f069a575cc Add EULA
The End-User License Agreement applies to official Caddy binaries;
the source code is still under the same open source license.
2017-09-06 19:03:53 -06:00
Matt Holt 32bb6a4cde Merge pull request #1856 from twdkeule/fix-index-push
Do not push index file when not in a rule
2017-09-06 06:59:55 -06:00
Fiisio a59bdd08ca fastcgi: use bytes.Contains and strconv.Itoa (#1857) 2017-09-06 06:33:48 -06:00
Thomas De Keulenaer b324a32b61 Do not push index file when not in a rule
+ test
2017-09-04 15:53:41 +02:00
John Chadwick 10484cfad2 fastcgi: Fix SCRIPT_NAME when path in address (#1852)
* Add tests for SCRIPT_NAME

* fastcgi: Include vhost path prefix in SCRIPT_NAME
2017-09-01 22:15:53 -06:00
Matthew Holt 129efde9b0 Support nacl compilation 2017-08-29 16:21:26 -06:00
Mattias Wadman a16a80ca52 Make filename column fill out space (#1848) 2017-08-29 23:04:36 +01:00
Mateusz Gajewski 6d7462ac99 push: Allow pushing multiple resources via Link header (#1798)
* Allow pushing multiple resources via Link header

* Add nopush test case

* Extract Link header parsing to separate function

* Parser regexp-free

* Remove dead code, thx gometalinter

* Redundant condition - won't happen

* Reduce duplication
2017-08-28 19:38:29 -06:00
Matthew Holt c0c7437fa5 caddytls: Fix data race in test (close #1844)
The race was in the test only; not in the production code
2017-08-28 19:21:17 -06:00
Matthew Holt 01f3593fd6 Update test case 2017-08-26 08:11:43 -06:00
Matthew Holt 4cce8c7b6b Rename parse errors to errors during parsing (#1838) 2017-08-26 07:27:59 -06:00
Matthew Holt 0d99751a2f Fix typos in changes file 2017-08-26 07:15:06 -06:00
Matthew Holt 0a31c32fb7 browse: Clarify test skip on Windows and log a message 2017-08-26 07:14:40 -06:00
Matthew Holt 0b4dda0aba Update readme for v0.10.7 2017-08-25 16:54:05 -06:00
Matt Holt c7868affe1 browse: Ignore one Test function on Windows (temporary) (#1839)
* browse: Attempt to fix tests on Windows

* browse: Make tests verbose for debugging

* Moar debugging

* Trying path.Join instead

* browse: Just skip the tests for now

* browse: Remove debug prints
2017-08-25 16:52:44 -06:00
Matthew Holt 74316fe01b Replace build.bash with build.go; limit timestamp inclusion
build.go is (should be) cross-platform compatible.

Timestamps are now excluded from all builds on a clean commit,
in an effort to be byte-for-byte reproducible.
2017-08-25 15:59:36 -06:00
Matthew Holt ef3d63e3e5 Update CI scripts for Go 1.9 2017-08-24 21:02:47 -06:00
Matt Holt 4b1b329edb templates: Execute template loaded by later middlewares (#1649)
* templates: Execute template loaded by later middlewares

This is the beginning of an attempt to make the staticfiles file server
the only middleware that hits the disk and loads content. This may have
unknown implications. But the goal is to reduce duplication without
sacrificing performance. (We now call ServeContent here.)

This change loses about 15% of the req/sec of the old way of doing it,
but this way is arguably more correct since the file server is good at
serving static files; duplicating that logic in every middleware that
needs to hit the disk is not practical.

* httpserver: Introduce ResponseRecorder as per Tw's suggestions

It implements io.ReaderFrom and has some allocation-reducing
optimizations baked into it

* templates: Increase execution speed by ~10-15% after perf regression

By using httpserver.ResponseBuffer, we can reduce allocations and still
get what we want. It's a little tricky but it works so far.
2017-08-24 07:13:53 -06:00
Matt Holt e49474a4f5 Merge pull request #1821 from mholt/ocspfix
tls: Fix OCSP stapling bug when certificate names overlap other certs
2017-08-23 12:26:01 -06:00
Matt Holt c026e2b734 Merge pull request #1825 from thejmazz/systemd-allow-more-threads
Double systemd LimitNPROC to allow more threads
2017-08-23 10:45:14 -06:00
Matthew Holt be36fec7ea vendor: Update quic-go 2017-08-23 10:32:08 -06:00
Matt Holt 49e98a1518 Merge pull request #1833 from mholt/add-forwardproxy
Add forwardproxy to directives' list
2017-08-18 11:43:51 -06:00
Sergey Frolov a7498bee68 Add forwardproxy to directives' list 2017-08-18 12:25:39 -04:00
Julian Mazzitelli 280ae833d4 Set LimitNPROC=512 for systemd 2017-08-14 19:25:08 -04:00
Matt Holt 261547b42c Merge pull request #1823 from klingtnet/systemd-restart-limit-fix
Fix restart restart behaviour of the systemd service
2017-08-13 09:46:26 -06:00
Andreas Linz 53ae9b8521 Increase restart rate limit
The previous setting caused the service to hit a rate-limit when it was
restarted more than 5 times in 24h.
Editing the Caddyfile and restarting the service could also easily
trigger this rate limit.
One could argue that users could simply call `systemctl reset-failed
caddy` to reset the rate-limit counter, but this is counterintuitive
because most users won't know this command and are possibly unaware that
they had hit a rate-limit.

The service is now allowed to restart 10 times in 10 seconds before
hitting a rate limit.
This should be conservative enough to rate limit quickly failing
services and to allow users to edit and test their caddy configuration.

This closes #1718

Remove restart limit settings and use defaults

By default 5 restarts within 10 seconds are allowed without
encountering a restart limit hit, see  `man systemd.unit` for details.

Set Restart to on-abnormal

The table in https://www.freedesktop.org/software/systemd/man/systemd.service.html#Restart=
shows the conditions for which on-abnormal would restart the service.
It will *not* restart the service in the following cases:

- a non-zero exit status, e.g. an invalid Caddyfile
- a zero exit code (or those specified in SuccessExitStatus=) and a clean signal
    clean signals are SIGHUP, SIGINT, SIGTERM or SIGPIPE
    https://github.com/systemd/systemd/blob/3536f49e8fa281539798a7bc5004d73302f39673/src/basic/exit-status.c#L205

The service *will be restarted* in the following cases:

- a unclean signal, e.g. SIGKILL
- on start and watchdog timeout (we don't use those systemd service
constructs explicitly)
2017-08-13 16:38:19 +02:00
Matt Holt 20fbc7303c Merge pull request #1796 from mholt/bugfix_rewrite_1794
Fix for #1794: Fixes issues with IfMatcher and regular expressions.
2017-08-12 15:17:28 -06:00
Matt Holt 6b546389b8 Merge pull request #1815 from wader/browse-abs-recursive-dir-symlink
browse: Support absolute and recursive directory symlinks
2017-08-12 12:19:25 -06:00
Matthew Holt ff56151931 Build tags for Caddy to build on nacl 2017-08-12 12:18:37 -06:00
Mattias Wadman 981f364845 browse: Support absolute and recursive directory symlinks 2017-08-12 19:29:43 +02:00
Matt Holt 5e0896305c SIGUSR2 triggers graceful binary upgrades (spawns new process) (#1814)
* SIGUSR2 triggers graceful binary upgrades (spawns new process)

* Move some functions around, hopefully fixing Windows build

* Clean up a couple file closes and add links to useful debugging thread

* Use two underscores in upgrade env var

To help ensure uniqueness / avoid possible collisions
2017-08-12 11:04:32 -06:00
Mark Severson d2fa8600fc httpserver: Add 'awses' plugin directive (#1818) 2017-08-12 09:28:53 -06:00
Henrique Dias ebce0b7aec httpserver: Add 'jekyll' plugin. (#1817) 2017-08-12 09:28:05 -06:00
Matthew Holt b699a17a1b tls: Fix OCSP stapling bug when certificate names overlap other certs
https://caddy.community/t/random-ocsp-response-errors-for-random-clients/2473?u=matt

Certificates are keyed by name in the cache, optimized for fast lookups
during TLS handshakes using SNI. A more "correct" way that is truly a
1:1 would be to cache certificates by a hash of the leaf's DER bytes,
but this involves an extra index to maintain. So instead of that, we
simply choose to prevent overlap when keying certificates by server
name. This avoids the ambiguity when updating OCSP staples, for instance.
2017-08-12 00:12:22 -06:00
Matthew Holt b5ec462299 internal: Allow use for only X-Accel-Redir (closes #1020)
(allow no arguments of paths to protect)
2017-08-09 10:36:54 -06:00
Matthew Holt 617988844b Enable compilation for Plan 9 (closes #1093) 2017-08-08 23:00:35 -06:00
Dusty Doris 4e52b3fe8a staticfiles: fix handling of filenames that end with index file names (#1812)
* static files ending with an index were redirected improperly

* optimize requestPage
2017-08-07 18:10:47 -06:00
Dhananjay Balan bd67ec99f0 freebsd init: typo in filename. (#1799) 2017-08-04 18:20:00 -06:00
Matthew Holt a7ed0cf69e Avoid panic on QUIC server close (fixes #1805) 2017-08-03 11:20:14 -06:00
Simon Lightfoot d48e51cb78 Changed IfCond to store the condition function and the compiled regular expression.
Updated ifCondition test to deep test all fields.
Changed NewComplexRule to not return a pointer.
Corrected panic detection in formatting.
Fixed failing test cases.
Fixed review bug for test.
Fixes bug caused by Replacer running on the regular expressions in IfMatcher. We also now compile regular expressions up front to detect errors.
Fixes rewrite bugs that come from formatting a rule as a string and failing with nil dereference caused by embedding Regexp pointer in a Rule. Re: Issue #1794
2017-08-03 11:59:30 +01:00
Matt Holt d3e5f9d456 Merge pull request #1800 from mholt/exitcodes
Distinguishable exit codes
2017-08-02 09:32:36 -06:00
Matthew Holt cbb85532a8 Distinguishable exit codes
0: normal or expected exit
1: error before server finished starting
2: double SIGINT (force quit)
3: error stopping with SIGQUIT
4: shutdown callback(s) returned error(s)
2017-08-01 23:41:24 -06:00
Matthew Holt 65bc696b0c Fix force quit using SIGINT
Only the outside function call is executed in a new goroutine when
invoking 'go'. Oops. Force quits (2 SIGINTs) now work again.
2017-07-31 17:43:37 -06:00
Matthew Holt e7f08bff38 Update changes and readme for v0.10.6 2017-07-28 12:21:09 -06:00
Matt Holt 16fa3ecb0f Merge pull request #1789 from abiosoft/fix-atomic-add
fastcgi: Fix for #1788. Align atomic fields to 64-bit word (Go bug)
2017-07-28 11:53:21 -06:00
Abiola Ibrahim dd3f460cf8 Fix for #1788. Rearrange struct fields. 2017-07-28 17:33:40 +01:00
Henrique Dias 36d8d2c7de Fix links on caddy-hugo and caddy-filemanager (#1787) 2017-07-28 09:19:00 -06:00
Matthew Holt c06ff1cb37 Update changes and readme for version 0.10.5 2017-07-27 16:22:39 -06:00
Matthew Holt a48e4ecb5a vendor: Update dependencies 2017-07-27 16:11:56 -06:00
Matthew Holt 74940af624 httpserver: Set default idle timeout of 5 minutes (closes #1733)
Also clarified a comment in SiteConfig
2017-07-27 16:01:47 -06:00
Matt Holt 32ec39cdea Merge pull request #1784 from mholt/trie-fallbacks
Move fallbackHosts to vhostTrie
2017-07-27 15:53:07 -06:00
Sergey Frolov a197c864e8 Move fallbackHosts to vhostTrie 2017-07-27 17:23:13 -04:00
Matt Holt 4991d702fd Merge pull request #1781 from mholt/global-fallback-hosts
httpserver: Add global FallbackHosts for vhost matching
2017-07-25 19:55:14 -06:00
Matt Holt 76a282718d Merge pull request #1779 from mholt/mitm-panic
mitm: Fix out of bounds error when checking software version in UA
2017-07-25 15:35:51 -06:00
Sergey Frolov c8307409c9 Add global FallbackHosts for vhost matching 2017-07-25 16:10:51 -04:00
Matt Holt 1366a44639 Merge pull request #1780 from sergeyfrolov/master
httpserver: Encapsulate WriteSiteNotFound error
2017-07-25 13:33:25 -06:00
Sergey Frolov ea245b5af5 Encapsulate WriteSiteNotFound error 2017-07-25 15:14:23 -04:00
Matthew Holt 10d5422c3e mitm: Fix out of bounds error when checking software version in UA 2017-07-25 13:00:49 -06:00
Matt Holt b63d9fdc68 Merge pull request #1777 from tw4452852/chunked_ws
proxy: fix hang on chunked websocket server
2017-07-25 09:59:42 -06:00
Tw 9b073aad58 proxy: fix hang on chunked websocket server
Signed-off-by: Tw <tw19881113@gmail.com>
2017-07-25 15:12:38 +08:00
Matthew Holt ae7e098240 httpserver: Only enable QUIC for sites with TLS & HTTP2 enabled 2017-07-24 19:05:48 -06:00
Matt Holt 6e0317a703 Merge pull request #1747 from twdkeule/fix-index-push
Pushes for /index.html work when surfing to /
2017-07-24 14:00:27 -06:00
Thomas De Keulenaer 20f76a256e Push resources for indexFiles when surfing to directories
Use httpserver.IndexFile() to determine index files

Test if middleware pushes indexfile when requesting directory

Fix codereview issues

Serve original request first, push later

Revert "Serve original request first, push later"

This reverts commit 2c66f01115747e5665ba7f2d33e2fd551dc31877.
2017-07-24 12:36:07 +02:00
Matt Holt 40b52fb02e Merge pull request #1775 from tw4452852/roller_parse
log,error: fix roller parser issue
2017-07-20 14:19:17 -06:00
Tw 91150bb770 log,error: fix roller parser issue
Signed-off-by: Tw <tw19881113@gmail.com>
2017-07-20 15:21:06 +08:00
Matthew Holt f1dd9f2b79 mitm: Improve detection related to Chrome and Safari on iOS
Include test for iOS 11 beta
2017-07-19 11:16:41 -06:00
Abiola Ibrahim 6aba4a311a fastcgi: Revert persistent connections (#1739)
* Revert fastcgi to emove persistent connections.

* Fix linting errors

* reintroduce timeout tests

* check for non-zero timeout

* ensure resp is not nil
2017-07-18 12:52:53 -06:00
Jaume Martin 56153e0bb3 httpserver: Adding nobots directive (#1767)
* Adding nobots directive

* Moving nobots directive behind log one.

* Move nobots directive to a better position
2017-07-17 12:39:06 -06:00
Matt Holt 905eb70773 Merge pull request #1753 from spacewander/weak_etag_after_gzip
gzip: change ETag to weak etag after gzip
2017-07-13 22:36:01 -06:00
spacewander e2544597a1 gzip: change ETag to weak ETag after gzip
According to https://tools.ietf.org/html/rfc7232#section-2.1
> Likewise, a validator is weak if it is shared by two or more
representations of a given resource at the same time, unless those
representations have identical representation data.  For example, if
the origin server sends the same validator for a representation with
a gzip content coding applied as it does for a representation with no
content coding, then that validator is weak.

Therefore, after gzip, we should change the original etag to weak etag.
2017-07-14 11:48:34 +08:00
Nicolas ba1132214e httpserver: Add nicolasazrak/caddy-cache plugin directive (#1759) 2017-07-13 05:28:00 -06:00
Matthew Holt b987c7893c vendor: Update lego/acme to v0.4.0
Not much changed but it's a clean pull now; easier for future reference.
2017-07-12 20:34:10 -06:00
Ning Xie aebe387f72 basicauth: remove magic number (#1760) 2017-07-12 19:32:24 -06:00
Henrique Dias 0985024670 httpserver: Add webdav plugin directive (#1752) 2017-07-11 09:43:57 -06:00
Richard Bowden 25a596a98f freebsd init: added new functionality and enabled better logging (#1740)
* uses more of the builtin functionality for starting and stopping of the process by using command and command_args along with procname
* removed -f from daemon as this was hiding error message that were sent to stdout on startup, now writing stdout to the logfile directly

for example, this was being hidden:

“Activating privacy features.. [www.domain.com] failed to get certificate: Error presenting token: Could not find the start of authority”

it now shows up in the log

* aded “caddy_env” to allow the setting of environment variables that caddy might need, for example when setting creds for “DNS Challenge”

* added a check to ensure caddy_config_path file exists
2017-07-10 16:20:30 -06:00
Toby Allen acc67eb3b2 Rename directive requestid to request_id (#1757)
* rename requestid request_id

* rename folder

* folder name match package name requestid
2017-07-10 14:47:48 -06:00
Matt Holt 4c700efbbb Merge pull request #1751 from zikes/header_policy
proxy: add Header load balancing policy
2017-07-10 14:44:07 -06:00
Matt Holt 9ad96b33ff Merge pull request #1754 from spacewander/correct_test_message
gzip,mime: show response header instead of the request one in test message
2017-07-08 14:20:43 -06:00
spacewander 387a083255 gzip,mime: show response header instead of the request one in test message 2017-07-08 11:53:34 +08:00
Jason Hutchinson 95366e41c4 add Header proxy policy 2017-07-07 10:37:49 -05:00
Abiola Ibrahim a6ec51b349 Merge pull request #1748 from bananenmannfrau/master
adds unix timestamp placeholder
2017-07-06 07:46:20 +00:00
bananenmannfrau f6a96227c4 adds unix timestamp placeholder 2017-07-05 22:08:07 +02:00
Matthew Holt 56b3ea876b Correct URL to restic plugin in comment 2017-07-05 08:43:55 -06:00
Tw 2d9273f915 Merge pull request #1746 from JoshHarmon/proxy-ci-fix
proxy: Fix CI fail from format token in Error call
2017-07-05 01:23:33 -05:00
Josh Harmon 8bc7b93bc8 proxy: Fix CI fail from format token in Error call
go vet caused a build fail in https://travis-ci.org/mholt/caddy/jobs/248392875:
   upstream_test.go:480::error: possible formatting directive in Error call (vet)

This patch changes the Error call added in commit 078c991574 to
an Errorf call to support the use of the %d token.
2017-07-04 23:03:59 -07:00
Matt Holt 4750699ab0 Merge pull request #1734 from tw4452852/ineffassign
markdown: fix the real ineffectual assignments in test
2017-06-29 08:28:57 -06:00
Tw a4bf6e586d markdown: fix the real ineffectual assignments in test
Signed-off-by: Tw <tw19881113@gmail.com>
2017-06-29 18:30:18 +08:00
Matthew Holt dfa389c9df Update README and CHANGES for 0.10.4 2017-06-28 16:10:30 -06:00
Martin Redmond 078c991574 proxy: custom upstream health check by body string, closes #324 (#1691) 2017-06-28 15:54:29 -06:00
Fernando Álvarez bf7b25482e log, errors: Introduce rotate_compress option (#1731)
* vendor: update Lumberjack dep

* httpserver/roller: introduce rotate_compress directive

This directive will enable gzip compression provided by [Lumberjack](https://github.com/natefinch/lumberjack/pull/43).

The directive `rotate_compress` can be `true` or `false`, being `false` by default.

* httpserver/roller: remove need to set bool with rotate_compress option
2017-06-28 09:06:32 -06:00
Matt Holt 3bc925400b Merge pull request #1682 from tw4452852/markdown
markdown: reload template on each request and fix fake tests
2017-06-27 23:02:59 -06:00
Tw 655e61ab32 markdown: fix ineffectual assignment CI issue
Signed-off-by: Tw <tw19881113@gmail.com>
2017-06-28 09:28:57 +08:00
Matthew Holt 43b56d621b Allow duplicate Server headers when proxying response
See discussion on commit c9b022b5e0

If we overwrite the Server header, it becomes difficult/impossible to
know from the client whether the request was proxied through Caddy.
2017-06-27 12:11:03 -06:00
Matt Holt 7b5efb5d75 Add restic plugin directive (#1730) 2017-06-25 08:26:57 -07:00
Tw 3390862918 markdown: reload template on each request
Signed-off-by: Tw <tw19881113@gmail.com>
2017-06-25 19:31:12 +08:00
Tw 47fc35acc0 markdown: fix fake tests
Signed-off-by: Tw <tw19881113@gmail.com>
2017-06-25 09:09:21 +08:00
Jason Hutchinson d3fc9f7a9b add gopkg plugin (#1725) 2017-06-24 14:58:33 -07:00
Shannon Wynter a63a6ecb04 Add reauth directive (#1716) 2017-06-24 14:42:40 -07:00
Matthew Holt 47e770621c Use comments for notes on issue and PR templates 2017-06-24 14:23:15 -07:00
elcore 7516b4b533 Fix TestListenerAddrEqual (#1727) 2017-06-24 13:55:36 -07:00
Toby Allen 133ed18374 Create request_id directive #1590 (#1711)
* Create request_id directive #1590

* Address Comments

* Fix TestListenerAddrEqual

* requestid: Add some tests

* Address Comments by tobya

* Address Comments
2017-06-24 13:54:35 -07:00
Marcel Ludwig b0ab3d4281 use caddy.AppName instead of fixed string in 'Server' header (#1709) 2017-06-24 11:17:06 -07:00
George Macon f68233a1ba Configure systemd to send SIGQUIT on stop (#1702) 2017-06-24 11:15:13 -07:00
Jason Hutchinson f3721c103c tls: add optional 'ca' tls directive, closes #1689 (#1699) 2017-06-24 11:10:44 -07:00
lbogdan 3e2b1d145a rewrite: treat "if a not_op b" uniformly by negating "op". (#1696) 2017-06-15 16:45:42 -06:00
Matthew Holt f4b6f15e07 staticfiles: Build redirect based on rewritten URL (fixes #1706) 2017-06-07 14:40:17 -06:00
Matthew Holt 95a6237693 mitm: Add missing import 2017-06-07 14:22:55 -06:00
Matthew Holt 0da76e2b76 mitm: Add experimental Tor support for interception detection 2017-06-07 14:20:15 -06:00
Matthew Holt 8051c73cc3 Add sourcegraph links to readme and contributing guide 2017-06-06 23:18:59 -06:00
bamling a368230ba5 caddytls: introduced own ChallengeProvider type to fix imports related to vendor (#1700)
* introduced own ChallengeProvider type, based on acme.ChallengeProvider to avoid vendoring/version mismatches in Caddy plugins; see Caddy issue #1697

* fixed up comments for ChallengeProvider

* moved ChallengeProvider to caddytls/tls.go
2017-06-06 09:23:00 -06:00
Daniel van Dorp 8a058828a3 Merge pull request #1703 from messyidea/patch-1
Update Initscripts
2017-06-04 20:32:58 +02:00
Messyidea ee124a6d3c Update Initscripts
"$(which caddy)" is not work at startup. 
After this change, I can run "insserv -d caddy" to start caddy automatically on boot.
2017-06-04 12:18:33 +08:00
Yang Luo 97a631ec4c httpserver: Register authz directive (#1693) 2017-06-03 09:34:14 -06:00
Henrique Dias cbdd3a4f8e Add ExtraInfo to EventHook (#1692)
* Update plugins.go

* Add extraInfo to eventHook

* Add extraInfo to eventHook

* Update run.go

* Update run.go

* Update run.go
2017-06-03 07:28:16 -06:00
Matthew Holt 6b8e40b3fb browse: Fix symlink indicators for files in folders other than cwd
Related to #1660 and #1667
2017-06-02 17:40:25 -06:00
Jonas Östanbäck 132f2a9cc3 browse: Show symbolic links and target's type properly (#1667)
* Browse: Show symbolic links and targets type properly
 * gofmt

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>

* Move symbolic link check in to isSymlinkTargetDir

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>

* Revert template change and show sym link folders as normal folders

* browse: Updated icons including symlink indicators
2017-06-01 06:18:07 -06:00
Andrew Steinborn baf269d4e2 gzip: cleaned up writer pool initialization code (#1695) 2017-05-30 18:29:28 -06:00
Pieter Louw 20a047f7e1 httpserver: Add grpc plugin directive (#1694)
* Add grpc plugin directive

* Update plugin.go

Removed whitespace line

* Update plugin.go
2017-05-30 09:57:24 -06:00
Matt Holt 6ab0d8d8d9 Merge pull request #1651 from mholt/vendoring
Vendor all dependencies
2017-05-28 08:22:43 -06:00
Matthew Holt 6fde3632ef Vendor all dependencies (Warning: Huge changeset.)
The vendor/ folder was created with the help of @FiloSottile's gvt and
vendorcheck.

Any dependencies of Caddy plugins outside this repo are not vendored.

We do not remove any unused, vendored packages because vendorcheck -u
only checks using the current build configuration; i.e. packages that
may be imported by files toggled by build tags of other systems.

CI tests have been updated to ignore the vendor/ folder. When Go 1.9 is
released, a few of the go commands should be revised to again use ./...
as it will ignore the vendor folder by default.
2017-05-27 13:30:11 -06:00
Connor S. Parks 474f119702 httpserver: add not_ends_with (#1688)
* de-duplicates code for 'not' ops and replicates 'not' op for ends_with

* fixes incorrect test expectations
2017-05-25 06:01:24 -06:00
Taylor Otwell 33e1560d53 httpserver: Add not_starts_with condition. (#1687)
* Add not_starts_with condition.

This adds the opposite of the starts_with condition, to check if a
given string does not start with another string.

* Correct white space problems
2017-05-24 09:32:53 -06:00
Matthew Holt a5eb552215 mitm: Add a couple more test cases for Firefox 53 2017-05-23 16:18:56 -06:00
Matthew Holt 7fc0940fe6 mitm: Fix false positive for Firefox 55 nightly 2017-05-23 14:49:10 -06:00
Matthew Holt 7323b14580 Minor change to readme/changes 2017-05-19 15:25:16 -06:00
Matthew Holt 1845e5cf52 Update readme and changelog for v0.10.3 2017-05-19 08:35:32 -06:00
Matthew Holt 410ece831f tls: Only require renewed cert at startup 7 days out (issue #1680) 2017-05-19 08:30:01 -06:00
Jonas Östanbäck ebf4279e98 proxy: Add new URI hashing load balancing policy (#1679)
* Add uri policy test cases
 * Add function definition
 * Add uri hashing policy
 * Refactor and extract hostByHashing and use in IP and URI policy
 * Rename to URIHash

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>
2017-05-17 10:46:57 -06:00
Andrew Steinborn b0cf3f0d2d tls: Prefer ChaCha20 if AES-NI instruction set is unavailable (#1675)
Fixes #1674
2017-05-17 10:45:17 -06:00
Jonas Östanbäck 8d3f336971 proxy: Correct policy documentation (#1678)
* Correct proxy policy documentation

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>

* Change first's select() wording

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>
2017-05-17 09:57:57 -06:00
Jonas Östanbäck 05ea5c32be Fix lint warning by renaming MaxBytesExceededErr -> ErrMaxBytesExceeded (#1676)
Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>
2017-05-17 09:57:11 -06:00
Jonas Östanbäck a3b2a6a296 log: Add check for maximum number of arguments to log directive (#1672)
* Add check for maximum number of arguments to log directive
 * Add failing test case

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>

* Change else ifs into switch

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>

* Refactor

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>

* Typo

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>
2017-05-17 09:51:58 -06:00
Lucas Fontes 724829b689 proxy: leftover from #1666 (#1669) 2017-05-14 12:27:19 -06:00
Lucas Fontes 73494ce63a proxy: added 'health_check_port' to upstream (#1666)
* proxy: added 'health_check_port' to upstream

* proxy: `net.JoinHostPort` instead of `fmt.Printf` for upstream checks

* proxy: changing health_check_port type (int->string)

adding tests for invalid port config
2017-05-13 16:49:06 -06:00
Leonard Hecker 5f860d3a9f proxy: Fixed #1502: Proxying of unannounced trailers (#1588) 2017-05-13 10:08:33 -06:00
Matt Holt 6bb84ba19c Merge pull request #1664 from tw4452852/1663-log
log: allow additional prefix/suffix with predefined format
2017-05-11 18:10:49 -06:00
Tw 710f38043e log: allow additional prefix/suffix with predefined format
Signed-off-by: Tw <tw19881113@gmail.com>
2017-05-11 16:15:41 +08:00
Tw 958abcfa4c proxy: synchronize websocket test (#1654)
fix issue #1652

Signed-off-by: Tw <tw19881113@gmail.com>
2017-05-08 21:16:18 -06:00
Thibault Meyer ea24744bbf Add datadog plugin directive (#1655)
Signed-off-by: Thibault Meyer <meyer.thibault@gmail.com>
2017-05-08 11:48:22 -06:00
Matt Holt f06b825f44 Merge pull request #1656 from tw4452852/1587-limits
Introduce `limits` middleware
2017-05-08 11:39:10 -06:00
George Lesica 642aa63a9c markdown: Support Include arguments for Markdown. (#1653)
Previously, the `Include` override used with the markdown plugin did not
provide the optional `args` parameter. This made it impossible to pass
arguments to a template used with that plugin.
2017-05-08 10:32:14 -06:00
Tw ae645ef2e9 Introduce limits middleware
1. Replace original `maxrequestbody` directive.
2. Add request header limit.

fix issue #1587

Signed-off-by: Tw <tw19881113@gmail.com>
2017-05-08 17:18:04 +08:00
Matthew Holt 90efff68e5 dist: Delete old build automation program
We now use the release program to assist in deploying Caddy (it is much
more integrated and automated): https://github.com/caddyserver/releaser

This older automation code can still be found in this gist:
https://gist.github.com/mholt/cb7285f4950cb93f23be0aa6050fb043
2017-05-05 18:12:41 -06:00
Tw e38921f4a5 httpserver: rename context Push action for more general use (#1641)
Signed-off-by: Tw <tw19881113@gmail.com>
2017-05-05 17:36:20 -06:00
Tw 8e7a36de45 ResponseWriterWrapper and HTTPInterfaces (#1644)
Signed-off-by: Tw <tw19881113@gmail.com>
2017-05-05 09:42:06 -06:00
Sebastian Mancke 86d107f641 added 'login' directive for github.com/tarent/loginsrv/caddy (#1648) 2017-05-04 05:17:53 -06:00
Matthew Holt dfebffb1ee Update readme and changes for version 0.10.2 2017-05-02 12:02:28 -06:00
Matthew Holt 59a5afab29 fastcgi: Prepend missing leading slash when matching paths (see #1645)
httpserver: More path matching tests
2017-05-02 11:20:50 -06:00
Matthew Holt d8fb2ddc2d Fix contributing link in readme 2017-05-02 11:02:25 -06:00
Matthew Holt 5e467883b8 httpserver: Base path of "/" matches all paths, even empty ones
Fixes #1645
2017-05-02 09:43:43 -06:00
Matthew Holt 9fbac10a4b Revert "rewrite: Raise error if rewrite path does not begin with / #1610 (#1629)"
This reverts commit e0ed709397.
2017-05-02 09:30:18 -06:00
Matthew Holt 6d9783a267 Update changelog and readme for 0.10.1. 2017-05-01 23:50:58 -06:00
Matt Holt d5371aff22 httpserver/all: Clean up and standardize request URL handling (#1633)
* httpserver/all: Clean up and standardize request URL handling

The HTTP server now always creates a context value on the request which
is a copy of the request's URL struct. It should not be modified by
middlewares, but it is safe to get the value out of the request and make
changes to it locally-scoped. Thus, the value in the context always
stores the original request URL information as it was received. Any
rewrites that happen will be to the request's URL field directly.

The HTTP server no longer cleans /sanitizes the request URL. It made too
many strong assumptions and ended up making a lot of middleware more
complicated, including upstream proxying (and fastcgi). To alleviate
this complexity, we no longer change the request URL. Middlewares are
responsible to access the disk safely by using http.Dir or, if not
actually opening files, they can use httpserver.SafePath().

I'm hoping this will address issues with #1624, #1584, #1582, and others.

* staticfiles: Fix test on Windows

@abiosoft: I still can't figure out exactly what this is for. 😅

* Use (potentially) changed URL for browse redirects, as before

* Use filepath.ToSlash, clean up a couple proxy test cases

* Oops, fix variable name
2017-05-01 23:11:10 -06:00
Matt Holt 5685a16449 Merge pull request #1642 from tw4452852/internal
internal: inherit original ResponseWriter's interfaces
2017-05-01 23:06:45 -06:00
Tw f58653bc13 internal: inherit original ResponseWriter's interfaces
Signed-off-by: Tw <tw19881113@gmail.com>
2017-05-02 10:53:16 +08:00
Toby Allen e0ed709397 rewrite: Raise error if rewrite path does not begin with / #1610 (#1629)
* Raise syntax error if no '/' prefix to rewrite. Added Tests

* fix case where to keyword is used.

* Fixed spelling issue

* Changes to use Errf rather than new Err function

* Remove new RewritePathErr Function
2017-05-01 13:45:40 -06:00
Matt Holt b3dd604904 Merge pull request #1637 from devangels/bugfix1628
Bugfix for issue #1628 where `Caddyfile` is not being hidden correctly
2017-05-01 08:47:44 -06:00
Simon Lightfoot 8f09ed8f0d Bugfix for issue #1628 where Caddyfile is not being hidden correctly on windows.
Added test case to check if Caddyfile is added to HiddenFiles correctly.
2017-05-01 14:21:49 +01:00
Matt Holt 49d79d7ebc Merge pull request #1598 from tw4452852/1589
proxy: recognize client's cancellation
2017-04-30 08:19:03 -06:00
Matt Holt 4c034f6ad1 Merge pull request #1613 from tw4452852/addlink
Context: add Push action
2017-04-30 08:09:53 -06:00
Matt Holt 503c6b392c Merge pull request #1635 from aaronellington/lint-fixes
lint fixes
2017-04-29 23:49:59 -06:00
Tw 0146bb4e49 proxy: recognize client's cancellation
fix issue #1589

Signed-off-by: Tw <tw19881113@gmail.com>
2017-04-30 10:14:19 +08:00
Aaron Ellington 7ee4ea244f lint fixes 2017-04-29 20:53:58 -04:00
Matthew Holt 705cb98865 Remove TODO because Let's Encrypt has fixed issuance/OCSP ordering 2017-04-29 16:49:07 -06:00
Matthew Holt ff45801cda Minor improvements to PR template 2017-04-29 16:49:07 -06:00
Tw 761a32a080 context: add Push action
Signed-off-by: Tw <tw19881113@gmail.com>
2017-04-27 09:51:33 +08:00
AJ ONeal aa7ecb02af update macOS launchd example (#1600)
* update to match same paths as systemd

* update to match new launchd plist

* generated from scratch with LaunchControl, flawless

Had some issues with the previous go and found LaunchControl which made it easy to generate a perfectly correct launchd conf

* Update README.md
2017-04-26 13:44:32 -06:00
Matthew Holt 5d7db89a90 httpserver: Proper HTTP->HTTPS for wildcard sites (fixes #1625) 2017-04-26 12:32:15 -06:00
Francis Lavoie 1bae36ef29 Fix 1592: Allow insecure CA URL on internal networks (#1607)
* Strip brackets in IsInternal if no port, allow loopback for CA URLs

* Fix a mistake

* Improve the trim

* Fix comment
2017-04-26 12:00:49 -06:00
emersion 52fd4f89bf dist/init: Better way to get systemd version (#1612) 2017-04-26 10:45:57 -06:00
Tw cad89a07e0 gzip: pool gzip.Writer to reduce allocation (#1618)
* gzip: add benchmark

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

* gzip: pool gzip.Writer to reduce allocation

Signed-off-by: Tw <tw19881113@gmail.com>
2017-04-26 00:23:50 -06:00
Matthew Holt b18527285d Optimizations should be benchmarked or profiled. 2017-04-25 11:05:20 -06:00
Matthew Holt 1deb99c75c Update contributing guide, make PR template, move github assets 2017-04-25 11:01:22 -06:00
Matthew Holt 0775f9123c Change forum links to new domain 2017-04-23 12:51:08 -06:00
Matthew Holt 5fbd63e35d Update README to match new site, branding, and latest version 2017-04-22 17:32:42 -06:00
Matthew Holt f09fff3d8b Remove ineffectual assignment created by reverting 344017d (#1584) 2017-04-21 22:26:38 -06:00
Matthew Holt 0a798aafac mitm, templates, context: Pool buffers to reduce allocations
Also disable some tests on context.Hostname because they're not portable
2017-04-21 19:54:25 -06:00
Matthew Holt f8614b877d Revert 344017dc21 (#1584) 2017-04-21 13:02:15 -06:00
Matt Holt 182e1b4fb2 Merge pull request #1605 from tw4452852/1604
proxy: take original URL path into account when remove prefix ("without")
2017-04-21 12:07:14 -06:00
Tw c684de9a88 proxy: take original URL path into account when remove prefix
fix issue #1604

Signed-off-by: Tw <tw19881113@gmail.com>
2017-04-21 19:35:32 +08:00
Matthew Holt 27785f7993 Update readme and changelog for v0.10 2017-04-20 11:36:40 -06:00
Matt Holt ad4191a07e Merge pull request #1596 from mholt/closinglogs
httpserver: Don't close stdout or stderr when closing logs (fix #1471)
2017-04-20 05:39:42 -06:00
Jonas Östanbäck 91da965a39 Disable warning for insecure CA if located on private network. (#1599)
* Disable warning for insecure CA if located on private network.
 * Add IsPrivateNetwork function
 * Add tests

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>

* Add more testcases

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>

* Rename IsPrivateNetwork -> IsInternal

Signed-off-by: Jonas Östanbäck <jonas.ostanback@gmail.com>
2017-04-20 05:38:54 -06:00
Matt Holt b37da03989 Merge pull request #1595 from jannickfahlbusch/feature/hostnameInTemplateActions
templates: Add Hostname template action
2017-04-19 16:20:16 -06:00
Jannick Fahlbusch 92af3ee4d8 Add hostname template action
This adds the ability to display the remote hostname
of the visitors IP with template actions.
2017-04-19 09:18:12 +02:00
Matthew Holt 1e8ab1cadf httpserver: Don't close stdout or stderr when closing logs (fix #1471) 2017-04-18 16:01:11 -06:00
Matt Holt 729e4f0239 Merge pull request #1594 from tw4452852/templateFuncsTest
template: add test for custom function
2017-04-18 09:23:06 -06:00
Tw 790c842fad template: add test for custom function
Signed-off-by: Tw <tw19881113@gmail.com>
2017-04-18 22:49:20 +08:00
Matt Holt f28a159b72 Merge pull request #1591 from tw4452852/template_funcs
template: support custom functions
2017-04-18 08:14:00 -06:00
Tw f77a7a805a template: support custom functions
Signed-off-by: Tw <tw19881113@gmail.com>
2017-04-18 16:17:30 +08:00
Matt Holt 236341f78b Merge pull request #1584 from tw4452852/encoded_path
proxy: use untouched URL for concatenating
2017-04-17 23:39:14 -06:00
Matthew Holt ac3bbdbd3f tls: Arrange cipher suites in map in preference order for convenience
Maps are NOT ordered at runtime but I still want the cipher suites
listed in order of preference in the source file for convenience.
2017-04-17 22:11:05 -06:00
Matthew Holt ce2a9cd8f9 push: Reorder before proxy; and allow zero arguments (cf. #1573) 2017-04-17 22:06:17 -06:00
Matthew Holt 4462e3978b httpserver: max_certs now forces On-Demand TLS even if name is known
Original feature request in forum:
https://forum.caddyserver.com/t/caddy-with-specific-hosts-but-on-demand-tls/1704?u=matt

Before, Caddy obtained certificates for every name it could at startup.
And it would only obtain certificates during the handshake for sites
defined with a hostname that didn't qualify at startup (like
"*.example.com" or ":443"). This made sense for most situations, and
helped ensure that certificates were obtained as early and reliably as
possible.

With this change, Caddy will NOT obtain certificates for hostnames it
knows at startup (even if they qualify) if OnDemand is enabled.

But I think this change generalizes well, because a user who specifies
max_certs is deliberately turning on On-Demand TLS, fully aware of
the consequences. It seems dubious to ignore that config when the user
deliberately put it there. We'll see how this goes.
2017-04-17 19:53:15 -06:00
Tw 344017dc21 proxy: use untouched URL for concatenating
Signed-off-by: Tw <tw19881113@gmail.com>
2017-04-18 09:07:38 +08:00
elcore a56a833423 caddyhttp: New index directive for alternate index file names (#1567)
* caddyhttp: Allow to alternate Index

* Move Index directive

* Fix misspelling outside this PR
2017-04-17 11:02:44 -06:00
Matt Holt 6b66b19deb Merge pull request #1583 from tw4452852/1529
log: only allow new roller related options in a block
2017-04-17 10:01:15 -06:00
Francis Lavoie 33257de2e8 proxy: Fix #1574; health check now respects hostname when upstream Host header is configured (#1577)
* Implement adding Host header to health check

* Fix type problems

* Fix duplicate function, Replace args

* Add debugging

* Add debugging

* Add debugging

* Add debugging

* Attempt to set req.Host instead of the header

* Clean up debugging

* Fix missing newline

* Fix spelling

* Add test, refactoring

* Fix with gofmt

* Add error check on NewRequest
2017-04-17 09:58:47 -06:00
Tw 702dec0647 log: only allow new roller related options in a block
fix issue #1529

Signed-off-by: Tw <tw19881113@gmail.com>
2017-04-17 16:55:45 +08:00
Matthew Holt 8d1da68b47 D'oh, commit all changes to file 2017-04-15 22:58:34 -06:00
Matthew Holt 7a7e3d160b context: Use crypto/rand in case method used for crypto purposes 2017-04-15 22:32:41 -06:00
Matthew Holt 5a1243ff42 context: Fix computation for random length of random string 2017-04-15 16:48:44 -06:00
Matthew Holt edf9cd34cc context: RandomString action produces a random string of random length 2017-04-15 16:38:45 -06:00
Matthew Holt f415ea263e browse: Use more standard sort icon over icons 2017-04-14 16:11:26 -06:00
Matthew Holt 3ca419e2cf browse: Correct links when site defined with a path (fixes #1561) 2017-04-14 12:29:26 -06:00
Toby Allen 7d15435361 markdown: Match index file for each extension; fix #1418 (#1559)
* Create list of index files based on extensions and check on a per config
basis

* remove log lines

* fixed tests

* made gofmt suggested change

* Changes made to simplify
2017-04-08 00:47:33 -06:00
Matt Holt e26a855d8b Merge pull request #1546 from wmark/browse-sort-size
browse: fix arbitrary ordering of directories with sort=size
2017-04-07 18:32:02 -06:00
Peer Beckmann c0ce2b1d50 proxy: Respect insecure_skip_verify for health check (#1558)
* Respect the 'insecure_skip_verify' for the health check.

* WIP: Trying to add a test. Non functional.

* Fixing tests.

* Creating better error messages.

* Optimize two more error messages.

* Move the tests into an extra function.
2017-04-03 15:16:31 -06:00
Angel Santiago 59bf71c293 proxy: Cleanly shutdown health checks on restart (#1524)
* Add a shutdown function and context to staticUpstream so that running goroutines can be cancelled. Add a GetShutdownFunc to Upstream interface to expose the shutdown function to the caddy Controller for performing it on restarts.

* Make fakeUpstream implement new Upstream methods.

Implement new Upstream method for fakeWSUpstream as well.

* Rename GetShutdownFunc to Stop(). Add a waitgroup to the staticUpstream for controlling individual object's goroutines. Add the Stop function to OnRestart and OnShutdown. Add tests for checking to see if healthchecks continue hitting a backend server after stop has been called.

* Go back to using a stop channel since the context adds no additional benefit.
Only register stop function for onShutdown since it's called as part of restart.

* Remove assignment to atomic value

* Incrementing WaitGroup outside of goroutine to avoid race condition. Loading atomic values in test.

* Linting: change counter to just use the default zero value instead of setting it

* Clarify Stop method comments, add comments to stop channel and waitgroup and remove out of date comment about handling stopping the proxy. Stop the ticker when the stop signal is sent
2017-04-02 14:58:15 -06:00
Toby Allen 464ade1da7 Add new browse sort - namedirfirst (#1551)
* Revert "browse: sort listing by dir first (#1527)"

 commit 4e1229e7c9.

* Add new browse sort order  namedirfirst. Make namedirfirst default sort
2017-04-02 20:38:14 +01:00
Matthew Holt ce47cf51be Add launch event information to readme 2017-03-31 20:59:59 -06:00
Matt Holt 6be0386716 Merge pull request #1541 from lucafavatella/review-certs-dir
init: Make CADDYPATH equal across init scripts
2017-03-31 12:45:56 -06:00
W-Mark Kubacki 398d9a6bb5 browse: when sorting by size, sort directory section by name
Previously directories have been merely pulled to the front, and then
sorted arbitrarily. That is, their order among themselves depended on
the filesystem implementations. Something opaque to the visitor.

This fixes said inconsistency, and implements the by-size-then-by-name
order I initially intended for this.
2017-03-29 16:40:29 +02:00
Luca Favatella 956266cd79 Make CADDYPATH equal across init scripts
See also https://github.com/mholt/caddy/pull/1272#issue-191256343
2017-03-28 22:04:19 +01:00
Henrique Dias 6cabc9bfe3 Add Event Hook plugins to DescribePlugins function (#1540)
* Add Event Hook plugins to DescribePlugins function

* Update plugins.go

* Update plugins.go
2017-03-28 08:19:29 -06:00
Henrique Dias da674fd599 Introducing Event Hooks Plugins (#1537)
* add Hook

* update hooks

* remove parenthesis

* Update requests
2017-03-27 18:05:59 -06:00
Toby Allen 4e1229e7c9 browse: sort listing by dir first (#1527)
* Default Browse sort by Dir, File

* Ignore temp template files

* Add folder to test

* unneeded chagne
2017-03-24 22:54:16 -06:00
Matthew Holt 5341c85a27 Update issue template to ask for relevant HTTP requests 2017-03-22 18:58:57 -06:00
Matthew Holt fbd6412359 Don't modify request URL values from Caddyfile (fixes #1528)
HTTP redirects can use host-relative URLs. See discussion in #1497.
2017-03-20 21:05:11 -06:00
ericdreeves 36d2027493 browse: Use helper functions in staticfiles to redirect (#1497)
* Use helper functions in staticfiles to redirect.

Previously the browse package invoked staticfiles.Redirect when
redirecting clients who requested a directory but with a Request-URI
that did not contain a trailing '/'. staticfiles.Redirect only used a
relative URI. This change defers the decision of how to format the
Location header value to the helper methods in the staticfiles package.

* Update const URLPathCtxKey in browse package.
2017-03-15 10:17:12 -06:00
Peer Beckmann a148b92381 proxy: Add the first policy (#1513)
* Add the first policy which sends the request to the first available host

* Make the error message clear. As we expect the second not first upstream
host.
2017-03-14 09:57:08 -06:00
Chao Huang 36a62f0915 markdown: backup title should use TrimSuffix, not TrimRight (#1515) 2017-03-13 15:24:03 -06:00
Matthew Holt d85e90a7b4 tls: Only update OCSP staple if field is not nil (fixes #1514) 2017-03-13 09:30:58 -06:00
Toby Allen d5cc10f7aa Added Const for use of CtxKeys (#1511)
* Added Const for CtxKeys

* Move CtxKey Const declarations

* Fixed tests

* fix test
2017-03-13 09:22:46 -06:00
Martin Bertschler 96bfb9f347 staticfiles: add Content-Length header (closes #1479) (#1492)
* staticfiles: add Content-Length header (closes #1479)

* make linter happy, rename "Html" in identifiers to "HTML"
2017-03-12 16:41:49 -06:00
Matt Holt 5e48f0a412 Merge pull request #1510 from zmwangx/systemd-readme-fix-caddy.service-mode
systemd README: set mode of caddy.service to 644 instead of 744
2017-03-12 00:03:24 -07:00
Zhiming Wang 18c93756b4 systemd README: set mode of caddy.service to 644 instead of 744 2017-03-11 22:43:54 -05:00
Toby Allen cfe52084aa Fix issue #1346 {path} logging {uri} and add {rewrite_uri} placeholder (#1481)
* Fixed issue with {path} actually {uri}

* Test added for path rewrite

* add in uri_escaped

* added rewrite_uri and test

* fix broken test.  Just checks for existance of rewrite header

* gitignore

* Use context to store uri value

* ignore .vscode

* tidy up, removal of comments and invalidated tests

* Remove commented out code.

* added comment as requested by lint

* fixed spelling mistake

* clarified code with variable name

* added context for uri and test

* added TODO comment to move consts
2017-03-11 14:59:47 -07:00
Matthew Holt 6aa0e30af3 basicauth: Don't remove Authorization header on good auth (fixes #1508) 2017-03-10 16:45:51 -07:00
Leonard Hecker 5a41e8bc1a proxy: Fixed #1484, websockets with h2 disabled (#1488)
* Fixed #1484

Fixed a nil pointer runtime error in newConnHijackerTransport,
where the access to the TLSClientConfig did not check for nil values.

* Minor improvement to UseInsecureTransport

This prevents overwriting a possibly preexisting TLSClientConfig,
even though only a single field should be changed.
2017-03-10 10:41:37 -07:00
Nathan Caza 9e4eeb4fb7 Add proxyprotocol directive and listener middleware plugin type (#1349)
* add support for listener middleware

* add proxyprotocol directive

* make caddy.Listener interface required

* Remove tcpKeepAliveListener wrapper from Serve()
This is now done in the Listen() function, along with other potential middleware.
2017-03-09 22:31:37 -07:00
ssut c62b6b9f1a fastcgi: Fix for missing content-length header when using QUIC (#1501)
* Fix for missing content-length header when using QUIC

If request.ContentLength is set then it will be used instead of getting
it from request.Header map since quic-go(lucas-clemente/quic-go@bb24be8)
will not store (and pass) the Content-Length header using its header
map.

This fixes a potential issue where FastCGI POST requests body empty when
QUIC is enabled. (#1370)

* Change the data type for fastcgi contentLength to int64

quic-go uses int64 for contentLength

* Fix an error for undeclared variable

* Fix test for fcgiclient

the data type for contentLength
2017-03-09 19:36:04 -07:00
Matt Holt 52584f7f23 Merge pull request #1499 from mholt/issue1394
Log certificate location
2017-03-09 17:45:06 -07:00
Matt Holt 2be0dc40f0 templates: Merge pull request #1482 from crvv/master
templates: Set MIME type
2017-03-09 17:26:08 -07:00
Kurt Jung e3e62a952d basicauth: Ability to customize realm (#1491)
* Support realms with basic authentication

* Add test for default basicauth directive in which realm is not specified

* Correct typo: missing space

* Remove 'path' subdirective
2017-03-09 13:20:14 -07:00
Matthew Holt 6bc3e7536e tls: Command line flags to disable HTTP and TLS-SNI challenges
This could have just as easily been a tls directive property in the
Caddyfile, but I figure if these challenges are being disabled, it's
because of port availability or process privileges, both of which would
affect all sites served by this process. The names of the flag are long
but descriptive.

I've never needed this but I hear of quite a few people who say they
need this ability, so here it is.
2017-03-08 00:06:49 -07:00
Matt Holt df9d062a8f Merge pull request #1500 from mholt/customports
httpserver: Flags to customize HTTP and HTTPS ports (including for ACME challenges)
2017-03-07 11:31:47 -07:00
Matt Holt eafbf0b218 Merge pull request #1474 from jtyr/jtyr-local_ip
Adding ServerIP context
2017-03-07 08:55:40 -07:00
Jiri Tyr 73d52490d0 Adding support for ServerIP context 2017-03-07 11:59:26 +00:00
crvv 4a095590b1 templates: Set right response Content-Type
If use gzip and templates at the same time, the response body will
be gzipped data. And in this case, the Content-Type header won't be
set by Caddy code. Then Go http package will set "Content-Type" to
wrong value "application/x-gzip" which is determined by response body.
So the header Contenty-Type should be set in templates middleware.
2017-03-07 11:11:52 +08:00
Matthew Holt c8514ad7b7 Avoid panic if reloading before server is started
See: https://forum.caddyserver.com/t/reloading-template-files-as-they-change/1483/3?u=matt

The server takes a moment to start; if USR1 is received before the
instance is saved, it would panic because no instances have been saved.
Instead, we just ignore the signal since no config has finished loading.
2017-03-06 19:20:26 -07:00
Matthew Holt e3f2d96a5e httpserver: Flags to customize HTTP and HTTPS ports (incl. for ACME)
This commit removes _almost_ all instances of hard-coded ports 80 and
443 strings, and now allows the user to define what the HTTP and HTTPS
ports are by the -http-port and -https-ports flags.

(One instance of "80" is still hard-coded in tls.go because it cannot
import httpserver to get access to the HTTP port variable. I don't
suspect this will be a problem in practice, but one workaround would be
to define an exported variable in the caddytls package and let the
httpserver package set it as well as its own HTTPPort variable.)

The port numbers required by the ACME challenges HTTP-01 and TLS-SNI-01
are hard-coded into the spec as ports 80 and 443 for good reasons,
but the big question is whether they necessarily need to be the HTTP
and HTTPS ports. Although the answer is probably no, they chose those
ports for convenience and widest compatibility/deployability. So this
commit also assumes that the "HTTP port" is necessarily the same port
on which to serve the HTTP-01 challenge, and the "HTTPS port" is
necessarily the same one on which to serve the TLS-SNI-01 challenge. In
other words, changing the HTTP and HTTPS ports also changes the ports
the challenges will be served on.

If you change the HTTP and HTTPS ports, you are responsible for
configuring your system to forward ports 80 and 443 properly.

Closes #918 and closes #1293. Also related: #468.
2017-03-06 18:18:49 -07:00
Toby Allen bcddfb2daa Log certificate location 2017-03-06 21:56:24 +00:00
Samuel BERTHE 75ccc05d84 Request placeholders: extract query argument (#1478)
* feat(request placeholders): adds {?arg}

* test(request placeholders): test query argument extractor {?arg}
2017-03-02 23:25:28 -07:00
ericdreeves 0a0d2cc1cf Use RequestURI when redirecting to canonical path. (#1331)
* Use RequestURI when redirecting to canonical path.

Caddy may trim a request's URL path when it starts with the path that's
associated with the virtual host. This change uses the path from the request's
RequestURI when performing a redirect.

Fix issue #1327.

* Rename redirurl to redirURL.

* Redirect to the full URL.

The scheme and host from the virtual host's site configuration is used
in order to redirect to the full URL.

* Add comment and remove redundant check.

* Store the original URL path in request context.

By storing the original URL path as a value in the request context,
middlewares can access both it and the sanitized path. The default
default FileServer handler will use the original URL on redirects.

* Replace contextKey type with CtxKey.

In addition to moving the CtxKey definition to the caddy package, this
change updates the CtxKey references in the httpserver, fastcgi, and
basicauth packages.

* httpserver: Fix reference to CtxKey
2017-02-28 05:54:12 -07:00
Matthew Holt 50749b4e84 httpserver: Improve MITM tests for Chrome on iOS, BlueCoat connections 2017-02-27 18:40:40 -07:00
Matthew Holt 06873175bf httpserver: Add user agent to test case (closes #1454) 2017-02-22 14:29:19 -07:00
Matthew Holt f49e0c9b56 httpserver: Disable default timeouts (closes #1464)
Timeouts are important for mitigating slowloris, yes. But after a number
of complaints and seeing that default timeouts are a sore point of
confusion, we're disabling them now. However, the code that sets
default timeouts remains intact; the defaults are just the zero value.

While Caddy aims to be secure by default, Caddy also aims to serve a
worldwide audience. Even my own internet here in Utah is poor at times,
with bad WiFi signal, causing some connections to take over 10s to
be established. Many use the Internet while commuting on slower
connection speeds. Latency across country borders is another concern.

As such, disabling default timeouts will serve a greater population of
users than enabling them, as slowloris is easy to mitigate and does
not seem to be reported often (I've only seen it once). It's also very
difficult sometimes to distinguish slowloris from genuine slow networks.
That decision is best left to the site owner for now.
2017-02-22 08:52:08 -07:00
Matthew Holt ccdc28631a httpserver: Remove unused ReadTimeout from tlsHelloListener 2017-02-21 21:51:07 -07:00
Matthew Holt a2c410b8e1 Add some MITM test cases 2017-02-21 10:07:12 -07:00
Matt Holt 73794f2a2c tls: Refactor internals related to TLS configurations (#1466)
* tls: Refactor TLS config innards with a few minor syntax changes

muststaple -> must_staple
"http2 off" -> "alpn" with list of ALPN values

* Fix typo

* Fix QUIC handler

* Inline struct field assignments
2017-02-21 09:49:22 -07:00
Matt Holt 4b877eebc4 Revert "Revert removed method" (#1465) 2017-02-20 08:47:56 -07:00
Toby Allen c4842e0fc1 Merge pull request #1462 from mholt/fix_1461
Revert removed method IncrNest
2017-02-20 15:20:10 +00:00
Mateusz Gajewski ff8c430ff0 Revert removed method 2017-02-20 13:43:42 +01:00
Mateusz Gajewski 1262ae92e9 Disable TLS completely if there is no listener with tls enabled (#1456)
* Disable TLS completely if there is no listener with tls enabled

* Format code
2017-02-19 08:09:35 -07:00
Rick Beton 6083871088 Revised fileserver Accept-Encoding and ETag (#1435)
* Revised fileserver Accept-Encoding and ETag

* calculateEtag improved following microbenchmarking
2017-02-18 15:52:50 -07:00
Mateusz Gajewski ce3580bf91 Push down headers from client (#1453)
* Push down headers from client

* Push first, serve later

* After review fixes
2017-02-18 15:50:36 -07:00
Matthew Holt 9720da5bc8 proxy: Fix race in test 2017-02-18 15:42:11 -07:00
Mateusz Gajewski 286d8d1e89 tls: Per-site TLS configs using GetClientConfig, including http2 switch (#1389)
* Remove manual TLS clone method

* WiP tls

* Use GetClientConfig for tls.Config

* gofmt -s -w

* GetConfig

* Handshake

* Removed comment

* Disable HTTP2 on demand

* Remove junk

* Remove http2 enable (no-op)
2017-02-18 15:26:23 -07:00
Kurt Jung 977a3c3226 basicauth: Store name of authenticated user (#1426)
* Store name of authenticated user in basicauth for use by upstream middleware such as fastcgi and cgi.

* Use request context to transfer name of authorized user from basicauth to upstream middleware. Test retrieval of name from context.

* Remove development code that was inadvertently left in place

* Use keys of type httpserver.CtxKey to access Context values
2017-02-17 15:37:58 -07:00
Matt Holt 82cbd7a96b Detect HTTPS interception (#1430)
* WIP: Implement HTTPS interception detection by Durumeric, et. al.

Special thanks to @FiloSottile for guidance with the custom listener.

* Add {{.IsMITM}} context action and {mitm} placeholder

* Improve MITM detection heuristics for Firefox and Edge

* Add tests for MITM detection heuristics

* Improve Safari heuristics for interception detection

* Read ClientHello during first Read() instead of during Accept()

As far as I can tell, reading the ClientHello during Accept() prevents
new connections from being accepted during the read. Since Read() should
be called in its own goroutine, this keeps Accept() non-blocking.

* Clean up MITM detection handler; make possible to close connection

* Use standard lib cipher suite values when possible

* Improve Edge heuristics and test cases

* Refactor MITM checking logic; add some debug statements for now

* Fix bug in MITM heuristic tests and actual heuristic code

* Fix gofmt

* Remove debug statements; preparing for merge
2017-02-17 14:07:57 -07:00
Mateusz Gajewski cdf7cf5c3f HTTP/2 push support (golang 1.8) (#1215)
* WIP

* HTTP2/Push for golang 1.8

* Push plugin completed for review

* Correct build tag

* Move push plugin position

* Add build tags to tests

* Gofmt that code

* Add header/method validations

* Load push plugin

* Fixes for wrapping writers

* Push after delivering file

* Fixes, review changes

* Remove build tags, support new syntax

* Fix spelling

* gofmt -s -w .

* Gogland time

* Add interface guards

* gofmt

* After review fixes
2017-02-17 09:25:22 -07:00
elcore 579007822f Add support for ChaCha20-Poly1305 (#1443) 2017-02-16 22:16:29 -07:00
Matt Holt e50de809a5 Merge pull request #1378 from tw4452852/1362
proxy: handle encoded path in URL
2017-02-16 21:22:11 -07:00
Tw c37481cc7b proxy: handle encoded path in URL
fix issue #1362

Signed-off-by: Tw <tw19881113@gmail.com>
2017-02-17 09:41:00 +08:00
elcore 91ff734327 Implement curve X25519 (Golang 1.8) (#1376)
* Implement curve X25519

* caddytls: Added a default curves list

* caddytls: Improve tests
2017-02-16 17:19:58 -07:00
Matt Holt 524dcee9f6 Merge pull request #1373 from mholt/go18shutdown
Replace our old faithful gracefulListener with Go 1.8's Shutdown()
2017-02-16 16:56:05 -07:00
Matt Holt 0cc48e849c Merge pull request #1374 from mholt/go18timeouts
Set Go 1.8's ReadHeaderTimeout and IdleTimeout
2017-02-16 16:51:00 -07:00
Matt Holt 58b2edd229 Merge pull request #1354 from mholt/keyrotationfix
Set session ticket keys properly (fixed in Go 1.8)
2017-02-16 16:43:02 -07:00
Matt Holt 6271abb22a Update CI to use Go 1.8 (#1444) 2017-02-16 16:37:12 -07:00
Toby Allen 58053fce48 Merge branch 'master' into go18shutdown 2017-02-16 22:46:11 +00:00
Alex Harrington 55bded68c2 fixing panic when root is symlink (#1429)
* fixing panic when root is symlink
checking root path is a symlink before os.Stat which panics

* fixing formatting

* adding test to verify symlink root path check

* fixing typo
2017-02-15 22:02:51 -07:00
Augusto Roman dc3efc939c Add request placeholder support for querying request cookies. (#1392)
* Add request placeholder support for querying request cookies.

This adds the ability to query the request cookies for placeholders
using the syntax "@cookiename".

For example, this would allow rewriting based on a cookie:
  rewrite {
    if @version is 'dev'
    to /dev/index.html
  }

* Switch cookie special char from @ to :

* Switch special char for cookies from : to ~
2017-02-15 21:59:24 -07:00
Matt Holt bdb61f4a1d Merge pull request #1409 from mastercactapus/not_a_directory
return 404 for "not a directory" errors
2017-02-15 18:34:01 -07:00
Matt Holt 1183d91c7b Merge pull request #1365 from tw4452852/1297
redirect: determine the FromScheme at runtime (#1297)
2017-02-15 17:59:29 -07:00
Augusto Roman 463c9d9dd2 Fix data race for max connection limiting in proxy directive. (#1438)
* Fix data race for max connection limiting in proxy directive.

The Conns and Unhealthy fields are updated concurrently across all active
requests.  Because of this, they must use atomic operations for reads and
writes.

Prior to this change, Conns was incremented atomically, but read unsafely.
Unhealthly was updated & read unsafely.  The new test
TestReverseProxyMaxConnLimit exposes this race when run with -race.

Switching to atomic operations makes the race detector happy.

* oops, remove leftover dead code.
2017-02-15 08:09:42 -07:00
Matt Holt 1bd9e9e590 Merge pull request #1436 from rohanpai/master
Added Sourcegraph badge to README
2017-02-14 16:32:55 -07:00
Rohan Pai b650a26727 Added Sourcegraph badge to README 2017-02-14 14:32:12 -08:00
Matt Holt 943ed931db Merge pull request #1425 from jung-kurt/cgi
httpserver: Register cgi plugin
2017-02-14 06:59:32 -07:00
Toby Allen 2417d70bcb Merge pull request #1431 from mholt/issue1388c
Fix for #1388 starting with no Caddyfile
2017-02-14 10:23:46 +00:00
Toby Allen 1a7612071a remove whitespace 2017-02-13 21:28:19 +00:00
Toby Allen 5072d70f38 Fix for #1388 dont attempt to hide Caddyfile if non existant 2017-02-13 21:22:19 +00:00
Kurt b210101f45 Register cgi plugin 2017-02-11 09:38:25 -05:00
Nathan Caza 18edf5864e add fix from golang/go 2017-02-10 21:02:00 -06:00
Julian V. Modesto ce7d3db1be Roll all logs by default (#1379)
* Use new subdirectives and flatten rolling config

* Set default rotate config

* Set default rolling config (hopefully) errwhere

* Make private

* Flatten errors directive and remove c.IncrNest()

* Don't skip first error log roller subdirective we see

* Remove hadBlock

* Try lumberjack import

* Unname import
2017-02-08 09:23:33 -07:00
Mateusz Gajewski f32eed1912 Feature #1246 - Remote syslog (#1301)
* Remote syslog

* golint

* Initialize mutex
2017-02-08 08:02:09 -07:00
Matt Holt cdb79a60f2 Merge pull request #1410 from JRaspass/patch-1
Replace magic number 308 with http.StatusPermanentRedirect
2017-02-07 17:10:05 -07:00
James Raspass 7419573266 Replace magic number 308 with http.StatusPermanentRedirect 2017-02-07 23:55:36 +00:00
Matt Holt d8f92baee2 Merge pull request #1404 from mholt/combinedrollers
Create only one log roller per file across whole process (fixes #1363)
2017-02-06 20:08:50 -07:00
Toby Allen 9e9298ee5d Added additional - to common log file format (#1399) 2017-02-04 15:29:29 -07:00
Matt Holt dc6c986b3f Merge pull request #1381 from tw4452852/958
httpserver: support QUIC reload
2017-01-28 09:40:14 -07:00
Tw 65cb966d38 httpserver: support QUIC reload
fix issue #958

Signed-off-by: Tw <tw19881113@gmail.com>
2017-01-28 19:41:24 +08:00
Matthew Holt d264a2cf0a Set Go 1.8's ReadHeaderTimeout and IdleTimeout 2017-01-24 20:09:03 -07:00
Matthew Holt 139a3cfb13 Replace our old faithful gracefulListener with Go 1.8's Shutdown() 2017-01-24 20:05:53 -07:00
Matthew Holt 04da9c7374 Create only one log roller per file across whole process (fixes #1363) 2017-01-24 19:16:54 -07:00
Matthew Holt 16250da3f0 errors: Fix low risk race condition at server close
See issue #1371 for more information.
2017-01-24 19:09:44 -07:00
Matthew Holt 45a0e4cf49 log: Fix race when stopping server
High improbability of being an actual problem. Logs are safe for
concurrent use, but os.Files are apparently not... Fixes #1371.
2017-01-24 18:48:19 -07:00
Matthew Holt e14a62f188 pprof: Set proper Content-Type header
The standard lib pprof library doesn't set its own Content-Type header
properly. If pprof is used with gzip, the index endpoint will be
interpreted as a .gz file; so we force its hand and set the header.
2017-01-24 16:55:43 -07:00
Matthew Holt 94e382ef0a Version 0.9.5 2017-01-24 08:29:01 -07:00
Matt Holt d8d339740b New 'timeouts' directive to configure timeouts; default timeouts enabled (#1368) 2017-01-24 08:15:25 -07:00
Matthew Holt 205aee6662 Godoc comment; report -validate results to stdout too 2017-01-23 23:48:12 -07:00
Matthew Holt 62fea30e87 browse: Sanitize file names and links in default template
Thanks to Kevin Froman (@beardog108) for the responsible heads up.
2017-01-23 22:37:46 -07:00
Toby Allen bbee961415 Introduce new Replacer fields {rewrite_path}, {rewrite_path_escaped}; issue #1185 (#1364)
* Fix #1185

* Return normal path if no rewrite has happened

* Revert change, not required

* Updated tests
2017-01-23 22:15:27 -07:00
Matthew Holt 59e6ceb518 Minor test cleanup 2017-01-23 22:06:55 -07:00
Matthew Holt 82929b122a Ensure active Caddyfile, if in site, is hidden no matter the cwd 2017-01-23 22:06:29 -07:00
Tw 38c76647c9 proxy: use a new context for the outgoing request (#1358)
* proxy: use a new context for the outgoing request

fix issue #1345

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

* proxy: add test for canceling the request

Signed-off-by: Tw <tw19881113@gmail.com>
2017-01-23 18:03:42 -07:00
Matt Holt 696b46f075 Merge pull request #1356 from mholt/fix_hijack
proxy: Fixed #1352: invalid use of the HTTP hijacker
2017-01-23 09:40:47 -07:00
Peer Beckmann e5ef285e59 Generate meta elements from prelude items description and keywords (#1335)
* Generate meta elements from useful front matters.
Limited to the default template and specific elements.

* Rerun gofmt

* Add "keywords" and remove "language" to/from the list of meta tags.

* Add a simple positive list test for the meta tag generation.

* Move the meta tag list to a var at the begin of the file.
Seperate the Meta tags from the other front matters:
	- Don't override user settings with name `meta`
	- Cleaner Code.

* Remove the uneccessary `[:]` in the []Bytes to String casting.
@mholt was right ;)

* One minor refinement. Combining two statements.
2017-01-22 19:16:38 -07:00
Matthew Holt 11adb2e5a7 tls: Always stop and report cert renewal error if operator is present 2017-01-21 15:14:04 -07:00
Matt Holt 9369b81498 Merge pull request #1366 from mholt/tls-sni-renew-fix
tls: Fix background certificate renewals that use TLS-SNI challenge
2017-01-21 15:05:22 -07:00
Matthew Holt 0e34c7c970 tls: Fix background certificate renewals that use TLS-SNI challenge
The loop which performs renewals in the background obtains a read lock
on the certificate cache map, so that it can be safely iterated. Before
this fix, it would obtain the renewals in the read lock. This has been
fine, except that the TLS-SNI challenge, when invoked after Caddy has
already started, requires adding a certificate to the cache. Doing this
requires an exclusive write lock. But it cannot obtain a write lock
because a read lock is obtained higher in the stack, while the loop
iterates. In other words, it's a deadlock.

I was able to reproduce this issue consistently locally, after jumping
through many hoops to force a renewal in a short time that bypasses
Let's Encrypt's authz caching. I was also able to verify that by queuing
renewals (like we do deletions and OCSP updates), lock contention is
relieved and the deadlock is avoided.

This only affects background renewals where the TLS-SNI(-01) challenge
are used. Users report seeing strange errors in the logs after this
happens ("tls: client offered an unsupported, maximum protocol version
of 301"), but I was not able to reproduce these locally. I was also not
able to reproduce the leak of sockets which are left in CLOSE_WAIT.
I am not sure if those are symptoms of running in production on Linux
and are related to this bug, or not.

Either way, this is an important fix. I do not yet know the ripple
effects this will have on other symptoms we've been chasing. But it
definitely resolves a deadlock during renewals.
2017-01-21 14:39:36 -07:00
Tw eeb23a2469 redirect: determine the FromScheme at runtime (#1297)
Signed-off-by: Tw <tw19881113@gmail.com>
2017-01-21 09:53:44 +08:00
Tw ecf852ea43 proxy: fix TestReverseProxy failure on go tip (#1360)
Because of this commit(6e36811c37399d60cbce587b7c48e611009c5aec) on go tip,
it will probe the request's body to determine whether to use chunked transfer
encoding which trailers depend on it.
So we just offer a non empty body to make trailers work.

fix issue #1359

Signed-off-by: Tw <tw19881113@gmail.com>
2017-01-18 15:34:25 -07:00
Mike Pastore 6bac558c98 Add root option to fastcgi directive (#1337) 2017-01-17 10:34:17 -07:00
Leonard Hecker ae10122f7e proxy: Fixed #1352: invalid use of the HTTP hijacker 2017-01-17 15:55:11 +01:00
Matthew Holt c6ba43f888 Set session ticket keys properly (fixed in Go 1.8) 2017-01-15 09:30:02 -07:00
M-A 8464020f7c Add {whenISO} to record timestamp in ISO 8601 format in UTC. (#1353)
* Add {whenISO} to record timestamp in ISO 8601 format in UTC.

ISO 8601 is the standard time format and is easy to parse.

This change assumes users desiring ISO 8016 generally prefer UTC for simplicity.
This results in {whenISO} to be significantly shorter than {when}:
{when}    = "02/Jan/2006:15:04:05 +0000"
{whenISO} = "2006-01-02T15:04:12Z"

Add unit test to verify both, as there was no unit test for {when}.

* Rename {whenISO} to {when_iso}
2017-01-14 15:54:27 -07:00
Henrique Dias 0155b0c5fb Add StartupHooks to Plugins (#1330)
* Update run.go

* Update plugins.go

* Update plugins.go

* Update run.go

* typo

* Update plugins.go

* Update plugins.go

* Requested changes by @mholt
2017-01-14 07:25:57 -07:00
Toby Allen 21d92d6873 Add a cli parameter to -validate a Caddyfile. Issue #1328 (#1344)
* Allow -validate flag to validate caddyfile and return

* Ensure logging without -log flag

* Changes to validate seperatly to Starup func

* Removed change to Start signature.  Created function to ValidateCaddyfile

* comment and tidyup

* ValidateandExecuteDirectives with justValidate option

* remove debugging code

* Tidy up comments

* additional parameter added to calls to mustLogFataf

* ValidateAndExecuteDirectives needs to only return err
2017-01-13 23:42:00 -07:00
Matt Holt 696792781a Merge pull request #1314 from mholt/unbuffered_proxy
proxy: Unbuffered request optimization
2017-01-11 13:02:25 -07:00
Leonard Hecker 601838ac96 proxy: Added TestReverseProxyLargeBody test case
This test ensures that the optimizations in 8048e9c are actually effective.
2017-01-11 19:38:52 +01:00
Leonard Hecker 8048e9c3bc proxy: Added unbuffered request optimization
If only one upstream is defined we don't need to buffer the body.
Instead we directly stream the body to the upstream host,
which reduces memory usage as well as latency.
Furthermore this enables different kinds of HTTP streaming
applications like gRPC for instance.
2017-01-11 19:38:14 +01:00
Matt Holt fab3b5bf19 Merge pull request #1343 from bengadbois/add_misspell_travis
Add misspell to travis build
2017-01-10 17:46:01 -07:00
Ben Gadbois c7c34266da Add misspell to travis build
Replace test text with real words so misspell doesn't throw errors
2017-01-10 15:39:00 -08:00
Matt Holt d6a35381e9 Merge pull request #1342 from bengadbois/fix_misspellings
Fix small misspellings
2017-01-10 15:09:06 -07:00
Ben Gadbois eee9d00255 Fix small misspellings 2017-01-10 13:09:24 -08:00
Matt Holt 6a84d9392e Merge pull request #1334 from tw4452852/1329
proxy: refactor TestUpstreamHeadersUpdate and TestDownstreamHeadersUpdate
2017-01-08 19:29:38 -07:00
Tw 633567744d proxy: refactor TestUpstreamHeadersUpdate and TestDownstreamHeadersUpdate
Signed-off-by: Tw <tw19881113@gmail.com>
2017-01-09 09:07:04 +08:00
Matt Holt c3523305f0 Merge pull request #1325 from mholt/authheader
basicauth: Remove Authorization header on successful authz (issue #1324)
2017-01-07 19:41:55 -07:00
Matthew Holt 3f770603bc browse: Simple filter textbox for default template
Typing in this box will filter the list of items by name.
2017-01-03 23:59:04 -07:00
Matthew Holt 54acb9b2de basicauth: Remove Authorization header on successful authz (issue #1324)
If a site owner protects a path with basicauth, no need
to use the Authorization header elsewhere upstream, especially since it
contains credentials.

If this breaks anyone, it means they're double-dipping. It's usually
good practice to clear out credentials as soon as they're not needed
anymore. (Note that we only clear credentials after they're used,
they stay for any other reason.)
2017-01-03 17:47:27 -07:00
Gregor Noczinski 8b9c9efdba Fix position of "filter" in directives (#1323)
* Fix #2 (Replacement doesn't happen - https://github.com/echocat/caddy-filter/issues/2) bug of caddy-filter

* Fixed gofmt issue.

* Remove comment of reason why we do a reorder
2017-01-03 09:03:13 -07:00
Matthew Holt a1a8d0f655 Merge branch 'master' of github.com:mholt/caddy 2017-01-01 10:27:58 -07:00
Matthew Holt 5d813a1b58 Close connection on automatic HTTP->HTTPS redirects 2017-01-01 10:27:53 -07:00
Sawood Alam 04bee0f36d Implementing custom PathClean function to allow masking, closes #1298 (#1317)
* Added path cleanup functions with masking to preserve certain patterns + unit tests, #1298

* Use custom PathClean function instead of path.Clean to apply masks to preserve protocol separator in the path

* Indentation corrected in the test data map to pass the lint

* Fixing ineffassign of a temporary string variable

* Improved variable naming and documentation

* Improved variable naming

* Added benchmarks and improved variable naming in tests

* Removed unnecessary value capture when iterating over a map for keys

* A typo correction
2016-12-31 20:29:14 -07:00
Matt Holt 7cbbb01f94 Merge pull request #1309 from lhecker/master
Fixed #1292 and resulting issues from #1300
2016-12-31 19:51:29 -07:00
Matthew Holt 466efb7e67 Post init script guidelines 2016-12-30 11:56:37 -07:00
Matthew Holt d98a7aad0f Replace "magic" quotes with regular ones 2016-12-30 11:56:12 -07:00
Leonard Hecker 4babe4b201 proxy: Added support for HTTP trailers 2016-12-30 18:34:26 +01:00
Leonard Hecker 533039e6d8 proxy: Removed leftover restriction to HTTP/1.1 2016-12-29 16:07:22 +01:00
Leonard Hecker b857265f9c proxy: Fixed support for TLS verification of WebSocket connections 2016-12-28 17:38:54 +01:00
Leonard Hecker 153d4a5ac6 proxy: Improved handling of bufferPool 2016-12-28 17:17:52 +01:00
Mateusz Gajewski d5fe4928f2 Remove pre 0.9 code (#1304)
* Remove pre 0.9 code

* Unused import

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

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

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

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

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

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

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

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

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

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

Fix issue #355

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

Fix issue #346

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

Fix issue #341

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

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

respond to maintainer comments

reinstate assignment of t

correct typo

correct typo

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

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

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

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

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

	websocket /api5 "cmd arg1 arg2 arg3"

Optional block:

	websocket /api6 cat {
		respawn
	}

Invalid option in optional block:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Add tests

Remove old implementation

Fix inconsistency with git interval pulling.

Remove '\n' from logging statements and put the initial pull into a startup function
2015-06-04 03:24:16 +02:00
Viacheslav Biriukov 4790dacbf7 add without to proxy middleware 2015-06-03 18:06:24 +00:00
Matthew Holt 4852f0580b Bump to 0.7.1 2015-06-02 12:19:08 -06:00
Matt Holt 9d7639a9d3 Merge pull request #98 from mholt/clientauth
tls: Client authentication
2015-06-02 12:05:05 -06:00
Matt Holt 6c52368124 tls: Fix format string 2015-06-02 07:42:38 -06:00
Matthew Holt c78eb50eb8 tls: Client authentication 2015-06-01 23:22:11 -06:00
Matthew Holt 9ce0e8e17c proxy: Added tests for reverse websocket proxy 2015-06-01 22:39:53 -06:00
Matthew Holt 32825e8a79 basicauth: Patch timing vulnerability 2015-06-01 20:33:07 -06:00
Matthew Holt cb8691a381 Add abiosoft's docker container to the list 2015-06-01 20:33:07 -06:00
Matt Holt 5ddcd60332 Merge pull request #96 from acmacalister/feature/ws-proxy
add supported for ws in reverse proxy
2015-06-01 20:31:41 -06:00
Austin 68cd4bdeab check server response instead of client 2015-06-01 19:29:32 -07:00
Austin ccd3e55b32 changes as noted in PR 2015-06-01 10:23:57 -07:00
Austin 56ec7b9887 websocket directive, upgrade comparison 2015-05-30 11:34:54 -07:00
Austin 2d6ff40649 add supported for ws in reverse proxy 2015-05-29 19:21:50 -07:00
Matt Holt 9bdd9bdecc Merge pull request #94 from acmacalister/feature/custom-policies
added custom policy support
2015-05-29 12:33:08 -06:00
Austin dd946f8ab5 moved init to policy.go 2015-05-28 18:16:23 -07:00
Austin 593aec9ab1 changes per comment 2015-05-28 16:53:54 -07:00
Austin 6b173b5170 added custom policy support 2015-05-28 15:56:11 -07:00
Matt Holt 130301e32e Merge pull request #92 from abiosoft/master
Git: code refactor. replace Sleep with Ticker
2015-05-28 08:37:13 -06:00
Abiola Ibrahim 2013838bfd Git: mock time functions in tests. 2015-05-28 10:20:26 +01:00
Abiola Ibrahim 879558b9ee Git: code refactor. replace Sleep with Ticker 2015-05-26 20:20:57 +01:00
Matt Holt ee059c0910 Merge pull request #90 from abiosoft/master
Git: More tests. Code refactor.
2015-05-25 22:15:56 -06:00
Abiola Ibrahim 6c6e0e3f73 Git: More tests. Code refactor. 2015-05-26 04:44:47 +01:00
Matthew Holt b1c8b48e6e core: Version 0.7.0 2015-05-25 15:48:25 -06:00
Matthew Holt 6f05794bb8 git: Fixed unusual but potent race condition 2015-05-25 15:39:04 -06:00
Matt Holt 346135fed3 Merge pull request #89 from guilhermebr/master
removed tls cache option
2015-05-25 15:00:36 -06:00
Guilherme Rezende 69939108e1 removed tls cache option 2015-05-25 14:42:09 -03:00
Matthew Holt 674f454e70 t.Fatal -> t.Fatalf 2015-05-25 08:27:54 -06:00
Abiola Ibrahim a881838836 Merge pull request #88 from zmb3/lintwarnings
Fix lint warnings
2015-05-25 06:28:25 +01:00
Zac Bergquist e4b50aa814 Fix more lint warnings 2015-05-24 22:52:34 -04:00
Zac Bergquist fd8490c689 Fix lint warnings for middleware/websockets 2015-05-24 21:04:03 -04:00
Zac Bergquist d0a51048d7 Fix lint warnings in middleware/rewrite 2015-05-24 21:00:54 -04:00
Zac Bergquist 506f131428 Fix lint warnings for middleware/proxy 2015-05-24 20:58:17 -04:00
Matthew Holt e42c6bf0bb Updated readme 2015-05-23 14:29:59 -06:00
Abiola Ibrahim 535f956682 Merge pull request #86 from abiosoft/master
FastCGI: support for unix sockets.
2015-05-23 03:46:47 +01:00
Abiola Ibrahim 4e94b85ec2 FastCGI: support for unix sockets. 2015-05-23 03:39:23 +01:00
Abiola Ibrahim 6b3b04ffb7 Merge pull request #85 from jpoehls/master
Tweaked ulimit warning message.
2015-05-23 02:37:45 +01:00
Joshua Poehls 36bc3a453f Tweaked ulimit warning message. 2015-05-22 20:24:48 -05:00
Matt Holt 99c069df15 Merge pull request #84 from jpoehls/master
Updated ulimit warning message to include the recommended min value.
2015-05-22 18:50:12 -06:00
Joshua Poehls 04fd7ce9e1 Updated ulimit warning message to include the recommended min value. 2015-05-22 19:34:00 -05:00
Matt Holt cc958947e5 Merge pull request #83 from abiosoft/master
Git: Minor fixes. Refactor. Added tests.
2015-05-22 17:13:29 -06:00
Abiola Ibrahim f44cd5d740 Git: Minor fixes. Refactor. Added tests. 2015-05-22 20:50:04 +01:00
Matthew Holt bba2d63de8 Updated contribution guidelines 2015-05-22 11:08:24 -06:00
Matthew Holt 3420bd6e06 Another test case for parser 2015-05-21 23:50:37 -06:00
Matthew Holt b10d846019 More parser tests 2015-05-21 23:36:17 -06:00
Matthew Holt 11ddb5c6ca Tests for the parser 2015-05-21 18:46:42 -06:00
Matthew Holt b37fed4cc8 Beginning some tests for the parser 2015-05-21 16:31:01 -06:00
Matthew Holt 5397eef234 Added tests for allTokens 2015-05-21 15:42:49 -06:00
Matthew Holt d311345aa5 Fix for running ulimit check 2015-05-21 11:21:08 -06:00
Matthew Holt d6df615588 tls: Mainstream compatibility improvements, better security rating 2015-05-21 10:37:39 -06:00
Matthew Holt ce6e30c09e Lil' bit of godoc 2015-05-21 00:40:58 -06:00
Matthew Holt ee754b4a47 Bug fixes 2015-05-21 00:40:05 -06:00
Matthew Holt 5f72b7438a Created app package, and better TLS compatibility with HTTP/2 2015-05-21 00:06:53 -06:00
Matthew Holt ea9607302a Whoops 2015-05-20 20:50:19 -06:00
Matthew Holt 26bb17337e Warn if ulimit is too low when serving production sites 2015-05-20 20:46:27 -06:00
Matthew Holt 5e8491cf7f Allow IPv6 address without port (fixes #80) 2015-05-20 20:15:39 -06:00
Matthew Holt e42c6ab520 Notice displayed if non-localhost hosts resolve to loopback 2015-05-20 20:06:30 -06:00
Matt Holt 9c039474b3 Merge pull request #78 from guilhermebr/master
tls: Optional block for ciphers, protocols and cache options
2015-05-18 22:57:38 -06:00
Guilherme Rezende b378316103 replace c.ArgErr with c.Err in tls when is the case 2015-05-18 23:27:35 -03:00
Guilherme Rezende a94c7dd788 change tls ssl3 protocol key and tests 2015-05-18 18:15:41 -03:00
Guilherme Rezende 823a7eac03 Added tls option block including: ciphers, protocols and cache options
Signed-off-by: Guilherme Rezende <guilhermebr@gmail.com>
2015-05-18 16:38:21 -03:00
Matt Holt cf2808ae45 Merge pull request #75 from abiosoft/master
Rewrite: Support for Regular Expressions.
2015-05-16 18:44:20 -07:00
Abiola Ibrahim c382c885e4 use middleware.Path for base path comparison 2015-05-16 16:57:57 +01:00
Abiola Ibrahim 7ae9e3a262 Rewrite: Added new variables file, dir, fragment. 2015-05-16 16:30:15 +01:00
Abiola Ibrahim 74d162f377 Rewrite: Support for Regular Expressions. 2015-05-16 13:03:48 +01:00
Abiola Ibrahim ad7b453f03 Rewrite: modified syntax. 2015-05-15 18:47:26 +01:00
Abiola Ibrahim b2afc30d12 Rewrite: added regexp. awaiting documentation and tests. 2015-05-15 02:43:29 +01:00
Matt Holt 1076daa8c9 Merge pull request #73 from abiosoft/master
Fix for Issue 72: Markdown: 500 for YAML metadata
2015-05-12 18:04:03 -06:00
Abiola Ibrahim 8394d72f48 Fix for Issue 72: Markdown: 500 for YAML metadata 2015-05-13 00:44:35 +01:00
Abiola Ibrahim 018fd21741 Merge pull request #71 from abiosoft/master
browse: return forbidden (403) only when it is a permission error.
2015-05-10 17:59:55 +01:00
Abiola Ibrahim a1312465b5 browse: return forbidden (403) only when it is a permission error. 2015-05-10 17:58:44 +01:00
Matt Holt e2273ea676 Merge pull request #69 from peterhellberg/headers_test
headers: Initial test for Headers and change of Rule.Url to Rule.Path
2015-05-10 07:49:11 -06:00
Matt Holt c6eaf0db36 Merge pull request #62 from jordic/testing_basicauth
basicauth: Should write an unauthorized code to response. (Actually only returns it)
2015-05-10 07:47:16 -06:00
Michael Schöbel a0eca49795 Merge pull request #70 from mschoebel/internal_middleware_tests
Internal middleware: added tests
2015-05-10 14:57:16 +02:00
Michael Schoebel e0173ec4c7 internal middleware: added tests 2015-05-10 14:41:48 +02:00
jordi collell 99fa4581aa basicauth: patch for overlapping rules 2015-05-10 08:20:58 +02:00
Peter Hellberg 37b1a81fc7 headers: Corrected copy paste (BasicAuth->Headers) 2015-05-10 07:44:43 +02:00
Matthew Holt 4272536518 markdown: sitegen keyword and run generation at startup
Also fixed bug for markdown files that don't contain front matter
2015-05-09 21:12:52 -06:00
Peter Hellberg 0d5a8a7383 headers: Test for Headers and headersParse funcs 2015-05-09 23:10:18 +02:00
Peter Hellberg df6efe5d88 headers: Replaced usage of Url to Path in setup 2015-05-09 21:57:55 +02:00
Peter Hellberg d9dc9326f2 headers: Initial test for Headers 2015-05-09 21:47:02 +02:00
Peter Hellberg b5fff09b54 headers: Changed Rule.Url to Rule.Path
Updated ServeHTTP comment to indicate that it is 
setting headers and not adding them to existing values.
2015-05-09 21:45:28 +02:00
Matt Holt a96c4d707b Merge pull request #67 from peterhellberg/redirect_test
redirect: initial test for Redirect
2015-05-09 12:21:59 -06:00
Peter Hellberg 0f9df18dfb Check if the Next handler was called unexpectedly
Removed body check and replaced urlPrinter with a inlined Next handler
2015-05-09 20:07:29 +02:00
Matthew Holt 8ea98f8cce markdown: Fix panic: assignment to entry in nil map
Ensures metadata.Variables is made
2015-05-09 11:49:28 -06:00
Matt Holt f2f7e6825f Merge pull request #68 from abiosoft/master
Markdown: support for templates and metadata
2015-05-09 09:13:40 -06:00
Abiola Ibrahim 2f5e2f39cb markdown: remove identifier from Json metadata 2015-05-09 11:49:54 +01:00
jordi collell 4c11854927 added header match and a new failing test 2015-05-09 08:11:02 +02:00
Abiola Ibrahim 6ce83aad2b markdown: Refactor fixes 2015-05-09 00:54:39 +01:00
Matt Holt 2743f97892 Merge pull request #66 from stanislavromanov/master
Close #64 (Make it possible to remove headers)
2015-05-08 17:26:36 -06:00
Abiola Ibrahim 978aef2ae7 Merge remote-tracking branch 'upstream/master' 2015-05-08 23:51:09 +01:00
Abiola Ibrahim 48a12c605a markdown: Added template support. 2015-05-08 23:45:31 +01:00
Peter Hellberg 95b4e61a07 redirect: initial test for Redirect 2015-05-09 00:36:58 +02:00
Matthew Holt 2501691ea4 lexer: Fixed backslashes in quoted strings (closes #65) 2015-05-08 10:32:57 -06:00
stanislavromanov a5a90fe6fc close #64 2015-05-08 17:47:37 +02:00
Abiola Ibrahim 0fccd3707d markdown: documentation done. awaiting test 2015-05-08 16:20:07 +01:00
jordi collell 253c069b26 if basic auth fails should write unauthorized to response 2015-05-08 09:41:17 +02:00
Abiola Ibrahim 2c7de8f328 Merge remote-tracking branch 'upstream/master' 2015-05-08 06:52:14 +01:00
jordi collell 64d203491c Some failing tests 2015-05-08 07:41:48 +02:00
Matthew Holt b2ee6638e4 Tests for rewrite middleware 2015-05-07 14:55:34 -06:00
Matt Holt 557410ffd7 Merge pull request #58 from mschoebel/internal_middleware
Adding "internal" middleware
2015-05-07 13:28:09 -06:00
Matthew Holt 40105094e7 Some tests and utilities for setup functions 2015-05-07 13:11:03 -06:00
Matthew Holt 0dba8d406b NextBlock() doesn't enter an empty block 2015-05-07 13:10:00 -06:00
Matthew Holt 2ce5102473 Add -version flag 2015-05-07 13:09:40 -06:00
Michael Schoebel e3d64169ed Adapted internal middleware
- Detect too many internal redirects - return 500 in this case
2015-05-07 20:48:29 +02:00
Michael Schoebel a5b565e193 Adapted internal middleware
- redirect internally regardless of proxy status code
- support multiple internal redirects
2015-05-07 20:20:45 +02:00
Abiola Ibrahim 7443fd0973 Merge remote-tracking branch 'upstream/master' 2015-05-07 14:02:40 +01:00
Abiola Ibrahim ba613a1567 markdown: template integration done. awaiting documentation and test 2015-05-07 13:45:27 +01:00
Abiola Ibrahim 0bfdb50ade markdown: working version of template integration. Awaiting static site generation and tests. 2015-05-07 00:19:02 +01:00
Michael Schoebel 0650dd7171 New internal middleware 2015-05-06 22:44:37 +02:00
Abiola Ibrahim 434ec7b6ea Merge remote-tracking branch 'upstream/master' 2015-05-06 09:06:14 +01:00
Abiola Ibrahim 25847a6192 markdown: added template codes. awaiting integration and tests 2015-05-06 03:37:29 +01:00
323 changed files with 44521 additions and 5732 deletions
+19
View File
@@ -0,0 +1,19 @@
# 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
*.txt text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.tpl text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.htm text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.html text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.md text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
.git* text eol=auto core.whitespace whitespace=trailing-space
+160
View File
@@ -0,0 +1,160 @@
Contributing to Caddy
=====================
Welcome! Thank you for choosing to be a part of our community. Caddy wouldn't be great without your involvement!
For starters, we invite you to join [the Caddy forum](https://caddy.community) where you can hang out with other Caddy users and developers.
## Common Tasks
- [Contributing code](#contributing-code)
- [Writing a plugin](#writing-a-plugin)
- [Asking or answering questions for help using Caddy](#getting-help-using-caddy)
- [Reporting a bug](#reporting-bugs)
- [Suggesting an enhancement or a new feature](#suggesting-features)
- [Improving documentation](#improving-documentation)
Other menu items:
- [Values](#values)
- [Responsible Disclosure](#responsible-disclosure)
- [Thank You](#thank-you)
### Contributing code
You can have a direct impact on the project by helping with its code. To contribute code to Caddy, open a [pull request](https://github.com/mholt/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/mholt/caddy/-/search).
We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions&mdash;even if it seems small or insignificant. Please don't take it personally. :wink: If your change is on the right track, we can guide you to make it mergable.
Here are some of the expectations we have of contributors:
- If your change is more than just a minor alteration, **open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that changes are in-line with the project's goals and the best interests of its users. If there's already an issue about it, comment on the existing issue to claim it.
- **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we DON'T do.](https://twitter.com/iamdevloper/status/397664295875805184)
- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other.
- **Write tests.** Tests are essential! Written properly, they ensure your change works, and that other changes in the future won't break your change. CI checks should pass.
- **Benchmarks should be included for optimizations.** Optimizations sometimes make code harder to read or have changes that are less than obvious. They should be proven with benchmarks or profiling.
- **[Squash](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) insignificant commits.** Every commit should be significant. Commits which merely rewrite a comment or fix a typo can be combined into another commit that has more substance. Interactive rebase can do this, or a simpler way is `git reset --soft <diverging-commit>` then `git commit -s`.
- **Own your contributions.** Caddy is a growing project, and it's much better when individual contributors help maintain their change after it is merged.
- **Use comments properly.** We expect good godoc comments for package-level functions, types, and values. Comments are also useful whenever the purpose for a line of code is not obvious.
We often grant [collaborator status](#collaborator-instructions) to contributors who author one or more significant, high-quality PRs that are merged into the code base!
#### HOW TO MAKE A PULL REQUEST TO CADDY
Contributing to Go projects on GitHub is fun and easy. We recommend the following workflow:
1. [Fork this repo](https://github.com/mholt/caddy). This makes a copy of the code you can write to.
2. If you don't already have this repo (mholt/caddy.git) repo on your computer, get it with `go get github.com/mholt/caddy/caddy`.
3. Tell git that it can push the mholt/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/you/caddy.git`
4. Make your changes in the mholt/caddy.git repo on your computer.
5. Push your changes to your fork: `git push myfork`
6. [Create a pull request](https://github.com/mholt/caddy/pull/new/master) to merge your changes into mholt/caddy @ master. (Click "compare across forks" and change the head fork.)
This workflow is nice because you don't have to change import paths. You can get fancier by using different branches if you want.
### Writing a plugin
Caddy can do more with plugins! Anyone can write a plugin. Plugins are Go libraries that get compiled into Caddy, extending its feature set. They can add directives to the Caddyfile, change how the Caddyfile is loaded, and even implement new server types (e.g. HTTP, DNS). When it's ready, you can submit your plugin to the Caddy website so others can download it.
[Learn how to write and submit a plugin](https://github.com/mholt/caddy/wiki) on the wiki. You should also share and discuss your plugin idea [on the forums](https://caddy.community) to have people test it out. We don't use the Caddy issue tracker for plugins.
### Getting help using Caddy
If you have a question about using Caddy, [ask on our forum](https://caddy.community)! There will be more people there who can help you than just the Caddy developers who follow our issue tracker. Issues are not the place for usage questions.
Many people on the forums could benefit from your experience and expertise, too. Once you've been helped, consider giving back by answering other people's questions and participating in other discussions.
### Reporting bugs
Like every software, Caddy has its flaws. If you find one, [search the issues](https://github.com/mholt/caddy/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/mholt/caddy/issues/new) and describe the bug, and somebody will look into it! (This repository is only for Caddy, not plugins.)
**You can help stop bugs in their tracks!** Speed up the patch by identifying the bug in the code. This can sometimes be done by adding `fmt.Println()` statements (or similar) in relevant code paths to narrow down where the problem may be. It's a good way to [introduce yourself to the Go language](https://tour.golang.org), too.
Please follow the issue template so we have all the needed information. Unredacted&mdash;yes, actual values matter. We need to be able to repeat the bug using your instructions. Please simplify the issue as much as possible. 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!
We suggest reading [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're getting free support when we fix your issues. If we helped you, please consider helping someone else!
### Suggesting features
First, [search to see if your feature has already been requested](https://github.com/mholt/caddy/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. You don't have to follow the bug template for feature requests. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and without clarification will have to be closed.
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good plugins](https://github.com/mholt/caddy/wiki), though, which can be made by anyone! But if a feature is not in the best interest of the Caddy project or its users in general, we may politely decline to implement it into Caddy core.
### Improving documentation
Caddy's documentation is available at [https://caddyserver.com/docs](https://caddyserver.com/docs). If you would like to make a fix to the docs, please submit an issue here describing the change to make.
Note that plugin documentation is not hosted by the Caddy website, other than basic usage examples. They are managed by the individual plugin authors, and you will have to contact them to change their documentation.
## Collaborator Instructions
Collaborators have push rights to the repository. We grant this permission after one or more successful, high-quality PRs are merged! We thank them for their help.The expectations we have of collaborators are:
- **Help review pull requests.** Be meticulous, but also kind. We love our contributors, but we critique the contribution to make it better. Multiple, thorough reviews make for the best contributions! Here are some questions to consider:
- Can the change be made more elegant?
- Is this a maintenance burden?
- What assumptions does the code make?
- Is it well-tested?
- Is the change a good fit for the project?
- Does it actually fix the problem or is it creating a special case instead?
- Does the change incur any new dependencies? (Avoid these!)
- **Answer issues.** If every collaborator helped out with issues, we could count the number of open issues on two hands. This means getting involved in the discussion, investigating the code, and yes, debugging it. It's fun. Really! :smile: Please, please help with open issues. Granted, some issues need to be done before others. And of course some are larger than others: you don't have to do it all yourself. Work with other collaborators as a team!
- **Do not merge pull requests until they have been approved by one or two other collaborators.** If a project owner approves the PR, it can be merged (as long as the conversation has finished too).
- **Prefer squashed commits over a messy merge.** If there are many little commits, please [squash the commits](https://stackoverflow.com/a/11732910/1048862) so we don't clutter the commit history.
- **Don't accept new dependencies lightly.** Dependencies can make the world crash and burn, but they are sometimes necessary. Choose carefully. Extremely small dependencies (a few lines of code) can be inlined. The rest may not be needed. For those that are, Caddy vendors all dependencies with the help of [gvt](https://github.com/FiloSottile/gvt). All external dependencies must be vendored, and _Caddy must not export any types defined by those dependencies_. Check this diligently!
- **Be extra careful in some areas of the code.** There are some critical areas in the Caddy code base that we review extra meticulously: the `caddy` and `caddytls` packages especially.
- **Make sure tests test the actual thing.** Double-check that the tests fail without the change, and pass with it. It's important that they assert what they're purported to assert.
- **Recommended reading**
- [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) for an idea of what we look for in good, clean Go code
- [Linus Torvalds describes a good commit message](https://gist.github.com/matthewhudson/1475276)
- [Best Practices for Maintainers](https://opensource.guide/best-practices/)
- [Shrinking Code Review](https://alexgaynor.net/2015/dec/29/shrinking-code-review/)
## Values
- A person is always more important than code. People don't like being handled "efficiently". But we can still process issues and pull requests efficiently while being kind, patient, and considerate.
- The ends justify the means, if the means are good. A good tree won't produce bad fruit. But if we cut corners or are hasty in our process, the end result will not be good.
## Responsible Disclosure
If you've found a security vulnerability, please email me, the author, directly: Matthew dot Holt at Gmail. I'll need enough information to verify the bug and make a patch. It will speed things up if you suggest a working patch. If your report is valid and a patch is released, we will not reveal your identity by default. If you wish to be credited, please give me the name to use. Thanks for responsibly helping Caddy&mdash;and thousands of websites&mdash;be more secure!
## Thank you
Thanks for your help! Caddy would not be what it is today without your
contributions.
+83
View File
@@ -0,0 +1,83 @@
---
name: Bug report
about: For behaviors which violate documentation or cause incorrect results
title: ''
labels: ''
assignees: ''
---
<!--
This template is for bug reports. The lack of a feature is not a bug; to request a feature, please switch templates.
Are you asking for help with using Caddy? Please ask on our forum: https://caddy.community
Please do not skip relevant questions; this will slow down the debugging process and your issue may be closed.
-->
## 1. Which version of Caddy are you using (`caddy -version`)?
<!-- If there is no version information, please paste commit SHA instead. -->
## 2. What are you trying to do?
<!-- Please clearly describe what you are trying to do thoroughly enough so that a reader with no background information can repeat it. -->
## 3. What is your Caddyfile?
```text
paste entire Caddyfile here - DO NOT REDACT ANYTHING (except credentials)
```
<!-- Changing or hiding parts of your Caddyfile only slows things down and may result in your report being closed.
For more information, see https://caddy.community/t/how-to-get-help-with-caddy-more-effectively/5222 -->
<!-- If you are unable to post this publicly, we offer private support: https://caddyserver.com/products/support -->
## 4. How did you run Caddy (give the full command and describe the execution environment)?
<!-- IMPORTANT: Please eliminate Docker, systemd, reverse proxies, upstream dependencies, caches, firewalls, and other unnecessary, external factors from your setup first. This will help prove that this is a bug in Caddy and not a misconfiguration of your environment. We may close issues that are too complex to replicate. Thank you! -->
## 5. Please paste any relevant HTTP request(s) here.
<!-- Paste curl command, or full HTTP request including headers and body. You may skip this if the bug does not require HTTP requests. -->
## 6. What did you expect to see?
<!-- Describe your expected results as precisely as possible. -->
## 7. What did you see instead (give full error messages and/or log)?
<!-- Please run Caddy with the -log flag, and use the log and errors directives as needed. DO NOT REDACT INFORMATION except for credentials. See https://caddy.community/t/how-to-get-help-with-caddy-more-effectively/5222 -->
## 8. Why is this a bug, and how do you think this should be fixed?
<!-- Help us understand why it is a bug; it is not always obvious. You can help us get this resolved faster by thinking about the problem and describing possible solutions! -->
## 9. What are you doing to work around the problem in the meantime?
<!-- This can help others who encounter the same problem, until we implement a fix. -->
## 10. Please link to any related issues, pull requests, and/or discussion.
<!-- This can add crucial context to your report. -->
## Bonus: What do you use Caddy for? Why did you choose Caddy?
<!-- We'd like to know! -->
+35
View File
@@ -0,0 +1,35 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature request
assignees: ''
---
<!--
This template is for feature requests. If you are reporting a bug instead, please switch templates.
Are you asking for help with using Caddy? Please ask on our forum: https://caddy.community
-->
## 1. What would you like to have changed?
<!-- Fully describe the feature or enhancement you are requesting; examples can be helpful too -->
## 2. Why is this feature a useful, necessary, and/or important addition to this project?
<!-- Please justify why this change adds value to the project, considering the added maintenance burden and complexity the change introduces -->
## 3. What alternatives are there, or what are you doing in the meantime to work around the lack of this feature?
<!-- We want to get an idea of what is being done in practice, or how other projects support your feature -->
## 4. Please link to any relevant issues, pull requests, or other discussions.
<!-- This adds crucial context to your feature request and can speed things up -->
@@ -0,0 +1,40 @@
---
name: Pull request
about: Propose changes to the code
title: ''
labels: ''
assignees: ''
---
<!--
Thank you for contributing to Caddy! Please fill this out to help us make the most of your pull request.
Was this change discussed in an issue first? That can help save time in case the change is not a good fit for the project. Not all pull requests get merged.
It is not uncommon for pull requests to go through several, iterative reviews. Please be patient with us! Every reviewer is a volunteer, and each has their own style.
-->
## 1. What does this change do, exactly?
<!-- Please be specific. Motivate the problem, and justify why this is the best solution. -->
## 2. Please link to the relevant issues.
<!-- This adds crucial context to your change. -->
## 3. Which documentation changes (if any) need to be made because of this PR?
<!-- Reviewers will often reference this first in order to know what to expect from the change. Please be specific enough so that they can paste your wording into the documentation directly. -->
## 4. Checklist
- [ ] I have written tests and verified that they fail without my change
- [ ] I have squashed any insignificant commits
- [ ] This change has comments explaining package types, values, functions, and non-obvious lines of code
- [ ] I am willing to help maintain this change if there are issues with it later
+11 -1
View File
@@ -3,10 +3,20 @@ Thumbs.db
_gitignore/
Vagrantfile
.vagrant/
dist/
/.idea
dist/builds/
dist/release/
error.log
access.log
/*.conf
Caddyfile
!caddyfile/
og_static/
.vscode/
*.bat
-2
View File
@@ -1,2 +0,0 @@
language: go
script: go test ./...
-11
View File
@@ -1,11 +0,0 @@
## Contributing to Caddy
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup), then join the #caddy channel.)
This project gladly accepts contributions. Interested users are encouraged to get involved by [opening issues](https://github.com/mholt/caddy/issues) with their ideas, questions, and bug reports. Bug reports should contain clear instructions to reproduce the problem and state expected behavior.
For small tweaks and bug fixes, feel free to submit [pull requests](https://github.com/mholt/caddy/pulls) at any time. For new features or to change existing behavior, please open an issue first to discuss it and claim it. This prevents overlapping efforts and also keeps the project in-line with its goals.
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.
Thanks for your help!
View File
+159 -68
View File
@@ -1,134 +1,225 @@
[![Caddy](https://caddyserver.com/resources/images/caddy-boxed.png)](https://caddyserver.com)
<p align="center">
<a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36338535-05fb646a-136f-11e8-987b-e6901e717d5a.png" alt="Caddy" width="450"></a>
</p>
<h3 align="center">Every Site on HTTPS <!-- Serve Confidently --></h3>
<p align="center">Caddy is a general-purpose HTTP/2 web server that serves HTTPS by default.</p>
<p align="center">
<a href="https://dev.azure.com/mholt-dev/Caddy/_build?definitionId=1"><img src="https://img.shields.io/azure-devops/build/mholt-dev/afec6074-9842-457f-98cf-69df6adbbf2e/1/master.svg?label=cross-platform%20tests"></a>
<a href="https://godoc.org/github.com/mholt/caddy"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
<a href="https://goreportcard.com/report/mholt/caddy"><img src="https://goreportcard.com/badge/github.com/mholt/caddy"></a>
<br>
<a href="https://twitter.com/caddyserver" title="@caddyserver on Twitter"><img src="https://img.shields.io/badge/twitter-@caddyserver-55acee.svg" alt="@caddyserver on Twitter"></a>
<a href="https://caddy.community" title="Caddy Forum"><img src="https://img.shields.io/badge/community-forum-ff69b4.svg" alt="Caddy Forum"></a>
<a href="https://sourcegraph.com/github.com/mholt/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/mholt/caddy/-/badge.svg" alt="Caddy on Sourcegraph"></a>
</p>
<p align="center">
<a href="https://caddyserver.com/download">Download</a> ·
<a href="https://caddyserver.com/docs">Documentation</a> ·
<a href="https://caddy.community">Community</a>
</p>
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/mholt/caddy) [![Build Status](https://img.shields.io/travis/mholt/caddy.svg?style=flat-square)](https://travis-ci.org/mholt/caddy)
---
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 web servers that is easy to use.
Caddy is a **production-ready** open-source web server that is fast, easy to use, and makes you more productive.
The most notable features are HTTP/2, Virtual Hosts, TLS + SNI, and easy configuration with a [Caddyfile](https://caddyserver.com/docs/caddyfile). Usually, you have one Caddyfile per site. Most directives for the Caddyfile invoke a layer of middleware which can be [used in your own Go programs](https://github.com/mholt/caddy/wiki/Using-Caddy-Middleware-in-Your-Own-Programs).
Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android).
[Download](https://github.com/mholt/caddy/releases) · [User Guide](https://caddyserver.com/docs)
<p align="center">
<b>Thanks to our special sponsor:</b>
<br><br>
<a href="https://relicabackup.com"><img src="https://caddyserver.com/resources/images/sponsors/relica.png" width="220" alt="Relica - Cross-platform file backup to the cloud, local disks, or other computers"></a>
</p>
## Menu
### Menu
- [Getting Caddy](#getting-caddy)
- [Running from Source](#running-from-source)
- [Features](#features)
- [Install](#install)
- [Quick Start](#quick-start)
- [Running in Production](#running-in-production)
- [Contributing](#contributing)
- [Donors](#donors)
- [About the Project](#about-the-project)
## Features
- **Easy configuration** with the Caddyfile
- **Automatic HTTPS** on by default (via [Let's Encrypt](https://letsencrypt.org))
- **HTTP/2** by default
- **Virtual hosting** so multiple sites just work
- Experimental **QUIC support** for cutting-edge transmissions
- TLS session ticket **key rotation** for more secure connections
- **Extensible with plugins** because a convenient web server is a helpful one
- **Runs anywhere** with **no external dependencies** (not even libc)
[See a more complete list of features built into Caddy.](https://caddyserver.com/features) On top of all those, Caddy does even more with plugins: choose which plugins you want at [download](https://caddyserver.com/download).
Altogether, Caddy can do things other web servers simply cannot do. Its features and plugins save you time and mistakes, and will cheer you up. Your Caddy instance takes care of the details for you!
## Getting Caddy
Caddy binaries have no dependencies and are available for nearly every platform.
[Latest release](https://github.com/mholt/caddy/releases/latest)
<p align="center">
<b>Powered by</b>
<br>
<a href="https://github.com/mholt/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
</p>
## Running from Source
## Install
Note: You will need **[Go 1.4](https://golang.org/dl)** or newer
Caddy binaries have no dependencies and are available for every platform. Get Caddy any of these ways:
1. `$ go get github.com/mholt/caddy`
2. `cd` into your website's directory
3. Run `caddy` (assumes `$GOPATH/bin` is in your `$PATH`)
If you're tinkering, you can also use `go run main.go`.
By default, Caddy serves the current directory at [localhost:2015](http://localhost:2015). You can place a Caddyfile to configure Caddy for serving your site.
Caddy accepts some flags from the command line. Run `caddy -h` to view the help for flags. You can also pipe a Caddyfile into the caddy command.
- **[Download page](https://caddyserver.com/download)** (RECOMMENDED) allows you to customize your build in the browser
- **[Latest release](https://github.com/mholt/caddy/releases/latest)** for pre-built, vanilla binaries
- **[AWS Marketplace](https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C)** makes it easy to deploy directly to your cloud environment. <a href="https://aws.amazon.com/marketplace/pp/B07J1WNK75?qid=1539015041932&sr=0-1&ref_=srh_res_product_title&cl_spe=C" target="_blank">
<img src="https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png" alt="Get Caddy on the AWS Marketplace" height="25"/></a>
## Build
#### Docker Container
To build from source you need **[Git](https://git-scm.com/downloads)** and **[Go](https://golang.org/doc/install)** (1.12 or newer).
Caddy is [available as a Docker container](https://registry.hub.docker.com/u/darron/caddy/).
**To build Caddy without plugins:**
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
2. Run `go get github.com/mholt/caddy/caddy`
Caddy will be installed to your `$GOPATH/bin` folder.
#### 3rd-party libraries
With these instructions, the binary will not have embedded version information (see [golang/go#29228](https://github.com/golang/go/issues/29228)), but it is fine for a quick start.
Although Caddy's binaries are completely static, Caddy relies on some excellent libraries that really make the project possible.
**To build Caddy with plugins (and with version information):**
- [bradfitz/http2](https://github.com/bradfitz/http2) for HTTP/2 support
- [russross/blackfriday](https://github.com/russross/blackfriday) for Markdown rendering
- [dustin/go-humanize](https://github.com/dustin/go-humanize) for pleasant times and sizes
- [flynn/go-shlex](https://github.com/flynn/go-shlex) to parse shell commands properly
There is no need to modify the Caddy code to build it with plugins. We will create a simple Go module with our own `main()` that you can use to make custom Caddy builds.
This list may not be comprehensive, but [godoc.org](https://godoc.org/github.com/mholt/caddy) will list all packages that any given package imports.
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
2. Create a new folder anywhere, and put this Go file into it, then import the plugins you want to include:
```go
package main
import (
"github.com/mholt/caddy/caddy/caddymain"
// plug in plugins here, for example:
// _ "import/path/here"
)
func main() {
// optional: disable telemetry
// caddymain.EnableTelemetry = false
caddymain.Run()
}
```
3. `go mod init caddy`
4. Run `go get github.com/mholt/caddy`
5. `go install` will then create your binary at `$GOPATH/bin`, or `go build` will put it in the current directory.
**To install Caddy's source code for development:**
<!-- TODO: This env variable will not be required starting with Go 1.13 -->
1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
2. Run `git clone https://github.com/mholt/caddy.git` in any folder (doesn't have to be in GOPATH).
You can make changes to the source code from that clone and checkout any commit or tag you wish to develop on.
When building from source, telemetry is enabled by default. You can disable it by changing `caddymain.EnableTelemetry = false` in run.go, or use the `-disabled-metrics` flag at runtime to disable only certain metrics.
## Quick Start
The website has [full documentation](https://caddyserver.com/docs) but this will get you started in about 30 seconds:
Place a file named "Caddyfile" with your site. Paste this into it and save:
To serve static files from the current working directory, run:
```
caddy
```
Caddy's default port is 2015, so open your browser to [http://localhost:2015](http://localhost:2015).
### Go from 0 to HTTPS in 5 seconds
If the `caddy` binary has permission to bind to low ports and your domain name's DNS records point to the machine you're on:
```
caddy -host example.com
```
This command serves static files from the current directory over HTTPS. Certificates are automatically obtained and renewed for you! Caddy is also automatically configuring ports 80 and 443 for you, and redirecting HTTP to HTTPS. Cool, huh?
### Customizing your site
To customize how your site is served, create a file named Caddyfile by your site and paste this into it:
```plain
localhost
gzip
push
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.
That simple file enables compression, allows directory browsing (for folders without an index file), serves clean URLs, hosts an echo server for WebSocket connections at /echo, logs accesses to access.log, and adds the coveted `Access-Control-Allow-Origin: *` header for all responses from some API.
This simple file enables server push (via Link headers), allows directory browsing (for folders without an index file), hosts a WebSocket echo server at /echo, serves clean URLs, logs requests to an 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.
#### Defining multiple sites
### Doing more with Caddy
You can run multiple sites from the same Caddyfile, too:
To host multiple sites and do more with the Caddyfile, please see the [Caddyfile tutorial](https://caddyserver.com/tutorial/caddyfile).
```
http://mysite.com,
http://www.mysite.com {
redir https://mysite.com
}
Sites with qualifying hostnames are served over [HTTPS by default](https://caddyserver.com/docs/automatic-https).
https://mysite.com {
tls mysite.crt mysite.key
# ...
}
```
Note that the secure host will automatically be served with HTTP/2 if the client supports 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 nice little command line interface. Run `caddy -h` to view basic help or see the [CLI documentation](https://caddyserver.com/docs/cli) for details.
## Running in Production
Caddy is production-ready if you find it to be a good fit for your site and workflow.
**Running as root:** We advise against this. You can still listen on ports < 1024 on Linux using setcap like so: `sudo setcap cap_net_bind_service=+ep ./caddy`
The Caddy project does not officially maintain any system-specific integrations nor suggest how to administer your own system. 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.
If you have questions or concerns about Caddy' underlying crypto implementations, consult Go's [crypto packages](https://golang.org/pkg/crypto), starting with their documentation, then issues, then the code itself; as Caddy uses mainly those libraries.
## Contributing
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup), then join the #caddy channel.)
**[Join our forum](https://caddy.community) where you can chat with other Caddy users and developers!** To get familiar with the code base, try [Caddy code search on Sourcegraph](https://sourcegraph.com/github.com/mholt/caddy/)!
This project gladly accepts contributions. Interested users are encouraged to get involved by opening issues with their ideas, questions, and bug reports. Bug reports should contain clear instructions to reproduce the problem and state expected behavior.
Please see our [contributing guidelines](https://github.com/mholt/caddy/blob/master/.github/CONTRIBUTING.md) for instructions. If you want to write a plugin, check out the [developer wiki](https://github.com/mholt/caddy/wiki).
For small tweaks and bug fixes, feel free to submit pull requests at any time. For new features or to change existing behavior, please open an issue first to discuss it and claim it. This prevents overlapping efforts and also keeps the project in-line with its goals.
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://caddy.community)!
If you want to contribute to the documentation, please [submit an issue](https://github.com/mholt/caddy/issues/new) describing the change that should be made.
### Good First Issue
If you are looking for somewhere to start and would like to help out by working on an existing issue, take a look at our [`Good First Issue`](https://github.com/mholt/caddy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag
Thanks for making Caddy -- and the Web -- better!
## Donors
- [DigitalOcean](https://m.do.co/c/6d7bdafccf96) is hosting the Caddy project.
- [DNSimple](https://dnsimple.link/resolving-caddy) provides DNS services for Caddy's sites.
- [DNS Spy](https://dnsspy.io) keeps an eye on Caddy's DNS properties.
We thank them for their services. **If you want to help keep Caddy free, please [become a sponsor](https://caddyserver.com/pricing)!**
## 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 inspiration from [spark](https://github.com/rif/spark), [nginx](https://github.com/nginx/nginx), lighttpd,
[Websocketd](https://github.com/joewalnes/websocketd) and [Vagrant](https://www.vagrantup.com/), which provides a pleasant mixture of features from each of them.
Caddy was born out of the need for a "batteries-included" web server that runs anywhere and doesn't have to take its configuration with it. Caddy took inspiration from nginx, lighttpd, Websocketd, and Vagrant, and provides a pleasant mixture of features from each of them.
**The name "Caddy" is trademarked:** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". See [brand guidelines](https://caddyserver.com/brand). Caddy is a registered trademark of Light Code Labs, LLC.
*Twitter: [@mholt6](https://twitter.com/mholt6)*
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
+48
View File
@@ -0,0 +1,48 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"os"
"path/filepath"
"runtime"
)
// 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")
}
// userHomeDir returns the user's home directory according to
// environment variables.
//
// Credit: http://stackoverflow.com/a/7922977/1048862
func userHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
}
return os.Getenv("HOME")
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"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)
}
err := os.Setenv("CADDYPATH", "testpath")
if err != nil {
t.Error("Could not set CADDYPATH")
}
if actual, expected := AssetsPath(), "testpath"; actual != expected {
t.Errorf("Expected path to be %v, got: %v", expected, actual)
}
err = os.Setenv("CADDYPATH", "")
if err != nil {
t.Error("Could not set CADDYPATH")
}
}
+88
View File
@@ -0,0 +1,88 @@
# Mutilated beyond recognition from the example at:
# https://docs.microsoft.com/azure/devops/pipelines/languages/go
trigger:
- master
strategy:
matrix:
linux:
imageName: ubuntu-16.04
gorootDir: /usr/local
mac:
imageName: macos-10.13
gorootDir: /usr/local
windows:
imageName: windows-2019
gorootDir: C:\
pool:
vmImage: $(imageName)
variables:
GOROOT: $(gorootDir)/go
GOPATH: $(system.defaultWorkingDirectory)/gopath
GOBIN: $(GOPATH)/bin
modulePath: '$(GOPATH)/src/github.com/$(build.repository.name)'
# TODO: modules should be the default in Go 1.13, so this won't be needed
GO111MODULE: on
steps:
- bash: |
latestGo=$(curl "https://golang.org/VERSION?m=text")
echo "##vso[task.setvariable variable=LATEST_GO]$latestGo"
echo "Latest Go version: $latestGo"
displayName: "Get latest Go version"
- bash: |
sudo rm -f $(which go)
echo '##vso[task.prependpath]$(GOBIN)'
echo '##vso[task.prependpath]$(GOROOT)/bin'
mkdir -p '$(modulePath)'
shopt -s extglob
shopt -s dotglob
mv !(gopath) '$(modulePath)'
displayName: Remove old Go, set GOBIN/GOROOT, and move project into GOPATH
# Install Go (this varies by platform)
- bash: |
wget "https://dl.google.com/go/$(LATEST_GO).linux-amd64.tar.gz"
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).linux-amd64.tar.gz"
condition: eq( variables['Agent.OS'], 'Linux' )
displayName: Install Go on Linux
- bash: |
wget "https://dl.google.com/go/$(LATEST_GO).darwin-amd64.tar.gz"
sudo tar -C $(gorootDir) -xzf "$(LATEST_GO).darwin-amd64.tar.gz"
condition: eq( variables['Agent.OS'], 'Darwin' )
displayName: Install Go on macOS
- powershell: |
Write-Host "Downloading Go... (please be patient, I am very slow)"
(New-Object System.Net.WebClient).DownloadFile("https://dl.google.com/go/$(LATEST_GO).windows-amd64.zip", "$(LATEST_GO).windows-amd64.zip")
Write-Host "Extracting Go... (I'm slow too)"
Expand-Archive "$(LATEST_GO).windows-amd64.zip" -DestinationPath "$(gorootDir)"
condition: eq( variables['Agent.OS'], 'Windows_NT' )
displayName: Install Go on Windows
# TODO: When this issue is fixed, replace with installer script:
# https://github.com/golangci/golangci-lint/issues/472
- script: go get -v github.com/golangci/golangci-lint/cmd/golangci-lint
displayName: Install golangci-lint
- bash: |
printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n"
printf "\n\nGo environment:\n\n"
go env
printf "\n\nSystem environment:\n\n"
env
displayName: Print Go version and environment
- script: |
go get -v -t -d ./...
golangci-lint run -E gofmt -E goimports -E misspell
go test -race ./...
workingDirectory: '$(modulePath)'
displayName: Run tests
+1026
View File
File diff suppressed because it is too large Load Diff
+601
View File
@@ -0,0 +1,601 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddymain
import (
"bufio"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/klauspost/cpuid"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/caddytls"
"github.com/mholt/caddy/telemetry"
"github.com/mholt/certmagic"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
_ "github.com/mholt/caddy/caddyhttp" // plug in the HTTP server type
// This is where other plugins get plugged in (imported)
)
func init() {
caddy.TrapSignals()
flag.BoolVar(&certmagic.Default.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
flag.StringVar(&certmagic.Default.CA, "ca", certmagic.Default.CA, "URL to certificate authority's ACME server directory")
flag.StringVar(&certmagic.Default.DefaultServerName, "default-sni", certmagic.Default.DefaultServerName, "If a ClientHello ServerName is empty, use this ServerName to choose a TLS certificate")
flag.BoolVar(&certmagic.Default.DisableHTTPChallenge, "disable-http-challenge", certmagic.Default.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
flag.BoolVar(&certmagic.Default.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", certmagic.Default.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge")
flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable")
flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
flag.BoolVar(&printEnv, "env", false, "Enable to print environment variables")
flag.StringVar(&envFile, "envfile", "", "Path to file with environment variables to load in KEY=VALUE format")
flag.BoolVar(&fromJSON, "json-to-caddyfile", false, "From JSON stdin to Caddyfile stdout")
flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
flag.StringVar(&certmagic.Default.Email, "email", "", "Default ACME CA account email address")
flag.DurationVar(&certmagic.HTTPTimeout, "catimeout", certmagic.HTTPTimeout, "Default ACME CA HTTP timeout")
flag.StringVar(&logfile, "log", "", "Process log file")
flag.IntVar(&logRollMB, "log-roll-mb", 100, "Roll process log when it reaches this many megabytes (0 to disable rolling)")
flag.BoolVar(&logRollCompress, "log-roll-compress", true, "Gzip-compress rolled process log files")
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(&toJSON, "caddyfile-to-json", false, "From Caddyfile stdin to JSON stdout")
flag.BoolVar(&version, "version", false, "Show version")
flag.BoolVar(&validate, "validate", false, "Parse the Caddyfile but do not start the server")
caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
}
// Run is Caddy's main() function.
func Run() {
flag.Parse()
module := getBuildModule()
cleanModVersion := strings.TrimPrefix(module.Version, "v")
caddy.AppName = appName
caddy.AppVersion = module.Version
certmagic.UserAgent = appName + "/" + cleanModVersion
// 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:
if logRollMB > 0 {
log.SetOutput(&lumberjack.Logger{
Filename: logfile,
MaxSize: logRollMB,
MaxAge: 14,
MaxBackups: 10,
Compress: logRollCompress,
})
} else {
err := os.MkdirAll(filepath.Dir(logfile), 0755)
if err != nil {
mustLogFatalf("%v", err)
}
f, err := os.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
mustLogFatalf("%v", err)
}
// don't close file; log should be writeable for duration of process
log.SetOutput(f)
}
}
// load all additional envs as soon as possible
if err := LoadEnvFromFile(envFile); err != nil {
mustLogFatalf("%v", err)
}
if printEnv {
for _, v := range os.Environ() {
fmt.Println(v)
}
}
// initialize telemetry client
if EnableTelemetry {
err := initTelemetry()
if err != nil {
mustLogFatalf("[ERROR] Initializing telemetry: %v", err)
}
} else if disabledMetrics != "" {
mustLogFatalf("[ERROR] Cannot disable specific metrics because telemetry is disabled")
}
// Check for one-time actions
if revoke != "" {
err := caddytls.Revoke(revoke)
if err != nil {
mustLogFatalf("%v", err)
}
fmt.Printf("Revoked certificate for %s\n", revoke)
os.Exit(0)
}
if version {
if module.Sum != "" {
// a build with a known version will also have a checksum
fmt.Printf("Caddy %s (%s)\n", module.Version, module.Sum)
} else {
fmt.Println(module.Version)
}
os.Exit(0)
}
if plugins {
fmt.Println(caddy.DescribePlugins())
os.Exit(0)
}
// Check if we just need to do a Caddyfile Convert and exit
checkJSONCaddyfile()
// Set CPU cap
err := setCPU(cpu)
if err != nil {
mustLogFatalf("%v", err)
}
// Executes Startup events
caddy.EmitEvent(caddy.StartupEvent, nil)
// Get Caddyfile input
caddyfileinput, err := caddy.LoadCaddyfile(serverType)
if err != nil {
mustLogFatalf("%v", err)
}
if validate {
err := caddy.ValidateAndExecuteDirectives(caddyfileinput, nil, true)
if err != nil {
mustLogFatalf("%v", err)
}
msg := "Caddyfile is valid"
fmt.Println(msg)
log.Printf("[INFO] %s", msg)
os.Exit(0)
}
// Start your engines
instance, err := caddy.Start(caddyfileinput)
if err != nil {
mustLogFatalf("%v", err)
}
// Begin telemetry (these are no-ops if telemetry disabled)
telemetry.Set("caddy_version", module.Version)
telemetry.Set("num_listeners", len(instance.Servers()))
telemetry.Set("server_type", serverType)
telemetry.Set("os", runtime.GOOS)
telemetry.Set("arch", runtime.GOARCH)
telemetry.Set("cpu", struct {
BrandName string `json:"brand_name,omitempty"`
NumLogical int `json:"num_logical,omitempty"`
AESNI bool `json:"aes_ni,omitempty"`
}{
BrandName: cpuid.CPU.BrandName,
NumLogical: runtime.NumCPU(),
AESNI: cpuid.CPU.AesNi(),
})
if containerized := detectContainer(); containerized {
telemetry.Set("container", containerized)
}
telemetry.StartEmitting()
// 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)
}
var contents []byte
if strings.Contains(conf, "*") {
// Let caddyfile.doImport logic handle the globbed path
contents = []byte("import " + conf)
} else {
var err error
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
}
// getBuildModule returns the build info of Caddy
// from debug.BuildInfo (requires Go modules). If
// no version information is available, a non-nil
// value will still be returned, but with an
// unknown version.
func getBuildModule() *debug.Module {
bi, ok := debug.ReadBuildInfo()
if ok {
// The recommended way to build Caddy involves
// creating a separate main module, which
// preserves caddy a read-only dependency
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
for _, mod := range bi.Deps {
if mod.Path == "github.com/mholt/caddy" {
return mod
}
}
}
return &debug.Module{Version: "unknown"}
}
func checkJSONCaddyfile() {
if fromJSON {
jsonBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
os.Exit(1)
}
caddyfileBytes, err := caddyfile.FromJSON(jsonBytes)
if err != nil {
fmt.Fprintf(os.Stderr, "Converting from JSON failed: %v", err)
os.Exit(2)
}
fmt.Println(string(caddyfileBytes))
os.Exit(0)
}
if toJSON {
caddyfileBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
os.Exit(1)
}
jsonBytes, err := caddyfile.ToJSON(caddyfileBytes)
if err != nil {
fmt.Fprintf(os.Stderr, "Converting to JSON failed: %v", err)
os.Exit(2)
}
fmt.Println(string(jsonBytes))
os.Exit(0)
}
}
// 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%).
// If the percent resolves to less than a single
// GOMAXPROCS, it rounds it up to GOMAXPROCS=1.
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)
if numCPU < 1 {
numCPU = 1
}
} 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
}
// detectContainer attempts to determine whether the process is
// being run inside a container. References:
// https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
// https://stackoverflow.com/a/20012536/1048862
// https://gist.github.com/anantkamath/623ce7f5432680749e087cf8cfba9b69
func detectContainer() bool {
if runtime.GOOS != "linux" {
return false
}
file, err := os.Open("/proc/1/cgroup")
if err != nil {
return false
}
defer file.Close()
i := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
i++
if i > 1000 {
return false
}
line := scanner.Text()
parts := strings.SplitN(line, ":", 3)
if len(parts) < 3 {
continue
}
if strings.Contains(parts[2], "docker") ||
strings.Contains(parts[2], "lxc") ||
strings.Contains(parts[2], "moby") {
return true
}
}
return false
}
// initTelemetry initializes the telemetry engine.
func initTelemetry() error {
uuidFilename := filepath.Join(caddy.AssetsPath(), "uuid")
if customUUIDFile := os.Getenv("CADDY_UUID_FILE"); customUUIDFile != "" {
uuidFilename = customUUIDFile
}
newUUID := func() uuid.UUID {
id := uuid.New()
err := os.MkdirAll(caddy.AssetsPath(), 0700)
if err != nil {
log.Printf("[ERROR] Persisting instance UUID: %v", err)
return id
}
err = ioutil.WriteFile(uuidFilename, []byte(id.String()), 0600) // human-readable as a string
if err != nil {
log.Printf("[ERROR] Persisting instance UUID: %v", err)
}
return id
}
var id uuid.UUID
// load UUID from storage, or create one if we don't have one
if uuidFile, err := os.Open(uuidFilename); os.IsNotExist(err) {
// no UUID exists yet; create a new one and persist it
id = newUUID()
} else if err != nil {
log.Printf("[ERROR] Loading persistent UUID: %v", err)
id = newUUID()
} else {
defer uuidFile.Close()
uuidBytes, err := ioutil.ReadAll(uuidFile)
if err != nil {
log.Printf("[ERROR] Reading persistent UUID: %v", err)
id = newUUID()
} else {
id, err = uuid.ParseBytes(uuidBytes)
if err != nil {
log.Printf("[ERROR] Parsing UUID: %v", err)
id = newUUID()
}
}
}
// parse and check the list of disabled metrics
var disabledMetricsSlice []string
if len(disabledMetrics) > 0 {
if len(disabledMetrics) > 1024 {
// mitigate disk space exhaustion at the collection endpoint
return fmt.Errorf("too many metrics to disable")
}
disabledMetricsSlice = splitTrim(disabledMetrics, ",")
for _, metric := range disabledMetricsSlice {
if metric == "instance_id" || metric == "timestamp" || metric == "disabled_metrics" {
return fmt.Errorf("instance_id, timestamp, and disabled_metrics cannot be disabled")
}
}
}
// initialize telemetry
telemetry.Init(id, disabledMetricsSlice)
// if any metrics were disabled, report which ones (so we know how representative the data is)
if len(disabledMetricsSlice) > 0 {
telemetry.Set("disabled_metrics", disabledMetricsSlice)
log.Printf("[NOTICE] The following telemetry metrics are disabled: %s", disabledMetrics)
}
return nil
}
// Split string s into all substrings separated by sep and returns a slice of
// the substrings between those separators.
//
// If s does not contain sep and sep is not empty, Split returns a
// slice of length 1 whose only element is s.
//
// If sep is empty, Split splits after each UTF-8 sequence. If both s
// and sep are empty, Split returns an empty slice.
//
// Each item that in result is trim space and not empty string
func splitTrim(s string, sep string) []string {
splitItems := strings.Split(s, sep)
trimItems := make([]string, 0, len(splitItems))
for _, item := range splitItems {
if item = strings.TrimSpace(item); item != "" {
trimItems = append(trimItems, item)
}
}
return trimItems
}
// LoadEnvFromFile loads additional envs if file provided and exists
// Envs in file should be in KEY=VALUE format
func LoadEnvFromFile(envFile string) error {
if envFile == "" {
return nil
}
file, err := os.Open(envFile)
if err != nil {
return err
}
defer file.Close()
envMap, err := ParseEnvFile(file)
if err != nil {
return err
}
for k, v := range envMap {
if err := os.Setenv(k, v); err != nil {
return err
}
}
return nil
}
// ParseEnvFile implements parse logic for environment files
func ParseEnvFile(envInput io.Reader) (map[string]string, error) {
envMap := make(map[string]string)
scanner := bufio.NewScanner(envInput)
var line string
lineNumber := 0
for scanner.Scan() {
line = strings.TrimSpace(scanner.Text())
lineNumber++
// skip lines starting with comment
if strings.HasPrefix(line, "#") {
continue
}
// skip empty line
if len(line) == 0 {
continue
}
fields := strings.SplitN(line, "=", 2)
if len(fields) != 2 {
return nil, fmt.Errorf("Can't parse line %d; line should be in KEY=VALUE format", lineNumber)
}
if strings.Contains(fields[0], " ") {
return nil, fmt.Errorf("Can't parse line %d; KEY contains whitespace", lineNumber)
}
key := fields[0]
val := fields[1]
if key == "" {
return nil, fmt.Errorf("Can't parse line %d; KEY can't be empty string", lineNumber)
}
envMap[key] = val
}
if err := scanner.Err(); err != nil {
return nil, err
}
return envMap, nil
}
const appName = "Caddy"
// Flags that control program flow or startup
var (
serverType string
conf string
cpu string
envFile string
fromJSON bool
logfile string
logRollMB int
logRollCompress bool
revoke string
toJSON bool
version bool
plugins bool
printEnv bool
validate bool
disabledMetrics string
)
// EnableTelemetry defines whether telemetry is enabled in Run.
var EnableTelemetry = true
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddymain
import (
"reflect"
"runtime"
"strings"
"testing"
)
func TestSetCPU(t *testing.T) {
currentCPU := runtime.GOMAXPROCS(-1)
maxCPU := runtime.NumCPU()
halfCPU := int(0.5 * float32(maxCPU))
if halfCPU < 1 {
halfCPU = 1
}
for i, test := range []struct {
input string
output int
shouldErr bool
}{
{"1", 1, false},
{"-1", currentCPU, true},
{"0", currentCPU, true},
{"100%", maxCPU, false},
{"50%", halfCPU, false},
{"110%", currentCPU, true},
{"-10%", currentCPU, true},
{"invalid input", currentCPU, true},
{"invalid input%", currentCPU, true},
{"9999", maxCPU, false}, // over available CPU
{"1%", 1, false}, // under a single CPU; assume maxCPU < 100
} {
err := setCPU(test.input)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error, but there wasn't any", i)
}
if !test.shouldErr && err != nil {
t.Errorf("Test %d: Expected no error, but there was one: %v", i, err)
}
if actual, expected := runtime.GOMAXPROCS(-1), test.output; actual != expected {
t.Errorf("Test %d: GOMAXPROCS was %d but expected %d", i, actual, expected)
}
// teardown
runtime.GOMAXPROCS(currentCPU)
}
}
func TestSplitTrim(t *testing.T) {
for i, test := range []struct {
input string
output []string
sep string
}{
{"os,arch,cpu,caddy_version", []string{"os", "arch", "cpu", "caddy_version"}, ","},
{"os,arch,cpu,caddy_version,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
{"os,,, arch, cpu, caddy_version,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
{", , os, arch, cpu , caddy_version,, ,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
{"os, ,, arch, cpu , caddy_version,, ,", []string{"os", "arch", "cpu", "caddy_version"}, ","},
} {
got := splitTrim(test.input, test.sep)
if len(got) != len(test.output) {
t.Errorf("Test %d: spliteTrim() = %v, want %v", i, got, test.output)
continue
}
for j, item := range test.output {
if item != got[j] {
t.Errorf("Test %d: spliteTrim() = %v, want %v", i, got, test.output)
break
}
}
}
}
func TestParseEnvFile(t *testing.T) {
tests := []struct {
name string
input string
want map[string]string
wantErr bool
}{
{"parsing KEY=VALUE", "PORT=4096", map[string]string{"PORT": "4096"}, false},
{"empty KEY", "=4096", nil, true},
{"one value", "test", nil, true},
{"comments skipped", "#TEST=1\nPORT=8888", map[string]string{"PORT": "8888"}, false},
{"empty line", "\nPORT=7777", map[string]string{"PORT": "7777"}, false},
{"comments with space skipped", " #TEST=1", map[string]string{}, false},
{"KEY with space", "PORT =8888", nil, true},
{"only spaces", " ", map[string]string{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := strings.NewReader(tt.input)
got, err := ParseEnvFile(reader)
if (err != nil) != tt.wantErr {
t.Errorf("ParseEnvFile() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseEnvFile() = %v, want %v", got, tt.want)
}
})
}
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// 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://caddy.community/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()
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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")
}
}
+212
View File
@@ -0,0 +1,212 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"fmt"
"log"
"reflect"
"sync"
"testing"
"github.com/mholt/caddy/caddyfile"
)
/*
// 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)
}
}
}
*/
// CallbackTestContext implements Context interface
type CallbackTestContext struct {
// If MakeServersFail is set to true then MakeServers returns an error
MakeServersFail bool
}
func (h *CallbackTestContext) InspectServerBlocks(name string, sblock []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
return sblock, nil
}
func (h *CallbackTestContext) MakeServers() ([]Server, error) {
if h.MakeServersFail {
return make([]Server, 0), fmt.Errorf("MakeServers failed")
}
return make([]Server, 0), nil
}
func TestCaddyRestartCallbacks(t *testing.T) {
for i, test := range []struct {
restartFail bool
expectedCalls []string
}{
{false, []string{"OnRestart", "OnShutdown"}},
{true, []string{"OnRestart", "OnRestartFailed"}},
} {
serverName := fmt.Sprintf("%v", i)
// RegisterServerType to make successful restart possible
RegisterServerType(serverName, ServerType{
Directives: func() []string { return []string{} },
// If MakeServersFail is true then the restart will fail due to context failure
NewContext: func(inst *Instance) Context { return &CallbackTestContext{MakeServersFail: test.restartFail} },
})
c := NewTestController(serverName, "")
c.instance = &Instance{
serverType: serverName,
wg: new(sync.WaitGroup),
}
// Register callbacks which save the calls order
calls := make([]string, 0)
c.OnRestart(func() error {
calls = append(calls, "OnRestart")
return nil
})
c.OnRestartFailed(func() error {
calls = append(calls, "OnRestartFailed")
return nil
})
c.OnShutdown(func() error {
calls = append(calls, "OnShutdown")
return nil
})
_, err := c.instance.Restart(CaddyfileInput{Contents: []byte(""), ServerTypeName: serverName})
if err != nil {
log.Printf("[ERROR] Restart failed: %v", err)
}
if !reflect.DeepEqual(calls, test.expectedCalls) {
t.Errorf("Test %d: Callbacks expected: %v, got: %v", i, test.expectedCalls, calls)
}
err = c.instance.Stop()
if err != nil {
log.Printf("[ERROR] Stop failed: %v", err)
}
c.instance.Wait()
}
}
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 TestIsInternal(t *testing.T) {
for i, test := range []struct {
input string
expect bool
}{
{"9.255.255.255", false},
{"10.0.0.0", true},
{"10.0.0.1", true},
{"10.255.255.254", true},
{"10.255.255.255", true},
{"11.0.0.0", false},
{"10.0.0.5:1234", true},
{"11.0.0.5:1234", false},
{"172.15.255.255", false},
{"172.16.0.0", true},
{"172.16.0.1", true},
{"172.31.255.254", true},
{"172.31.255.255", true},
{"172.32.0.0", false},
{"172.16.0.1:1234", true},
{"192.167.255.255", false},
{"192.168.0.0", true},
{"192.168.0.1", true},
{"192.168.255.254", true},
{"192.168.255.255", true},
{"192.169.0.0", false},
{"192.168.0.1:1234", true},
{"fbff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", false},
{"fc00::", true},
{"fc00::1", true},
{"[fc00::1]", true},
{"[fc00::1]:8888", true},
{"fdff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", true},
{"fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true},
{"fe00::", false},
{"fd12:3456:789a:1::1:1234", true},
{"example.com", false},
{"localhost", false},
{"localhost:1234", false},
{"localhost:", false},
{"127.0.0.1", false},
{"127.0.0.1:443", false},
{"127.0.1.5", false},
{"12.7.0.1", false},
{"[::1]", false},
{"[::1]:1234", false},
{"::1", false},
{"::", false},
{"[::]", false},
{"local", false},
} {
if got, want := IsInternal(test.input), test.expect; got != want {
t.Errorf("Test %d (%s): expected %v but was %v", i, test.input, want, got)
}
}
}
@@ -1,4 +1,18 @@
package parse
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"errors"
@@ -12,22 +26,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,
@@ -37,7 +52,7 @@ func NewDispenserTokens(filename string, tokens []token) Dispenser {
// Next loads the next token. Returns true if a token
// was loaded; false otherwise. If false, all tokens
// have already been consumed.
// have been consumed.
func (d *Dispenser) Next() bool {
if d.cursor < len(d.tokens)-1 {
d.cursor++
@@ -49,7 +64,7 @@ func (d *Dispenser) Next() bool {
// NextArg loads the next token if it is on the same
// line. Returns true if a token was loaded; false
// otherwise. If false, all tokens on the line have
// been consumed.
// been consumed. It handles imported tokens correctly.
func (d *Dispenser) NextArg() bool {
if d.cursor < 0 {
d.cursor++
@@ -59,7 +74,8 @@ func (d *Dispenser) NextArg() bool {
return false
}
if d.cursor < len(d.tokens)-1 &&
(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
}
@@ -69,7 +85,7 @@ func (d *Dispenser) NextArg() bool {
// NextLine loads the next token only if it is not on the same
// line as the current token, and returns true if a token was
// loaded; false otherwise. If false, there is not another token
// or it is on the same line.
// or it is on the same line. It handles imported tokens correctly.
func (d *Dispenser) NextLine() bool {
if d.cursor < 0 {
d.cursor++
@@ -79,7 +95,8 @@ func (d *Dispenser) NextLine() bool {
return false
}
if d.cursor < len(d.tokens)-1 &&
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
}
@@ -109,6 +126,10 @@ func (d *Dispenser) NextBlock() bool {
return false
}
d.Next()
if d.Val() == "}" {
// Open and then closed right away
return false
}
d.nesting++
return true
}
@@ -119,7 +140,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
@@ -128,7 +149,19 @@ 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,
// it returns the filename originally given when parsing started.
func (d *Dispenser) File() string {
if d.cursor < 0 || d.cursor >= len(d.tokens) {
return d.filename
}
if tokenFilename := d.tokens[d.cursor].File; tokenFilename != "" {
return tokenFilename
}
return d.filename
}
// Args is a convenience function that loads the next arguments
@@ -181,19 +214,19 @@ func (d *Dispenser) ArgErr() error {
// SyntaxErr creates a generic syntax error which explains what was
// found and what was expected.
func (d *Dispenser) SyntaxErr(expected string) error {
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.filename, d.Line(), d.Val(), expected)
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
return errors.New(msg)
}
// EofErr returns an EOF error, meaning that end of input
// was found when another token was expected.
func (d *Dispenser) EofErr() error {
// EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error {
return d.Errf("Unexpected EOF")
}
// Err generates a custom parse error with a message of msg.
// Err generates a custom parse-time error with a message of msg.
func (d *Dispenser) Err(msg string) error {
msg = fmt.Sprintf("%s:%d - Parse error: %s", d.filename, d.Line(), msg)
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
return errors.New(msg)
}
@@ -209,5 +242,19 @@ 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
// line (higher line number) than the previous token. It handles imported
// tokens correctly. If there isn't a previous token, it returns true.
func (d *Dispenser) isNewLine() bool {
if d.cursor < 1 {
return true
}
if d.cursor > len(d.tokens)-1 {
return false
}
return d.tokens[d.cursor-1].File != d.tokens[d.cursor].File ||
d.tokens[d.cursor-1].Line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].Line
}
@@ -1,4 +1,18 @@
package parse
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"reflect"
@@ -64,7 +78,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 +88,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 {
@@ -149,9 +163,8 @@ func TestDispenser_NextBlock(t *testing.T) {
assertNextBlock(true, 3, 1)
assertNextBlock(true, 4, 1)
assertNextBlock(false, 5, 0)
d.Next() // foobar2
assertNextBlock(true, 8, 1)
assertNextBlock(false, 8, 0)
d.Next() // foobar2
assertNextBlock(false, 8, 0) // empty block is as if it didn't exist
}
func TestDispenser_Args(t *testing.T) {
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
)
const filename = "Caddyfile"
// ToJSON converts caddyfile to its JSON representation.
func ToJSON(caddyfile []byte) ([]byte, error) {
var j EncodedCaddyfile
serverBlocks, err := Parse(filename, bytes.NewReader(caddyfile), nil)
if err != nil {
return nil, err
}
for _, sb := range serverBlocks {
block := EncodedServerBlock{
Keys: sb.Keys,
Body: [][]interface{}{},
}
// Extract directives deterministically by sorting them
var directives = make([]string, len(sb.Tokens))
for dir := range sb.Tokens {
directives = append(directives, dir)
}
sort.Strings(directives)
// Convert each directive's tokens into our JSON structure
for _, dir := range directives {
disp := NewDispenserTokens(filename, sb.Tokens[dir])
for disp.Next() {
block.Body = append(block.Body, constructLine(&disp))
}
}
// tack this block onto the end of the list
j = append(j, block)
}
result, err := json.Marshal(j)
if err != nil {
return nil, err
}
return result, nil
}
// constructLine transforms tokens into a JSON-encodable structure;
// but only one line at a time, to be used at the top-level of
// a server block only (where the first token on each line is a
// directive) - not to be used at any other nesting level.
func constructLine(d *Dispenser) []interface{} {
var args []interface{}
args = append(args, d.Val())
for d.NextArg() {
if d.Val() == "{" {
args = append(args, constructBlock(d))
continue
}
args = append(args, d.Val())
}
return args
}
// constructBlock recursively processes tokens into a
// JSON-encodable structure. To be used in a directive's
// block. Goes to end of block.
func constructBlock(d *Dispenser) [][]interface{} {
block := [][]interface{}{}
for d.Next() {
if d.Val() == "}" {
break
}
block = append(block, constructLine(d))
}
return block
}
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
func FromJSON(jsonBytes []byte) ([]byte, error) {
var j EncodedCaddyfile
var result string
err := json.Unmarshal(jsonBytes, &j)
if err != nil {
return nil, err
}
for sbPos, sb := range j {
if sbPos > 0 {
result += "\n\n"
}
for i, key := range sb.Keys {
if i > 0 {
result += ", "
}
//result += standardizeScheme(key)
result += key
}
result += jsonToText(sb.Body, 1)
}
return []byte(result), nil
}
// jsonToText recursively transforms a scope of JSON into plain
// Caddyfile text.
func jsonToText(scope interface{}, depth int) string {
var result string
switch val := scope.(type) {
case string:
if strings.ContainsAny(val, "\" \n\t\r") {
result += `"` + strings.Replace(val, "\"", "\\\"", -1) + `"`
} else {
result += val
}
case int:
result += strconv.Itoa(val)
case float64:
result += fmt.Sprintf("%v", val)
case bool:
result += fmt.Sprintf("%t", val)
case [][]interface{}:
result += " {\n"
for _, arg := range val {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
case []interface{}:
for i, v := range val {
if block, ok := v.([]interface{}); ok {
result += "{\n"
for _, arg := range block {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
continue
}
result += jsonToText(v, depth)
if i < len(val)-1 {
result += " "
}
}
}
return result
}
// 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 {
if hostname, port, err := net.SplitHostPort(addr); err == nil {
if port == "http" || port == "https" {
addr = port + "://" + hostname
}
}
return strings.TrimSuffix(addr, ":")
}
*/
// EncodedCaddyfile encapsulates a slice of EncodedServerBlocks.
type EncodedCaddyfile []EncodedServerBlock
// EncodedServerBlock represents a server block ripe for encoding.
type EncodedServerBlock struct {
Keys []string `json:"keys"`
Body [][]interface{} `json:"body"`
}
+178
View File
@@ -0,0 +1,178 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import "testing"
var tests = []struct {
caddyfile, json string
}{
{ // 0
caddyfile: `foo {
root /bar
}`,
json: `[{"keys":["foo"],"body":[["root","/bar"]]}]`,
},
{ // 1
caddyfile: `host1, host2 {
dir {
def
}
}`,
json: `[{"keys":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
},
{ // 2
caddyfile: `host1, host2 {
dir abc {
def ghi
jkl
}
}`,
json: `[{"keys":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
},
{ // 3
caddyfile: `host1:1234, host2:5678 {
dir abc {
}
}`,
json: `[{"keys":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
},
{ // 4
caddyfile: `host {
foo "bar baz"
}`,
json: `[{"keys":["host"],"body":[["foo","bar baz"]]}]`,
},
{ // 5
caddyfile: `host, host:80 {
foo "bar \"baz\""
}`,
json: `[{"keys":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
},
{ // 6
caddyfile: `host {
foo "bar
baz"
}`,
json: `[{"keys":["host"],"body":[["foo","bar\nbaz"]]}]`,
},
{ // 7
caddyfile: `host {
dir 123 4.56 true
}`,
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: `[{"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: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
},
{ // 10
caddyfile: `host {
dir a b
dir c d
}`,
json: `[{"keys":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
},
{ // 11
caddyfile: `host {
dir1 a b
dir2 {
c
d
}
}`,
json: `[{"keys":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
},
{ // 12
caddyfile: `host1 {
dir1
}
host2 {
dir2
}`,
json: `[{"keys":["host1"],"body":[["dir1"]]},{"keys":["host2"],"body":[["dir2"]]}]`,
},
}
func TestToJSON(t *testing.T) {
for i, test := range tests {
output, err := ToJSON([]byte(test.caddyfile))
if err != nil {
t.Errorf("Test %d: %v", i, err)
}
if string(output) != test.json {
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.json, string(output))
}
}
}
func TestFromJSON(t *testing.T) {
for i, test := range tests {
output, err := FromJSON([]byte(test.json))
if err != nil {
t.Errorf("Test %d: %v", i, err)
}
if string(output) != test.caddyfile {
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.caddyfile, string(output))
}
}
}
// 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 := `[{"keys":["https://host"],"body":[]}]`, string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
output, err = FromJSON([]byte(`[{"keys":["https://host"],"body":[]}]`))
if err != nil {
t.Fatal(err)
}
if expected, actual := "https://host {\n}", string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
// host: should be converted to just host
output, err = ToJSON([]byte(`host:`))
if err != nil {
t.Fatal(err)
}
if expected, actual := `[{"keys":["host"],"body":[]}]`, string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
output, err = FromJSON([]byte(`[{"keys":["host:"],"body":[]}]`))
if err != nil {
t.Fatal(err)
}
if expected, actual := "host {\n}", string(output); expected != actual {
t.Errorf("Expected:\n'%s'\nActual:\n'%s'", expected, actual)
}
}
*/
+49 -13
View File
@@ -1,4 +1,18 @@
package parse
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"bufio"
@@ -13,21 +27,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 {
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
}
@@ -36,15 +65,17 @@ func (l *lexer) load(input io.Reader) error {
// the token starts with a quotes character (")
// in which case the token goes until the closing
// quotes (the enclosing quotes are not included).
// The rest of the line is skipped if a "#"
// character is read in. Returns true if a token
// was loaded; false otherwise.
// Inside quoted strings, quotes may be escaped
// with a preceding \ character. No other chars
// may be escaped. The rest of the line is skipped
// if a "#" character is read in. Returns true if
// a token was loaded; false otherwise.
func (l *lexer) next() bool {
var val []rune
var comment, quoted, escaped bool
makeToken := func() bool {
l.token.text = string(val)
l.token.Text = string(val)
return true
}
@@ -56,9 +87,8 @@ func (l *lexer) next() bool {
}
if err == io.EOF {
return false
} else {
panic(err)
}
panic(err)
}
if quoted {
@@ -74,6 +104,12 @@ func (l *lexer) next() bool {
if ch == '\n' {
l.line++
}
if escaped {
// only escape quotes
if ch != '"' {
val = append(val, '\\')
}
}
val = append(val, ch)
escaped = false
continue
@@ -102,7 +138,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
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"log"
"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{}
if err := l.load(strings.NewReader(input)); err != nil {
log.Printf("[ERROR] load failed: %v", err)
}
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
}
}
}
+493
View File
@@ -0,0 +1,493 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/mholt/caddy/telemetry"
)
// 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
definedSnippets map[string][]Token
}
func (p *parser) parseAll() ([]ServerBlock, error) {
var blocks []ServerBlock
for p.Next() {
err := p.parseOne()
if err != nil {
return blocks, err
}
if len(p.block.Keys) > 0 {
blocks = append(blocks, p.block)
}
}
return blocks, nil
}
func (p *parser) parseOne() error {
p.block = ServerBlock{Tokens: make(map[string][]Token)}
return p.begin()
}
func (p *parser) begin() error {
if len(p.tokens) == 0 {
return nil
}
err := p.addresses()
if err != nil {
return err
}
if p.eof {
// this happens if the Caddyfile consists of only
// a line of addresses and nothing else
return nil
}
if ok, name := p.isSnippet(); ok {
if p.definedSnippets == nil {
p.definedSnippets = map[string][]Token{}
}
if _, found := p.definedSnippets[name]; found {
return p.Errf("redeclaration of previously declared snippet %s", name)
}
// consume all tokens til matched close brace
tokens, err := p.snippetTokens()
if err != nil {
return err
}
p.definedSnippets[name] = tokens
// empty block keys so we don't save this block as a real server.
p.block.Keys = nil
return nil
}
return p.blockContents()
}
func (p *parser) addresses() error {
var expectingAnother bool
for {
tkn := replaceEnvVars(p.Val())
// special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() {
err := p.doImport()
if err != nil {
return err
}
continue
}
// Open brace definitely indicates end of addresses
if tkn == "{" {
if expectingAnother {
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
}
break
}
if tkn != "" { // empty token possible if user typed ""
// Trailing comma indicates another address will follow, which
// may possibly be on the next line
if tkn[len(tkn)-1] == ',' {
tkn = tkn[:len(tkn)-1]
expectingAnother = true
} else {
expectingAnother = false // but we may still see another one on this line
}
p.block.Keys = append(p.block.Keys, tkn)
}
// Advance token and possibly break out of loop or return error
hasNext := p.Next()
if expectingAnother && !hasNext {
return p.EOFErr()
}
if !hasNext {
p.eof = true
break // EOF
}
if !expectingAnother && p.isNewLine() {
break
}
}
return nil
}
func (p *parser) blockContents() error {
errOpenCurlyBrace := p.openCurlyBrace()
if errOpenCurlyBrace != nil {
// single-server configs don't need curly braces
p.cursor--
}
err := p.directives()
if err != nil {
return err
}
// Only look for close curly brace if there was an opening
if errOpenCurlyBrace == nil {
err = p.closeCurlyBrace()
if err != nil {
return err
}
}
return nil
}
// directives parses through all the lines for directives
// and it expects the next token to be the first
// directive. It goes until EOF or closing curly brace
// which ends the server block.
func (p *parser) directives() error {
for p.Next() {
// end of server block
if p.Val() == "}" {
break
}
// special case: import directive replaces tokens during parse-time
if p.Val() == "import" {
err := p.doImport()
if err != nil {
return err
}
p.cursor-- // cursor is advanced when we continue, so roll back one more
continue
}
// normal case: parse a directive on this line
if err := p.directive(); err != nil {
return err
}
}
return nil
}
// doImport swaps out the import directive and its argument
// (a total of 2 tokens) with the tokens in the specified file
// or globbing pattern. When the function returns, the cursor
// is on the token before where the import directive was. In
// other words, call Next() to access the first token that was
// imported.
func (p *parser) doImport() error {
// syntax checks
if !p.NextArg() {
return p.ArgErr()
}
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)")
}
// splice out the import directive and its argument (2 tokens total)
tokensBefore := p.tokens[:p.cursor-1]
tokensAfter := p.tokens[p.cursor+1:]
var importedTokens []Token
// first check snippets. That is a simple, non-recursive replacement
if p.definedSnippets != nil && p.definedSnippets[importPattern] != nil {
importedTokens = p.definedSnippets[importPattern]
} else {
// make path relative to the file of the _token_ being processed rather
// than current working directory (issue #867) and then use glob to get
// list of matching filenames
absFile, err := filepath.Abs(p.Dispenser.File())
if err != nil {
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
}
var matches []string
var globPattern string
if !filepath.IsAbs(importPattern) {
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
} else {
globPattern = importPattern
}
if strings.Count(globPattern, "*") > 1 || strings.Count(globPattern, "?") > 1 ||
(strings.Contains(globPattern, "[") && strings.Contains(globPattern, "]")) {
// See issue #2096 - a pattern with many glob expansions can hang for too long
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
}
matches, err = filepath.Glob(globPattern)
if err != nil {
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
}
if len(matches) == 0 {
if strings.ContainsAny(globPattern, "*?[]") {
log.Printf("[WARNING] No files matching import glob pattern: %s", importPattern)
} else {
return p.Errf("File to import not found: %s", importPattern)
}
}
// collect all the imported tokens
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
if err != nil {
return err
}
importedTokens = append(importedTokens, newTokens...)
}
}
// splice the imported tokens in the place of the import statement
// and rewind cursor so Next() will land on first imported token
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
p.cursor--
return nil
}
// doSingleImport lexes the individual file at importFile and returns
// its tokens or an error, if any.
func (p *parser) doSingleImport(importFile string) ([]Token, error) {
file, err := os.Open(importFile)
if err != nil {
return nil, p.Errf("Could not import %s: %v", importFile, err)
}
defer file.Close()
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 file path onto these tokens so errors show the imported file's name
// (we use full, absolute path to avoid bugs: issue #1892)
filename, err := filepath.Abs(importFile)
if err != nil {
return nil, p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
}
for i := 0; i < len(importedTokens); i++ {
importedTokens[i].File = filename
}
return importedTokens, nil
}
// directive collects tokens until the directive's scope
// closes (either end of line or end of curly brace block).
// It expects the currently-loaded token to be a directive
// (or } that ends a server block). The collected tokens
// are loaded into the current server block for later use
// by directive setup functions.
func (p *parser) directive() error {
dir := replaceEnvVars(p.Val())
nesting := 0
// 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
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
telemetry.AppendUnique("directives", dir)
for p.Next() {
if p.Val() == "{" {
nesting++
} else if p.isNewLine() && nesting == 0 {
p.cursor-- // read too far
break
} else if p.Val() == "}" && nesting > 0 {
nesting--
} else if p.Val() == "}" && nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace")
} else if p.Val() == "import" && p.isNewLine() {
if err := p.doImport(); err != nil {
return err
}
p.cursor-- // cursor is advanced when we continue, so roll back one more
continue
}
p.tokens[p.cursor].Text = replaceEnvVars(p.tokens[p.cursor].Text)
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
}
if nesting > 0 {
return p.EOFErr()
}
return nil
}
// openCurlyBrace expects the current token to be an
// opening curly brace. This acts like an assertion
// because it returns an error if the token is not
// a opening curly brace. It does NOT advance the token.
func (p *parser) openCurlyBrace() error {
if p.Val() != "{" {
return p.SyntaxErr("{")
}
return nil
}
// closeCurlyBrace expects the current token to be
// a closing curly brace. This acts like an assertion
// because it returns an error if the token is not
// a closing curly brace. It does NOT advance the token.
func (p *parser) closeCurlyBrace() error {
if p.Val() != "}" {
return p.SyntaxErr("}")
}
return nil
}
// validDirective returns true if dir is in p.validDirectives.
func (p *parser) validDirective(dir string) bool {
if p.validDirectives == nil {
return true
}
for _, d := range p.validDirectives {
if d == dir {
return true
}
}
return false
}
// replaceEnvVars replaces environment variables that appear in the token
// and understands both the $UNIX and %WINDOWS% syntaxes.
func replaceEnvVars(s string) string {
s = replaceEnvReferences(s, "{%", "%}")
s = replaceEnvReferences(s, "{$", "}")
return s
}
// replaceEnvReferences performs the actual replacement of env variables
// in s, given the placeholder start and placeholder end strings.
func replaceEnvReferences(s, refStart, refEnd string) string {
index := strings.Index(s, refStart)
for index != -1 {
endIndex := strings.Index(s[index:], refEnd)
if endIndex == -1 {
break
}
endIndex += index
if endIndex > index+len(refStart) {
ref := s[index : endIndex+len(refEnd)]
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
} else {
return s
}
index = strings.Index(s, refStart)
}
return s
}
// 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
}
func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies.
if len(keys) == 1 && strings.HasPrefix(keys[0], "(") && strings.HasSuffix(keys[0], ")") {
return true, strings.TrimSuffix(keys[0][1:], ")")
}
return false, ""
}
// read and store everything in a block for later replay.
func (p *parser) snippetTokens() ([]Token, error) {
// TODO: disallow imports in snippets for simplicity at import time
// snippet must have curlies.
err := p.openCurlyBrace()
if err != nil {
return nil, err
}
count := 1
tokens := []Token{}
for p.Next() {
if p.Val() == "}" {
count--
if count == 0 {
break
}
}
if p.Val() == "{" {
count++
}
tokens = append(tokens, p.tokens[p.cursor])
}
// make sure we're matched up
if count != 0 {
return nil, p.SyntaxErr("}")
}
return tokens, nil
}
+720
View File
@@ -0,0 +1,720 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyfile
import (
"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{}},
// test cases found by fuzzing!
{`import }{$"`, true, []string{}, map[string]int{}},
{`import /*/*.txt`, true, []string{}, map[string]int{}},
{`import /???/?*?o`, true, []string{}, map[string]int{}},
{`import /??`, true, []string{}, map[string]int{}},
{`import /[a-z]`, true, []string{}, map[string]int{}},
{`import {$}`, true, []string{}, map[string]int{}},
{`import {%}`, true, []string{}, map[string]int{}},
{`import {$$}`, true, []string{}, map[string]int{}},
{`import {%%}`, true, []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 TestDirectiveImport(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["proxy"]) != 8 {
t.Errorf("got unexpect tokens: %v", got.Tokens)
return false
}
return true
}
directiveFile, err := filepath.Abs("testdata/directive_import_test")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(directiveFile, []byte(`prop1 1
prop2 2`), 0644)
if err != nil {
t.Fatal(err)
}
defer os.Remove(directiveFile)
// import from existing file
result, err := testParseOne(`localhost
dir1
proxy {
import testdata/directive_import_test
transparent
}`)
if err != nil {
t.Fatal(err)
}
if !isExpected(result) {
t.Error("directive import failed")
}
// import from nonexistent file
_, err = testParseOne(`localhost
dir1
proxy {
import testdata/nonexistent_file
transparent
}`)
if err == nil {
t.Fatal("expected error when importing a nonexistent file")
}
}
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")
os.Setenv("PARTIAL_DIR", "r1")
// 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)
}
// basic test; unix-style env vars
p = testParser(`di{$PARTIAL_DIR}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Keys[0], "dir1"; 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)
}
// after end token
p = testParser(":1234\nanswer \"{{ .Name }} {$FOOBAR}\"")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["answer"][1].Text, "{{ .Name }} foobar"; 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
}
func TestSnippets(t *testing.T) {
p := testParser(`
(common) {
gzip foo
errors stderr
}
http://example.com {
import common
}
`)
blocks, err := p.parseAll()
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Tokens)
}
if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
}
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
}
if len(blocks[0].Tokens) != 2 {
t.Fatalf("Server block should have tokens from import")
}
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Tokens["errors"][1].Text, "stderr"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
}
func writeStringToTempFileOrDie(t *testing.T, str string) (pathToFile string) {
file, err := ioutil.TempFile("", t.Name())
if err != nil {
panic(err) // get a stack trace so we know where this was called from.
}
if _, err := file.WriteString(str); err != nil {
panic(err)
}
if err := file.Close(); err != nil {
panic(err)
}
return file.Name()
}
func TestImportedFilesIgnoreNonDirectiveImportTokens(t *testing.T) {
fileName := writeStringToTempFileOrDie(t, `
http://example.com {
# This isn't an import directive, it's just an arg with value 'import'
basicauth / import password
}
`)
// Parse the root file that imports the other one.
p := testParser(`import ` + fileName)
blocks, err := p.parseAll()
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Tokens)
}
auth := blocks[0].Tokens["basicauth"]
line := auth[0].Text + " " + auth[1].Text + " " + auth[2].Text + " " + auth[3].Text
if line != "basicauth / import password" {
// Previously, it would be changed to:
// basicauth / import /path/to/test/dir/password
// referencing a file that (probably) doesn't exist and changing the
// password!
t.Errorf("Expected basicauth tokens to be 'basicauth / import password' but got %#q", line)
}
}
func TestSnippetAcrossMultipleFiles(t *testing.T) {
// Make the derived Caddyfile that expects (common) to be defined.
fileName := writeStringToTempFileOrDie(t, `
http://example.com {
import common
}
`)
// Parse the root file that defines (common) and then imports the other one.
p := testParser(`
(common) {
gzip foo
}
import ` + fileName + `
`)
blocks, err := p.parseAll()
if err != nil {
t.Fatal(err)
}
for _, b := range blocks {
t.Log(b.Keys)
t.Log(b.Tokens)
}
if len(blocks) != 1 {
t.Fatalf("Expect exactly one server block. Got %d.", len(blocks))
}
if actual, expected := blocks[0].Keys[0], "http://example.com"; expected != actual {
t.Errorf("Expected server name to be '%s' but was '%s'", expected, actual)
}
if len(blocks[0].Tokens) != 1 {
t.Fatalf("Server block should have tokens from import")
}
if actual, expected := blocks[0].Tokens["gzip"][0].Text, "gzip"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
}
+6
View File
@@ -0,0 +1,6 @@
glob0.host0 {
dir2 arg1
}
glob0.host1 {
}
+4
View File
@@ -0,0 +1,4 @@
glob1.host0 {
dir1
dir2 arg1
}
+3
View File
@@ -0,0 +1,3 @@
glob2.host0 {
dir2 arg1
}
+2
View File
@@ -0,0 +1,2 @@
dir2 arg1 arg2
dir3
+4
View File
@@ -0,0 +1,4 @@
host1 {
dir1
dir2 arg1
}
+209
View File
@@ -0,0 +1,209 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package 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"
"context"
"crypto/sha1"
"crypto/subtle"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/jimstudt/http-authentication/basic"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// BasicAuth is middleware to protect resources with a username and password.
// Note that HTTP Basic Authentication is not secure by itself and should
// not be used to protect important assets without HTTPS. Even then, the
// security of HTTP Basic Auth is disputed. Use discretion when deciding
// what to protect with BasicAuth.
type BasicAuth struct {
Next httpserver.Handler
SiteRoot string
Rules []Rule
}
// ServeHTTP implements the httpserver.Handler interface.
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var protected, isAuthenticated bool
var realm string
var username string
var password string
var ok bool
// do not check for basic auth on OPTIONS call
if r.Method == http.MethodOptions {
// Pass-through when no paths match
return a.Next.ServeHTTP(w, r)
}
for _, rule := range a.Rules {
for _, res := range rule.Resources {
if !httpserver.Path(r.URL.Path).Matches(res) {
continue
}
// path matches; this endpoint is protected
protected = true
realm = rule.Realm
// parse auth header
username, password, ok = r.BasicAuth()
// check credentials
if !ok ||
username != rule.Username ||
!rule.Password(password) {
continue
}
// by this point, authentication was successful
isAuthenticated = true
// let upstream middleware (e.g. fastcgi and cgi) know about authenticated
// user; this replaces the request with a wrapped instance
r = r.WithContext(context.WithValue(r.Context(),
httpserver.RemoteUserCtxKey, username))
// Provide username to be used in log by replacer
repl := httpserver.NewReplacer(r, nil, "-")
repl.Set("user", username)
}
}
if protected && !isAuthenticated {
// browsers show a message that says something like:
// "The website says: <realm>"
// which is kinda dumb, but whatever.
if realm == "" {
realm = "Restricted"
}
w.Header().Set("WWW-Authenticate", "Basic realm=\""+realm+"\"")
// Get a replacer so we can provide basic info for the authentication error.
repl := httpserver.NewReplacer(r, nil, "-")
repl.Set("user", username)
errstr := repl.Replace("BasicAuth: user \"{user}\" was not found or password was incorrect. {remote} {host} {uri} {proto}")
err := fmt.Errorf("%s", errstr)
return http.StatusUnauthorized, err
}
// Pass-through when no paths match
return a.Next.ServeHTTP(w, r)
}
// Rule represents a BasicAuth rule. A username and password
// combination protect the associated resources, which are
// file or directory paths.
type Rule struct {
Username string
Password func(string) bool
Resources []string
Realm string // See RFC 1945 and RFC 2617, default: "Restricted"
}
// PasswordMatcher determines whether a password matches a rule.
type PasswordMatcher func(pw string) bool
var (
htpasswords map[string]map[string]PasswordMatcher
htpasswordsMu sync.Mutex
)
// GetHtpasswdMatcher matches password rules.
func GetHtpasswdMatcher(filename, username, siteRoot string) (PasswordMatcher, error) {
filename = filepath.Join(siteRoot, filename)
htpasswordsMu.Lock()
if htpasswords == nil {
htpasswords = make(map[string]map[string]PasswordMatcher)
}
pm := htpasswords[filename]
if pm == nil {
fh, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("open %q: %v", filename, err)
}
defer fh.Close()
pm = make(map[string]PasswordMatcher)
if err = parseHtpasswd(pm, fh); err != nil {
return nil, fmt.Errorf("parsing htpasswd %q: %v", fh.Name(), err)
}
htpasswords[filename] = pm
}
htpasswordsMu.Unlock()
if pm[username] == nil {
return nil, fmt.Errorf("username %q not found in %q", username, filename)
}
return pm[username], nil
}
func parseHtpasswd(pm map[string]PasswordMatcher, r io.Reader) error {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.IndexByte(line, '#') == 0 {
continue
}
i := strings.IndexByte(line, ':')
if i <= 0 {
return fmt.Errorf("malformed line, no color: %q", line)
}
user, encoded := line[:i], line[i+1:]
for _, p := range basic.DefaultSystems {
matcher, err := p(encoded)
if err != nil {
return err
}
if matcher != nil {
pm[user] = matcher.MatchesPassword
break
}
}
}
return scanner.Err()
}
// PlainMatcher returns a PasswordMatcher that does a constant-time
// byte 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()
if _, err := passwHash.Write([]byte(passw)); err != nil {
log.Printf("[ERROR] unable to write password hash: %v", err)
}
passwSum := passwHash.Sum(nil)
return func(pw string) bool {
pwHash := sha1.New()
if _, err := pwHash.Write([]byte(pw)); err != nil {
log.Printf("[ERROR] unable to write password hash: %v", err)
}
pwSum := pwHash.Sum(nil)
return subtle.ConstantTimeCompare([]byte(pwSum), []byte(passwSum)) == 1
}
}
+230
View File
@@ -0,0 +1,230 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package basicauth
import (
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestBasicAuth(t *testing.T) {
var i int
// This handler is registered for tests in which the only authorized user is
// "okuser"
upstreamHandler := func(w http.ResponseWriter, r *http.Request) (int, error) {
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
if remoteUser != "okuser" {
t.Errorf("Test %d: expecting remote user 'okuser', got '%s'", i, remoteUser)
}
return http.StatusOK, nil
}
rws := []BasicAuth{
{
Next: httpserver.HandlerFunc(upstreamHandler),
Rules: []Rule{
{Username: "okuser", Password: PlainMatcher("okpass"),
Resources: []string{"/testing"}, Realm: "Resources"},
},
},
{
Next: httpserver.HandlerFunc(upstreamHandler),
Rules: []Rule{
{Username: "okuser", Password: PlainMatcher("okpass"),
Resources: []string{"/testing"}},
},
},
}
type testType struct {
from string
result int
user string
password string
haserror bool
}
tests := []testType{
{"/testing", http.StatusOK, "okuser", "okpass", false},
{"/testing", http.StatusUnauthorized, "baduser", "okpass", true},
{"/testing", http.StatusUnauthorized, "okuser", "badpass", true},
{"/testing", http.StatusUnauthorized, "OKuser", "okpass", true},
{"/testing", http.StatusUnauthorized, "OKuser", "badPASS", true},
{"/testing", http.StatusUnauthorized, "", "okpass", true},
{"/testing", http.StatusUnauthorized, "okuser", "", true},
{"/testing", http.StatusUnauthorized, "", "", true},
}
var test testType
for _, rw := range rws {
expectRealm := rw.Rules[0].Realm
if expectRealm == "" {
expectRealm = "Restricted" // Default if Realm not specified in rule
}
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)
}
req.SetBasicAuth(test.user, test.password)
rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req)
if err != nil {
if !test.haserror || !strings.HasPrefix(err.Error(), "BasicAuth: user") {
t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err)
}
}
if result != test.result {
t.Errorf("Test %d: Expected status code %d but was %d",
i, test.result, result)
}
if test.result == http.StatusUnauthorized {
headers := rec.Header()
if val, ok := headers["Www-Authenticate"]; ok {
if got, want := val[0], "Basic realm=\""+expectRealm+"\""; got != want {
t.Errorf("Test %d: Www-Authenticate header should be '%s', got: '%s'", i, want, got)
}
} else {
t.Errorf("Test %d: response should have a 'Www-Authenticate' header", i)
}
} else {
if req.Header.Get("Authorization") == "" {
// see issue #1508: https://github.com/mholt/caddy/issues/1508
t.Errorf("Test %d: Expected Authorization header to be retained after successful auth, but was empty", i)
}
}
}
}
}
func TestMultipleOverlappingRules(t *testing.T) {
rw := BasicAuth{
Next: httpserver.HandlerFunc(contentHandler),
Rules: []Rule{
{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}},
{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}},
},
}
tests := []struct {
from string
result int
cred string
haserror bool
}{
{"/t", http.StatusOK, "t:p1", false},
{"/t/t", http.StatusOK, "t:p1", false},
{"/t/t", http.StatusOK, "t1:p2", false},
{"/a", http.StatusOK, "t1:p2", false},
{"/t/t", http.StatusUnauthorized, "t1:p3", true},
{"/t", http.StatusUnauthorized, "t1:p2", true},
}
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)
}
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(test.cred))
req.Header.Set("Authorization", auth)
rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req)
if err != nil {
if !test.haserror || !strings.HasPrefix(err.Error(), "BasicAuth: user") {
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
}
func TestHtpasswd(t *testing.T) {
htpasswdPasswd := "IedFOuGmTpT8"
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
htfh, err := ioutil.TempFile("", "basicauth-")
if err != nil {
t.Skip("Error creating temp file, will skip htpassword test")
return
}
defer os.Remove(htfh.Name())
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
}
htfh.Close()
for i, username := range []string{"sha1", "md5"} {
rule := Rule{Username: username, Resources: []string{"/testing"}}
siteRoot := filepath.Dir(htfh.Name())
filename := filepath.Base(htfh.Name())
if rule.Password, err = GetHtpasswdMatcher(filename, rule.Username, siteRoot); err != nil {
t.Fatalf("GetHtpasswdMatcher(%q, %q): %v", htfh.Name(), rule.Username, err)
}
t.Logf("%d. username=%q", i, rule.Username)
if !rule.Password(htpasswdPasswd) || rule.Password(htpasswdPasswd+"!") {
t.Errorf("%d (%s) password does not match.", i, rule.Username)
}
}
}
func TestOptionsMethod(t *testing.T) {
rw := BasicAuth{
Next: httpserver.HandlerFunc(contentHandler),
Rules: []Rule{
{Username: "username", Password: PlainMatcher("password"), Resources: []string{"/testing"}},
},
}
req, err := http.NewRequest(http.MethodOptions, "/testing", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
// add basic auth with invalid username
// and password to make sure basic auth is ignored
req.SetBasicAuth("invaliduser", "invalidpassword")
rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req)
if err != nil {
t.Fatalf("Could not ServeHTTP: %v", err)
}
if result != http.StatusOK {
t.Errorf("Expected status code %d but was %d", http.StatusOK, result)
}
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package basicauth
import (
"strings"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
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 err
}
basic := BasicAuth{Rules: rules}
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
basic.Next = next
basic.SiteRoot = root
return basic
})
return nil
}
func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
var rules []Rule
cfg := httpserver.GetConfig(c)
var err error
for c.Next() {
var rule Rule
args := c.RemainingArgs()
switch len(args) {
case 2:
rule.Username = args[0]
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)
}
case 3:
rule.Resources = append(rule.Resources, args[0])
rule.Username = args[1]
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:
return rules, c.ArgErr()
}
// If nested block is present, process it here
for c.NextBlock() {
val := c.Val()
args = c.RemainingArgs()
switch len(args) {
case 0:
// Assume single argument is path resource
rule.Resources = append(rule.Resources, val)
case 1:
if val == "realm" {
if rule.Realm == "" {
rule.Realm = strings.Replace(args[0], `"`, `\"`, -1)
} else {
return rules, c.Errf("\"realm\" subdirective can only be specified once")
}
} else {
return rules, c.Errf("expecting \"realm\", got \"%s\"", val)
}
default:
return rules, c.ArgErr()
}
}
rules = append(rules, rule)
}
return rules, nil
}
func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) {
htpasswdPrefix := "htpasswd="
if !strings.HasPrefix(passw, htpasswdPrefix) {
return PlainMatcher(passw), nil
}
return GetHtpasswdMatcher(passw[len(htpasswdPrefix):], username, siteRoot)
}
+178
View File
@@ -0,0 +1,178 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package basicauth
import (
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
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)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, got 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(BasicAuth)
if !ok {
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
}
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestBasicAuthParse(t *testing.T) {
htpasswdPasswd := "IedFOuGmTpT8"
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
var skipHtpassword bool
htfh, err := ioutil.TempFile(".", "basicauth-")
if err != nil {
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
skipHtpassword = true
} else {
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
}
htfh.Close()
defer os.Remove(htfh.Name())
}
tests := []struct {
input string
shouldErr bool
password string
expected []Rule
}{
{`basicauth user pwd`, false, "pwd", []Rule{
{Username: "user"},
}},
{`basicauth user pwd {
}`, false, "pwd", []Rule{
{Username: "user"},
}},
{`basicauth /resource1 user pwd {
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1"}},
}},
{`basicauth /resource1 user pwd {
realm Resources
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1"}, Realm: "Resources"},
}},
{`basicauth user pwd {
/resource1
/resource2
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
}},
{`basicauth user pwd {
/resource1
/resource2
realm "Secure resources"
}`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource1", "/resource2"}, Realm: "Secure resources"},
}},
{`basicauth user pwd {
/resource1
realm "Secure resources"
realm Extra
/resource2
}`, true, "pwd", []Rule{}},
{`basicauth user pwd {
/resource1
foo "Resources"
/resource2
}`, true, "pwd", []Rule{}},
{`basicauth /resource user pwd`, false, "pwd", []Rule{
{Username: "user", Resources: []string{"/resource"}},
}},
{`basicauth /res1 user1 pwd1
basicauth /res2 user2 pwd2`, false, "pwd", []Rule{
{Username: "user1", Resources: []string{"/res1"}},
{Username: "user2", Resources: []string{"/res2"}},
}},
{`basicauth user`, true, "", []Rule{}},
{`basicauth`, true, "", []Rule{}},
{`basicauth /resource user pwd asdf`, true, "", []Rule{}},
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []Rule{
{Username: "sha1"},
}},
}
for i, test := range tests {
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)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, expectedRule := range test.expected {
actualRule := actual[j]
if actualRule.Username != expectedRule.Username {
t.Errorf("Test %d, rule %d: Expected username '%s', got '%s'",
i, j, expectedRule.Username, actualRule.Username)
}
if actualRule.Realm != expectedRule.Realm {
t.Errorf("Test %d, rule %d: Expected realm '%s', got '%s'",
i, j, expectedRule.Realm, actualRule.Realm)
}
if strings.Contains(test.input, "htpasswd=") && skipHtpassword {
continue
}
pwd := test.password
if len(actual) > 1 {
pwd = fmt.Sprintf("%s%d", pwd, j+1)
}
if !actualRule.Password(pwd) || actualRule.Password(test.password+"!") {
t.Errorf("Test %d, rule %d: Expected password '%v', got '%v'",
i, j, test.password, actualRule.Password(""))
}
expectedRes := fmt.Sprintf("%v", expectedRule.Resources)
actualRes := fmt.Sprintf("%v", actualRule.Resources)
if actualRes != expectedRes {
t.Errorf("Test %d, rule %d: Expected resource list %s, but got %s",
i, j, expectedRes, actualRes)
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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.Manager.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
}
return nil
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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.Manager.ListenHost, "1.2.3.4"; got != want {
t.Errorf("Expected the TLS config's ListenHost to be %s, was %s", want, got)
}
}
+527
View File
@@ -0,0 +1,527 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package 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"
sortByNameDirFirst = "namedirfirst"
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 // the base path the URL must match to enable browsing
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 browse-able.
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
}
// Crumb represents part of a breadcrumb menu.
type Crumb struct {
Link, Text string
}
// Breadcrumbs returns l.Path where every element maps
// the link to the text to display.
func (l Listing) Breadcrumbs() []Crumb {
var result []Crumb
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 := range parts {
txt := parts[i]
if i == 0 && parts[i] == "" {
txt = "/"
}
result = append(result, Crumb{Link: strings.Repeat("../", len(parts)-i-1), Text: txt})
}
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
IsSymlink 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 byNameDirFirst 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 Name Dir First
func (l byNameDirFirst) Len() int { return len(l.Items) }
func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
// Treat upper and lower case equally
func (l byNameDirFirst) Less(i, j int) bool {
// if both are dir or file sort normally
if l.Items[i].IsDir == l.Items[j].IsDir {
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
}
// always sort dir ahead of file
return l.Items[i].IsDir
}
// 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
// Directory sizes depend on the filesystem implementation,
// which is opaque to a visitor, and should indeed does not change if the operator chooses to change the fs.
// For a consistent user experience directories are pulled to the front…
if l.Items[i].IsDir {
iSize = directoryOffset
}
if l.Items[j].IsDir {
jSize = directoryOffset
}
// … and sorted by name.
if l.Items[i].IsDir && l.Items[j].IsDir {
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
}
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 sortByNameDirFirst:
sort.Sort(sort.Reverse(byNameDirFirst(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 sortByNameDirFirst:
sort.Sort(byNameDirFirst(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 config.Fs.IndexPages {
if name == indexName {
hasIndexFile = true
break
}
}
isDir := f.IsDir() || isSymlinkTargetDir(f, urlPath, config)
if isDir {
name += "/"
dirCount++
} else {
fileCount++
}
if config.Fs.IsHidden(f) {
continue
}
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
fileInfos = append(fileInfos, FileInfo{
IsDir: isDir,
IsSymlink: isSymlink(f),
Name: f.Name(),
Size: f.Size(),
URL: u.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
}
// isSymlink return true if f is a symbolic link
func isSymlink(f os.FileInfo) bool {
return f.Mode()&os.ModeSymlink != 0
}
// isSymlinkTargetDir return true if f's symbolic link target
// is a directory. Return false if not a symbolic link.
func isSymlinkTargetDir(f os.FileInfo, urlPath string, config *Config) bool {
if !isSymlink(f) {
return false
}
// a bit strange, but we want Stat thru the jailed filesystem to be safe
target, err := config.Fs.Root.Open(path.Join(urlPath, f.Name()))
if err != nil {
return false
}
defer target.Close()
targetInfo, err := target.Stat()
if err != nil {
return false
}
return targetInfo.IsDir()
}
// 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) {
// See if there's a browse configuration to match the path
var bc *Config
for i := range b.Configs {
if httpserver.Path(r.URL.Path).Matches(b.Configs[i].PathScope) {
bc = &b.Configs[i]
break
}
}
if bc == nil {
return b.Next.ServeHTTP(w, r)
}
// 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)
u := *r.URL
if u.Path == "" {
u.Path = "/"
}
if u.Path[len(u.Path)-1] != '/' {
u.Path += "/"
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
return http.StatusMovedPermanently, 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 = sortByNameDirFirst
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
sort = sortCookie.Value
}
case sortByName, sortByNameDirFirst, 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
}
+625
View File
@@ -0,0 +1,625 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package browse
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
"text/template"
"time"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddyhttp/staticfiles"
)
const testDirPrefix = "caddy_browse_test"
func TestSort(t *testing.T) {
// making up []fileInfo with bogus values;
// to be used to make up our "listing"
fileInfos := []FileInfo{
{
Name: "fizz",
Size: 4,
ModTime: time.Now().AddDate(-1, 1, 0),
},
{
Name: "buzz",
Size: 2,
ModTime: time.Now().AddDate(0, -3, 3),
},
{
Name: "bazz",
Size: 1,
ModTime: time.Now().AddDate(0, -2, -23),
},
{
Name: "jazz",
Size: 3,
ModTime: time.Now(),
},
}
listing := Listing{
Name: "foobar",
Path: "/fizz/buzz",
CanGoUp: false,
Items: fileInfos,
}
// sort by name
listing.Sort = "name"
listing.applySort()
if !sort.IsSorted(byName(listing)) {
t.Errorf("The listing isn't name sorted: %v", listing.Items)
}
// sort by size
listing.Sort = "size"
listing.applySort()
if !sort.IsSorted(bySize(listing)) {
t.Errorf("The listing isn't size sorted: %v", listing.Items)
}
// sort by Time
listing.Sort = "time"
listing.applySort()
if !sort.IsSorted(byTime(listing)) {
t.Errorf("The listing isn't time sorted: %v", listing.Items)
}
// sort by name dir first
listing.Sort = "namedirfirst"
listing.applySort()
if !sort.IsSorted(byNameDirFirst(listing)) {
t.Errorf("The listing isn't namedirfirst sorted: %v", listing.Items)
}
// reverse by name
listing.Sort = "name"
listing.Order = "desc"
listing.applySort()
if !isReversed(byName(listing)) {
t.Errorf("The listing isn't reversed by name: %v", listing.Items)
}
// reverse by size
listing.Sort = "size"
listing.Order = "desc"
listing.applySort()
if !isReversed(bySize(listing)) {
t.Errorf("The listing isn't reversed by size: %v", listing.Items)
}
// reverse by time
listing.Sort = "time"
listing.Order = "desc"
listing.applySort()
if !isReversed(byTime(listing)) {
t.Errorf("The listing isn't reversed by time: %v", listing.Items)
}
// reverse by name dir first
listing.Sort = "namedirfirst"
listing.Order = "desc"
listing.applySort()
if !isReversed(byNameDirFirst(listing)) {
t.Errorf("The listing isn't reversed by namedirfirst: %v", listing.Items)
}
}
func TestBrowseHTTPMethods(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) {
return http.StatusTeapot, nil // not t.Fatalf, or we will not see what other methods yield
}),
Configs: []Config{
{
PathScope: "/photos",
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)
}
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
req = req.WithContext(ctx)
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,
},
},
}
req, err := http.NewRequest("GET", "/photos/", nil)
if err != nil {
t.Fatalf("Test: Could not create HTTP request: %v", err)
}
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
code, _ := b.ServeHTTP(rec, req)
if code != http.StatusOK {
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
}
respBody := rec.Body.String()
expectedBody := `<!DOCTYPE html>
<html>
<head>
<title>Template</title>
</head>
<body>
<h1>Header</h1>
<h1>/photos/</h1>
<a href="./test1/">test1</a><br>
<a href="./test.html">test.html</a><br>
<a href="./test2.html">test2.html</a><br>
<a href="./test3.html">test3.html</a><br>
</body>
</html>
`
if respBody != expectedBody {
t.Fatalf("Expected body: '%v' got: '%v'", expectedBody, respBody)
}
}
func TestBrowseJson(t *testing.T) {
b := Browse{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Next shouldn't be called: %s", r.URL)
return 0, nil
}),
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 := filepath.Join("./testdata", "photos")
file, err := os.Open(testDataPath)
if err != nil {
if os.IsPermission(err) {
t.Fatalf("Os Permission Error")
}
}
defer file.Close()
files, err := file.Readdir(-1)
if err != nil {
t.Fatalf("Unable to Read Contents of the directory")
}
var fileinfos []FileInfo
for i, f := range files {
name := f.Name()
// Tests fail in CI environment because all file mod times are the same for
// some reason, making the sorting unpredictable. To hack around this,
// we ensure here that each file has a different mod time.
chTime := f.ModTime().UTC().Add(-(time.Duration(i) * time.Second))
if err := os.Chtimes(filepath.Join(testDataPath, name), chTime, chTime); err != nil {
t.Fatal(err)
}
if f.IsDir() {
name += "/"
}
url := url.URL{Path: "./" + name}
fileinfos = append(fileinfos, FileInfo{
IsDir: f.IsDir(),
Name: f.Name(),
Size: f.Size(),
URL: url.String(),
ModTime: chTime,
Mode: f.Mode(),
})
}
// Test that sort=name returns correct listing.
listing := Listing{Items: fileinfos} // this listing will be used for validation inside the tests
tests := []struct {
QueryURL string
SortBy string
OrderBy string
Limit int
shouldErr bool
expectedResult []FileInfo
}{
//test case 1: testing for default sort and order and without the limit parameter, default sort is by name and the default order is ascending
//without the limit query entire listing will be produced
{"/?sort=name", "", "", -1, false, listing.Items},
//test case 2: limit is set to 1, orderBy and sortBy is default
{"/?limit=1&sort=name", "", "", 1, false, listing.Items[:1]},
//test case 3 : if the listing request is bigger than total size of listing then it should return everything
{"/?limit=100000000&sort=name", "", "", 100000000, false, listing.Items},
//test case 4 : testing for negative limit
{"/?limit=-1&sort=name", "", "", -1, false, listing.Items},
//test case 5 : testing with limit set to -1 and order set to descending
{"/?limit=-1&order=desc&sort=name", "", "desc", -1, false, listing.Items},
//test case 6 : testing with limit set to 2 and order set to descending
{"/?limit=2&order=desc&sort=name", "", "desc", 2, false, listing.Items},
//test case 7 : testing with limit set to 3 and order set to descending
{"/?limit=3&order=desc&sort=name", "", "desc", 3, false, listing.Items},
//test case 8 : testing with limit set to 3 and order set to ascending
{"/?limit=3&order=asc&sort=name", "", "asc", 3, false, listing.Items},
//test case 9 : testing with limit set to 1111111 and order set to ascending
{"/?limit=1111111&order=asc&sort=name", "", "asc", 1111111, false, listing.Items},
//test case 10 : testing with limit set to default and order set to ascending and sorting by size
{"/?order=asc&sort=size&sort=name", "size", "asc", -1, false, listing.Items},
//test case 11 : testing with limit set to default and order set to ascending and sorting by last modified
{"/?order=asc&sort=time&sort=name", "time", "asc", -1, false, listing.Items},
//test case 12 : testing with limit set to 1 and order set to ascending and sorting by last modified
{"/?order=asc&sort=time&limit=1&sort=name", "time", "asc", 1, false, listing.Items},
//test case 13 : testing with limit set to -100 and order set to ascending and sorting by last modified
{"/?order=asc&sort=time&limit=-100&sort=name", "time", "asc", -100, false, listing.Items},
//test case 14 : testing with limit set to -100 and order set to ascending and sorting by size
{"/?order=asc&sort=size&limit=-100&sort=name", "size", "asc", -100, false, listing.Items},
}
for i, test := range tests {
var marsh []byte
req, err := http.NewRequest("GET", "/photos"+test.QueryURL, nil)
if err != nil && !test.shouldErr {
t.Errorf("Test %d errored when making request, but it shouldn't have; got '%v'", i, err)
}
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
req = req.WithContext(ctx)
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("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"))
}
actualJSONResponse := rec.Body.String()
copyOfListing := listing
if test.SortBy == "" {
copyOfListing.Sort = "name"
} else {
copyOfListing.Sort = test.SortBy
}
if test.OrderBy == "" {
copyOfListing.Order = "asc"
} else {
copyOfListing.Order = test.OrderBy
}
copyOfListing.applySort()
limit := test.Limit
if limit <= len(copyOfListing.Items) && limit > 0 {
marsh, err = json.Marshal(copyOfListing.Items[:limit])
} else { // if the 'limit' query is empty, or has the wrong value, list everything
marsh, err = json.Marshal(copyOfListing.Items)
}
if err != nil {
t.Fatalf("Unable to Marshal the listing ")
}
expectedJSON := string(marsh)
if actualJSONResponse != expectedJSON {
t.Errorf("JSON response doesn't match the expected for test number %d with sort=%s, order=%s\nExpected response %s\nActual response = %s\n",
i+1, test.SortBy, test.OrderBy, expectedJSON, actualJSONResponse)
}
}
}
// "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 TestBrowseRedirect(t *testing.T) {
testCases := []struct {
url string
statusCode int
returnCode int
location string
}{
{
"http://www.example.com/photos",
http.StatusMovedPermanently,
http.StatusMovedPermanently,
"http://www.example.com/photos/",
},
{
"/photos",
http.StatusMovedPermanently,
http.StatusMovedPermanently,
"/photos/",
},
}
for i, tc := range testCases {
b := Browse{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Test %d - Next shouldn't be called", i)
return 0, nil
}),
Configs: []Config{
{
PathScope: "/photos",
Fs: staticfiles.FileServer{
Root: http.Dir("./testdata"),
},
},
},
}
req, err := http.NewRequest("GET", tc.url, nil)
if err != nil {
t.Fatalf("Test %d - could not create HTTP request: %v", i, err)
}
ctx := context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
returnCode, _ := b.ServeHTTP(rec, req)
if returnCode != tc.returnCode {
t.Fatalf("Test %d - wrong return code, expected %d, got %d",
i, tc.returnCode, returnCode)
}
if got := rec.Code; got != tc.statusCode {
t.Errorf("Test %d - wrong status, expected %d, got %d",
i, tc.statusCode, got)
}
if got := rec.Header().Get("Location"); got != tc.location {
t.Errorf("Test %d - wrong Location header, expected %s, got %s",
i, tc.location, got)
}
}
}
func TestDirSymlink(t *testing.T) {
if runtime.GOOS == "windows" {
// Windows support for symlinks is limited, and we had a hard time getting
// all these tests to pass with the permissions of CI; so just skip them
fmt.Println("Skipping browse symlink tests on Windows...")
return
}
testCases := []struct {
source string
target string
pathScope string
url string
expectedName string
expectedURL string
}{
// test case can expect a directory "dir" and a symlink to it called "symlink"
{"dir", "$TMP/rel_symlink_to_dir", "/", "/",
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
{"$TMP/dir", "$TMP/abs_symlink_to_dir", "/", "/",
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
{"../../dir", "$TMP/sub/dir/rel_symlink_to_dir", "/", "/sub/dir/",
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
{"$TMP/dir", "$TMP/sub/dir/abs_symlink_to_dir", "/", "/sub/dir/",
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
{"../../dir", "$TMP/with/scope/rel_symlink_to_dir", "/with/scope", "/with/scope/",
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
{"$TMP/dir", "$TMP/with/scope/abs_symlink_to_dir", "/with/scope", "/with/scope/",
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
{"../../../../dir", "$TMP/with/scope/sub/dir/rel_symlink_to_dir", "/with/scope", "/with/scope/sub/dir/",
"rel_symlink_to_dir", "./rel_symlink_to_dir/"},
{"$TMP/dir", "$TMP/with/scope/sub/dir/abs_symlink_to_dir", "/with/scope", "/with/scope/sub/dir/",
"abs_symlink_to_dir", "./abs_symlink_to_dir/"},
{"symlink", "$TMP/rel_symlink_to_symlink", "/", "/",
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
{"$TMP/symlink", "$TMP/abs_symlink_to_symlink", "/", "/",
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
{"../../symlink", "$TMP/sub/dir/rel_symlink_to_symlink", "/", "/sub/dir/",
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
{"$TMP/symlink", "$TMP/sub/dir/abs_symlink_to_symlink", "/", "/sub/dir/",
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
{"../../symlink", "$TMP/with/scope/rel_symlink_to_symlink", "/with/scope", "/with/scope/",
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
{"$TMP/symlink", "$TMP/with/scope/abs_symlink_to_symlink", "/with/scope", "/with/scope/",
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
{"../../../../symlink", "$TMP/with/scope/sub/dir/rel_symlink_to_symlink", "/with/scope", "/with/scope/sub/dir/",
"rel_symlink_to_symlink", "./rel_symlink_to_symlink/"},
{"$TMP/symlink", "$TMP/with/scope/sub/dir/abs_symlink_to_symlink", "/with/scope", "/with/scope/sub/dir/",
"abs_symlink_to_symlink", "./abs_symlink_to_symlink/"},
}
for i, tc := range testCases {
func() {
tmpdir, err := ioutil.TempDir("", testDirPrefix)
if err != nil {
t.Fatalf("failed to create test directory: %v", err)
}
defer os.RemoveAll(tmpdir)
if err := os.MkdirAll(filepath.Join(tmpdir, "dir"), 0755); err != nil {
t.Fatalf("failed to create test dir 'dir': %v", err)
}
if err := os.Symlink("dir", filepath.Join(tmpdir, "symlink")); err != nil {
t.Fatalf("failed to create test symlink 'symlink': %v", err)
}
sourceResolved := strings.Replace(tc.source, "$TMP", tmpdir, -1)
targetResolved := strings.Replace(tc.target, "$TMP", tmpdir, -1)
if err := os.MkdirAll(filepath.Dir(sourceResolved), 0755); err != nil {
t.Fatalf("failed to create source symlink dir: %v", err)
}
if err := os.MkdirAll(filepath.Dir(targetResolved), 0755); err != nil {
t.Fatalf("failed to create target symlink dir: %v", err)
}
if err := os.Symlink(sourceResolved, targetResolved); err != nil {
t.Fatalf("failed to create test symlink: %v", err)
}
b := Browse{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
t.Fatalf("Test %d - Next shouldn't be called", i)
return 0, nil
}),
Configs: []Config{
{
PathScope: tc.pathScope,
Fs: staticfiles.FileServer{
Root: http.Dir(tmpdir),
},
},
},
}
req, err := http.NewRequest("GET", tc.url, nil)
req.Header.Add("Accept", "application/json")
if err != nil {
t.Fatalf("Test %d - could not create HTTP request: %v", i, err)
}
rec := httptest.NewRecorder()
returnCode, _ := b.ServeHTTP(rec, req)
if returnCode != http.StatusOK {
t.Fatalf("Test %d - wrong return code, expected %d, got %d",
i, http.StatusOK, returnCode)
}
type jsonEntry struct {
Name string
IsDir bool
IsSymlink bool
URL string
}
var entries []jsonEntry
if err := json.Unmarshal(rec.Body.Bytes(), &entries); err != nil {
t.Fatalf("Test %d - failed to parse json: %v", i, err)
}
found := false
for _, e := range entries {
if e.Name != tc.expectedName {
continue
}
found = true
if !e.IsDir {
t.Errorf("Test %d - expected to be a dir, got %v", i, e.IsDir)
}
if !e.IsSymlink {
t.Errorf("Test %d - expected to be a symlink, got %v", i, e.IsSymlink)
}
if e.URL != tc.expectedURL {
t.Errorf("Test %d - wrong URL, expected %v, got %v", i, tc.expectedURL, e.URL)
}
}
if !found {
t.Errorf("Test %d - failed, could not find name %v", i, tc.expectedName)
}
}()
}
}
+515
View File
@@ -0,0 +1,515 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package browse
import (
"fmt"
"io/ioutil"
"net/http"
"text/template"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddyhttp/staticfiles"
)
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 err
}
b := Browse{
Configs: configs,
IgnoreIndexes: false,
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
b.Next = next
return b
})
return nil
}
func browseParse(c *caddy.Controller) ([]Config, error) {
var configs []Config
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)
}
}
configs = append(configs, bc)
return nil
}
for c.Next() {
var bc Config
// First argument is directory to allow browsing; default is site root
if c.NextArg() {
bc.PathScope = c.Val()
} else {
bc.PathScope = "/"
}
bc.Fs = staticfiles.FileServer{
Root: http.Dir(cfg.Root),
Hide: cfg.HiddenFiles,
IndexPages: cfg.IndexPages,
}
// Second argument would be the template file to use
var tplText string
if c.NextArg() {
tplBytes, err := ioutil.ReadFile(c.Val())
if err != nil {
return configs, err
}
tplText = string(tplBytes)
} else {
tplText = defaultTemplate
}
// Build the template
tpl, err := template.New("listing").Parse(tplText)
if err != nil {
return configs, err
}
bc.Template = tpl
// Save configuration
err = appendCfg(bc)
if err != nil {
return configs, err
}
}
return configs, nil
}
// The default template to use when serving up directory listings
const defaultTemplate = `<!DOCTYPE html>
<html>
<head>
<title>{{html .Name}}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { padding: 0; margin: 0; }
body {
font-family: sans-serif;
text-rendering: optimizespeed;
background-color: #ffffff;
}
a {
color: #006ed3;
text-decoration: none;
}
a:hover,
h1 a:hover {
color: #319cff;
}
header,
#summary {
padding-left: 5%;
padding-right: 5%;
}
th:first-child,
td:first-child {
width: 5%;
}
th:last-child,
td:last-child {
width: 5%;
}
header {
padding-top: 25px;
padding-bottom: 15px;
background-color: #f2f2f2;
}
h1 {
font-size: 20px;
font-weight: normal;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
color: #999;
}
h1 a {
color: #000;
margin: 0 4px;
}
h1 a:hover {
text-decoration: underline;
}
h1 a:first-child {
margin: 0;
}
main {
display: block;
}
.meta {
font-size: 12px;
font-family: Verdana, sans-serif;
border-bottom: 1px solid #9C9C9C;
padding-top: 10px;
padding-bottom: 10px;
}
.meta-item {
margin-right: 1em;
}
#filter {
padding: 4px;
border: 1px solid #CCC;
}
table {
width: 100%;
border-collapse: collapse;
}
tr {
border-bottom: 1px dashed #dadada;
}
tbody tr:hover {
background-color: #ffffec;
}
th,
td {
text-align: left;
padding: 10px 0;
}
th {
padding-top: 15px;
padding-bottom: 15px;
font-size: 16px;
white-space: nowrap;
}
th a {
color: black;
}
th svg {
vertical-align: middle;
}
td {
white-space: nowrap;
font-size: 14px;
}
td:nth-child(2) {
width: 80%;
}
td:nth-child(3) {
padding: 0 20px 0 20px;
}
th:nth-child(4),
td:nth-child(4) {
text-align: right;
}
td:nth-child(2) svg {
position: absolute;
}
td .name,
td .goup {
margin-left: 1.75em;
word-break: break-all;
overflow-wrap: break-word;
white-space: pre-wrap;
}
.icon {
margin-right: 5px;
}
.icon.sort {
display: inline-block;
width: 1em;
height: 1em;
position: relative;
top: .2em;
}
.icon.sort .top {
position: absolute;
left: 0;
top: -1px;
}
.icon.sort .bottom {
position: absolute;
bottom: -1px;
left: 0;
}
footer {
padding: 40px 20px;
font-size: 12px;
text-align: center;
}
@media (max-width: 600px) {
.hideable {
display: none;
}
td:nth-child(2) {
width: auto;
}
th:nth-child(3),
td:nth-child(3) {
padding-right: 5%;
text-align: right;
}
h1 {
color: #000;
}
h1 a {
margin: 0;
}
#filter {
max-width: 100px;
}
}
</style>
</head>
<body onload='filter()'>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="0" width="0" style="position: absolute;">
<defs>
<!-- Folder -->
<g id="folder" fill-rule="nonzero" fill="none">
<path d="M285.22 37.55h-142.6L110.9 0H31.7C14.25 0 0 16.9 0 37.55v75.1h316.92V75.1c0-20.65-14.26-37.55-31.7-37.55z" fill="#FFA000"/>
<path d="M285.22 36H31.7C14.25 36 0 50.28 0 67.74v158.7c0 17.47 14.26 31.75 31.7 31.75H285.2c17.44 0 31.7-14.3 31.7-31.75V67.75c0-17.47-14.26-31.75-31.7-31.75z" fill="#FFCA28"/>
</g>
<g id="folder-shortcut" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="folder-shortcut-group" fill-rule="nonzero">
<g id="folder-shortcut-shape">
<path d="M285.224876,37.5486902 L142.612438,37.5486902 L110.920785,0 L31.6916529,0 C14.2612438,0 0,16.8969106 0,37.5486902 L0,112.646071 L316.916529,112.646071 L316.916529,75.0973805 C316.916529,54.4456008 302.655285,37.5486902 285.224876,37.5486902 Z" id="Shape" fill="#FFA000"></path>
<path d="M285.224876,36 L31.6916529,36 C14.2612438,36 0,50.2838568 0,67.7419039 L0,226.451424 C0,243.909471 14.2612438,258.193328 31.6916529,258.193328 L285.224876,258.193328 C302.655285,258.193328 316.916529,243.909471 316.916529,226.451424 L316.916529,67.7419039 C316.916529,50.2838568 302.655285,36 285.224876,36 Z" id="Shape" fill="#FFCA28"></path>
</g>
<path d="M126.154134,250.559184 C126.850974,251.883673 127.300549,253.006122 127.772602,254.106122 C128.469442,255.206122 128.919016,256.104082 129.638335,257.002041 C130.559962,258.326531 131.728855,259 133.100057,259 C134.493737,259 135.415364,258.55102 136.112204,257.67551 C136.809044,257.002041 137.258619,255.902041 137.258619,254.577551 C137.258619,253.904082 137.258619,252.804082 137.033832,251.457143 C136.786566,249.908163 136.561779,249.032653 136.561779,248.583673 C136.089726,242.814286 135.864939,237.920408 135.864939,233.273469 C135.864939,225.057143 136.786566,217.514286 138.180246,210.846939 C139.798713,204.202041 141.889234,198.634694 144.429328,193.763265 C147.216689,188.869388 150.678411,184.873469 154.836973,181.326531 C158.995535,177.779592 163.626149,174.883673 168.481552,172.661224 C173.336954,170.438776 179.113983,168.665306 185.587852,167.340816 C192.061722,166.218367 198.760378,165.342857 205.481514,164.669388 C212.18017,164.220408 219.598146,163.995918 228.162535,163.995918 L246.055591,163.995918 L246.055591,195.514286 C246.055591,197.736735 246.752431,199.510204 248.370899,201.059184 C250.214153,202.608163 252.079886,203.506122 254.372715,203.506122 C256.463236,203.506122 258.531277,202.608163 260.172223,201.059184 L326.102289,137.797959 C327.720757,136.24898 328.642384,134.47551 328.642384,132.253061 C328.642384,130.030612 327.720757,128.257143 326.102289,126.708163 L260.172223,63.4469388 C258.553756,61.8979592 256.463236,61 254.395194,61 C252.079886,61 250.236632,61.8979592 248.393377,63.4469388 C246.77491,64.9959184 246.07807,66.7693878 246.07807,68.9918367 L246.07807,100.510204 L228.162535,100.510204 C166.863084,100.510204 129.166282,117.167347 115.274437,150.459184 C110.666301,161.54898 108.350993,175.310204 108.350993,191.742857 C108.350993,205.279592 113.903236,223.912245 124.760454,247.438776 C125.00772,248.112245 125.457294,249.010204 126.154134,250.559184 Z" id="Shape" fill="#FFFFFF" transform="translate(218.496689, 160.000000) scale(-1, 1) translate(-218.496689, -160.000000) "></path>
</g>
</g>
<!-- File -->
<g id="file" stroke="#000" stroke-width="25" fill="#FFF" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 24.12v274.76c0 6.16 5.87 11.12 13.17 11.12H239c7.3 0 13.17-4.96 13.17-11.12V136.15S132.6 13 128.37 13H26.17C18.87 13 13 17.96 13 24.12z"/>
<path d="M129.37 13L129 113.9c0 10.58 7.26 19.1 16.27 19.1H249L129.37 13z"/>
</g>
<g id="file-shortcut" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="file-shortcut-group" transform="translate(13.000000, 13.000000)">
<g id="file-shortcut-shape" stroke="#000000" stroke-width="25" fill="#FFFFFF" stroke-linecap="round" stroke-linejoin="round">
<path d="M0,11.1214886 L0,285.878477 C0,292.039924 5.87498876,296.999983 13.1728373,296.999983 L225.997983,296.999983 C233.295974,296.999983 239.17082,292.039942 239.17082,285.878477 L239.17082,123.145388 C239.17082,123.145388 119.58541,2.84217094e-14 115.369423,2.84217094e-14 L13.1728576,2.84217094e-14 C5.87500907,-1.71479982e-05 0,4.96022995 0,11.1214886 Z" id="rect1171"></path>
<path d="M116.37005,0 L116,100.904964 C116,111.483663 123.258008,120 132.273377,120 L236,120 L116.37005,0 L116.37005,0 Z" id="rect1794"></path>
</g>
<path d="M47.803141,294.093878 C48.4999811,295.177551 48.9495553,296.095918 49.4216083,296.995918 C50.1184484,297.895918 50.5680227,298.630612 51.2873415,299.365306 C52.2089688,300.44898 53.3778619,301 54.7490634,301 C56.1427436,301 57.0643709,300.632653 57.761211,299.916327 C58.4580511,299.365306 58.9076254,298.465306 58.9076254,297.381633 C58.9076254,296.830612 58.9076254,295.930612 58.6828382,294.828571 C58.4355724,293.561224 58.2107852,292.844898 58.2107852,292.477551 C57.7387323,287.757143 57.5139451,283.753061 57.5139451,279.95102 C57.5139451,273.228571 58.4355724,267.057143 59.8292526,261.602041 C61.44772,256.165306 63.5382403,251.610204 66.0783349,247.62449 C68.8656954,243.620408 72.3274172,240.35102 76.4859792,237.44898 C80.6445412,234.546939 85.2751561,232.177551 90.1305582,230.359184 C94.9859603,228.540816 100.76299,227.089796 107.236859,226.006122 C113.710728,225.087755 120.409385,224.371429 127.13052,223.820408 C133.829177,223.453061 141.247152,223.269388 149.811542,223.269388 L167.704598,223.269388 L167.704598,249.057143 C167.704598,250.87551 168.401438,252.326531 170.019905,253.593878 C171.86316,254.861224 173.728893,255.595918 176.021722,255.595918 C178.112242,255.595918 180.180284,254.861224 181.82123,253.593878 L247.751296,201.834694 C249.369763,200.567347 250.291391,199.116327 250.291391,197.297959 C250.291391,195.479592 249.369763,194.028571 247.751296,192.761224 L181.82123,141.002041 C180.202763,139.734694 178.112242,139 176.044201,139 C173.728893,139 171.885639,139.734694 170.042384,141.002041 C168.423917,142.269388 167.727077,143.720408 167.727077,145.538776 L167.727077,171.326531 L149.811542,171.326531 C88.5120908,171.326531 50.8152886,184.955102 36.9234437,212.193878 C32.3153075,221.267347 30,232.526531 30,245.971429 C30,257.046939 35.5522422,272.291837 46.4094607,291.540816 C46.6567266,292.091837 47.1063009,292.826531 47.803141,294.093878 Z" id="Shape-Copy" fill="#000000" fill-rule="nonzero" transform="translate(140.145695, 220.000000) scale(-1, 1) translate(-140.145695, -220.000000) "></path>
</g>
</g>
<!-- Up arrow -->
<g id="up-arrow" transform="translate(-279.22 -208.12)">
<path transform="matrix(.22413 0 0 .12089 335.67 164.35)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/>
</g>
<!-- Down arrow -->
<g id="down-arrow" transform="translate(-279.22 -208.12)">
<path transform="matrix(.22413 0 0 -.12089 335.67 257.93)" stroke-width="0" d="m-194.17 412.01h-28.827-28.827l14.414-24.965 14.414-24.965 14.414 24.965z"/>
</g>
</defs>
</svg>
<header>
<h1>
{{range $i, $crumb := .Breadcrumbs}}<a href="{{html $crumb.Link}}">{{html $crumb.Text}}</a>{{if ne $i 0}}/{{end}}{{end}}
</h1>
</header>
<main>
<div class="meta">
<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}}
<span class="meta-item"><input type="text" placeholder="filter" id="filter" onkeyup='filter()'></span>
</div>
</div>
<div class="listing">
<table aria-describedby="summary">
<thead>
<tr>
<th></th>
<th>
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}}
<a href="?sort=namedirfirst&order=desc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon"><svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
{{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}}
<a href="?sort=namedirfirst&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon"><svg width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
{{- else}}
<a href="?sort=namedirfirst&order=asc{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}" class="icon sort"><svg class="top" width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg><svg class="bottom" width="1em" height=".5em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></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=".5em" 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=".5em" 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{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Size <svg width="1em" height=".5em" 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=".5em" 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{{if ne 0 .ItemsLimitedTo}}&limit={{.ItemsLimitedTo}}{{end}}">Modified <svg width="1em" height=".5em" 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=".5em" 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>
<th class="hideable"></th>
</tr>
</thead>
<tbody>
{{- if .CanGoUp}}
<tr>
<td></td>
<td>
<a href="..">
<span class="goup">Go up</span>
</a>
</td>
<td>&mdash;</td>
<td class="hideable">&mdash;</td>
<td class="hideable"></td>
</tr>
{{- end}}
{{- range .Items}}
<tr class="file">
<td></td>
<td>
<a href="{{html .URL}}">
{{- if .IsDir}}
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 317 259"><use xlink:href="#folder{{if .IsSymlink}}-shortcut{{end}}"></use></svg>
{{- else}}
<svg width="1.5em" height="1em" version="1.1" viewBox="0 0 265 323"><use xlink:href="#file{{if .IsSymlink}}-shortcut{{end}}"></use></svg>
{{- end}}
<span class="name">{{html .Name}}</span>
</a>
</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>
<td class="hideable"></td>
</tr>
{{- end}}
</tbody>
</table>
</div>
</main>
<footer>
Served with <a rel="noopener noreferrer" href="https://caddyserver.com">Caddy</a>
</footer>
<script>
var filterEl = document.getElementById('filter');
filterEl.focus();
function filter() {
var q = filterEl.value.trim().toLowerCase();
var elems = document.querySelectorAll('tr.file');
elems.forEach(function(el) {
if (!q) {
el.style.display = '';
return;
}
var nameEl = el.querySelector('.name');
var nameVal = nameEl.textContent.trim().toLowerCase();
if (nameVal.indexOf(q) !== -1) {
el.style.display = '';
} else {
el.style.display = 'none';
}
});
}
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([], {day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit"});
}
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
timeList.forEach(localizeDatetime);
</script>
</body>
</html>`
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package browse
import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
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)
}
nonExistentDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano())))
tempTemplate, err := ioutil.TempFile(".", "tempTemplate")
if err != nil {
t.Fatalf("BeforeTest: Failed to create a temporary file in the working directory! Error was: %v", err)
}
defer os.Remove(tempTemplate.Name())
tempTemplatePath := filepath.Join(".", tempTemplate.Name())
for i, test := range []struct {
input string
expectedPathScope []string
shouldErr bool
}{
// test case #0 tests handling of multiple pathscopes
{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false},
// test case #1 tests instantiation of Config with default values
{"browse /", []string{"/"}, false},
// test case #2 tests detection of custom template
{"browse . " + tempTemplatePath, []string{"."}, false},
// test case #3 tests detection of non-existent template
{"browse . " + nonExistentDirPath, nil, true},
// test case #4 tests detection of duplicate pathscopes
{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true},
} {
c := caddy.NewTestController("http", test.input)
err := setup(c)
if err != nil && !test.shouldErr {
t.Errorf("Test case #%d received an error of %v", i, err)
}
if test.expectedPathScope == nil {
continue
}
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 @@
<h1>Header</h1>
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Template</title>
</head>
<body>
{{.Include "header.html"}}
<h1>{{.Path}}</h1>
{{range .Items}}
<a href="{{.URL}}">{{.Name}}</a><br>
{{end}}
</body>
</html>
+1
View File
@@ -0,0 +1 @@
Should be hidden
+8
View File
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>Test 2</title>
</head>
<body>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
<!DOCTYPE html>
<html>
</html>
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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/index"
_ "github.com/mholt/caddy/caddyhttp/internalsrv"
_ "github.com/mholt/caddy/caddyhttp/limits"
_ "github.com/mholt/caddy/caddyhttp/log"
_ "github.com/mholt/caddy/caddyhttp/markdown"
_ "github.com/mholt/caddy/caddyhttp/mime"
_ "github.com/mholt/caddy/caddyhttp/pprof"
_ "github.com/mholt/caddy/caddyhttp/proxy"
_ "github.com/mholt/caddy/caddyhttp/push"
_ "github.com/mholt/caddy/caddyhttp/redirect"
_ "github.com/mholt/caddy/caddyhttp/requestid"
_ "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/timeouts"
_ "github.com/mholt/caddy/caddyhttp/websocket"
_ "github.com/mholt/caddy/onevent"
)
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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 := 32 // importing caddyhttp plugs in this many plugins
s := caddy.DescribePlugins()
if got, want := strings.Count(s, "\n"), numStandardPlugins+4; got != want {
t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s)
}
}
+164
View File
@@ -0,0 +1,164 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package errors implements an HTTP error handling middleware.
package errors
import (
"fmt"
"io"
"net/http"
"os"
"runtime"
"strings"
"time"
"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 httpserver.Handler
GenericErrorPage string // default error page filename
ErrorPages map[int]string // map of status code to filename
Log *httpserver.Logger
Debug bool // if true, errors are written out to client rather than to a log
}
func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
defer h.recovery(w, r)
status, err := h.Next.ServeHTTP(w, r)
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 0 signals that a response has been written
}
h.Log.Println(errMsg)
}
if status >= 400 {
h.errorPage(w, r, status)
return 0, err
}
return status, err
}
// errorPage serves a static error page to w according to the status
// code. If there is an error serving the error page, a plaintext error
// message is written instead, and the extra error is logged.
func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int) {
// See if an error page for this status code was specified
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)
httpserver.DefaultErrorFunc(w, r, code)
return
}
defer errorPage.Close()
// Copy the page body into the response
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(code)
_, err = io.Copy(w, errorPage)
if err != nil {
// 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)
httpserver.DefaultErrorFunc(w, r, code)
}
return
}
// Default error response
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) {
rec := recover()
if rec == nil {
return
}
// Obtain source of panic
// From: https://gist.github.com/swdunlop/9629168
var name, file string // function name, file name
var line int
var pc [16]uintptr
n := runtime.Callers(3, pc[:])
for _, pc := range pc[:n] {
fn := runtime.FuncForPC(pc)
if fn == nil {
continue
}
file, line = fn.FileLine(pc)
name = fn.Name()
if !strings.HasPrefix(name, "runtime.") {
break
}
}
// Trim file path
delim := "/github.com/mholt/caddy/"
pkgPathPos := strings.Index(file, delim)
if pkgPathPos > -1 && len(file) > pkgPathPos+len(delim) {
file = file[pkgPathPos+len(delim):]
}
panicMsg := fmt.Sprintf("%s [PANIC %s] %s:%d - %v", time.Now().Format(timeFormat), r.URL.String(), file, line, rec)
if h.Debug {
// Write error and stack trace to the response rather than to a log
var stackBuf [4096]byte
stack := stackBuf[:runtime.Stack(stackBuf[:], false)]
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)
h.errorPage(w, r, http.StatusInternalServerError)
}
}
const timeFormat = "02/Jan/2006:15:04:05 -0700"
+272
View File
@@ -0,0 +1,272 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package errors
import (
"bytes"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestErrors(t *testing.T) {
// create a temporary page
const content = "This is a error page"
path, err := createErrorPageFile("errors_test.html", content)
if err != nil {
t.Fatal(err)
}
defer os.Remove(path)
buf := bytes.Buffer{}
em := ErrorHandler{
ErrorPages: map[int]string{
http.StatusNotFound: path,
http.StatusForbidden: "not_exist_file",
},
Log: httpserver.NewTestLogger(&buf),
}
_, notExistErr := os.Open("not_exist_file")
testErr := errors.New("test error")
tests := []struct {
next httpserver.Handler
expectedCode int
expectedBody string
expectedLog string
expectedErr error
}{
{
next: genErrorHandler(http.StatusOK, nil, "normal"),
expectedCode: http.StatusOK,
expectedBody: "normal",
expectedLog: "",
expectedErr: nil,
},
{
next: genErrorHandler(http.StatusMovedPermanently, testErr, ""),
expectedCode: http.StatusMovedPermanently,
expectedBody: "",
expectedLog: fmt.Sprintf("[ERROR %d %s] %v\n", http.StatusMovedPermanently, "/", testErr),
expectedErr: testErr,
},
{
next: genErrorHandler(http.StatusBadRequest, nil, ""),
expectedCode: 0,
expectedBody: fmt.Sprintf("%d %s\n", http.StatusBadRequest,
http.StatusText(http.StatusBadRequest)),
expectedLog: "",
expectedErr: nil,
},
{
next: genErrorHandler(http.StatusNotFound, nil, ""),
expectedCode: 0,
expectedBody: content,
expectedLog: "",
expectedErr: nil,
},
{
next: genErrorHandler(http.StatusForbidden, nil, ""),
expectedCode: 0,
expectedBody: fmt.Sprintf("%d %s\n", http.StatusForbidden,
http.StatusText(http.StatusForbidden)),
expectedLog: fmt.Sprintf("[NOTICE %d /] could not load error page: %v\n",
http.StatusForbidden, notExistErr),
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 TestVisibleErrorWithPanic(t *testing.T) {
const panicMsg = "I'm a panic"
eh := ErrorHandler{
ErrorPages: make(map[int]string),
Debug: true,
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
panic(panicMsg)
}),
}
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
code, err := eh.ServeHTTP(rec, req)
if code != 0 {
t.Errorf("Expected error handler to return 0 (it should write to response), got status %d", code)
}
if err != nil {
t.Errorf("Expected error handler to return nil error (it should panic!), but got '%v'", err)
}
body := rec.Body.String()
if !strings.Contains(body, "[PANIC /] 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) {
t.Errorf("Expected response body to contain panic message, but it didn't:\n%s", body)
}
if len(body) < 500 {
t.Errorf("Expected response body to contain stack trace, but it was too short: len=%d", len(body))
}
}
func 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: httpserver.NewTestLogger(&buf),
}
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)
}
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
}
+138
View File
@@ -0,0 +1,138 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package errors
import (
"log"
"os"
"path/filepath"
"strconv"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// setup configures a new errors middleware instance.
func setup(c *caddy.Controller) error {
handler, err := errorsParse(c)
if err != nil {
return err
}
handler.Log.Attach(c)
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
handler.Next = next
return handler
})
return nil
}
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 := &ErrorHandler{
ErrorPages: make(map[int]string),
Log: &httpserver.Logger{},
}
cfg := httpserver.GetConfig(c)
optionalBlock := func() error {
for c.NextBlock() {
what := c.Val()
where := c.RemainingArgs()
if httpserver.IsLogRollerSubdirective(what) {
var err error
err = httpserver.ParseRoller(handler.Log.Roller, what, where...)
if err != nil {
return err
}
} else {
if len(where) != 1 {
return c.ArgErr()
}
where := where[0]
// Error page; ensure it exists
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()
if what == "*" {
if handler.GenericErrorPage != "" {
return c.Errf("Duplicate status code entry: %s", what)
}
handler.GenericErrorPage = where
} else {
whatInt, err := strconv.Atoi(what)
if err != nil {
return c.Err("Expecting a numeric status code or '*', got '" + what + "'")
}
if _, exists := handler.ErrorPages[whatInt]; exists {
return c.Errf("Duplicate status code entry: %s", what)
}
handler.ErrorPages[whatInt] = where
}
}
}
return nil
}
for c.Next() {
// weird hack to avoid having the handler values overwritten.
if c.Val() == "}" {
continue
}
args := c.RemainingArgs()
if len(args) == 1 {
switch args[0] {
case "visible":
handler.Debug = true
default:
handler.Log.Output = args[0]
handler.Log.Roller = httpserver.DefaultLogRoller()
}
}
if len(args) > 1 {
return handler, c.Errf("Only 1 Argument expected for errors directive")
}
// Configuration may be in a block
err := optionalBlock()
if err != nil {
return handler, err
}
}
return handler, nil
}
+204
View File
@@ -0,0 +1,204 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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)
}
expectedLogger := &httpserver.Logger{}
if !reflect.DeepEqual(expectedLogger, myHandler.Log) {
t.Errorf("Expected '%v' as the default Log, got: '%v'", expectedLogger, myHandler.Log)
}
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{},
Log: &httpserver.Logger{},
}},
{`errors errors.txt`, false, ErrorHandler{
ErrorPages: map[int]string{},
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: httpserver.DefaultLogRoller(),
},
}},
{`errors visible`, false, ErrorHandler{
ErrorPages: map[int]string{},
Debug: true,
Log: &httpserver.Logger{},
}},
{`errors errors.txt {
404 404.html
500 500.html
}`, false, ErrorHandler{
ErrorPages: map[int]string{
404: "404.html",
500: "500.html",
},
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: httpserver.DefaultLogRoller(),
},
}},
{`errors errors.txt {
rotate_size 2
rotate_age 10
rotate_keep 3
rotate_compress
}`, false, ErrorHandler{
ErrorPages: map[int]string{},
Log: &httpserver.Logger{
Output: "errors.txt", Roller: &httpserver.LogRoller{
MaxSize: 2,
MaxAge: 10,
MaxBackups: 3,
Compress: true,
LocalTime: true,
},
},
}},
{`errors errors.txt {
rotate_size 3
rotate_age 11
rotate_keep 5
404 404.html
503 503.html
}`, false, ErrorHandler{
ErrorPages: map[int]string{
404: "404.html",
503: "503.html",
},
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: &httpserver.LogRoller{
MaxSize: 3,
MaxAge: 11,
MaxBackups: 5,
Compress: false,
LocalTime: true,
},
},
}},
{`errors errors.txt {
* generic_error.html
404 404.html
503 503.html
}`, false, ErrorHandler{
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: httpserver.DefaultLogRoller(),
},
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,
},
Log: &httpserver.Logger{},
}},
{`errors errors.txt { rotate_size 2 rotate_age 10 rotate_keep 3 rotate_compress }`,
true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
{`errors errors.txt {
rotate_compress invalid
}`,
true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
// Next two test cases is the detection of duplicate status codes
{`errors {
503 503.html
503 503.html
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
{`errors {
* generic_error.html
* generic_error.html
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
{`errors /path error.txt {
404
}`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
{`errors /path error.txt`, true, ErrorHandler{ErrorPages: map[int]string{}, Log: &httpserver.Logger{}}},
}
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,
test.expectedErrorHandler, actualErrorsRule)
}
}
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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
}
+83
View File
@@ -0,0 +1,83 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package expvar
import (
"expvar"
"runtime"
"sync"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("expvar", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new ExpVar middleware instance.
func setup(c *caddy.Controller) error {
resource, err := expVarParse(c)
if err != nil {
return err
}
// publish any extra information/metrics we may want to capture
publishExtraVars()
ev := ExpVar{Resource: resource}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
ev.Next = next
return ev
})
return nil
}
func expVarParse(c *caddy.Controller) (Resource, error) {
var resource Resource
var err error
for c.Next() {
args := c.RemainingArgs()
switch len(args) {
case 0:
resource = Resource(defaultExpvarPath)
case 1:
resource = Resource(args[0])
default:
return resource, c.ArgErr()
}
}
return resource, err
}
func publishExtraVars() {
// By using sync.Once instead of an init() function, we don't clutter
// the app's expvar export unnecessarily, or risk colliding with it.
publishOnce.Do(func() {
expvar.Publish("Goroutines", expvar.Func(func() interface{} {
return runtime.NumGoroutine()
}))
})
}
var publishOnce sync.Once // publishing variables should only be done once
var defaultExpvarPath = "/debug/vars"
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package expvar
import (
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `expvar`)
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 middleware, got 0 instead")
}
c = caddy.NewTestController("http", `expvar /d/v`)
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 middleware, got 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(ExpVar)
if !ok {
t.Fatalf("Expected handler to be type ExpVar, got: %#v", handler)
}
if myHandler.Resource != "/d/v" {
t.Errorf("Expected /d/v as expvar resource")
}
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package extensions contains middleware for clean URLs.
//
// The root path of the site is passed in as well as possible extensions
// to try internally for paths requested that don't match an existing
// resource. The first path+ext combination that matches a valid file
// will be used.
package extensions
import (
"net/http"
"os"
"path"
"strings"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// Ext can assume an extension from clean URLs.
// It tries extensions in the order listed in Extensions.
type Ext struct {
// Next handler in the chain
Next httpserver.Handler
// Path to site root
Root string
// List of extensions to try
Extensions []string
}
// ServeHTTP implements the httpserver.Handler interface.
func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
urlpath := strings.TrimSuffix(r.URL.Path, "/")
if len(r.URL.Path) > 0 && path.Ext(urlpath) == "" && r.URL.Path[len(r.URL.Path)-1] != '/' {
for _, ext := range e.Extensions {
_, err := os.Stat(httpserver.SafePath(e.Root, urlpath) + ext)
if err == nil {
r.URL.Path = urlpath + ext
break
}
}
}
return e.Next.ServeHTTP(w, r)
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extensions
import (
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("ext", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new instance of 'extensions' middleware for clean URLs.
func setup(c *caddy.Controller) error {
cfg := httpserver.GetConfig(c)
root := cfg.Root
exts, err := extParse(c)
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return Ext{
Next: next,
Extensions: exts,
Root: root,
}
})
return nil
}
// extParse sets up an instance of extension middleware
// from a middleware controller and returns a list of extensions.
func extParse(c *caddy.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
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package extensions
import (
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `ext .html .htm .php`)
err := setup(c)
if err != nil {
t.Fatalf("Expected no errors, got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, had 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(Ext)
if !ok {
t.Fatalf("Expected handler to be type Ext, got: %#v", handler)
}
if myHandler.Extensions[0] != ".html" {
t.Errorf("Expected .html in the list of Extensions")
}
if myHandler.Extensions[1] != ".htm" {
t.Errorf("Expected .htm in the list of Extensions")
}
if myHandler.Extensions[2] != ".php" {
t.Errorf("Expected .php in the list of Extensions")
}
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestExtParse(t *testing.T) {
tests := []struct {
inputExts string
shouldErr bool
expectedExts []string
}{
{`ext .html .htm .php`, false, []string{".html", ".htm", ".php"}},
{`ext .php .html .xml`, false, []string{".php", ".html", ".xml"}},
{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}},
}
for i, test := range tests {
actualExts, err := extParse(caddy.NewTestController("http", test.inputExts))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actualExts) != len(test.expectedExts) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expectedExts), len(actualExts))
}
for j, actualExt := range actualExts {
if actualExt != test.expectedExts[j] {
t.Fatalf("Test %d expected %dth extension to be %s , but got %s",
i, j, test.expectedExts[j], actualExt)
}
}
}
}
+496
View File
@@ -0,0 +1,496 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package fastcgi has middleware that acts as a FastCGI client. Requests
// that get forwarded to FastCGI stop the middleware execution chain.
// The most common use for this package is to serve PHP websites via php-fpm.
package fastcgi
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
"time"
"crypto/tls"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddytls"
)
// Handler is a middleware type that can handle requests as a FastCGI client.
type Handler struct {
Next httpserver.Handler
Rules []Rule
Root string
FileSys http.FileSystem
// These are sent to CGI scripts in env variables
SoftwareName string
SoftwareVersion string
ServerName string
ServerPort string
}
// ServeHTTP satisfies the httpserver.Handler interface.
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for _, rule := range h.Rules {
// First requirement: Base path must match request path. If it doesn't,
// we check to make sure the leading slash is not missing, and if so,
// we check again with it prepended. This is in case people forget
// a leading slash when performing rewrites, and we don't want to expose
// the contents of the (likely PHP) script. See issue #1645.
hpath := httpserver.Path(r.URL.Path)
if !hpath.Matches(rule.Path) {
if strings.HasPrefix(string(hpath), "/") {
// this is a normal-looking path, and it doesn't match; try next rule
continue
}
hpath = httpserver.Path("/" + string(hpath)) // prepend leading slash
if !hpath.Matches(rule.Path) {
// even after fixing the request path, it still doesn't match; try next rule
continue
}
}
// The path must also be allowed (not ignored).
if !rule.AllowedPath(r.URL.Path) {
continue
}
// In addition to matching the path, a request must meet some
// other criteria before being proxied as FastCGI. For example,
// we probably want to exclude static assets (CSS, JS, images...)
// but we also want to be flexible for the script we proxy to.
fpath := r.URL.Path
if idx, ok := httpserver.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
fpath = idx
// Index file present.
// If request path cannot be split, return error.
if !rule.canSplit(fpath) {
return http.StatusInternalServerError, ErrIndexMissingSplit
}
} else {
// No index file present.
// If request path cannot be split, ignore request.
if !rule.canSplit(fpath) {
continue
}
}
// These criteria work well in this order for PHP sites
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) {
// Create environment for CGI script
env, err := h.buildEnv(r, rule, fpath)
if err != nil {
return http.StatusInternalServerError, err
}
// Connect to FastCGI gateway
address, err := rule.Address()
if err != nil {
return http.StatusBadGateway, err
}
network, address := parseAddress(address)
ctx := context.Background()
if rule.ConnectTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, rule.ConnectTimeout)
defer cancel()
}
fcgiBackend, err := DialContext(ctx, network, address)
if err != nil {
return http.StatusBadGateway, err
}
defer fcgiBackend.Close()
// read/write timeouts
if err := fcgiBackend.SetReadTimeout(rule.ReadTimeout); err != nil {
return http.StatusInternalServerError, err
}
if err := fcgiBackend.SetSendTimeout(rule.SendTimeout); err != nil {
return http.StatusInternalServerError, err
}
var resp *http.Response
var contentLength int64
// if ContentLength is already set
if r.ContentLength > 0 {
contentLength = r.ContentLength
} else {
contentLength, _ = strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64)
}
switch r.Method {
case "HEAD":
resp, err = fcgiBackend.Head(env)
case "GET":
resp, err = fcgiBackend.Get(env, r.Body, contentLength)
case "OPTIONS":
resp, err = fcgiBackend.Options(env)
default:
resp, err = fcgiBackend.Post(env, r.Method, r.Header.Get("Content-Type"), r.Body, contentLength)
}
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
return http.StatusGatewayTimeout, err
} else if err != io.EOF {
return http.StatusBadGateway, err
}
}
// Write response header
writeHeader(w, resp)
// Write the response body
_, err = io.Copy(w, resp.Body)
if err != nil {
return http.StatusBadGateway, err
}
// Log any stderr output from upstream
if fcgiBackend.stderr.Len() != 0 {
// Remove trailing newline, error logger already does this.
err = LogError(strings.TrimSuffix(fcgiBackend.stderr.String(), "\n"))
}
// Normally we would return the status code if it is an error status (>= 400),
// however, upstream FastCGI apps don't know about our contract and have
// probably already written an error page. So we just return 0, indicating
// that the response body is already written. However, we do return any
// error value so it can be logged.
// Note that the proxy middleware works the same way, returning status=0.
return 0, err
}
}
return h.Next.ServeHTTP(w, r)
}
// parseAddress returns the network and address of fcgiAddress.
// The first string is the network, "tcp" or "unix", implied from the scheme and address.
// The second string is fcgiAddress, with scheme prefixes removed.
// The two returned strings can be used as parameters to the Dial() function.
func parseAddress(fcgiAddress string) (string, string) {
// check if address has tcp scheme explicitly set
if strings.HasPrefix(fcgiAddress, "tcp://") {
return "tcp", fcgiAddress[len("tcp://"):]
}
// check if address has fastcgi scheme explicitly set
if strings.HasPrefix(fcgiAddress, "fastcgi://") {
return "tcp", fcgiAddress[len("fastcgi://"):]
}
// check if unix socket
if trim := strings.HasPrefix(fcgiAddress, "unix"); strings.HasPrefix(fcgiAddress, "/") || trim {
if trim {
return "unix", fcgiAddress[len("unix:"):]
}
return "unix", fcgiAddress
}
// default case, a plain tcp address with no scheme
return "tcp", fcgiAddress
}
func writeHeader(w http.ResponseWriter, r *http.Response) {
for key, vals := range r.Header {
for _, val := range vals {
w.Header().Add(key, val)
}
}
w.WriteHeader(r.StatusCode)
}
func (h Handler) exists(path string) bool {
if _, err := os.Stat(h.Root + path); err == nil {
return true
}
return false
}
// buildEnv returns a set of CGI environment variables for the request.
func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) {
var env map[string]string
// Separate remote IP and port; more lenient than net.SplitHostPort
var ip, port string
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 {
ip = r.RemoteAddr[:idx]
port = r.RemoteAddr[idx+1:]
} else {
ip = r.RemoteAddr
}
// Remove [] from IPv6 addresses
ip = strings.Replace(ip, "[", "", 1)
ip = strings.Replace(ip, "]", "", 1)
// Split path in preparation for env variables.
// Previous rule.canSplit checks ensure this can never be -1.
splitPos := rule.splitPos(fpath)
// Request has the extension; path was split successfully
docURI := fpath[:splitPos+len(rule.SplitPath)]
pathInfo := fpath[splitPos+len(rule.SplitPath):]
scriptName := fpath
// Strip PATH_INFO from SCRIPT_NAME
scriptName = strings.TrimSuffix(scriptName, pathInfo)
// SCRIPT_FILENAME is the absolute path of SCRIPT_NAME
scriptFilename := filepath.Join(rule.Root, scriptName)
// Add vhost path prefix to scriptName. Otherwise, some PHP software will
// have difficulty discovering its URL.
pathPrefix, _ := r.Context().Value(caddy.CtxKey("path_prefix")).(string)
scriptName = path.Join(pathPrefix, scriptName)
// Get the request URI from context. The context stores the original URI in case
// it was changed by a middleware such as rewrite. By default, we pass the
// original URI in as the value of REQUEST_URI (the user can overwrite this
// if desired). Most PHP apps seem to want the original URI. Besides, this is
// how nginx defaults: http://stackoverflow.com/a/12485156/1048862
reqURL, _ := r.Context().Value(httpserver.OriginalURLCtxKey).(url.URL)
// Retrieve name of remote user that was set by some downstream middleware such as basicauth.
remoteUser, _ := r.Context().Value(httpserver.RemoteUserCtxKey).(string)
requestScheme := "http"
if r.TLS != nil {
requestScheme = "https"
}
// Some variables are unused but cleared explicitly to prevent
// the parent environment from interfering.
env = map[string]string{
// Variables defined in CGI 1.1 spec
"AUTH_TYPE": "", // Not used
"CONTENT_LENGTH": r.Header.Get("Content-Length"),
"CONTENT_TYPE": r.Header.Get("Content-Type"),
"GATEWAY_INTERFACE": "CGI/1.1",
"PATH_INFO": pathInfo,
"QUERY_STRING": r.URL.RawQuery,
"REMOTE_ADDR": ip,
"REMOTE_HOST": ip, // For speed, remote host lookups disabled
"REMOTE_PORT": port,
"REMOTE_IDENT": "", // Not used
"REMOTE_USER": remoteUser,
"REQUEST_METHOD": r.Method,
"REQUEST_SCHEME": requestScheme,
"SERVER_NAME": h.ServerName,
"SERVER_PORT": h.ServerPort,
"SERVER_PROTOCOL": r.Proto,
"SERVER_SOFTWARE": h.SoftwareName + "/" + h.SoftwareVersion,
// Other variables
"DOCUMENT_ROOT": rule.Root,
"DOCUMENT_URI": docURI,
"HTTP_HOST": r.Host, // added here, since not always part of headers
"REQUEST_URI": reqURL.RequestURI(),
"SCRIPT_FILENAME": scriptFilename,
"SCRIPT_NAME": scriptName,
}
// compliance with the CGI specification requires that
// PATH_TRANSLATED should only exist if PATH_INFO is defined.
// Info: https://www.ietf.org/rfc/rfc3875 Page 14
if env["PATH_INFO"] != "" {
env["PATH_TRANSLATED"] = filepath.Join(rule.Root, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html
}
// Some web apps rely on knowing HTTPS or not
if r.TLS != nil {
env["HTTPS"] = "on"
// and pass the protocol details in a manner compatible with apache's mod_ssl
// (which is why they have a SSL_ prefix and not TLS_).
v, ok := tlsProtocolStringToMap[r.TLS.Version]
if ok {
env["SSL_PROTOCOL"] = v
}
// and pass the cipher suite in a manner compatible with apache's mod_ssl
for k, v := range caddytls.SupportedCiphersMap {
if v == r.TLS.CipherSuite {
env["SSL_CIPHER"] = k
break
}
}
}
// Add env variables from config (with support for placeholders in values)
replacer := httpserver.NewReplacer(r, nil, "")
for _, envVar := range rule.EnvVars {
env[envVar[0]] = replacer.Replace(envVar[1])
}
// Add all HTTP headers to env variables
for field, val := range r.Header {
header := strings.ToUpper(field)
header = headerNameReplacer.Replace(header)
env["HTTP_"+header] = strings.Join(val, ", ")
}
return env, nil
}
// Rule represents a FastCGI handling rule.
// It is parsed from the fastcgi directive in the Caddyfile, see setup.go.
type Rule struct {
// The base path to match. Required.
Path string
// upstream load balancer
balancer
// Always process files with this extension with fastcgi.
Ext string
// Use this directory as the fastcgi root directory. Defaults to the root
// directory of the parent virtual host.
Root string
// The path in the URL will be split into two, with the first piece ending
// with the value of SplitPath. The first piece will be assumed as the
// actual resource (CGI script) name, and the second piece will be set to
// PATH_INFO for the CGI script to use.
SplitPath string
// If the URL ends with '/' (which indicates a directory), these index
// files will be tried instead.
IndexFiles []string
// Environment Variables
EnvVars [][2]string
// Ignored paths
IgnoredSubPaths []string
// The duration used to set a deadline when connecting to an upstream.
ConnectTimeout time.Duration
// The duration used to set a deadline when reading from the FastCGI server.
ReadTimeout time.Duration
// The duration used to set a deadline when sending to the FastCGI server.
SendTimeout time.Duration
}
// balancer is a fastcgi upstream load balancer.
type balancer interface {
// Address picks an upstream address from the
// underlying load balancer.
Address() (string, error)
}
// roundRobin is a round robin balancer for fastcgi upstreams.
type roundRobin struct {
// Known Go bug: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
// must be first field for 64 bit alignment
// on x86 and arm.
index int64
addresses []string
}
func (r *roundRobin) Address() (string, error) {
index := atomic.AddInt64(&r.index, 1) % int64(len(r.addresses))
return r.addresses[index], nil
}
// srvResolver is a private interface used to abstract
// the DNS resolver. It is mainly used to facilitate testing.
type srvResolver interface {
LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error)
}
// srv is a service locator for fastcgi upstreams
type srv struct {
resolver srvResolver
service string
}
// Address looks up the service and returns the address:port
// from first result in resolved list.
// No explicit balancing is required because net.LookupSRV
// sorts the results by priority and randomizes within priority.
func (s *srv) Address() (string, error) {
_, addrs, err := s.resolver.LookupSRV(context.Background(), "", "", s.service)
if err != nil {
return "", err
}
return fmt.Sprintf("%s:%d", strings.TrimRight(addrs[0].Target, "."), addrs[0].Port), nil
}
// canSplit checks if path can split into two based on rule.SplitPath.
func (r Rule) canSplit(path string) bool {
return r.splitPos(path) >= 0
}
// splitPos returns the index where path should be split
// based on rule.SplitPath.
func (r Rule) splitPos(path string) int {
if httpserver.CaseSensitivePath {
return strings.Index(path, r.SplitPath)
}
return strings.Index(strings.ToLower(path), strings.ToLower(r.SplitPath))
}
// AllowedPath checks if requestPath is not an ignored path.
func (r Rule) AllowedPath(requestPath string) bool {
for _, ignoredSubPath := range r.IgnoredSubPaths {
if httpserver.Path(path.Clean(requestPath)).Matches(path.Join(r.Path, ignoredSubPath)) {
return false
}
}
return true
}
var (
headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_")
// ErrIndexMissingSplit describes an index configuration error.
ErrIndexMissingSplit = errors.New("configured index file(s) must include split value")
)
// LogError is a non fatal error that allows requests to go through.
type LogError string
// Error satisfies error interface.
func (l LogError) Error() string {
return string(l)
}
// Map of supported protocols to Apache ssl_mod format
// Note that these are slightly different from SupportedProtocols in caddytls/config.go's
var tlsProtocolStringToMap = map[uint16]string{
tls.VersionTLS10: "TLSv1",
tls.VersionTLS11: "TLSv1.1",
tls.VersionTLS12: "TLSv1.2",
}
+424
View File
@@ -0,0 +1,424 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fastcgi
import (
"context"
"log"
"net"
"net/http"
"net/http/fcgi"
"net/http/httptest"
"net/url"
"path/filepath"
"strconv"
"sync"
"testing"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestServeHTTP(t *testing.T) {
body := "This is some test body content"
bodyLenStr := strconv.Itoa(len(body))
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Unable to create listener for test: %v", err)
}
defer func() { _ = listener.Close() }()
go func() {
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", bodyLenStr)
_, err := w.Write([]byte(body))
if err != nil {
log.Printf("[ERROR] unable to write header: %v", err)
}
}))
if err != nil {
log.Printf("[ERROR] unable to start server: %v", err)
}
}()
handler := Handler{
Next: nil,
Rules: []Rule{{Path: "/", balancer: address(listener.Addr().String())}},
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Unable to create request: %v", err)
}
w := httptest.NewRecorder()
status, err := handler.ServeHTTP(w, r)
if got, want := status, 0; got != want {
t.Errorf("Expected returned status code to be %d, got %d", want, got)
}
if err != nil {
t.Errorf("Expected nil error, got: %v", err)
}
if got, want := w.Header().Get("Content-Length"), bodyLenStr; got != want {
t.Errorf("Expected Content-Length to be '%s', got: '%s'", want, got)
}
if got, want := w.Body.String(), body; got != want {
t.Errorf("Expected response body to be '%s', got: '%s'", want, got)
}
}
func TestRuleParseAddress(t *testing.T) {
getClientTestTable := []struct {
rule *Rule
expectednetwork string
expectedaddress string
}{
{&Rule{balancer: address("tcp://172.17.0.1:9000")}, "tcp", "172.17.0.1:9000"},
{&Rule{balancer: address("fastcgi://localhost:9000")}, "tcp", "localhost:9000"},
{&Rule{balancer: address("172.17.0.15")}, "tcp", "172.17.0.15"},
{&Rule{balancer: address("/my/unix/socket")}, "unix", "/my/unix/socket"},
{&Rule{balancer: address("unix:/second/unix/socket")}, "unix", "/second/unix/socket"},
}
for _, entry := range getClientTestTable {
addr, err := entry.rule.Address()
if err != nil {
t.Errorf("Unexpected error in retrieving address: %s", err.Error())
}
if actualnetwork, _ := parseAddress(addr); actualnetwork != entry.expectednetwork {
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", addr, actualnetwork, entry.expectednetwork)
}
if _, actualaddress := parseAddress(addr); actualaddress != entry.expectedaddress {
t.Errorf("Unexpected parsed address for address string %v. Got %v, expected %v", addr, actualaddress, entry.expectedaddress)
}
}
}
func TestRuleIgnoredPath(t *testing.T) {
rule := &Rule{
Path: "/fastcgi",
IgnoredSubPaths: []string{"/download", "/static"},
}
tests := []struct {
url string
expected bool
}{
{"/fastcgi", true},
{"/fastcgi/dl", true},
{"/fastcgi/download", false},
{"/fastcgi/download/static", false},
{"/fastcgi/static", false},
{"/fastcgi/static/download", false},
{"/fastcgi/something/download", true},
{"/fastcgi/something/static", true},
{"/fastcgi//static", false},
{"/fastcgi//static//download", false},
{"/fastcgi//download", false},
}
for i, test := range tests {
allowed := rule.AllowedPath(test.url)
if test.expected != allowed {
t.Errorf("Test %d: expected %v found %v", i, test.expected, allowed)
}
}
}
func TestBuildEnv(t *testing.T) {
testBuildEnv := func(r *http.Request, rule Rule, fpath string, envExpected map[string]string) {
var h Handler
env, err := h.buildEnv(r, rule, fpath)
if err != nil {
t.Error("Unexpected error:", err.Error())
}
for k, v := range envExpected {
if env[k] != v {
t.Errorf("Unexpected %v. Got %v, expected %v", k, env[k], v)
}
}
}
rule := Rule{
Ext: ".php",
SplitPath: ".php",
IndexFiles: []string{"index.php"},
}
u, err := url.Parse("http://localhost:2015/fgci_test.php?test=foobar")
if err != nil {
t.Error("Unexpected error:", err.Error())
}
var newReq = func() *http.Request {
r := http.Request{
Method: "GET",
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Host: "localhost:2015",
RemoteAddr: "[2b02:1810:4f2d:9400:70ab:f822:be8a:9093]:51688",
RequestURI: "/fgci_test.php",
Header: map[string][]string{
"Foo": {"Bar", "two"},
},
}
ctx := context.WithValue(r.Context(), httpserver.OriginalURLCtxKey, *r.URL)
return r.WithContext(ctx)
}
fpath := "/fgci_test.php"
var newEnv = func() map[string]string {
return map[string]string{
"REMOTE_ADDR": "2b02:1810:4f2d:9400:70ab:f822:be8a:9093",
"REMOTE_PORT": "51688",
"SERVER_PROTOCOL": "HTTP/1.1",
"QUERY_STRING": "test=foobar",
"REQUEST_METHOD": "GET",
"HTTP_HOST": "localhost:2015",
"SCRIPT_NAME": "/fgci_test.php",
}
}
// request
var r *http.Request
// expected environment variables
var envExpected map[string]string
// 1. Test for full canonical IPv6 address
r = newReq()
testBuildEnv(r, rule, fpath, envExpected)
// 2. Test for shorthand notation of IPv6 address
r = newReq()
r.RemoteAddr = "[::1]:51688"
envExpected = newEnv()
envExpected["REMOTE_ADDR"] = "::1"
testBuildEnv(r, rule, fpath, envExpected)
// 3. Test for IPv4 address
r = newReq()
r.RemoteAddr = "192.168.0.10:51688"
envExpected = newEnv()
envExpected["REMOTE_ADDR"] = "192.168.0.10"
testBuildEnv(r, rule, fpath, envExpected)
// 4. Test for environment variable
r = newReq()
rule.EnvVars = [][2]string{
{"HTTP_HOST", "localhost:2016"},
{"REQUEST_METHOD", "POST"},
}
envExpected = newEnv()
envExpected["HTTP_HOST"] = "localhost:2016"
envExpected["REQUEST_METHOD"] = "POST"
testBuildEnv(r, rule, fpath, envExpected)
// 5. Test for environment variable placeholders
r = newReq()
rule.EnvVars = [][2]string{
{"HTTP_HOST", "{host}"},
{"CUSTOM_URI", "custom_uri{uri}"},
{"CUSTOM_QUERY", "custom=true&{query}"},
}
envExpected = newEnv()
envExpected["HTTP_HOST"] = "localhost:2015"
envExpected["CUSTOM_URI"] = "custom_uri/fgci_test.php?test=foobar"
envExpected["CUSTOM_QUERY"] = "custom=true&test=foobar"
testBuildEnv(r, rule, fpath, envExpected)
// 6. Test SCRIPT_NAME includes path prefix
r = newReq()
ctx := context.WithValue(r.Context(), caddy.CtxKey("path_prefix"), "/test")
r = r.WithContext(ctx)
envExpected = newEnv()
envExpected["SCRIPT_NAME"] = "/test/fgci_test.php"
testBuildEnv(r, rule, fpath, envExpected)
// 7. Test SCRIPT_NAME,SCRIPT_FILENAME do not include PATH_INFO
fpath = "/fgci_test.php/extra/paths"
r = newReq()
envExpected = newEnv()
envExpected["PATH_INFO"] = "/extra/paths"
envExpected["SCRIPT_NAME"] = "/fgci_test.php"
envExpected["SCRIPT_FILENAME"] = filepath.FromSlash("/fgci_test.php")
testBuildEnv(r, rule, fpath, envExpected)
// 8. Test REQUEST_SCHEME in env
r = newReq()
envExpected = newEnv()
envExpected["REQUEST_SCHEME"] = "http"
testBuildEnv(r, rule, fpath, envExpected)
}
func TestReadTimeout(t *testing.T) {
tests := []struct {
sleep time.Duration
readTimeout time.Duration
shouldErr bool
}{
{75 * time.Millisecond, 50 * time.Millisecond, true},
{0, -1 * time.Second, true},
{0, time.Minute, false},
}
var wg sync.WaitGroup
for i, test := range tests {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
}
defer func() { _ = listener.Close() }()
handler := Handler{
Next: nil,
Rules: []Rule{
{
Path: "/",
balancer: address(listener.Addr().String()),
ReadTimeout: test.readTimeout,
},
},
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Test %d: Unable to create request: %v", i, err)
}
w := httptest.NewRecorder()
wg.Add(1)
go func() {
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(test.sleep)
w.WriteHeader(http.StatusOK)
wg.Done()
}))
if err != nil {
log.Printf("[ERROR] unable to start server: %v", err)
}
}()
got, err := handler.ServeHTTP(w, r)
if test.shouldErr {
if err == nil {
t.Errorf("Test %d: Expected i/o timeout error but had none", i)
} else if err, ok := err.(net.Error); !ok || !err.Timeout() {
t.Errorf("Test %d: Expected i/o timeout error, got: '%s'", i, err.Error())
}
want := http.StatusGatewayTimeout
if got != want {
t.Errorf("Test %d: Expected returned status code to be %d, got: %d",
i, want, got)
}
} else if err != nil {
t.Errorf("Test %d: Expected nil error, got: %v", i, err)
}
wg.Wait()
}
}
func TestSendTimeout(t *testing.T) {
tests := []struct {
sendTimeout time.Duration
shouldErr bool
}{
{-1 * time.Second, true},
{time.Minute, false},
}
for i, test := range tests {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
}
defer func() { _ = listener.Close() }()
handler := Handler{
Next: nil,
Rules: []Rule{
{
Path: "/",
balancer: address(listener.Addr().String()),
SendTimeout: test.sendTimeout,
},
},
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("Test %d: Unable to create request: %v", i, err)
}
w := httptest.NewRecorder()
go func() {
err := fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
if err != nil {
log.Printf("[ERROR] unable to start server: %v", err)
}
}()
got, err := handler.ServeHTTP(w, r)
if test.shouldErr {
if err == nil {
t.Errorf("Test %d: Expected i/o timeout error but had none", i)
} else if err, ok := err.(net.Error); !ok || !err.Timeout() {
t.Errorf("Test %d: Expected i/o timeout error, got: '%s'", i, err.Error())
}
want := http.StatusGatewayTimeout
if got != want {
t.Errorf("Test %d: Expected returned status code to be %d, got: %d",
i, want, got)
}
} else if err != nil {
t.Errorf("Test %d: Expected nil error, got: %v", i, err)
}
}
}
func TestBalancer(t *testing.T) {
tests := [][]string{
{"localhost", "host.local"},
{"localhost"},
{"localhost", "host.local", "example.com"},
{"localhost", "host.local", "example.com", "127.0.0.1"},
}
for i, test := range tests {
b := address(test...)
for _, host := range test {
a, err := b.Address()
if err != nil {
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
}
if a != host {
t.Errorf("Test %d: expected %s, found %s", i, host, a)
}
}
}
}
func address(addresses ...string) balancer {
return &roundRobin{
addresses: addresses,
index: -1,
}
}
@@ -1,3 +1,17 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Forked Jan. 2015 from http://bitbucket.org/PinIdea/fcgi_client
// (which is forked from https://code.google.com/p/go-fastcgi-client/)
@@ -13,6 +27,7 @@ package fastcgi
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"errors"
"io"
@@ -28,47 +43,78 @@ import (
"strconv"
"strings"
"sync"
"time"
)
const FCGI_LISTENSOCK_FILENO uint8 = 0
const FCGI_HEADER_LEN uint8 = 8
const VERSION_1 uint8 = 1
const FCGI_NULL_REQUEST_ID uint8 = 0
const FCGI_KEEP_CONN uint8 = 1
const doubleCRLF = "\r\n\r\n"
// FCGIListenSockFileno describes listen socket file number.
const FCGIListenSockFileno uint8 = 0
// FCGIHeaderLen describes header length.
const FCGIHeaderLen uint8 = 8
// Version1 describes the version.
const Version1 uint8 = 1
// FCGINullRequestID describes the null request ID.
const FCGINullRequestID uint8 = 0
// FCGIKeepConn describes keep connection mode.
const FCGIKeepConn uint8 = 1
const (
FCGI_BEGIN_REQUEST uint8 = iota + 1
FCGI_ABORT_REQUEST
FCGI_END_REQUEST
FCGI_PARAMS
FCGI_STDIN
FCGI_STDOUT
FCGI_STDERR
FCGI_DATA
FCGI_GET_VALUES
FCGI_GET_VALUES_RESULT
FCGI_UNKNOWN_TYPE
FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
// BeginRequest is the begin request flag.
BeginRequest uint8 = iota + 1
// AbortRequest is the abort request flag.
AbortRequest
// EndRequest is the end request flag.
EndRequest
// Params is the parameters flag.
Params
// Stdin is the standard input flag.
Stdin
// Stdout is the standard output flag.
Stdout
// Stderr is the standard error flag.
Stderr
// Data is the data flag.
Data
// GetValues is the get values flag.
GetValues
// GetValuesResult is the get values result flag.
GetValuesResult
// UnknownType is the unknown type flag.
UnknownType
// MaxType is the maximum type flag.
MaxType = UnknownType
)
const (
FCGI_RESPONDER uint8 = iota + 1
FCGI_AUTHORIZER
FCGI_FILTER
// Responder is the responder flag.
Responder uint8 = iota + 1
// Authorizer is the authorizer flag.
Authorizer
// Filter is the filter flag.
Filter
)
const (
FCGI_REQUEST_COMPLETE uint8 = iota
FCGI_CANT_MPX_CONN
FCGI_OVERLOADED
FCGI_UNKNOWN_ROLE
// RequestComplete is the completed request flag.
RequestComplete uint8 = iota
// CantMultiplexConns is the multiplexed connections flag.
CantMultiplexConns
// Overloaded is the overloaded flag.
Overloaded
// UnknownRole is the unknown role flag.
UnknownRole
)
const (
FCGI_MAX_CONNS string = "MAX_CONNS"
FCGI_MAX_REQS string = "MAX_REQS"
FCGI_MPXS_CONNS string = "MPXS_CONNS"
// MaxConns is the maximum connections flag.
MaxConns string = "MAX_CONNS"
// MaxRequests is the maximum requests flag.
MaxRequests string = "MAX_REQS"
// MultiplexConns is the multiplex connections flag.
MultiplexConns string = "MPXS_CONNS"
)
const (
@@ -79,7 +125,7 @@ const (
type header struct {
Version uint8
Type uint8
Id uint16
ID uint16
ContentLength uint16
PaddingLength uint8
Reserved uint8
@@ -89,10 +135,10 @@ type header struct {
// not synchronized because we don't care what the contents are
var pad [maxPad]byte
func (h *header) init(recType uint8, reqId uint16, contentLength int) {
func (h *header) init(recType uint8, reqID uint16, contentLength int) {
h.Version = 1
h.Type = recType
h.Id = reqId
h.ID = reqID
h.ContentLength = uint16(contentLength)
h.PaddingLength = uint8(-contentLength & 7)
}
@@ -110,7 +156,7 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
err = errors.New("fcgi: invalid header version")
return
}
if rec.h.Type == FCGI_END_REQUEST {
if rec.h.Type == EndRequest {
err = io.EOF
return
}
@@ -118,7 +164,7 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
if len(rec.rbuf) < n {
rec.rbuf = make([]byte, n)
}
if n, err = io.ReadFull(r, rec.rbuf[:n]); err != nil {
if _, err = io.ReadFull(r, rec.rbuf[:n]); err != nil {
return
}
buf = rec.rbuf[:int(rec.h.ContentLength)]
@@ -126,21 +172,24 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
return
}
// FCGIClient implements a FastCGI client, which is a standard for
// interfacing external applications with Web servers.
type FCGIClient struct {
mutex sync.Mutex
rwc io.ReadWriteCloser
h header
buf bytes.Buffer
stderr bytes.Buffer
keepAlive bool
reqId uint16
reqID uint16
}
// Connects to the fcgi responder at the specified network address.
// DialWithDialerContext connects to the fcgi responder at the specified network address, using custom net.Dialer
// and a context.
// See func net.Dial for a description of the network and address parameters.
func Dial(network, address string) (fcgi *FCGIClient, err error) {
func DialWithDialerContext(ctx context.Context, network, address string, dialer net.Dialer) (fcgi *FCGIClient, err error) {
var conn net.Conn
conn, err = net.Dial(network, address)
conn, err = dialer.DialContext(ctx, network, address)
if err != nil {
return
}
@@ -148,49 +197,60 @@ func Dial(network, address string) (fcgi *FCGIClient, err error) {
fcgi = &FCGIClient{
rwc: conn,
keepAlive: false,
reqId: 1,
reqID: 1,
}
return
}
// Close fcgi connnection
func (this *FCGIClient) Close() {
this.rwc.Close()
// DialContext is like Dial but passes ctx to dialer.Dial.
func DialContext(ctx context.Context, network, address string) (fcgi *FCGIClient, err error) {
return DialWithDialerContext(ctx, network, address, net.Dialer{})
}
func (this *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
this.mutex.Lock()
defer this.mutex.Unlock()
this.buf.Reset()
this.h.init(recType, this.reqId, len(content))
if err := binary.Write(&this.buf, binary.BigEndian, this.h); err != nil {
// Dial connects to the fcgi responder at the specified network address, using default net.Dialer.
// See func net.Dial for a description of the network and address parameters.
func Dial(network, address string) (fcgi *FCGIClient, err error) {
return DialContext(context.Background(), network, address)
}
// Close closes fcgi connection
func (c *FCGIClient) Close() {
c.rwc.Close()
}
func (c *FCGIClient) writeRecord(recType uint8, content []byte) (err error) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.buf.Reset()
c.h.init(recType, c.reqID, len(content))
if err := binary.Write(&c.buf, binary.BigEndian, c.h); err != nil {
return err
}
if _, err := this.buf.Write(content); err != nil {
if _, err := c.buf.Write(content); err != nil {
return err
}
if _, err := this.buf.Write(pad[:this.h.PaddingLength]); err != nil {
if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil {
return err
}
_, err = this.rwc.Write(this.buf.Bytes())
_, err = c.rwc.Write(c.buf.Bytes())
return err
}
func (this *FCGIClient) writeBeginRequest(role uint16, flags uint8) error {
func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error {
b := [8]byte{byte(role >> 8), byte(role), flags}
return this.writeRecord(FCGI_BEGIN_REQUEST, b[:])
return c.writeRecord(BeginRequest, b[:])
}
func (this *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error {
func (c *FCGIClient) writeEndRequest(appStatus int, protocolStatus uint8) error {
b := make([]byte, 8)
binary.BigEndian.PutUint32(b, uint32(appStatus))
b[4] = protocolStatus
return this.writeRecord(FCGI_END_REQUEST, b)
return c.writeRecord(EndRequest, b)
}
func (this *FCGIClient) writePairs(recType uint8, pairs map[string]string) error {
w := newWriter(this, recType)
func (c *FCGIClient) writePairs(recType uint8, pairs map[string]string) error {
w := newWriter(c, recType)
b := make([]byte, 8)
nn := 0
for k, v := range pairs {
@@ -222,29 +282,6 @@ func (this *FCGIClient) writePairs(recType uint8, pairs map[string]string) error
return nil
}
func readSize(s []byte) (uint32, int) {
if len(s) == 0 {
return 0, 0
}
size, n := uint32(s[0]), 1
if size&(1<<7) != 0 {
if len(s) < 4 {
return 0, 0
}
n = 4
size = binary.BigEndian.Uint32(s)
size &^= 1 << 31
}
return size, n
}
func readString(s []byte, size uint32) string {
if size > uint32(len(s)) {
return ""
}
return string(s[:size])
}
func encodeSize(b []byte, size uint32) int {
if size > 127 {
size |= 1 << 31
@@ -313,10 +350,22 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
if len(p) > 0 {
if len(w.buf) == 0 {
rec := &record{}
w.buf, err = rec.read(w.c.rwc)
if err != nil {
return
// filter outputs for error log
for {
rec := &record{}
var buf []byte
buf, err = rec.read(w.c.rwc)
if err != nil {
return
}
// standard error output
if rec.h.Type == Stderr {
w.c.stderr.Write(buf)
continue
}
w.buf = buf
break
}
}
@@ -333,32 +382,40 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
// Do made the request and returns a io.Reader that translates the data read
// from fcgi responder out of fcgi packet before returning it.
func (this *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
err = this.writeBeginRequest(uint16(FCGI_RESPONDER), 0)
func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
err = c.writeBeginRequest(uint16(Responder), 0)
if err != nil {
return
}
err = this.writePairs(FCGI_PARAMS, p)
err = c.writePairs(Params, p)
if err != nil {
return
}
body := newWriter(this, FCGI_STDIN)
body := newWriter(c, Stdin)
if req != nil {
io.Copy(body, req)
_, _ = io.Copy(body, req)
}
body.Close()
r = &streamReader{c: this}
r = &streamReader{c: c}
return
}
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
// that closes FCGIClient connection.
type clientCloser struct {
*FCGIClient
io.Reader
}
func (f clientCloser) Close() error { return f.rwc.Close() }
// Request returns a HTTP Response with Header and Body
// from fcgi responder
func (this *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
r, err := this.Do(p, req)
func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
r, err := c.Do(p, req)
if err != nil {
return
}
@@ -380,7 +437,10 @@ func (this *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.
if err != nil {
return
}
resp.Status = statusParts[1]
if len(statusParts) > 1 {
resp.Status = statusParts[1]
}
} else {
resp.StatusCode = http.StatusOK
}
@@ -390,93 +450,74 @@ func (this *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.
resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
if chunked(resp.TransferEncoding) {
resp.Body = ioutil.NopCloser(httputil.NewChunkedReader(rb))
resp.Body = clientCloser{c, httputil.NewChunkedReader(rb)}
} else {
resp.Body = ioutil.NopCloser(rb)
resp.Body = clientCloser{c, ioutil.NopCloser(rb)}
}
return
}
// Get issues a GET request to the fcgi responder.
func (this *FCGIClient) Get(p map[string]string) (resp *http.Response, err error) {
func (c *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) {
p["REQUEST_METHOD"] = "GET"
p["CONTENT_LENGTH"] = "0"
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
return this.Request(p, nil)
return c.Request(p, body)
}
// Head issues a HEAD request to the fcgi responder.
func (this *FCGIClient) Head(p map[string]string) (resp *http.Response, err error) {
func (c *FCGIClient) Head(p map[string]string) (resp *http.Response, err error) {
p["REQUEST_METHOD"] = "HEAD"
p["CONTENT_LENGTH"] = "0"
return this.Request(p, nil)
return c.Request(p, nil)
}
// Options issues an OPTIONS request to the fcgi responder.
func (this *FCGIClient) Options(p map[string]string) (resp *http.Response, err error) {
func (c *FCGIClient) Options(p map[string]string) (resp *http.Response, err error) {
p["REQUEST_METHOD"] = "OPTIONS"
p["CONTENT_LENGTH"] = "0"
return this.Request(p, nil)
return c.Request(p, nil)
}
// Post issues a POST request to the fcgi responder. with request body
// in the format that bodyType specified
func (this *FCGIClient) Post(p map[string]string, bodyType string, body io.Reader, l int) (resp *http.Response, err error) {
func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, body io.Reader, l int64) (resp *http.Response, err error) {
if p == nil {
p = make(map[string]string)
}
p["REQUEST_METHOD"] = strings.ToUpper(method)
if len(p["REQUEST_METHOD"]) == 0 || p["REQUEST_METHOD"] == "GET" {
p["REQUEST_METHOD"] = "POST"
}
p["CONTENT_LENGTH"] = strconv.Itoa(l)
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
if len(bodyType) > 0 {
p["CONTENT_TYPE"] = bodyType
} else {
p["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
}
return this.Request(p, body)
}
// Put issues a PUT request to the fcgi responder.
func (this *FCGIClient) Put(p map[string]string, bodyType string, body io.Reader, l int) (resp *http.Response, err error) {
p["REQUEST_METHOD"] = "PUT"
return this.Post(p, bodyType, body, l)
}
// Patch issues a PATCH request to the fcgi responder.
func (this *FCGIClient) Patch(p map[string]string, bodyType string, body io.Reader, l int) (resp *http.Response, err error) {
p["REQUEST_METHOD"] = "PATCH"
return this.Post(p, bodyType, body, l)
}
// Delete issues a DELETE request to the fcgi responder.
func (this *FCGIClient) Delete(p map[string]string, bodyType string, body io.Reader, l int) (resp *http.Response, err error) {
p["REQUEST_METHOD"] = "DELETE"
return this.Post(p, bodyType, body, l)
return c.Request(p, body)
}
// PostForm issues a POST to the fcgi responder, with form
// as a string key to a list values (url.Values)
func (this *FCGIClient) PostForm(p map[string]string, data url.Values) (resp *http.Response, err error) {
func (c *FCGIClient) PostForm(p map[string]string, data url.Values) (resp *http.Response, err error) {
body := bytes.NewReader([]byte(data.Encode()))
return this.Post(p, "application/x-www-form-urlencoded", body, body.Len())
return c.Post(p, "POST", "application/x-www-form-urlencoded", body, int64(body.Len()))
}
// PostFile issues a POST to the fcgi responder in multipart(RFC 2046) standard,
// with form as a string key to a list values (url.Values),
// and/or with file as a string key to a list file path.
func (this *FCGIClient) PostFile(p map[string]string, data url.Values, file map[string]string) (resp *http.Response, err error) {
func (c *FCGIClient) PostFile(p map[string]string, data url.Values, file map[string]string) (resp *http.Response, err error) {
buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
bodyType := writer.FormDataContentType()
@@ -502,6 +543,9 @@ func (this *FCGIClient) PostFile(p map[string]string, data url.Values, file map[
return nil, e
}
_, err = io.Copy(part, fd)
if err != nil {
return
}
}
err = writer.Close()
@@ -509,7 +553,25 @@ func (this *FCGIClient) PostFile(p map[string]string, data url.Values, file map[
return
}
return this.Post(p, bodyType, buf, buf.Len())
return c.Post(p, "POST", bodyType, buf, int64(buf.Len()))
}
// SetReadTimeout sets the read timeout for future calls that read from the
// fcgi responder. A zero value for t means no timeout will be set.
func (c *FCGIClient) SetReadTimeout(t time.Duration) error {
if conn, ok := c.rwc.(net.Conn); ok && t != 0 {
return conn.SetReadDeadline(time.Now().Add(t))
}
return nil
}
// SetSendTimeout sets the read timeout for future calls that send data to
// the fcgi responder. A zero value for t means no timeout will be set.
func (c *FCGIClient) SetSendTimeout(t time.Duration) error {
if conn, ok := c.rwc.(net.Conn); ok && t != 0 {
return conn.SetWriteDeadline(time.Now().Add(t))
}
return nil
}
// Checks whether chunked is part of the encodings stack
@@ -1,3 +1,17 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// NOTE: These tests were adapted from the original
// repository from which this package was forked.
// The tests are slow (~10s) and in dire need of rewriting.
@@ -30,46 +44,46 @@ import (
// test fcgi protocol includes:
// Get, Post, Post in multipart/form-data, and Post with files
// each key should be the md5 of the value or the file uploaded
// sepicify remote fcgi responer ip:port to test with php
// specify remote fcgi responder ip:port to test with php
// test failed if the remote fcgi(script) failed md5 verification
// and output "FAILED" in response
const (
script_file = "/tank/www/fcgic_test.php"
//ip_port = "remote-php-serv:59000"
ip_port = "127.0.0.1:59000"
scriptFile = "/tank/www/fcgic_test.php"
//ipPort = "remote-php-serv:59000"
ipPort = "127.0.0.1:59000"
)
var (
t_ *testing.T = nil
)
var globalt *testing.T
type FastCGIServer struct{}
func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
req.ParseMultipartForm(100000000)
if err := req.ParseMultipartForm(100000000); err != nil {
log.Printf("[ERROR] failed to parse: %v", err)
}
stat := "PASSED"
fmt.Fprintln(resp, "-")
file_num := 0
fileNum := 0
{
length := 0
for k0, v0 := range req.Form {
h := md5.New()
io.WriteString(h, v0[0])
md5 := fmt.Sprintf("%x", h.Sum(nil))
_, _ = io.WriteString(h, v0[0])
_md5 := fmt.Sprintf("%x", h.Sum(nil))
length += len(k0)
length += len(v0[0])
// echo error when key != md5(val)
if md5 != k0 {
fmt.Fprintln(resp, "server:err ", md5, k0)
// echo error when key != _md5(val)
if _md5 != k0 {
fmt.Fprintln(resp, "server:err ", _md5, k0)
stat = "FAILED"
}
}
if req.MultipartForm != nil {
file_num = len(req.MultipartForm.File)
fileNum = len(req.MultipartForm.File)
for kn, fns := range req.MultipartForm.File {
//fmt.Fprintln(resp, "server:filekey ", kn )
length += len(kn)
@@ -101,11 +115,11 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "server:got data length", length)
}
fmt.Fprintln(resp, "-"+stat+"-POST(", len(req.Form), ")-FILE(", file_num, ")--")
fmt.Fprintln(resp, "-"+stat+"-POST(", len(req.Form), ")-FILE(", fileNum, ")--")
}
func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) {
fcgi, err := Dial("tcp", ip_port)
func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) {
fcgi, err := Dial("tcp", ipPort)
if err != nil {
log.Println("err:", err)
return
@@ -119,16 +133,17 @@ func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map
if len(data) > 0 {
length = len(data)
rd := bytes.NewReader(data)
resp, err = fcgi.Post(fcgi_params, "", rd, rd.Len())
resp, err = fcgi.Post(fcgiParams, "", "", rd, int64(rd.Len()))
} else if len(posts) > 0 {
values := url.Values{}
for k, v := range posts {
values.Set(k, v)
length += len(k) + 2 + len(v)
}
resp, err = fcgi.PostForm(fcgi_params, values)
resp, err = fcgi.PostForm(fcgiParams, values)
} else {
resp, err = fcgi.Get(fcgi_params)
rd := bytes.NewReader(data)
resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len()))
}
default:
@@ -142,7 +157,7 @@ func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map
fi, _ := os.Lstat(v)
length += len(k) + int(fi.Size())
}
resp, err = fcgi.PostFile(fcgi_params, values, files)
resp, err = fcgi.PostFile(fcgiParams, values, files)
}
if err != nil {
@@ -151,14 +166,14 @@ func sendFcgi(reqType int, fcgi_params map[string]string, data []byte, posts map
}
defer resp.Body.Close()
content, err = ioutil.ReadAll(resp.Body)
content, _ = ioutil.ReadAll(resp.Body)
log.Println("c: send data length ≈", length, string(content))
fcgi.Close()
time.Sleep(1 * time.Second)
if bytes.Index(content, []byte("FAILED")) >= 0 {
t_.Error("Server return failed message")
if bytes.Contains(content, []byte("FAILED")) {
globalt.Error("Server return failed message")
}
return
@@ -184,99 +199,103 @@ func generateRandFile(size int) (p string, m string) {
for i := 0; i < size/16; i++ {
buf := make([]byte, 16)
binary.PutVarint(buf, rand.Int63())
fo.Write(buf)
h.Write(buf)
if _, err := fo.Write(buf); err != nil {
log.Printf("[ERROR] failed to write buffer: %v\n", err)
}
if _, err := h.Write(buf); err != nil {
log.Printf("[ERROR] failed to write buffer: %v\n", err)
}
}
m = fmt.Sprintf("%x", h.Sum(nil))
return
}
func Disabled_Test(t *testing.T) {
func DisabledTest(t *testing.T) {
// TODO: test chunked reader
globalt = t
t_ = t
rand.Seed(time.Now().UTC().UnixNano())
// server
go func() {
listener, err := net.Listen("tcp", ip_port)
listener, err := net.Listen("tcp", ipPort)
if err != nil {
// handle error
log.Println("listener creatation failed: ", err)
log.Println("listener creation failed: ", err)
}
srv := new(FastCGIServer)
fcgi.Serve(listener, srv)
if err := fcgi.Serve(listener, srv); err != nil {
log.Print("[ERROR] failed to start server: ", err)
}
}()
time.Sleep(1 * time.Second)
// init
fcgi_params := make(map[string]string)
fcgi_params["REQUEST_METHOD"] = "GET"
fcgi_params["SERVER_PROTOCOL"] = "HTTP/1.1"
fcgiParams := make(map[string]string)
fcgiParams["REQUEST_METHOD"] = "GET"
fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1"
//fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1"
fcgi_params["SCRIPT_FILENAME"] = script_file
fcgiParams["SCRIPT_FILENAME"] = scriptFile
// simple GET
log.Println("test:", "get")
sendFcgi(0, fcgi_params, nil, nil, nil)
sendFcgi(0, fcgiParams, nil, nil, nil)
// simple post data
log.Println("test:", "post")
sendFcgi(0, fcgi_params, []byte("c4ca4238a0b923820dcc509a6f75849b=1&7b8b965ad4bca0e41ab51de7b31363a1=n"), nil, nil)
sendFcgi(0, fcgiParams, []byte("c4ca4238a0b923820dcc509a6f75849b=1&7b8b965ad4bca0e41ab51de7b31363a1=n"), nil, nil)
log.Println("test:", "post data (more than 60KB)")
data := ""
length := 0
for i := 0x00; i < 0xff; i++ {
v0 := strings.Repeat(string(i), 256)
h := md5.New()
io.WriteString(h, v0)
_, _ = io.WriteString(h, v0)
k0 := fmt.Sprintf("%x", h.Sum(nil))
length += len(k0)
length += len(v0)
data += k0 + "=" + url.QueryEscape(v0) + "&"
}
sendFcgi(0, fcgi_params, []byte(data), nil, nil)
sendFcgi(0, fcgiParams, []byte(data), nil, nil)
log.Println("test:", "post form (use url.Values)")
p0 := make(map[string]string, 1)
p0["c4ca4238a0b923820dcc509a6f75849b"] = "1"
p0["7b8b965ad4bca0e41ab51de7b31363a1"] = "n"
sendFcgi(1, fcgi_params, nil, p0, nil)
sendFcgi(1, fcgiParams, nil, p0, nil)
log.Println("test:", "post forms (256 keys, more than 1MB)")
p1 := make(map[string]string, 1)
for i := 0x00; i < 0xff; i++ {
v0 := strings.Repeat(string(i), 4096)
h := md5.New()
io.WriteString(h, v0)
_, _ = io.WriteString(h, v0)
k0 := fmt.Sprintf("%x", h.Sum(nil))
p1[k0] = v0
}
sendFcgi(1, fcgi_params, nil, p1, nil)
sendFcgi(1, fcgiParams, nil, p1, nil)
log.Println("test:", "post file (1 file, 500KB)) ")
f0 := make(map[string]string, 1)
path0, m0 := generateRandFile(500000)
f0[m0] = path0
sendFcgi(1, fcgi_params, nil, p1, f0)
sendFcgi(1, fcgiParams, nil, p1, f0)
log.Println("test:", "post multiple files (2 files, 5M each) and forms (256 keys, more than 1MB data")
path1, m1 := generateRandFile(5000000)
f0[m1] = path1
sendFcgi(1, fcgi_params, nil, p1, f0)
sendFcgi(1, fcgiParams, nil, p1, f0)
log.Println("test:", "post only files (2 files, 5M each)")
sendFcgi(1, fcgi_params, nil, nil, f0)
sendFcgi(1, fcgiParams, nil, nil, f0)
log.Println("test:", "post only 1 file")
delete(f0, "m0")
sendFcgi(1, fcgi_params, nil, nil, f0)
sendFcgi(1, fcgiParams, nil, nil, f0)
os.Remove(path0)
os.Remove(path1)
if err := os.Remove(path0); err != nil {
log.Println("[ERROR] failed to remove path: ", err)
}
if err := os.Remove(path1); err != nil {
log.Println("[ERROR] failed to remove path: ", err)
}
}
+220
View File
@@ -0,0 +1,220 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fastcgi
import (
"errors"
"fmt"
"net"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
var defaultTimeout = 60 * time.Second
func init() {
caddy.RegisterPlugin("fastcgi", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new FastCGI middleware instance.
func setup(c *caddy.Controller) error {
cfg := httpserver.GetConfig(c)
rules, err := fastcgiParse(c)
if err != nil {
return err
}
cfg.AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return Handler{
Next: next,
Rules: rules,
Root: cfg.Root,
FileSys: http.Dir(cfg.Root),
SoftwareName: caddy.AppName,
SoftwareVersion: caddy.AppVersion,
ServerName: cfg.Addr.Host,
ServerPort: cfg.Addr.Port,
}
})
return nil
}
func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
var rules []Rule
cfg := httpserver.GetConfig(c)
absRoot, err := filepath.Abs(cfg.Root)
if err != nil {
return nil, err
}
for c.Next() {
args := c.RemainingArgs()
if len(args) < 2 || len(args) > 3 {
return rules, c.ArgErr()
}
rule := Rule{
Root: absRoot,
Path: args[0],
ConnectTimeout: defaultTimeout,
ReadTimeout: defaultTimeout,
SendTimeout: defaultTimeout,
}
upstreams := []string{args[1]}
srvUpstream := false
if strings.HasPrefix(upstreams[0], "srv://") {
srvUpstream = true
}
if len(args) == 3 {
if err := fastcgiPreset(args[2], &rule); err != nil {
return rules, err
}
}
var err error
for c.NextBlock() {
switch c.Val() {
case "root":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.Root = 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 "upstream":
if srvUpstream {
return rules, c.Err("additional upstreams are not supported with SRV upstream")
}
args := c.RemainingArgs()
if len(args) != 1 {
return rules, c.ArgErr()
}
upstreams = append(upstreams, args[0])
case "env":
envArgs := c.RemainingArgs()
if len(envArgs) < 2 {
return rules, c.ArgErr()
}
rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]})
case "except":
ignoredPaths := c.RemainingArgs()
if len(ignoredPaths) == 0 {
return rules, c.ArgErr()
}
rule.IgnoredSubPaths = ignoredPaths
case "connect_timeout":
if !c.NextArg() {
return rules, c.ArgErr()
}
rule.ConnectTimeout, err = time.ParseDuration(c.Val())
if err != nil {
return rules, err
}
case "read_timeout":
if !c.NextArg() {
return rules, c.ArgErr()
}
readTimeout, err := time.ParseDuration(c.Val())
if err != nil {
return rules, err
}
rule.ReadTimeout = readTimeout
case "send_timeout":
if !c.NextArg() {
return rules, c.ArgErr()
}
sendTimeout, err := time.ParseDuration(c.Val())
if err != nil {
return rules, err
}
rule.SendTimeout = sendTimeout
}
}
if srvUpstream {
balancer, err := parseSRV(upstreams[0])
if err != nil {
return rules, c.Err("malformed service locator string: " + err.Error())
}
rule.balancer = balancer
} else {
rule.balancer = &roundRobin{addresses: upstreams, index: -1}
}
rules = append(rules, rule)
}
return rules, nil
}
func parseSRV(locator string) (*srv, error) {
if locator[6:] == "" {
return nil, fmt.Errorf("%s does not include the host", locator)
}
return &srv{
service: locator[6:],
resolver: &net.Resolver{},
}, nil
}
// fastcgiPreset configures rule according to name. It returns an error if
// name is not a recognized preset name.
func fastcgiPreset(name string, rule *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
}
+255
View File
@@ -0,0 +1,255 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fastcgi
import (
"context"
"fmt"
"net"
"testing"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `fastcgi / 127.0.0.1:9000`)
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 middleware, got 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(Handler)
if !ok {
t.Fatalf("Expected handler to be type , got: %#v", handler)
}
if myHandler.Rules[0].Path != "/" {
t.Errorf("Expected / as the Path")
}
addr, err := myHandler.Rules[0].Address()
if err != nil {
t.Errorf("Unexpected error in trying to retrieve address: %s", err.Error())
}
if addr != "127.0.0.1:9000" {
t.Errorf("Expected 127.0.0.1:9000 as the Address")
}
if myHandler.Rules[0].ConnectTimeout != 60*time.Second {
t.Errorf("Expected default value of 60 seconds")
}
if myHandler.Rules[0].ReadTimeout != 60*time.Second {
t.Errorf("Expected default value of 60 seconds")
}
if myHandler.Rules[0].SendTimeout != 60*time.Second {
t.Errorf("Expected default value of 60 seconds")
}
}
func TestFastcgiParse(t *testing.T) {
tests := []struct {
inputFastcgiConfig string
shouldErr bool
expectedFastcgiConfig []Rule
}{
{`fastcgi /blog 127.0.0.1:9000 php`,
false, []Rule{{
Path: "/blog",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9000"}},
Ext: ".php",
SplitPath: ".php",
IndexFiles: []string{"index.php"},
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / 127.0.0.1:9001 {
split .html
}`,
false, []Rule{{
Path: "/",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
SplitPath: ".html",
IndexFiles: []string{},
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / 127.0.0.1:9001 {
split .html
except /admin /user
}`,
false, []Rule{{
Path: "/",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
SplitPath: ".html",
IndexFiles: []string{},
IgnoredSubPaths: []string{"/admin", "/user"},
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / 127.0.0.1:9001 {
send_timeout 30s
}`,
false, []Rule{{
Path: "/",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
IndexFiles: []string{},
SendTimeout: 30 * time.Second,
}}},
}
for i, test := range tests {
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
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)
}
actualAddr, err := actualFastcgiConfig.Address()
if err != nil {
t.Errorf("Test %d unexpected error in trying to retrieve %dth actual address: %s", i, j, err.Error())
}
expectedAddr, err := test.expectedFastcgiConfig[j].Address()
if err != nil {
t.Errorf("Test %d unexpected error in trying to retrieve %dth expected address: %s", i, j, err.Error())
}
if actualAddr != expectedAddr {
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
i, j, expectedAddr, actualAddr)
}
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)
}
if fmt.Sprint(actualFastcgiConfig.IgnoredSubPaths) != fmt.Sprint(test.expectedFastcgiConfig[j].IgnoredSubPaths) {
t.Errorf("Test %d expected %dth FastCGI IgnoredSubPaths to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].IgnoredSubPaths, actualFastcgiConfig.IgnoredSubPaths)
}
if actualFastcgiConfig.SendTimeout != test.expectedFastcgiConfig[j].SendTimeout {
t.Errorf("Test %d expected %dth FastCGI SendTimeout to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].SendTimeout, actualFastcgiConfig.SendTimeout)
}
}
}
}
func TestFastCGIResolveSRV(t *testing.T) {
tests := []struct {
inputFastcgiConfig string
locator string
target string
port uint16
shouldErr bool
}{
{
`fastcgi / srv://fpm.tcp.service.consul {
upstream yolo
}`,
"fpm.tcp.service.consul",
"127.0.0.1",
9000,
true,
},
{
`fastcgi / srv://fpm.tcp.service.consul`,
"fpm.tcp.service.consul",
"127.0.0.1",
9000,
false,
},
}
for i, test := range tests {
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
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)
}
for _, actualFastcgiConfig := range actualFastcgiConfigs {
resolver, ok := (actualFastcgiConfig.balancer).(*srv)
if !ok {
t.Errorf("Test %d upstream balancer is not srv", i)
}
resolver.resolver = buildTestResolver(test.target, test.port)
addr, err := actualFastcgiConfig.Address()
if err != nil {
t.Errorf("Test %d failed to retrieve upstream address. %s", i, err.Error())
}
expectedAddr := fmt.Sprintf("%s:%d", test.target, test.port)
if addr != expectedAddr {
t.Errorf("Test %d expected upstream address to be %s, got %s", i, expectedAddr, addr)
}
}
}
}
func buildTestResolver(target string, port uint16) srvResolver {
return &testSRVResolver{target, port}
}
type testSRVResolver struct {
target string
port uint16
}
func (r *testSRVResolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*net.SRV, error) {
return "", []*net.SRV{
{Target: r.target,
Port: r.port,
Priority: 1,
Weight: 1}}, nil
}
+163
View File
@@ -0,0 +1,163 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package gzip provides a middleware layer that performs
// gzip compression on the response.
package gzip
import (
"compress/gzip"
"io"
"net/http"
"strings"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("gzip", caddy.Plugin{
ServerType: "http",
Action: setup,
})
initWriterPool()
}
// Gzip is a middleware type which gzips HTTP responses. It is
// imperative that any handler which writes to a gzipped response
// specifies the Content-Type, otherwise some clients will assume
// application/x-gzip and try to download a file.
type Gzip struct {
Next httpserver.Handler
Configs []Config
}
// Config holds the configuration for Gzip middleware
type Config struct {
RequestFilters []RequestFilter
ResponseFilters []ResponseFilter
Level int // Compression level
}
// ServeHTTP serves a gzipped response if the client supports it.
func (g Gzip) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
return g.Next.ServeHTTP(w, r)
}
outer:
for _, c := range g.Configs {
// Check request filters to determine if gzipping is permitted for this request
for _, filter := range c.RequestFilters {
if !filter.ShouldCompress(r) {
continue outer
}
}
// In order to avoid unused memory allocation, gzip.putWriter only be called when gzip compression happened.
// see https://github.com/mholt/caddy/issues/2395
gz := &gzipResponseWriter{
ResponseWriterWrapper: &httpserver.ResponseWriterWrapper{ResponseWriter: w},
newWriter: func() io.Writer {
// gzipWriter modifies underlying writer at init,
// use a discard writer instead to leave ResponseWriter in
// original form.
return getWriter(c.Level)
},
}
defer func() {
if gzWriter, ok := gz.internalWriter.(*gzip.Writer); ok {
putWriter(c.Level, gzWriter)
}
}()
var rw http.ResponseWriter
// if no response filter is used
if len(c.ResponseFilters) == 0 {
// replace discard writer with ResponseWriter
if gzWriter, ok := gz.Writer().(*gzip.Writer); ok {
gzWriter.Reset(w)
}
rw = gz
} else {
// wrap gzip writer with ResponseFilterWriter
rw = NewResponseFilterWriter(c.ResponseFilters, gz)
}
// Any response in forward middleware will now be compressed
status, err := g.Next.ServeHTTP(rw, r)
// If there was an error that remained unhandled, we need
// to send something back before gzipWriter gets closed at
// the return of this method!
if status >= 400 {
httpserver.DefaultErrorFunc(w, r, status)
return 0, err
}
return status, err
}
// no matching filter
return g.Next.ServeHTTP(w, r)
}
// gzipResponseWriter wraps the underlying Write method
// with a gzip.Writer to compress the output.
type gzipResponseWriter struct {
internalWriter io.Writer
*httpserver.ResponseWriterWrapper
statusCodeWritten bool
newWriter func() io.Writer
}
// WriteHeader wraps the underlying WriteHeader method to prevent
// problems with conflicting headers from proxied backends. For
// example, a backend system that calculates Content-Length would
// be wrong because it doesn't know it's being gzipped.
func (w *gzipResponseWriter) WriteHeader(code int) {
w.Header().Del("Content-Length")
w.Header().Set("Content-Encoding", "gzip")
w.Header().Add("Vary", "Accept-Encoding")
originalEtag := w.Header().Get("ETag")
if originalEtag != "" && !strings.HasPrefix(originalEtag, "W/") {
w.Header().Set("ETag", "W/"+originalEtag)
}
w.ResponseWriterWrapper.WriteHeader(code)
w.statusCodeWritten = true
}
// Write wraps the underlying Write method to do compression.
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", http.DetectContentType(b))
}
if !w.statusCodeWritten {
w.WriteHeader(http.StatusOK)
}
n, err := w.Writer().Write(b)
return n, err
}
//Writer use a lazy way to initialize Writer
func (w *gzipResponseWriter) Writer() io.Writer {
if w.internalWriter == nil {
w.internalWriter = w.newWriter()
}
return w.internalWriter
}
// Interface guards
var _ httpserver.HTTPInterfaces = (*gzipResponseWriter)(nil)
+196
View File
@@ -0,0 +1,196 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gzip
import (
"compress/gzip"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestGzipHandler(t *testing.T) {
pathFilter := PathFilter{make(Set)}
badPaths := []string{"/bad", "/nogzip", "/nongzip"}
for _, p := range badPaths {
pathFilter.IgnoredPaths.Add(p)
}
extFilter := ExtFilter{make(Set)}
for _, e := range []string{".txt", ".html", ".css", ".md"} {
extFilter.Exts.Add(e)
}
gz := Gzip{Configs: []Config{
{RequestFilters: []RequestFilter{pathFilter, extFilter}},
}}
w := httptest.NewRecorder()
gz.Next = nextFunc(true)
var exts = []string{
".html", ".css", ".md",
}
for _, e := range exts {
url := "/file" + e
r, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Error(err)
}
r.Header.Set("Accept-Encoding", "gzip")
w.Header().Set("ETag", `"2n9cd"`)
_, err = gz.ServeHTTP(w, r)
if err != nil {
t.Error(err)
}
// The second pass, test if the ETag is already weak
w.Header().Set("ETag", `W/"2n9cd"`)
_, err = gz.ServeHTTP(w, r)
if err != nil {
t.Error(err)
}
}
w = httptest.NewRecorder()
gz.Next = nextFunc(false)
for _, p := range badPaths {
for _, e := range exts {
url := p + "/file" + e
r, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Error(err)
}
r.Header.Set("Accept-Encoding", "gzip")
_, err = gz.ServeHTTP(w, r)
if err != nil {
t.Error(err)
}
}
}
w = httptest.NewRecorder()
gz.Next = nextFunc(false)
exts = []string{
".htm1", ".abc", ".mdx",
}
for _, e := range exts {
url := "/file" + e
r, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Error(err)
}
r.Header.Set("Accept-Encoding", "gzip")
_, err = gz.ServeHTTP(w, r)
if err != nil {
t.Error(err)
}
}
// test all levels
w = httptest.NewRecorder()
gz.Next = nextFunc(true)
for i := 0; i <= gzip.BestCompression; i++ {
gz.Configs[0].Level = i
r, err := http.NewRequest("GET", "/file.txt", nil)
if err != nil {
t.Error(err)
}
r.Header.Set("Accept-Encoding", "gzip")
_, err = gz.ServeHTTP(w, r)
if err != nil {
t.Error(err)
}
}
}
func nextFunc(shouldGzip bool) httpserver.Handler {
return httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
// write a relatively large text file
b, err := ioutil.ReadFile("testdata/test.txt")
if err != nil {
return 500, err
}
if _, err := w.Write(b); err != nil {
return 500, err
}
if shouldGzip {
if w.Header().Get("Content-Encoding") != "gzip" {
return 0, fmt.Errorf("Content-Encoding must be gzip, found %v", w.Header().Get("Content-Encoding"))
}
if w.Header().Get("Vary") != "Accept-Encoding" {
return 0, fmt.Errorf("Vary must be Accept-Encoding, found %v", w.Header().Get("Vary"))
}
etag := w.Header().Get("ETag")
if etag != "" && etag != `W/"2n9cd"` {
return 0, fmt.Errorf("ETag must be converted to weak Etag, found %v", w.Header().Get("ETag"))
}
if _, ok := w.(*gzipResponseWriter); !ok {
return 0, fmt.Errorf("ResponseWriter should be gzipResponseWriter, found %T", w)
}
if strings.Contains(w.Header().Get("Content-Type"), "application/x-gzip") {
return 0, fmt.Errorf("Content-Type should not be gzip")
}
return 0, nil
}
if r.Header.Get("Accept-Encoding") == "" {
return 0, fmt.Errorf("Accept-Encoding header expected")
}
if w.Header().Get("Content-Encoding") == "gzip" {
return 0, fmt.Errorf("Content-Encoding must not be gzip, found gzip")
}
if _, ok := w.(*gzipResponseWriter); ok {
return 0, fmt.Errorf("ResponseWriter should not be gzipResponseWriter")
}
return 0, nil
})
}
func BenchmarkGzip(b *testing.B) {
pathFilter := PathFilter{make(Set)}
badPaths := []string{"/bad", "/nogzip", "/nongzip"}
for _, p := range badPaths {
pathFilter.IgnoredPaths.Add(p)
}
extFilter := ExtFilter{make(Set)}
for _, e := range []string{".txt", ".html", ".css", ".md"} {
extFilter.Exts.Add(e)
}
gz := Gzip{Configs: []Config{
{
RequestFilters: []RequestFilter{pathFilter, extFilter},
},
}}
w := httptest.NewRecorder()
gz.Next = nextFunc(true)
url := "/file.txt"
r, err := http.NewRequest("GET", url, nil)
if err != nil {
b.Fatal(err)
}
r.Header.Set("Accept-Encoding", "gzip")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err = gz.ServeHTTP(w, r)
if err != nil {
b.Fatal(err)
}
}
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gzip
import (
"net/http"
"path"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// RequestFilter determines if a request should be gzipped.
type RequestFilter interface {
// ShouldCompress tells if gzip compression
// should be done on the request.
ShouldCompress(*http.Request) bool
}
// defaultExtensions is the list of default extensions for which to enable gzipping.
var defaultExtensions = []string{"", ".txt", ".htm", ".html", ".css", ".php", ".js", ".json",
".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp", ".m3u", ".m3u8"}
// DefaultExtFilter creates an ExtFilter with default extensions.
func DefaultExtFilter() ExtFilter {
m := ExtFilter{Exts: make(Set)}
for _, extension := range defaultExtensions {
m.Exts.Add(extension)
}
return m
}
// ExtFilter is RequestFilter for file name extensions.
type ExtFilter struct {
// Exts is the file name extensions to accept
Exts Set
}
// ExtWildCard is the wildcard for extensions.
const ExtWildCard = "*"
// ShouldCompress checks if the request file extension matches any
// of the registered extensions. It returns true if the extension is
// found and false otherwise.
func (e ExtFilter) ShouldCompress(r *http.Request) bool {
ext := path.Ext(r.URL.Path)
return e.Exts.Contains(ExtWildCard) || e.Exts.Contains(ext)
}
// PathFilter is RequestFilter for request path.
type PathFilter struct {
// IgnoredPaths is the paths to ignore
IgnoredPaths Set
}
// ShouldCompress checks if the request path matches any of the
// registered paths to ignore. It returns false if an ignored path
// is found and true otherwise.
func (p PathFilter) ShouldCompress(r *http.Request) bool {
return !p.IgnoredPaths.ContainsFunc(func(value string) bool {
return httpserver.Path(r.URL.Path).Matches(value)
})
}
// Set stores distinct strings.
type Set map[string]struct{}
// Add adds an element to the set.
func (s Set) Add(value string) {
s[value] = struct{}{}
}
// Remove removes an element from the set.
func (s Set) Remove(value string) {
delete(s, value)
}
// Contains check if the set contains value.
func (s Set) Contains(value string) bool {
_, ok := s[value]
return ok
}
// ContainsFunc is similar to Contains. It iterates all the
// elements in the set and passes each to f. It returns true
// on the first call to f that returns true and false otherwise.
func (s Set) ContainsFunc(f func(string) bool) bool {
for k := range s {
if f(k) {
return true
}
}
return false
}
+127
View File
@@ -0,0 +1,127 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gzip
import (
"net/http"
"testing"
)
func TestSet(t *testing.T) {
set := make(Set)
set.Add("a")
if len(set) != 1 {
t.Errorf("Expected 1 found %v", len(set))
}
set.Add("a")
if len(set) != 1 {
t.Errorf("Expected 1 found %v", len(set))
}
set.Add("b")
if len(set) != 2 {
t.Errorf("Expected 2 found %v", len(set))
}
if !set.Contains("a") {
t.Errorf("Set should contain a")
}
if !set.Contains("b") {
t.Errorf("Set should contain a")
}
set.Add("c")
if len(set) != 3 {
t.Errorf("Expected 3 found %v", len(set))
}
if !set.Contains("c") {
t.Errorf("Set should contain c")
}
set.Remove("a")
if len(set) != 2 {
t.Errorf("Expected 2 found %v", len(set))
}
if set.Contains("a") {
t.Errorf("Set should not contain a")
}
if !set.ContainsFunc(func(v string) bool {
return v == "c"
}) {
t.Errorf("ContainsFunc should return true")
}
}
func TestExtFilter(t *testing.T) {
var filter RequestFilter = ExtFilter{make(Set)}
for _, e := range []string{".txt", ".html", ".css", ".md"} {
filter.(ExtFilter).Exts.Add(e)
}
r := urlRequest("file.txt")
if !filter.ShouldCompress(r) {
t.Errorf("Should be valid filter")
}
var exts = []string{
".html", ".css", ".md",
}
for i, e := range exts {
r := urlRequest("file" + e)
if !filter.ShouldCompress(r) {
t.Errorf("Test %v: Should be valid filter", i)
}
}
exts = []string{
".htm1", ".abc", ".mdx",
}
for i, e := range exts {
r := urlRequest("file" + e)
if filter.ShouldCompress(r) {
t.Errorf("Test %v: Should not be valid filter", i)
}
}
filter.(ExtFilter).Exts.Add(ExtWildCard)
for i, e := range exts {
r := urlRequest("file" + e)
if !filter.ShouldCompress(r) {
t.Errorf("Test %v: Should be valid filter. Wildcard used.", i)
}
}
}
func TestPathFilter(t *testing.T) {
paths := []string{
"/a", "/b", "/c", "/de",
}
var filter RequestFilter = PathFilter{make(Set)}
for _, p := range paths {
filter.(PathFilter).IgnoredPaths.Add(p)
}
for i, p := range paths {
r := urlRequest(p)
if filter.ShouldCompress(r) {
t.Errorf("Test %v: Should not be valid filter", i)
}
}
paths = []string{
"/f", "/g", "/h", "/ed",
}
for i, p := range paths {
r := urlRequest(p)
if !filter.ShouldCompress(r) {
t.Errorf("Test %v: Should be valid filter", i)
}
}
}
func urlRequest(url string) *http.Request {
r, _ := http.NewRequest("GET", url, nil)
return r
}
+107
View File
@@ -0,0 +1,107 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gzip
import (
"compress/gzip"
"net/http"
"strconv"
)
// ResponseFilter determines if the response should be gzipped.
type ResponseFilter interface {
ShouldCompress(http.ResponseWriter) bool
}
// LengthFilter is ResponseFilter for minimum content length.
type LengthFilter int64
// ShouldCompress returns if content length is greater than or
// equals to minimum length.
func (l LengthFilter) ShouldCompress(w http.ResponseWriter) bool {
contentLength := w.Header().Get("Content-Length")
length, err := strconv.ParseInt(contentLength, 10, 64)
if err != nil || length == 0 {
return false
}
return l != 0 && int64(l) <= length
}
// SkipCompressedFilter is ResponseFilter that will discard already compressed responses
type SkipCompressedFilter struct{}
// ShouldCompress returns true if served file is not already compressed
// encodings via https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
func (n SkipCompressedFilter) ShouldCompress(w http.ResponseWriter) bool {
switch w.Header().Get("Content-Encoding") {
case "gzip", "compress", "deflate", "br":
return false
default:
return true
}
}
// ResponseFilterWriter validates ResponseFilters. It writes
// gzip compressed data if ResponseFilters are satisfied or
// uncompressed data otherwise.
type ResponseFilterWriter struct {
filters []ResponseFilter
shouldCompress bool
statusCodeWritten bool
*gzipResponseWriter
}
// NewResponseFilterWriter creates and initializes a new ResponseFilterWriter.
func NewResponseFilterWriter(filters []ResponseFilter, gz *gzipResponseWriter) *ResponseFilterWriter {
return &ResponseFilterWriter{filters: filters, gzipResponseWriter: gz}
}
// WriteHeader wraps underlying WriteHeader method and
// compresses if filters are satisfied.
func (r *ResponseFilterWriter) WriteHeader(code int) {
// Determine if compression should be used or not.
r.shouldCompress = true
for _, filter := range r.filters {
if !filter.ShouldCompress(r) {
r.shouldCompress = false
break
}
}
if r.shouldCompress {
// replace discard writer with ResponseWriter
if gzWriter, ok := r.gzipResponseWriter.Writer().(*gzip.Writer); ok {
gzWriter.Reset(r.ResponseWriter)
}
// use gzip WriteHeader to include and delete
// necessary headers
r.gzipResponseWriter.WriteHeader(code)
} else {
r.ResponseWriter.WriteHeader(code)
}
r.statusCodeWritten = true
}
// Write wraps underlying Write method and compresses if filters
// are satisfied
func (r *ResponseFilterWriter) Write(b []byte) (int, error) {
if !r.statusCodeWritten {
r.WriteHeader(http.StatusOK)
}
if r.shouldCompress {
return r.gzipResponseWriter.Write(b)
}
return r.ResponseWriter.Write(b)
}
+135
View File
@@ -0,0 +1,135 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gzip
import (
"compress/gzip"
"fmt"
"log"
"net/http"
"net/http/httptest"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestLengthFilter(t *testing.T) {
var filters = []ResponseFilter{
LengthFilter(100),
LengthFilter(1000),
LengthFilter(0),
}
var tests = []struct {
length int64
shouldCompress [3]bool
}{
{20, [3]bool{false, false, false}},
{50, [3]bool{false, false, false}},
{100, [3]bool{true, false, false}},
{500, [3]bool{true, false, false}},
{1000, [3]bool{true, true, false}},
{1500, [3]bool{true, true, false}},
}
for i, ts := range tests {
for j, filter := range filters {
r := httptest.NewRecorder()
r.Header().Set("Content-Length", fmt.Sprint(ts.length))
wWriter := NewResponseFilterWriter([]ResponseFilter{filter}, &gzipResponseWriter{gzip.NewWriter(r), &httpserver.ResponseWriterWrapper{ResponseWriter: r}, false, nil})
if filter.ShouldCompress(wWriter) != ts.shouldCompress[j] {
t.Errorf("Test %v: Expected %v found %v", i, ts.shouldCompress[j], filter.ShouldCompress(r))
}
}
}
}
func TestResponseFilterWriter(t *testing.T) {
tests := []struct {
body string
shouldCompress bool
}{
{"Hello\t\t\t\n", false},
{"Hello the \t\t\t world is\n\n\n great", true},
{"Hello \t\t\nfrom gzip", true},
{"Hello gzip\n", false},
}
filters := []ResponseFilter{
LengthFilter(15),
}
server := Gzip{Configs: []Config{
{ResponseFilters: filters},
}}
for i, ts := range tests {
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
w.Header().Set("Content-Length", fmt.Sprint(len(ts.body)))
if _, err := w.Write([]byte(ts.body)); err != nil {
log.Println("[ERROR] failed to write response: ", err)
}
return 200, nil
})
r := urlRequest("/")
r.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
if _, err := server.ServeHTTP(w, r); err != nil {
log.Println("[ERROR] unable to serve a gzipped response: ", err)
}
resp := w.Body.String()
if !ts.shouldCompress {
if resp != ts.body {
t.Errorf("Test %v: No compression expected, found %v", i, resp)
}
} else {
if resp == ts.body {
t.Errorf("Test %v: Compression expected, found %v", i, resp)
}
}
}
}
func TestResponseGzippedOutput(t *testing.T) {
server := Gzip{Configs: []Config{
{ResponseFilters: []ResponseFilter{SkipCompressedFilter{}}},
}}
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
w.Header().Set("Content-Encoding", "gzip")
if _, err := w.Write([]byte("gzipped")); err != nil {
log.Println("[ERROR] failed to write response: ", err)
}
return 200, nil
})
r := urlRequest("/")
r.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
if _, err := server.ServeHTTP(w, r); err != nil {
log.Println("[ERROR] unable to serve a gzipped response: ", err)
}
resp := w.Body.String()
if resp != "gzipped" {
t.Errorf("Expected output not to be gzipped")
}
}
+183
View File
@@ -0,0 +1,183 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gzip
import (
"compress/gzip"
"fmt"
"io/ioutil"
"strconv"
"strings"
"sync"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// setup configures a new gzip middleware instance.
func setup(c *caddy.Controller) error {
configs, err := gzipParse(c)
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return Gzip{Next: next, Configs: configs}
})
return nil
}
func gzipParse(c *caddy.Controller) ([]Config, error) {
var configs []Config
for c.Next() {
config := Config{}
// Request Filters
pathFilter := PathFilter{IgnoredPaths: make(Set)}
extFilter := ExtFilter{Exts: make(Set)}
// Response Filters
lengthFilter := LengthFilter(0)
// No extra args expected
if len(c.RemainingArgs()) > 0 {
return configs, c.ArgErr()
}
for c.NextBlock() {
switch c.Val() {
case "ext":
exts := c.RemainingArgs()
if len(exts) == 0 {
return configs, c.ArgErr()
}
for _, e := range exts {
if !strings.HasPrefix(e, ".") && e != ExtWildCard && e != "" {
return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e)
}
extFilter.Exts.Add(e)
}
case "not":
paths := c.RemainingArgs()
if len(paths) == 0 {
return configs, c.ArgErr()
}
for _, p := range paths {
if p == "/" {
return configs, fmt.Errorf(`gzip: cannot exclude path "/" - remove directive entirely instead`)
}
if !strings.HasPrefix(p, "/") {
return configs, fmt.Errorf(`gzip: invalid path "%v" (must start with /)`, p)
}
pathFilter.IgnoredPaths.Add(p)
}
case "level":
if !c.NextArg() {
return configs, c.ArgErr()
}
level, _ := strconv.Atoi(c.Val())
config.Level = level
case "min_length":
if !c.NextArg() {
return configs, c.ArgErr()
}
length, err := strconv.ParseInt(c.Val(), 10, 64)
if err != nil {
return configs, err
} else if length == 0 {
return configs, fmt.Errorf(`gzip: min_length must be greater than 0`)
}
lengthFilter = LengthFilter(length)
default:
return configs, c.ArgErr()
}
}
// Request Filters
config.RequestFilters = []RequestFilter{}
// If ignored paths are specified, put in front to filter with path first
if len(pathFilter.IgnoredPaths) > 0 {
config.RequestFilters = []RequestFilter{pathFilter}
}
// Then, if extensions are specified, use those to filter.
// Otherwise, use default extensions filter.
if len(extFilter.Exts) > 0 {
config.RequestFilters = append(config.RequestFilters, extFilter)
} else {
config.RequestFilters = append(config.RequestFilters, DefaultExtFilter())
}
config.ResponseFilters = append(config.ResponseFilters, SkipCompressedFilter{})
// Response Filters
// If min_length is specified, use it.
if int64(lengthFilter) != 0 {
config.ResponseFilters = append(config.ResponseFilters, lengthFilter)
}
configs = append(configs, config)
}
return configs, nil
}
// pool gzip.Writer according to compress level
// so we can reuse allocations over time
var (
writerPool = map[int]*sync.Pool{}
defaultWriterPoolIndex int
)
func initWriterPool() {
var i int
newWriterPool := func(level int) *sync.Pool {
return &sync.Pool{
New: func() interface{} {
w, _ := gzip.NewWriterLevel(ioutil.Discard, level)
return w
},
}
}
for i = gzip.BestSpeed; i <= gzip.BestCompression; i++ {
writerPool[i] = newWriterPool(i)
}
// add default writer pool
defaultWriterPoolIndex = i
writerPool[defaultWriterPoolIndex] = newWriterPool(gzip.DefaultCompression)
}
func getWriter(level int) *gzip.Writer {
index := defaultWriterPoolIndex
if level >= gzip.BestSpeed && level <= gzip.BestCompression {
index = level
}
w := writerPool[index].Get().(*gzip.Writer)
w.Reset(ioutil.Discard)
return w
}
func putWriter(level int, w *gzip.Writer) {
index := defaultWriterPoolIndex
if level >= gzip.BestSpeed && level <= gzip.BestCompression {
index = level
}
w.Close()
writerPool[index].Put(w)
}
+143
View File
@@ -0,0 +1,143 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gzip
import (
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `gzip`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if mids == nil {
t.Fatal("Expected middleware, was nil instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(Gzip)
if !ok {
t.Fatalf("Expected handler to be type Gzip, got: %#v", handler)
}
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
tests := []struct {
input string
shouldErr bool
}{
{`gzip {`, true},
{`gzip {}`, true},
{`gzip a b`, true},
{`gzip a {`, true},
{`gzip { not f } `, true},
{`gzip { not } `, true},
{`gzip { not /file
ext .html
level 1
} `, false},
{`gzip { level 9 } `, false},
{`gzip { ext } `, true},
{`gzip { ext /f
} `, true},
{`gzip { not /file
ext .html
level 1
}
gzip`, false},
{`gzip {
ext ""
}`, false},
{`gzip { not /file
ext .html
level 1
}
gzip { not /file1
ext .htm
level 3
}
`, false},
{`gzip { not /file
ext .html
level 1
}
gzip { not /file1
ext .htm
level 3
}
`, false},
{`gzip { not /file
ext *
level 1
}
`, false},
{`gzip { not /file
ext *
level 1
min_length ab
}
`, true},
{`gzip { not /file
ext *
level 1
min_length 1000
}
`, false},
}
for i, test := range tests {
_, err := gzipParse(caddy.NewTestController("http", test.input))
if test.shouldErr && err == nil {
t.Errorf("Test %v: Expected error but found nil", i)
} else if !test.shouldErr && err != nil {
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
}
}
}
func TestShouldAddResponseFilters(t *testing.T) {
configs, err := gzipParse(caddy.NewTestController("http", `gzip { min_length 654 }`))
if err != nil {
t.Errorf("Test expected no error but found: %v", err)
}
filters := 0
for _, config := range configs {
for _, filter := range config.ResponseFilters {
switch filter.(type) {
case SkipCompressedFilter:
filters++
case LengthFilter:
filters++
if filter != LengthFilter(654) {
t.Errorf("Expected LengthFilter to have length 654, got: %v", filter)
}
}
}
if filters != 2 {
t.Errorf("Expected 2 response filters to be registered, got: %v", filters)
}
}
}
+308
View File
@@ -0,0 +1,308 @@
Sigh view am high neat half to what. Sent late held than set why wife our. If an blessing building steepest. Agreement distrusts mrs six affection satisfied. Day blushes visitor end company old prevent chapter. Consider declared out expenses her concerns. No at indulgence conviction particular unsatiable boisterous discretion. Direct enough off others say eldest may exeter she. Possible all ignorant supplied get settling marriage recurred.
Boy desirous families prepared gay reserved add ecstatic say. Replied joy age visitor nothing cottage. Mrs door paid led loud sure easy read. Hastily at perhaps as neither or ye fertile tedious visitor. Use fine bed none call busy dull when. Quiet ought match my right by table means. Principles up do in me favourable affronting. Twenty mother denied effect we to do on.
Compliment interested discretion estimating on stimulated apartments oh. Dear so sing when in find read of call. As distrusts behaviour abilities defective is. Never at water me might. On formed merits hunted unable merely by mr whence or. Possession the unpleasing simplicity her uncommonly.
Bringing so sociable felicity supplied mr. September suspicion far him two acuteness perfectly. Covered as an examine so regular of. Ye astonished friendship remarkably no. Window admire matter praise you bed whence. Delivered ye sportsmen zealously arranging frankness estimable as. Nay any article enabled musical shyness yet sixteen yet blushes. Entire its the did figure wonder off.
Inhabit hearing perhaps on ye do no. It maids decay as there he. Smallest on suitable disposed do although blessing he juvenile in. Society or if excited forbade. Here name off yet she long sold easy whom. Differed oh cheerful procured pleasure securing suitable in. Hold rich on an he oh fine. Chapter ability shyness article welcome be do on service.
An sincerity so extremity he additions. Her yet there truth merit. Mrs all projecting favourable now unpleasing. Son law garden chatty temper. Oh children provided to mr elegance marriage strongly. Off can admiration prosperous now devonshire diminution law.
Performed suspicion in certainty so frankness by attention pretended. Newspaper or in tolerably education enjoyment. Extremity excellent certainty discourse sincerity no he so resembled. Joy house worse arise total boy but. Elderly up chicken do at feeling is. Like seen drew no make fond at on rent. Behaviour extremely her explained situation yet september gentleman are who. Is thought or pointed hearing he.
Not far stuff she think the jokes. Going as by do known noise he wrote round leave. Warmly put branch people narrow see. Winding its waiting yet parlors married own feeling. Marry fruit do spite jokes an times. Whether at it unknown warrant herself winding if. Him same none name sake had post love. An busy feel form hand am up help. Parties it brother amongst an fortune of. Twenty behind wicket why age now itself ten.
On no twenty spring of in esteem spirit likely estate. Continue new you declared differed learning bringing honoured. At mean mind so upon they rent am walk. Shortly am waiting inhabit smiling he chiefly of in. Lain tore time gone him his dear sure. Fat decisively estimating affronting assistance not. Resolve pursuit regular so calling me. West he plan girl been my then up no.
Expenses as material breeding insisted building to in. Continual so distrusts pronounce by unwilling listening. Thing do taste on we manor. Him had wound use found hoped. Of distrusts immediate enjoyment curiosity do. Marianne numerous saw thoughts the humoured.
Tolerably earnestly middleton extremely distrusts she boy now not. Add and offered prepare how cordial two promise. Greatly who affixed suppose but enquire compact prepare all put. Added forth chief trees but rooms think may. Wicket do manner others seemed enable rather in. Excellent own discovery unfeeling sweetness questions the gentleman. Chapter shyness matters mr parlors if mention thought.
Or kind rest bred with am shed then. In raptures building an bringing be. Elderly is detract tedious assured private so to visited. Do travelling companions contrasted it. Mistress strongly remember up to. Ham him compass you proceed calling detract. Better of always missed we person mr. September smallness northward situation few her certainty something.
Moments its musical age explain. But extremity sex now education concluded earnestly her continual. Oh furniture acuteness suspected continual ye something frankness. Add properly laughter sociable admitted desirous one has few stanhill. Opinion regular in perhaps another enjoyed no engaged he at. It conveying he continual ye suspected as necessary. Separate met packages shy for kindness.
Conveying or northward offending admitting perfectly my. Colonel gravity get thought fat smiling add but. Wonder twenty hunted and put income set desire expect. Am cottage calling my is mistake cousins talking up. Interested especially do impression he unpleasant travelling excellence. All few our knew time done draw ask.
In it except to so temper mutual tastes mother. Interested cultivated its continuing now yet are. Out interested acceptance our partiality affronting unpleasant why add. Esteem garden men yet shy course. Consulted up my tolerably sometimes perpetual oh. Expression acceptance imprudence particular had eat unsatiable.
Son agreed others exeter period myself few yet nature. Mention mr manners opinion if garrets enabled. To an occasional dissimilar impossible sentiments. Do fortune account written prepare invited no passage. Garrets use ten you the weather ferrars venture friends. Solid visit seems again you nor all.
You vexed shy mirth now noise. Talked him people valley add use her depend letter. Allowance too applauded now way something recommend. Mrs age men and trees jokes fancy. Gay pretended engrossed eagerness continued ten. Admitting day him contained unfeeling attention mrs out.
Advantage old had otherwise sincerity dependent additions. It in adapted natural hastily is justice. Six draw you him full not mean evil. Prepare garrets it expense windows shewing do an. She projection advantages resolution son indulgence. Part sure on no long life am at ever. In songs above he as drawn to. Gay was outlived peculiar rendered led six.
Same an quit most an. Admitting an mr disposing sportsmen. Tried on cause no spoil arise plate. Longer ladies valley get esteem use led six. Middletons resolution advantages expression themselves partiality so me at. West none hope if sing oh sent tell is.
Meant balls it if up doubt small purse. Required his you put the outlived answered position. An pleasure exertion if believed provided to. All led out world these music while asked. Paid mind even sons does he door no. Attended overcame repeated it is perceive marianne in. In am think on style child of. Servants moreover in sensible he it ye possible.
Neat own nor she said see walk. And charm add green you these. Sang busy in this drew ye fine. At greater prepare musical so attacks as on distant. Improving age our her cordially intention. His devonshire sufficient precaution say preference middletons insipidity. Since might water hence the her worse. Concluded it offending dejection do earnestly as me direction. Nature played thirty all him.
Guest it he tears aware as. Make my no cold of need. He been past in by my hard. Warmly thrown oh he common future. Otherwise concealed favourite frankness on be at dashwoods defective at. Sympathize interested simplicity at do projecting increasing terminated. As edward settle limits at in.
Lose john poor same it case do year we. Full how way even the sigh. Extremely nor furniture fat questions now provision incommode preserved. Our side fail find like now. Discovered travelling for insensible partiality unpleasing impossible she. Sudden up my excuse to suffer ladies though or. Bachelor possible marianne directly confined relation as on he.
Is post each that just leaf no. He connection interested so we an sympathize advantages. To said is it shed want do. Occasional middletons everything so to. Have spot part for his quit may. Enable it is square my an regard. Often merit stuff first oh up hills as he. Servants contempt as although addition dashwood is procured. Interest in yourself an do of numerous feelings cheerful confined.
rnestly middleton extremely distrusts she boy now not. Add and offered prepare how cordial two promise. Greatly who affixed suppose but enquire compact prepare all put. Added forth chief trees but rooms think may. Wicket do manner others seemed enable rather in. Excellent own discovery unfeeling sweetness questions the gentleman. Chapter shyness matters mr parlors if mention thought.
Sudden looked elinor off gay estate nor silent. Son read such next see the rest two. Was use extent old entire sussex. Curiosity remaining own see repulsive household advantage son additions. Supposing exquisite daughters eagerness why repulsive for. Praise turned it lovers be warmly by. Little do it eldest former be if.
Certain but she but shyness why cottage. Gay the put instrument sir entreaties affronting. Pretended exquisite see cordially the you. Weeks quiet do vexed or whose. Motionless if no to affronting imprudence no precaution. My indulged as disposal strongly attended. Parlors men express had private village man. Discovery moonlight recommend all one not. Indulged to answered prospect it bachelor is he bringing shutters. Pronounce forfeited mr direction oh he dashwoods ye unwilling.
Of resolve to gravity thought my prepare chamber so. Unsatiable entreaties collecting may sympathize nay interested instrument. If continue building numerous of at relation in margaret. Lasted engage roused mother an am at. Other early while if by do to. Missed living excuse as be. Cause heard fat above first shall for. My smiling to he removal weather on anxious.
Tiled say decay spoil now walls meant house. My mr interest thoughts screened of outweigh removing. Evening society musical besides inhabit ye my. Lose hill well up will he over on. Increasing sufficient everything men him admiration unpleasing sex. Around really his use uneasy longer him man. His our pulled nature elinor talked now for excuse result. Admitted add peculiar get joy doubtful.
Had repulsive dashwoods suspicion sincerity but advantage now him. Remark easily garret nor nay. Civil those mrs enjoy shy fat merry. You greatest jointure saw horrible. He private he on be imagine suppose. Fertile beloved evident through no service elderly is. Blind there if every no so at. Own neglected you preferred way sincerity delivered his attempted. To of message cottage windows do besides against uncivil.
So if on advanced addition absolute received replying throwing he. Delighted consisted newspaper of unfeeling as neglected so. Tell size come hard mrs and four fond are. Of in commanded earnestly resources it. At quitting in strictly up wandered of relation answered felicity. Side need at in what dear ever upon if. Same down want joy neat ask pain help she. Alone three stuff use law walls fat asked. Near do that he help.
Out too the been like hard off. Improve enquire welcome own beloved matters her. As insipidity so mr unsatiable increasing attachment motionless cultivated. Addition mr husbands unpacked occasion he oh. Is unsatiable if projecting boisterous insensible. It recommend be resolving pretended middleton.
She literature discovered increasing how diminution understood. Though and highly the enough county for man. Of it up he still court alone widow seems. Suspected he remainder rapturous my sweetness. All vanity regard sudden nor simple can. World mrs and vexed china since after often.
Put all speaking her delicate recurred possible. Set indulgence inquietude discretion insensible bed why announcing. Middleton fat two satisfied additions. So continued he or commanded household smallness delivered. Door poor on do walk in half. Roof his head the what.
Seen you eyes son show. Far two unaffected one alteration apartments celebrated but middletons interested. Described deficient applauded consisted my me do. Passed edward two talent effect seemed engage six. On ye great do child sorry lived. Proceed cottage far letters ashamed get clothes day. Stairs regret at if matter to. On as needed almost at basket remain. By improved sensible servants children striking in surprise.
Living valley had silent eat merits esteem bed. In last an or went wise as left. Visited civilly am demesne so colonel he calling. So unreserved do interested increasing sentiments. Vanity day giving points within six not law. Few impression difficulty his use has comparison decisively.
To shewing another demands to. Marianne property cheerful informed at striking at. Clothes parlors however by cottage on. In views it or meant drift to. Be concern parlors settled or do shyness address. Remainder northward performed out for moonlight. Yet late add name was rent park from rich. He always do do former he highly.
Meant balls it if up doubt small purse. Required his you put the outlived answered position. An pleasure exertion if believed provided to. All led out world these music while asked. Paid mind even sons does he door no. Attended overcame repeated it is perceive marianne in. In am think on style child of. Servants moreover in sensible he it ye possible.
On it differed repeated wandered required in. Then girl neat why yet knew rose spot. Moreover property we he kindness greatest be oh striking laughter. In me he at collecting affronting principles apartments. Has visitor law attacks pretend you calling own excited painted. Contented attending smallness it oh ye unwilling. Turned favour man two but lovers. Suffer should if waited common person little oh. Improved civility graceful sex few smallest screened settling. Likely active her warmly has.
He an thing rapid these after going drawn or. Timed she his law the spoil round defer. In surprise concerns informed betrayed he learning is ye. Ignorant formerly so ye blessing. He as spoke avoid given downs money on we. Of properly carriage shutters ye as wandered up repeated moreover. Inquietude attachment if ye an solicitude to. Remaining so continued concealed as knowledge happiness. Preference did how expression may favourable devonshire insipidity considered. An length design regret an hardly barton mr figure.
Was certainty remaining engrossed applauded sir how discovery. Settled opinion how enjoyed greater joy adapted too shy. Now properly surprise expenses interest nor replying she she. Bore tall nay many many time yet less. Doubtful for answered one fat indulged margaret sir shutters together. Ladies so in wholly around whence in at. Warmth he up giving oppose if. Impossible is dissimilar entreaties oh on terminated. Earnest studied article country ten respect showing had. But required offering him elegance son improved informed.
Received overcame oh sensible so at an. Formed do change merely to county it. Am separate contempt domestic to to oh. On relation my so addition branched. Put hearing cottage she norland letters equally prepare too. Replied exposed savings he no viewing as up. Soon body add him hill. No father living really people estate if. Mistake do produce beloved demesne if am pursuit.
Finished her are its honoured drawings nor. Pretty see mutual thrown all not edward ten. Particular an boisterous up he reasonably frequently. Several any had enjoyed shewing studied two. Up intention remainder sportsmen behaviour ye happiness. Few again any alone style added abode ask. Nay projecting unpleasing boisterous eat discovered solicitude. Own six moments produce elderly pasture far arrival. Hold our year they ten upon. Gentleman contained so intention sweetness in on resolving.
Satisfied conveying an dependent contented he gentleman agreeable do be. Warrant private blushes removed an in equally totally if. Delivered dejection necessary objection do mr prevailed. Mr feeling do chiefly cordial in do. Water timed folly right aware if oh truth. Imprudence attachment him his for sympathize. Large above be to means. Dashwood do provided stronger is. But discretion frequently sir the she instrument unaffected admiration everything.
ndness to he horrible reserved ye. Effect twenty indeed beyond for not had county. The use him without greatly can private. Increasing it unpleasant no of contrasted no continuing. Nothing colonel my no removed in weather. It dissimilar in up devonshire inhabiting.
Is at purse tried jokes china ready decay an. Small its shy way had woody downs power. To denoting admitted speaking learning my exercise so in. Procured shutters mr it feelings. To or three offer house begin taken am at. As dissuade cheerful overcame so of friendly he indulged unpacked. Alteration connection to so as collecting me. Difficult in delivered extensive at direction allowance. Alteration put use diminution can considered sentiments interested discretion. An seeing feebly stairs am branch income me unable.
Agreed joy vanity regret met may ladies oppose who. Mile fail as left as hard eyes. Meet made call in mean four year it to. Prospect so branched wondered sensible of up. For gay consisted resolving pronounce sportsman saw discovery not. Northward or household as conveying we earnestly believing. No in up contrasted discretion inhabiting excellence. Entreaties we collecting unpleasant at everything conviction.
He moonlight difficult engrossed an it sportsmen. Interested has all devonshire difficulty gay assistance joy. Unaffected at ye of compliment alteration to. Place voice no arise along to. Parlors waiting so against me no. Wishing calling are warrant settled was luckily. Express besides it present if at an opinion visitor.
Scarcely on striking packages by so property in delicate. Up or well must less rent read walk so be. Easy sold at do hour sing spot. Any meant has cease too the decay. Since party burst am it match. By or blushes between besides offices noisier as. Sending do brought winding compass in. Paid day till shed only fact age its end.
Am if number no up period regard sudden better. Decisively surrounded all admiration and not you. Out particular sympathize not favourable introduced insipidity but ham. Rather number can and set praise. Distrusts an it contented perceived attending oh. Thoroughly estimating introduced stimulated why but motionless.
Is post each that just leaf no. He connection interested so we an sympathize advantages. To said is it shed want do. Occasional middletons everything so to. Have spot part for his quit may. Enable it is square my an regard. Often merit stuff first oh up hills as he. Servants contempt as although addition dashwood is procured. Interest in yourself an do of numerous feelings cheerful confined.
Two exquisite objection delighted deficient yet its contained. Cordial because are account evident its subject but eat. Can properly followed learning prepared you doubtful yet him. Over many our good lady feet ask that. Expenses own moderate day fat trifling stronger sir domestic feelings. Itself at be answer always exeter up do. Though or my plenty uneasy do. Friendship so considered remarkably be to sentiments. Offered mention greater fifteen one promise because nor. Why denoting speaking fat indulged saw dwelling raillery.
Sense child do state to defer mr of forty. Become latter but nor abroad wisdom waited. Was delivered gentleman acuteness but daughters. In as of whole as match asked. Pleasure exertion put add entrance distance drawings. In equally matters showing greatly it as. Want name any wise are able park when. Saw vicinity judgment remember finished men throwing.
Cottage out enabled was entered greatly prevent message. No procured unlocked an likewise. Dear but what she been over gay felt body. Six principles advantages and use entreaties decisively. Eat met has dwelling unpacked see whatever followed. Court in of leave again as am. Greater sixteen to forming colonel no on be. So an advice hardly barton. He be turned sudden engage manner spirit.
greatest at in learning steepest. Breakfast extremity suffering one who all otherwise suspected. He at no nothing forbade up moments. Wholly uneasy at missed be of pretty whence. John way sir high than law who week. Surrounded prosperous introduced it if is up dispatched. Improved so strictly produced answered elegance is.
Examine she brother prudent add day ham. Far stairs now coming bed oppose hunted become his. You zealously departure had procuring suspicion. Books whose front would purse if be do decay. Quitting you way formerly disposed perceive ladyship are. Common turned boy direct and yet.
Is we miles ready he might going. Own books built put civil fully blind fanny. Projection appearance at of admiration no. As he totally cousins warrant besides ashamed do. Therefore by applauded acuteness supported affection it. Except had sex limits county enough the figure former add. Do sang my he next mr soon. It merely waited do unable.
Real sold my in call. Invitation on an advantages collecting. But event old above shy bed noisy. Had sister see wooded favour income has. Stuff rapid since do as hence. Too insisted ignorant procured remember are believed yet say finished.
Cultivated who resolution connection motionless did occasional. Journey promise if it colonel. Can all mirth abode nor hills added. Them men does for body pure. Far end not horses remain sister. Mr parish is to he answer roused piqued afford sussex. It abode words began enjoy years no do no. Tried spoil as heart visit blush or. Boy possible blessing sensible set but margaret interest. Off tears are day blind smile alone had.
Difficulty on insensible reasonable in. From as went he they. Preference themselves me as thoroughly partiality considered on in estimating. Middletons acceptance discovered projecting so is so or. In or attachment inquietude remarkably comparison at an. Is surrounded prosperous stimulated am me discretion expression. But truth being state can she china widow. Occasional preference fat remarkably now projecting uncommonly dissimilar. Sentiments projection particular companions interested do at my delightful. Listening newspaper in advantage frankness to concluded unwilling.
Consulted he eagerness unfeeling deficient existence of. Calling nothing end fertile for venture way boy. Esteem spirit temper too say adieus who direct esteem. It esteems luckily mr or picture placing drawing no. Apartments frequently or motionless on reasonable projecting expression. Way mrs end gave tall walk fact bed.
Promotion an ourselves up otherwise my. High what each snug rich far yet easy. In companions inhabiting mr principles at insensible do. Heard their sex hoped enjoy vexed child for. Prosperous so occasional assistance it discovered especially no. Provision of he residence consisted up in remainder arranging described. Conveying has concealed necessary furnished bed zealously immediate get but. Terminated as middletons or by instrument. Bred do four so your felt with. No shameless principle dependent household do.
Not far stuff she think the jokes. Going as by do known noise he wrote round leave. Warmly put branch people narrow see. Winding its waiting yet parlors married own feeling. Marry fruit do spite jokes an times. Whether at it unknown warrant herself winding if. Him same none name sake had post love. An busy feel form hand am up help. Parties it brother amongst an fortune of. Twenty behind wicket why age now itself ten.
Fulfilled direction use continual set him propriety continued. Saw met applauded favourite deficient engrossed concealed and her. Concluded boy perpetual old supposing. Farther related bed and passage comfort civilly. Dashwoods see frankness objection abilities the. As hastened oh produced prospect formerly up am. Placing forming nay looking old married few has. Margaret disposed add screened rendered six say his striking confined.
At as in understood an remarkably solicitude. Mean them very seen she she. Use totally written the observe pressed justice. Instantly cordially far intention recommend estimable yet her his. Ladies stairs enough esteem add fat all enable. Needed its design number winter see. Oh be me sure wise sons no. Piqued ye of am spirit regret. Stimulated discretion impossible admiration in particular conviction up.
Bringing unlocked me an striking ye perceive. Mr by wound hours oh happy. Me in resolution pianoforte continuing we. Most my no spot felt by no. He he in forfeited furniture sweetness he arranging. Me tedious so to behaved written account ferrars moments. Too objection for elsewhere her preferred allowance her. Marianne shutters mr steepest to me. Up mr ignorant produced distance although is sociable blessing. Ham whom call all lain like.
Old education him departure any arranging one prevailed. Their end whole might began her. Behaved the comfort another fifteen eat. Partiality had his themselves ask pianoforte increasing discovered. So mr delay at since place whole above miles. He to observe conduct at detract because. Way ham unwilling not breakfast furniture explained perpetual. Or mr surrounded conviction so astonished literature. Songs to an blush woman be sorry young. We certain as removal attempt.
Is at purse tried jokes china ready decay an. Small its shy way had woody downs power. To denoting admitted speaking learning my exercise so in. Procured shutters mr it feelings. To or three offer house begin taken am at. As dissuade cheerful overcame so of friendly he indulged unpacked. Alteration connection to so as collecting me. Difficult in delivered extensive at direction allowance. Alteration put use diminution can considered sentiments interested discretion. An seeing feebly stairs am branch income me unable.
Spoke as as other again ye. Hard on to roof he drew. So sell side ye in mr evil. Longer waited mr of nature seemed. Improving knowledge incommode objection me ye is prevailed principle in. Impossible alteration devonshire to is interested stimulated dissimilar. To matter esteem polite do if.
Up am intention on dependent questions oh elsewhere september. No betrayed pleasure possible jointure we in throwing. And can event rapid any shall woman green. Hope they dear who its bred. Smiling nothing affixed he carried it clothes calling he no. Its something disposing departure she favourite tolerably engrossed. Truth short folly court why she their balls. Excellence put unaffected reasonable mrs introduced conviction she. Nay particular delightful but unpleasant for uncommonly who.
But why smiling man her imagine married. Chiefly can man her out believe manners cottage colonel unknown. Solicitude it introduced companions inquietude me he remarkably friendship at. My almost or horses period. Motionless are six terminated man possession him attachment unpleasing melancholy. Sir smile arose one share. No abroad in easily relied an whence lovers temper by. Looked wisdom common he an be giving length mr.
Gave read use way make spot how nor. In daughter goodness an likewise oh consider at procured wandered. Songs words wrong by me hills heard timed. Happy eat may doors songs. Be ignorant so of suitable dissuade weddings together. Least whole timed we is. An smallness deficient discourse do newspaper be an eagerness continued. Mr my ready guest ye after short at.
By impossible of in difficulty discovered celebrated ye. Justice joy manners boy met resolve produce. Bed head loud next plan rent had easy add him. As earnestly shameless elsewhere defective estimable fulfilled of. Esteem my advice it an excuse enable. Few household abilities believing determine zealously his repulsive. To open draw dear be by side like.
Be at miss or each good play home they. It leave taste mr in it fancy. She son lose does fond bred gave lady get. Sir her company conduct expense bed any. Sister depend change off piqued one. Contented continued any happiness instantly objection yet her allowance. Use correct day new brought tedious. By come this been in. Kept easy or sons my it done.
he who arrival end how fertile enabled. Brother she add yet see minuter natural smiling article painted. Themselves at dispatched interested insensible am be prosperous reasonably it. In either so spring wished. Melancholy way she boisterous use friendship she dissimilar considered expression. Sex quick arose mrs lived. Mr things do plenty others an vanity myself waited to. Always parish tastes at as mr father dining at.
Comfort reached gay perhaps chamber his six detract besides add. Moonlight newspaper up he it enjoyment agreeable depending. Timed voice share led his widen noisy young. On weddings believed laughing although material do exercise of. Up attempt offered ye civilly so sitting to. She new course get living within elinor joy. She her rapturous suffering concealed.
Her extensive perceived may any sincerity extremity. Indeed add rather may pretty see. Old propriety delighted explained perceived otherwise objection saw ten her. Doubt merit sir the right these alone keeps. By sometimes intention smallness he northward. Consisted we otherwise arranging commanded discovery it explained. Does cold even song like two yet been. Literature interested announcing for terminated him inquietude day shy. Himself he fertile chicken perhaps waiting if highest no it. Continued promotion has consulted fat improving not way.
Windows talking painted pasture yet its express parties use. Sure last upon he same as knew next. Of believed or diverted no rejoiced. End friendship sufficient assistance can prosperous met. As game he show it park do. Was has unknown few certain ten promise. No finished my an likewise cheerful packages we. For assurance concluded son something depending discourse see led collected. Packages oh no denoting my advanced humoured. Pressed be so thought natural.
Greatly hearted has who believe. Drift allow green son walls years for blush. Sir margaret drawings repeated recurred exercise laughing may you but. Do repeated whatever to welcomed absolute no. Fat surprise although outlived and informed shy dissuade property. Musical by me through he drawing savings an. No we stand avoid decay heard mr. Common so wicket appear to sudden worthy on. Shade of offer ye whole stood hoped.
In post mean shot ye. There out her child sir his lived. Design at uneasy me season of branch on praise esteem. Abilities discourse believing consisted remaining to no. Mistaken no me denoting dashwood as screened. Whence or esteem easily he on. Dissuade husbands at of no if disposal.
Talking chamber as shewing an it minutes. Trees fully of blind do. Exquisite favourite at do extensive listening. Improve up musical welcome he. Gay attended vicinity prepared now diverted. Esteems it ye sending reached as. Longer lively her design settle tastes advice mrs off who.
Alteration literature to or an sympathize mr imprudence. Of is ferrars subject as enjoyed or tedious cottage. Procuring as in resembled by in agreeable. Next long no gave mr eyes. Admiration advantages no he celebrated so pianoforte unreserved. Not its herself forming charmed amiable. Him why feebly expect future now.
Debating me breeding be answered an he. Spoil event was words her off cause any. Tears woman which no is world miles woody. Wished be do mutual except in effect answer. Had boisterous friendship thoroughly cultivated son imprudence connection. Windows because concern sex its. Law allow saved views hills day ten. Examine waiting his evening day passage proceed.
Led ask possible mistress relation elegance eat likewise debating. By message or am nothing amongst chiefly address. The its enable direct men depend highly. Ham windows sixteen who inquiry fortune demands. Is be upon sang fond must shew. Really boy law county she unable her sister. Feet you off its like like six. Among sex are leave law built now. In built table in an rapid blush. Merits behind on afraid or warmly.
Ignorant branched humanity led now marianne too strongly entrance. Rose to shew bore no ye of paid rent form. Old design are dinner better nearer silent excuse. She which are maids boy sense her shade. Considered reasonable we affronting on expression in. So cordial anxious mr delight. Shot his has must wish from sell nay. Remark fat set why are sudden depend change entire wanted. Performed remainder attending led fat residence far.
Him rendered may attended concerns jennings reserved now. Sympathize did now preference unpleasing mrs few. Mrs for hour game room want are fond dare. For detract charmed add talking age. Shy resolution instrument unreserved man few. She did open find pain some out. If we landlord stanhill mr whatever pleasure supplied concerns so. Exquisite by it admitting cordially september newspaper an. Acceptance middletons am it favourable. It it oh happen lovers afraid.
Had strictly mrs handsome mistaken cheerful. We it so if resolution invitation remarkably unpleasant conviction. As into ye then form. To easy five less if rose were. Now set offended own out required entirely. Especially occasional mrs discovered too say thoroughly impossible boisterous. My head when real no he high rich at with. After so power of young as. Bore year does has get long fat cold saw neat. Put boy carried chiefly shy general.
So delightful up dissimilar by unreserved it connection frequently. Do an high room so in paid. Up on cousin ye dinner should in. Sex stood tried walls manor truth shy and three his. Their to years so child truth. Honoured peculiar families sensible up likewise by on in.
Concerns greatest margaret him absolute entrance nay. Door neat week do find past he. Be no surprise he honoured indulged. Unpacked endeavor six steepest had husbands her. Painted no or affixed it so civilly. Exposed neither pressed so cottage as proceed at offices. Nay they gone sir game four. Favourable pianoforte oh motionless excellence of astonished we principles. Warrant present garrets limited cordial in inquiry to. Supported me sweetness behaviour shameless excellent so arranging.
Consulted he eagerness unfeeling deficient existence of. Calling nothing end fertile for venture way boy. Esteem spirit temper too say adieus who direct esteem. It esteems luckily mr or picture placing drawing no. Apartments frequently or motionless on reasonable projecting expression. Way mrs end gave tall walk fact bed.
Received the likewise law graceful his. Nor might set along charm now equal green. Pleased yet equally correct colonel not one. Say anxious carried compact conduct sex general nay certain. Mrs for recommend exquisite household eagerness preserved now. My improved honoured he am ecstatic quitting greatest formerly.
On then sake home is am leaf. Of suspicion do departure at extremely he believing. Do know said mind do rent they oh hope of. General enquire picture letters garrets on offices of no on. Say one hearing between excited evening all inhabit thought you. Style begin mr heard by in music tried do. To unreserved projection no introduced invitation.
At as in understood an remarkably solicitude. Mean them very seen she she. Use totally written the observe pressed justice. Instantly cordially far intention recommend estimable yet her his. Ladies stairs enough esteem add fat all enable. Needed its design number winter see. Oh be me sure wise sons no. Piqued ye of am spirit regret. Stimulated discretion impossible admiration in particular conviction up.
Drawings me opinions returned absolute in. Otherwise therefore sex did are unfeeling something. Certain be ye amiable by exposed so. To celebrated estimating excellence do. Coming either suffer living her gay theirs. Furnished do otherwise daughters contented conveying attempted no. Was yet general visitor present hundred too brother fat arrival. Friend are day own either lively new.
Situation admitting promotion at or to perceived be. Mr acuteness we as estimable enjoyment up. An held late as felt know. Learn do allow solid to grave. Middleton suspicion age her attention. Chiefly several bed its wishing. Is so moments on chamber pressed to. Doubtful yet way properly answered humanity its desirous. Minuter believe service arrived civilly add all. Acuteness allowance an at eagerness favourite in extensive exquisite ye.
Improved own provided blessing may peculiar domestic. Sight house has sex never. No visited raising gravity outward subject my cottage mr be. Hold do at tore in park feet near my case. Invitation at understood occasional sentiments insipidity inhabiting in. Off melancholy alteration principles old. Is do speedily kindness properly oh. Respect article painted cottage he is offices parlors.
One advanced diverted domestic sex repeated bringing you old. Possible procured her trifling laughter thoughts property she met way. Companions shy had solicitude favourable own. Which could saw guest man now heard but. Lasted my coming uneasy marked so should. Gravity letters it amongst herself dearest an windows by. Wooded ladies she basket season age her uneasy saw. Discourse unwilling am no described dejection incommode no listening of. Before nature his parish boy.
Am terminated it excellence invitation projection as. She graceful shy believed distance use nay. Lively is people so basket ladies window expect. Supply as so period it enough income he genius. Themselves acceptance bed sympathize get dissimilar way admiration son. Design for are edward regret met lovers. This are calm case roof and.
Had strictly mrs handsome mistaken cheerful. We it so if resolution invitation remarkably unpleasant conviction. As into ye then form. To easy five less if rose were. Now set offended own out required entirely. Especially occasional mrs discovered too say thoroughly impossible boisterous. My head when real no he high rich at with. After so power of young as. Bore year does has get long fat cold saw neat. Put boy carried chiefly shy general.
Remain valley who mrs uneasy remove wooded him you. Her questions favourite him concealed. We to wife face took he. The taste begin early old why since dried can first. Prepared as or humoured formerly. Evil mrs true get post. Express village evening prudent my as ye hundred forming. Thoughts she why not directly reserved packages you. Winter an silent favour of am tended mutual.
Old education him departure any arranging one prevailed. Their end whole might began her. Behaved the comfort another fifteen eat. Partiality had his themselves ask pianoforte increasing discovered. So mr delay at since place whole above miles. He to observe conduct at detract because. Way ham unwilling not breakfast furniture explained perpetual. Or mr surrounded conviction so astonished literature. Songs to an blush woman be sorry young. We certain as removal attempt.
Dependent certainty off discovery him his tolerably offending. Ham for attention remainder sometimes additions recommend fat our. Direction has strangers now believing. Respect enjoyed gay far exposed parlors towards. Enjoyment use tolerably dependent listening men. No peculiar in handsome together unlocked do by. Article concern joy anxious did picture sir her. Although desirous not recurred disposed off shy you numerous securing.
Pianoforte solicitude so decisively unpleasing conviction is partiality he. Or particular so diminution entreaties oh do. Real he me fond show gave shot plan. Mirth blush linen small hoped way its along. Resolution frequently apartments off all discretion devonshire. Saw sir fat spirit seeing valley. He looked or valley lively. If learn woody spoil of taken he cause.
Preserved defective offending he daughters on or. Rejoiced prospect yet material servants out answered men admitted. Sportsmen certainty prevailed suspected am as. Add stairs admire all answer the nearer yet length. Advantages prosperous remarkably my inhabiting so reasonably be if. Too any appearance announcing impossible one. Out mrs means heart ham tears shall power every.
So delightful up dissimilar by unreserved it connection frequently. Do an high room so in paid. Up on cousin ye dinner should in. Sex stood tried walls manor truth shy and three his. Their to years so child truth. Honoured peculiar families sensible up likewise by on in.
At ourselves direction believing do he departure. Celebrated her had sentiments understood are projection set. Possession ye no mr unaffected remarkably at. Wrote house in never fruit up. Pasture imagine my garrets an he. However distant she request behaved see nothing. Talking settled at pleased an of me brother weather.
New had happen unable uneasy. Drawings can followed improved out sociable not. Earnestly so do instantly pretended. See general few civilly amiable pleased account carried. Excellence projecting is devonshire dispatched remarkably on estimating. Side in so life past. Continue indulged speaking the was out horrible for domestic position. Seeing rather her you not esteem men settle genius excuse. Deal say over you age from. Comparison new ham melancholy son themselves.
Improved own provided blessing may peculiar domestic. Sight house has sex never. No visited raising gravity outward subject my cottage mr be. Hold do at tore in park feet near my case. Invitation at understood occasional sentiments insipidity inhabiting in. Off melancholy alteration principles old. Is do speedily kindness properly oh. Respect article painted cottage he is offices parlors.
Must you with him from him her were more. In eldest be it result should remark vanity square. Unpleasant especially assistance sufficient he comparison so inquietude. Branch one shy edward stairs turned has law wonder horses. Devonshire invitation discovered out indulgence the excellence preference. Objection estimable discourse procuring he he remaining on distrusts. Simplicity affronting inquietude for now sympathize age. She meant new their sex could defer child. An lose at quit to life do dull.
Style never met and those among great. At no or september sportsmen he perfectly happiness attending. Depending listening delivered off new she procuring satisfied sex existence. Person plenty answer to exeter it if. Law use assistance especially resolution cultivated did out sentiments unsatiable. Way necessary had intention happiness but september delighted his curiosity. Furniture furnished or on strangers neglected remainder engrossed.
Is we miles ready he might going. Own books built put civil fully blind fanny. Projection appearance at of admiration no. As he totally cousins warrant besides ashamed do. Therefore by applauded acuteness supported affection it. Except had sex limits county enough the figure former add. Do sang my he next mr soon. It merely waited do unable.
By impossible of in difficulty discovered celebrated ye. Justice joy manners boy met resolve produce. Bed head loud next plan rent had easy add him. As earnestly shameless elsewhere defective estimable fulfilled of. Esteem my advice it an excuse enable. Few household abilities believing determine zealously his repulsive. To open draw dear be by side like.
In post mean shot ye. There out her child sir his lived. Design at uneasy me season of branch on praise esteem. Abilities discourse believing consisted remaining to no. Mistaken no me denoting dashwood as screened. Whence or esteem easily he on. Dissuade husbands at of no if disposal.
Passage its ten led hearted removal cordial. Preference any astonished unreserved mrs. Prosperous understood middletons in conviction an uncommonly do. Supposing so be resolving breakfast am or perfectly. Is drew am hill from mr. Valley by oh twenty direct me so. Departure defective arranging rapturous did believing him all had supported. Family months lasted simple set nature vulgar him. Picture for attempt joy excited ten carried manners talking how. Suspicion neglected he resolving agreement perceived at an.
Kept in sent gave feel will oh it we. Has pleasure procured men laughing shutters nay. Old insipidity motionless continuing law shy partiality. Depending acuteness dependent eat use dejection. Unpleasing astonished discovered not nor shy. Morning hearted now met yet beloved evening. Has and upon his last here must.
Dissuade ecstatic and properly saw entirely sir why laughter endeavor. In on my jointure horrible margaret suitable he followed speedily. Indeed vanity excuse or mr lovers of on. By offer scale an stuff. Blush be sorry no sight. Sang lose of hour then he left find.
Mr oh winding it enjoyed by between. The servants securing material goodness her. Saw principles themselves ten are possession. So endeavor to continue cheerful doubtful we to. Turned advice the set vanity why mutual. Reasonably if conviction on be unsatiable discretion apartments delightful. Are melancholy appearance stimulated occasional entreaties end. Shy ham had esteem happen active county. Winding morning am shyness evident to. Garrets because elderly new manners however one village she.
She wholly fat who window extent either formal. Removing welcomed civility or hastened is. Justice elderly but perhaps expense six her are another passage. Full her ten open fond walk not down. For request general express unknown are. He in just mr door body held john down he. So journey greatly or garrets. Draw door kept do so come on open mean. Estimating stimulated how reasonably precaution diminution she simplicity sir but. Questions am sincerity zealously concluded consisted or no gentleman it.
Was certainty remaining engrossed applauded sir how discovery. Settled opinion how enjoyed greater joy adapted too shy. Now properly surprise expenses interest nor replying she she. Bore tall nay many many time yet less. Doubtful for answered one fat indulged margaret sir shutters together. Ladies so in wholly around whence in at. Warmth he up giving oppose if. Impossible is dissimilar entreaties oh on terminated. Earnest studied article country ten respect showing had. But required offering him elegance son improved informed.
Raising say express had chiefly detract demands she. Quiet led own cause three him. Front no party young abode state up. Saved he do fruit woody of to. Met defective are allowance two perceived listening consulted contained. It chicken oh colonel pressed excited suppose to shortly. He improve started no we manners however effects. Prospect humoured mistress to by proposal marianne attended. Simplicity the far admiration preference everything. Up help home head spot an he room in.
Led ask possible mistress relation elegance eat likewise debating. By message or am nothing amongst chiefly address. The its enable direct men depend highly. Ham windows sixteen who inquiry fortune demands. Is be upon sang fond must shew. Really boy law county she unable her sister. Feet you off its like like six. Among sex are leave law built now. In built table in an rapid blush. Merits behind on afraid or warmly.
Exquisite cordially mr happiness of neglected distrusts. Boisterous impossible unaffected he me everything. Is fine loud deal an rent open give. Find upon and sent spot song son eyes. Do endeavor he differed carriage is learning my graceful. Feel plan know is he like on pure. See burst found sir met think hopes are marry among. Delightful remarkably new assistance saw literature mrs favourable.
Lose away off why half led have near bed. At engage simple father of period others except. My giving do summer of though narrow marked at. Spring formal no county ye waited. My whether cheered at regular it of promise blushes perhaps. Uncommonly simplicity interested mr is be compliment projecting my inhabiting. Gentleman he september in oh excellent.
Breakfast agreeable incommode departure it an. By ignorant at on wondered relation. Enough at tastes really so cousin am of. Extensive therefore supported by extremity of contented. Is pursuit compact demesne invited elderly be. View him she roof tell her case has sigh. Moreover is possible he admitted sociable concerns. By in cold no less been sent hard hill.
No in he real went find mr. Wandered or strictly raillery stanhill as. Jennings appetite disposed me an at subjects an. To no indulgence diminution so discovered mr apartments. Are off under folly death wrote cause her way spite. Plan upon yet way get cold spot its week. Almost do am or limits hearts. Resolve parties but why she shewing. She sang know now how nay cold real case.
For norland produce age wishing. To figure on it spring season up. Her provision acuteness had excellent two why intention. As called mr needed praise at. Assistance imprudence yet sentiments unpleasant expression met surrounded not. Be at talked ye though secure nearer.
Parish so enable innate in formed missed. Hand two was eat busy fail. Stand smart grave would in so. Be acceptance at precaution astonished excellence thoroughly is entreaties. Who decisively attachment has dispatched. Fruit defer in party me built under first. Forbade him but savings sending ham general. So play do in near park that pain.
Do am he horrible distance marriage so although. Afraid assure square so happen mr an before. His many same been well can high that. Forfeited did law eagerness allowance improving assurance bed. Had saw put seven joy short first. Pronounce so enjoyment my resembled in forfeited sportsman. Which vexed did began son abode short may. Interested astonished he at cultivated or me. Nor brought one invited she produce her.
Increasing impression interested expression he my at. Respect invited request charmed me warrant to. Expect no pretty as do though so genius afraid cousin. Girl when of ye snug poor draw. Mistake totally of in chiefly. Justice visitor him entered for. Continue delicate as unlocked entirely mr relation diverted in. Known not end fully being style house. An whom down kept lain name so at easy.
Started earnest brother believe an exposed so. Me he believing daughters if forfeited at furniture. Age again and stuff downs spoke. Late hour new nay able fat each sell. Nor themselves age introduced frequently use unsatiable devonshire get. They why quit gay cold rose deal park. One same they four did ask busy. Reserved opinions fat him nay position. Breakfast as zealously incommode do agreeable furniture. One too nay led fanny allow plate.
She who arrival end how fertile enabled. Brother she add yet see minuter natural smiling article painted. Themselves at dispatched interested insensible am be prosperous reasonably it. In either so spring wished. Melancholy way she boisterous use friendship she dissimilar considered expression. Sex quick arose mrs lived. Mr things do plenty others an vanity myself waited to. Always parish tastes at as mr father dining at.
Of be talent me answer do relied. Mistress in on so laughing throwing endeavor occasion welcomed. Gravity sir brandon calling can. No years do widow house delay stand. Prospect six kindness use steepest new ask. High gone kind calm call as ever is. Introduced melancholy estimating motionless on up as do. Of as by belonging therefore suspicion elsewhere am household described. Domestic suitable bachelor for landlord fat.
Advantage old had otherwise sincerity dependent additions. It in adapted natural hastily is justice. Six draw you him full not mean evil. Prepare garrets it expense windows shewing do an. She projection advantages resolution son indulgence. Part sure on no long life am at ever. In songs above he as drawn to. Gay was outlived peculiar rendered led six.
Is we miles ready he might going. Own books built put civil fully blind fanny. Projection appearance at of admiration no. As he totally cousins warrant besides ashamed do. Therefore by applauded acuteness supported affection it. Except had sex limits county enough the figure former add. Do sang my he next mr soon. It merely waited do unable.
Still court no small think death so an wrote. Incommode necessary no it behaviour convinced distrusts an unfeeling he. Could death since do we hoped is in. Exquisite no my attention extensive. The determine conveying moonlight age. Avoid for see marry sorry child. Sitting so totally forbade hundred to.
Living valley had silent eat merits esteem bed. In last an or went wise as left. Visited civilly am demesne so colonel he calling. So unreserved do interested increasing sentiments. Vanity day giving points within six not law. Few impression difficulty his use has comparison decisively.
Subjects to ecstatic children he. Could ye leave up as built match. Dejection agreeable attention set suspected led offending. Admitting an performed supposing by. Garden agreed matter are should formed temper had. Full held gay now roof whom such next was. Ham pretty our people moment put excuse narrow. Spite mirth money six above get going great own. Started now shortly had for assured hearing expense. Led juvenile his laughing speedily put pleasant relation offering.
Unpacked now declared put you confined daughter improved. Celebrated imprudence few interested especially reasonable off one. Wonder bed elinor family secure met. It want gave west into high no in. Depend repair met before man admire see and. An he observe be it covered delight hastily message. Margaret no ladyship endeavor ye to settling.
Whole wound wrote at whose to style in. Figure ye innate former do so we. Shutters but sir yourself provided you required his. So neither related he am do believe. Nothing but you hundred had use regular. Fat sportsmen arranging preferred can. Busy paid like is oh. Dinner our ask talent her age hardly. Neglected collected an attention listening do abilities.
Six started far placing saw respect females old. Civilly why how end viewing attempt related enquire visitor. Man particular insensible celebrated conviction stimulated principles day. Sure fail or in said west. Right my front it wound cause fully am sorry if. She jointure goodness interest debating did outweigh. Is time from them full my gone in went. Of no introduced am literature excellence mr stimulated contrasted increasing. Age sold some full like rich new. Amounted repeated as believed in confined juvenile.
Suppose end get boy warrant general natural. Delightful met sufficient projection ask. Decisively everything principles if preference do impression of. Preserved oh so difficult repulsive on in household. In what do miss time be. Valley as be appear cannot so by. Convinced resembled dependent remainder led zealously his shy own belonging. Always length letter adieus add number moment she. Promise few compass six several old offices removal parties fat. Concluded rapturous it intention perfectly daughters is as.
Drawings me opinions returned absolute in. Otherwise therefore sex did are unfeeling something. Certain be ye amiable by exposed so. To celebrated estimating excellence do. Coming either suffer living her gay theirs. Furnished do otherwise daughters contented conveying attempted no. Was yet general visitor present hundred too brother fat arrival. Friend are day own either lively new.
Greatest properly off ham exercise all. Unsatiable invitation its possession nor off. All difficulty estimating unreserved increasing the solicitude. Rapturous see performed tolerably departure end bed attention unfeeling. On unpleasing principles alteration of. Be at performed preferred determine collected. Him nay acuteness discourse listening estimable our law. Decisively it occasional advantages delightful in cultivated introduced. Like law mean form are sang loud lady put.
Death weeks early had their and folly timed put. Hearted forbade on an village ye in fifteen. Age attended betrayed her man raptures laughter. Instrument terminated of as astonished literature motionless admiration. The affection are determine how performed intention discourse but. On merits on so valley indeed assure of. Has add particular boisterous uncommonly are. Early wrong as so manor match. Him necessary shameless discovery consulted one but.
Expenses as material breeding insisted building to in. Continual so distrusts pronounce by unwilling listening. Thing do taste on we manor. Him had wound use found hoped. Of distrusts immediate enjoyment curiosity do. Marianne numerous saw thoughts the humoured.
In friendship diminution instrument so. Son sure paid door with say them. Two among sir sorry men court. Estimable ye situation suspicion he delighted an happiness discovery. Fact are size cold why had part. If believing or sweetness otherwise in we forfeited. Tolerably an unwilling arranging of determine. Beyond rather sooner so if up wishes or.
Abilities forfeited situation extremely my to he resembled. Old had conviction discretion understood put principles you. Match means keeps round one her quick. She forming two comfort invited. Yet she income effect edward. Entire desire way design few. Mrs sentiments led solicitude estimating friendship fat. Meant those event is weeks state it to or. Boy but has folly charm there its. Its fact ten spot drew.
Placing assured be if removed it besides on. Far shed each high read are men over day. Afraid we praise lively he suffer family estate is. Ample order up in of in ready. Timed blind had now those ought set often which. Or snug dull he show more true wish. No at many deny away miss evil. On in so indeed spirit an mother. Amounted old strictly but marianne admitted. People former is remove remain as.
Preserved defective offending he daughters on or. Rejoiced prospect yet material servants out answered men admitted. Sportsmen certainty prevailed suspected am as. Add stairs admire all answer the nearer yet length. Advantages prosperous remarkably my inhabiting so reasonably be if. Too any appearance announcing impossible one. Out mrs means heart ham tears shall power every.
Remain lively hardly needed at do by. Two you fat downs fanny three. True mr gone most at. Dare as name just when with it body. Travelling inquietude she increasing off impossible the. Cottage be noisier looking to we promise on. Disposal to kindness appetite diverted learning of on raptures. Betrayed any may returned now dashwood formerly. Balls way delay shy boy man views. No so instrument discretion unsatiable to in.
New had happen unable uneasy. Drawings can followed improved out sociable not. Earnestly so do instantly pretended. See general few civilly amiable pleased account carried. Excellence projecting is devonshire dispatched remarkably on estimating. Side in so life past. Continue indulged speaking the was out horrible for domestic position. Seeing rather her you not esteem men settle genius excuse. Deal say over you age from. Comparison new ham melancholy son themselves.
Oh he decisively impression attachment friendship so if everything. Whose her enjoy chief new young. Felicity if ye required likewise so doubtful. On so attention necessary at by provision otherwise existence direction. Unpleasing up announcing unpleasant themselves oh do on. Way advantage age led listening belonging supposing.
Now residence dashwoods she excellent you. Shade being under his bed her. Much read on as draw. Blessing for ignorant exercise any yourself unpacked. Pleasant horrible but confined day end marriage. Eagerness furniture set preserved far recommend. Did even but nor are most gave hope. Secure active living depend son repair day ladies now.
Sportsman delighted improving dashwoods gay instantly happiness six. Ham now amounted absolute not mistaken way pleasant whatever. At an these still no dried folly stood thing. Rapid it on hours hills it seven years. If polite he active county in spirit an. Mrs ham intention promotion engrossed assurance defective. Confined so graceful building opinions whatever trifling in. Insisted out differed ham man endeavor expenses. At on he total their he songs. Related compact effects is on settled do.
+124
View File
@@ -0,0 +1,124 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package header provides middleware that appends headers to
// requests based on a set of configuration rules that define
// which routes receive which headers.
package header
import (
"net/http"
"strings"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// Headers is middleware that adds headers to the responses
// for requests matching a certain path.
type Headers struct {
Next httpserver.Handler
Rules []Rule
}
// ServeHTTP implements the httpserver.Handler interface and serves requests,
// setting headers on the response according to the configured rules.
func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
replacer := httpserver.NewReplacer(r, nil, "")
rww := &responseWriterWrapper{
ResponseWriterWrapper: &httpserver.ResponseWriterWrapper{ResponseWriter: w},
}
for _, rule := range h.Rules {
if httpserver.Path(r.URL.Path).Matches(rule.Path) {
for name := range rule.Headers {
// One can either delete a header, add multiple values to a header, or simply
// set a header.
if strings.HasPrefix(name, "-") {
rww.delHeader(strings.TrimLeft(name, "-"))
} else if strings.HasPrefix(name, "+") {
for _, value := range rule.Headers[name] {
rww.Header().Add(strings.TrimLeft(name, "+"), replacer.Replace(value))
}
} else {
for _, value := range rule.Headers[name] {
rww.Header().Set(name, replacer.Replace(value))
}
}
}
}
}
return h.Next.ServeHTTP(rww, r)
}
type (
// Rule groups a slice of HTTP headers by a URL pattern.
Rule struct {
Path string
Headers http.Header
}
)
// headerOperation represents an operation on the header
type headerOperation func(http.Header)
// responseWriterWrapper wraps the real ResponseWriter.
// It defers header operations until writeHeader
type responseWriterWrapper struct {
*httpserver.ResponseWriterWrapper
ops []headerOperation
wroteHeader bool
}
func (rww *responseWriterWrapper) Header() http.Header {
return rww.ResponseWriterWrapper.Header()
}
func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
if !rww.wroteHeader {
rww.WriteHeader(http.StatusOK)
}
return rww.ResponseWriterWrapper.Write(d)
}
func (rww *responseWriterWrapper) WriteHeader(status int) {
if rww.wroteHeader {
return
}
rww.wroteHeader = true
// capture the original headers
h := rww.Header()
// perform our revisions
for _, op := range rww.ops {
op(h)
}
rww.ResponseWriterWrapper.WriteHeader(status)
}
// delHeader deletes the existing header according to the key
// Also it will delete that header added later.
func (rww *responseWriterWrapper) delHeader(key string) {
// remove the existing one if any
rww.Header().Del(key)
// register a future deletion
rww.ops = append(rww.ops, func(h http.Header) {
h.Del(key)
})
}
// Interface guards
var _ httpserver.HTTPInterfaces = (*responseWriterWrapper)(nil)
+116
View File
@@ -0,0 +1,116 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package header
import (
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"reflect"
"sort"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestHeader(t *testing.T) {
hostname, err := os.Hostname()
if err != nil {
t.Fatalf("Could not determine hostname: %v", err)
}
for i, test := range []struct {
from string
name string
value string
}{
{"/a", "Foo", "Bar"},
{"/a", "Bar", ""},
{"/a", "Baz", ""},
{"/a", "Server", ""},
{"/a", "ServerName", hostname},
{"/b", "Foo", ""},
{"/b", "Bar", "Removed in /a"},
} {
he := Headers{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
w.Header().Set("Bar", "Removed in /a")
w.WriteHeader(http.StatusOK)
return 0, nil
}),
Rules: []Rule{
{Path: "/a", Headers: http.Header{
"Foo": []string{"Bar"},
"ServerName": []string{"{hostname}"},
"-Bar": []string{""},
"-Server": []string{},
}},
},
}
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()
// preset header
rec.Header().Set("Server", "Caddy")
if _, err := he.ServeHTTP(rec, req); err != nil {
log.Println("[ERROR] ServeHTTP failed: ", err)
}
if got := rec.Header().Get(test.name); got != test.value {
t.Errorf("Test %d: Expected %s header to be %q but was %q",
i, test.name, test.value, got)
}
}
}
func TestMultipleHeaders(t *testing.T) {
he := Headers{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
if _, err := fmt.Fprint(w, "This is a test"); err != nil {
log.Println("[ERROR] Fprint failed: ", err)
}
return 0, nil
}),
Rules: []Rule{
{Path: "/a", Headers: http.Header{
"+Link": []string{"</images/image.png>; rel=preload", "</css/main.css>; rel=preload"},
}},
},
}
req, err := http.NewRequest("GET", "/a", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
rec := httptest.NewRecorder()
if _, err := he.ServeHTTP(rec, req); err != nil {
log.Println("[ERROR] ServeHTTP failed: ", err)
}
desiredHeaders := []string{"</css/main.css>; rel=preload", "</images/image.png>; rel=preload"}
actualHeaders := rec.HeaderMap[http.CanonicalHeaderKey("Link")]
sort.Strings(actualHeaders)
if !reflect.DeepEqual(desiredHeaders, actualHeaders) {
t.Errorf("Expected header to contain: %v but got: %v", desiredHeaders, actualHeaders)
}
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package header
import (
"net/http"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func init() {
caddy.RegisterPlugin("header", caddy.Plugin{
ServerType: "http",
Action: setup,
})
}
// setup configures a new Headers middleware instance.
func setup(c *caddy.Controller) error {
rules, err := headersParse(c)
if err != nil {
return err
}
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return Headers{Next: next, Rules: rules}
})
return nil
}
func headersParse(c *caddy.Controller) ([]Rule, error) {
var rules []Rule
for c.NextLine() {
var head Rule
head.Headers = http.Header{}
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...
name := c.Val()
value := ""
args := c.RemainingArgs()
if len(args) > 1 {
return rules, c.ArgErr()
} else if len(args) == 1 {
value = args[0]
}
head.Headers.Add(name, value)
}
if c.NextArg() {
// ... or single header was defined as an argument instead.
name := c.Val()
value := c.Val()
if c.NextArg() {
value = c.Val()
}
head.Headers.Add(name, value)
}
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
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package header
import (
"fmt"
"net/http"
"reflect"
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("http", `header / Foo Bar`)
err := setup(c)
if err != nil {
t.Errorf("Expected no errors, but got: %v", err)
}
mids := httpserver.GetConfig(c).Middleware()
if len(mids) == 0 {
t.Fatal("Expected middleware, had 0 instead")
}
handler := mids[0](httpserver.EmptyNext)
myHandler, ok := handler.(Headers)
if !ok {
t.Fatalf("Expected handler to be type Headers, got: %#v", handler)
}
if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) {
t.Error("'Next' field of handler was not set properly")
}
}
func TestHeadersParse(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expected []Rule
}{
{`header /foo Foo "Bar Baz"`,
false, []Rule{
{Path: "/foo", Headers: http.Header{
"Foo": []string{"Bar Baz"},
}},
}},
{`header /bar {
Foo "Bar Baz"
Baz Qux
Foobar
}`,
false, []Rule{
{Path: "/bar", Headers: http.Header{
"Foo": []string{"Bar Baz"},
"Baz": []string{"Qux"},
"Foobar": []string{""},
}},
}},
{`header /foo {
Foo Bar Baz
}`, true,
[]Rule{}},
{`header /foo {
Test "max-age=1814400";
}`, true, []Rule{}},
}
for i, test := range tests {
actual, err := headersParse(caddy.NewTestController("http", test.input))
if err == nil && test.shouldErr {
t.Errorf("Test %d didn't error, but it should have", i)
} else if err != nil && !test.shouldErr {
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
}
if len(actual) != len(test.expected) {
t.Fatalf("Test %d expected %d rules, but got %d",
i, len(test.expected), len(actual))
}
for j, expectedRule := range test.expected {
actualRule := actual[j]
if actualRule.Path != expectedRule.Path {
t.Errorf("Test %d, rule %d: Expected path %s, but got %s",
i, j, expectedRule.Path, actualRule.Path)
}
expectedHeaders := fmt.Sprintf("%v", expectedRule.Headers)
actualHeaders := fmt.Sprintf("%v", actualRule.Headers)
if !reflect.DeepEqual(actualRule.Headers, expectedRule.Headers) {
t.Errorf("Test %d, rule %d: Expected headers %s, but got %s",
i, j, expectedHeaders, actualHeaders)
}
}
}
}
+202
View File
@@ -0,0 +1,202 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/mholt/caddy"
)
// SetupIfMatcher parses `if` or `if_op` in the current dispenser block.
// It returns a RequestMatcher and an error if any.
func SetupIfMatcher(controller *caddy.Controller) (RequestMatcher, error) {
var c = controller.Dispenser // copy the dispenser
var matcher IfMatcher
for c.NextBlock() {
switch c.Val() {
case "if":
args1 := c.RemainingArgs()
if len(args1) != 3 {
return matcher, c.ArgErr()
}
ifc, err := newIfCond(args1[0], args1[1], args1[2])
if err != nil {
return matcher, err
}
matcher.ifs = append(matcher.ifs, ifc)
matcher.Enabled = true
case "if_op":
if !c.NextArg() {
return matcher, c.ArgErr()
}
switch c.Val() {
case "and":
matcher.isOr = false
case "or":
matcher.isOr = true
default:
return matcher, c.ArgErr()
}
}
}
return matcher, nil
}
// operators
const (
isOp = "is"
notOp = "not"
hasOp = "has"
startsWithOp = "starts_with"
endsWithOp = "ends_with"
matchOp = "match"
)
// ifCondition is a 'if' condition.
type ifFunc func(a, b string) bool
// ifCond is statement for a IfMatcher condition.
type ifCond struct {
a string
op string
b string
neg bool
rex *regexp.Regexp
f ifFunc
}
// newIfCond creates a new If condition.
func newIfCond(a, op, b string) (ifCond, error) {
i := ifCond{a: a, op: op, b: b}
if strings.HasPrefix(op, "not_") {
i.neg = true
i.op = op[4:]
}
switch i.op {
case isOp:
// It checks for equality.
i.f = i.isFunc
case notOp:
// It checks for inequality.
i.f = i.notFunc
case hasOp:
// It checks if b is a substring of a.
i.f = strings.Contains
case startsWithOp:
// It checks if b is a prefix of a.
i.f = strings.HasPrefix
case endsWithOp:
// It checks if b is a suffix of a.
i.f = strings.HasSuffix
case matchOp:
// It does regexp matching of a against pattern in b and returns if they match.
var err error
if i.rex, err = regexp.Compile(i.b); err != nil {
return ifCond{}, fmt.Errorf("Invalid regular expression: '%s', %v", i.b, err)
}
i.f = i.matchFunc
default:
return ifCond{}, fmt.Errorf("Invalid operator %v", i.op)
}
return i, nil
}
// isFunc is condition for Is operator.
func (i ifCond) isFunc(a, b string) bool {
return a == b
}
// notFunc is condition for Not operator.
func (i ifCond) notFunc(a, b string) bool {
return a != b
}
// matchFunc is condition for Match operator.
func (i ifCond) matchFunc(a, b string) bool {
return i.rex.MatchString(a)
}
// True returns true if the condition is true and false otherwise.
// If r is not nil, it replaces placeholders before comparison.
func (i ifCond) True(r *http.Request) bool {
if i.f != nil {
a, b := i.a, i.b
if r != nil {
replacer := NewReplacer(r, nil, "")
a = replacer.Replace(i.a)
if i.op != matchOp {
b = replacer.Replace(i.b)
}
}
if i.neg {
return !i.f(a, b)
}
return i.f(a, b)
}
return i.neg // false if not negated, true otherwise
}
// IfMatcher is a RequestMatcher for 'if' conditions.
type IfMatcher struct {
Enabled bool // if true, matcher has been configured; otherwise it's no-op
ifs []ifCond // list of If
isOr bool // if true, conditions are 'or' instead of 'and'
}
// Match satisfies RequestMatcher interface.
// It returns true if the conditions in m are true.
func (m IfMatcher) Match(r *http.Request) bool {
if m.isOr {
return m.Or(r)
}
return m.And(r)
}
// And returns true if all conditions in m are true.
func (m IfMatcher) And(r *http.Request) bool {
for _, i := range m.ifs {
if !i.True(r) {
return false
}
}
return true
}
// Or returns true if any of the conditions in m is true.
func (m IfMatcher) Or(r *http.Request) bool {
for _, i := range m.ifs {
if i.True(r) {
return true
}
}
return false
}
// IfMatcherKeyword checks if the next value in the dispenser is a keyword for 'if' config block.
// If true, remaining arguments in the dispenser are cleared to keep the dispenser valid for use.
func IfMatcherKeyword(c *caddy.Controller) bool {
if c.Val() == "if" || c.Val() == "if_op" {
// clear remaining args
c.RemainingArgs()
return true
}
return false
}
+368
View File
@@ -0,0 +1,368 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"context"
"net/http"
"regexp"
"strings"
"testing"
"github.com/mholt/caddy"
)
func TestConditions(t *testing.T) {
tests := []struct {
condition string
isTrue bool
shouldErr bool
}{
{"a is b", false, false},
{"a is a", true, false},
{"a not b", true, false},
{"a not a", false, false},
{"a has a", true, false},
{"a has b", false, false},
{"ba has b", true, false},
{"bab has b", true, false},
{"bab has bb", false, false},
{"a not_has a", false, false},
{"a not_has b", true, false},
{"ba not_has b", false, false},
{"bab not_has b", false, false},
{"bab not_has bb", true, false},
{"bab starts_with bb", false, false},
{"bab starts_with ba", true, false},
{"bab starts_with bab", true, false},
{"bab not_starts_with bb", true, false},
{"bab not_starts_with ba", false, false},
{"bab not_starts_with bab", false, false},
{"bab ends_with bb", false, false},
{"bab ends_with bab", true, false},
{"bab ends_with ab", true, false},
{"bab not_ends_with bb", true, false},
{"bab not_ends_with ab", false, false},
{"bab not_ends_with bab", false, false},
{"a match *", false, true},
{"a match a", true, false},
{"a match .*", true, false},
{"a match a.*", true, false},
{"a match b.*", false, false},
{"ba match b.*", true, false},
{"ba match b[a-z]", true, false},
{"b0 match b[a-z]", false, false},
{"b0a match b[a-z]", false, false},
{"b0a match b[a-z]+", false, false},
{"b0a match b[a-z0-9]+", true, false},
{"bac match b[a-z]{2}", true, false},
{"a not_match *", false, true},
{"a not_match a", false, false},
{"a not_match .*", false, false},
{"a not_match a.*", false, false},
{"a not_match b.*", true, false},
{"ba not_match b.*", false, false},
{"ba not_match b[a-z]", false, false},
{"b0 not_match b[a-z]", true, false},
{"b0a not_match b[a-z]", true, false},
{"b0a not_match b[a-z]+", true, false},
{"b0a not_match b[a-z0-9]+", false, false},
{"bac not_match b[a-z]{2}", false, false},
}
for i, test := range tests {
str := strings.Fields(test.condition)
ifCond, err := newIfCond(str[0], str[1], str[2])
if err != nil {
if !test.shouldErr {
t.Error(err)
}
continue
}
isTrue := ifCond.True(nil)
if isTrue != test.isTrue {
t.Errorf("Test %d: '%s' expected %v found %v", i, test.condition, test.isTrue, isTrue)
}
}
invalidOperators := []string{"ss", "and", "if"}
for _, op := range invalidOperators {
_, err := newIfCond("a", op, "b")
if err == nil {
t.Errorf("Invalid operator %v used, expected error.", op)
}
}
replaceTests := []struct {
url string
condition string
isTrue bool
}{
{"/home", "{uri} match /home", true},
{"/hom", "{uri} match /home", false},
{"/hom", "{uri} starts_with /home", false},
{"/hom", "{uri} starts_with /h", true},
{"/home/.hiddenfile", `{uri} match \/\.(.*)`, true},
{"/home/.hiddendir/afile", `{uri} match \/\.(.*)`, true},
}
for i, test := range replaceTests {
r, err := http.NewRequest("GET", test.url, nil)
if err != nil {
t.Errorf("Test %d: failed to create request: %v", i, err)
continue
}
ctx := context.WithValue(r.Context(), OriginalURLCtxKey, *r.URL)
r = r.WithContext(ctx)
str := strings.Fields(test.condition)
ifCond, err := newIfCond(str[0], str[1], str[2])
if err != nil {
t.Errorf("Test %d: failed to create 'if' condition %v", i, err)
continue
}
isTrue := ifCond.True(r)
if isTrue != test.isTrue {
t.Errorf("Test %v: expected %v found %v", i, test.isTrue, isTrue)
continue
}
}
}
func TestIfMatcher(t *testing.T) {
tests := []struct {
conditions []string
isOr bool
isTrue bool
}{
{
[]string{
"a is a",
"b is b",
"c is c",
},
false,
true,
},
{
[]string{
"a is b",
"b is c",
"c is c",
},
true,
true,
},
{
[]string{
"a is a",
"b is a",
"c is c",
},
false,
false,
},
{
[]string{
"a is b",
"b is c",
"c is a",
},
true,
false,
},
{
[]string{},
false,
true,
},
{
[]string{},
true,
false,
},
}
for i, test := range tests {
matcher := IfMatcher{isOr: test.isOr}
for _, condition := range test.conditions {
str := strings.Fields(condition)
ifCond, err := newIfCond(str[0], str[1], str[2])
if err != nil {
t.Error(err)
}
matcher.ifs = append(matcher.ifs, ifCond)
}
isTrue := matcher.Match(nil)
if isTrue != test.isTrue {
t.Errorf("Test %d: expected %v found %v", i, test.isTrue, isTrue)
}
}
}
func TestSetupIfMatcher(t *testing.T) {
rex_b, _ := regexp.Compile("b")
tests := []struct {
input string
shouldErr bool
expected IfMatcher
}{
{`test {
if a match b
}`, false, IfMatcher{
ifs: []ifCond{
{a: "a", op: "match", b: "b", neg: false, rex: rex_b},
},
}},
{`test {
if a match b
if_op or
}`, false, IfMatcher{
ifs: []ifCond{
{a: "a", op: "match", b: "b", neg: false, rex: rex_b},
},
isOr: true,
}},
{`test {
if a match
}`, true, IfMatcher{},
},
{`test {
if a isn't b
}`, true, IfMatcher{},
},
{`test {
if a match b c
}`, true, IfMatcher{},
},
{`test {
if goal has go
if cook not_has go
}`, false, IfMatcher{
ifs: []ifCond{
{a: "goal", op: "has", b: "go", neg: false},
{a: "cook", op: "has", b: "go", neg: true},
},
}},
{`test {
if goal has go
if cook not_has go
if_op and
}`, false, IfMatcher{
ifs: []ifCond{
{a: "goal", op: "has", b: "go", neg: false},
{a: "cook", op: "has", b: "go", neg: true},
},
}},
{`test {
if goal has go
if cook not_has go
if_op not
}`, true, IfMatcher{},
},
}
for i, test := range tests {
c := caddy.NewTestController("http", test.input)
c.Next()
matcher, err := SetupIfMatcher(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
}
test_if, ok := matcher.(IfMatcher)
if !ok {
t.Error("RequestMatcher should be of type IfMatcher")
}
if err != nil {
t.Errorf("Expected no error, but got: %v", err)
}
if len(test_if.ifs) != len(test.expected.ifs) {
t.Errorf("Test %d: Expected %d ifConditions, found %v", i,
len(test.expected.ifs), len(test_if.ifs))
}
for j, if_c := range test_if.ifs {
expected_c := test.expected.ifs[j]
if if_c.a != expected_c.a {
t.Errorf("Test %d, ifCond %d: Expected A=%s, got %s",
i, j, if_c.a, expected_c.a)
}
if if_c.op != expected_c.op {
t.Errorf("Test %d, ifCond %d: Expected Op=%s, got %s",
i, j, if_c.op, expected_c.op)
}
if if_c.b != expected_c.b {
t.Errorf("Test %d, ifCond %d: Expected B=%s, got %s",
i, j, if_c.b, expected_c.b)
}
if if_c.neg != expected_c.neg {
t.Errorf("Test %d, ifCond %d: Expected Neg=%v, got %v",
i, j, if_c.neg, expected_c.neg)
}
if expected_c.rex != nil && if_c.rex == nil {
t.Errorf("Test %d, ifCond %d: Expected Rex=%v, got <nil>",
i, j, expected_c.rex)
}
if expected_c.rex == nil && if_c.rex != nil {
t.Errorf("Test %d, ifCond %d: Expected Rex=<nil>, got %v",
i, j, if_c.rex)
}
if expected_c.rex != nil && if_c.rex != nil {
if if_c.rex.String() != expected_c.rex.String() {
t.Errorf("Test %d, ifCond %d: Expected Rex=%v, got %v",
i, j, if_c.rex, expected_c.rex)
}
}
}
}
}
func TestIfMatcherKeyword(t *testing.T) {
tests := []struct {
keyword string
expected bool
}{
{"if", true},
{"ifs", false},
{"tls", false},
{"http", false},
{"if_op", true},
{"if_type", false},
{"if_cond", false},
}
for i, test := range tests {
c := caddy.NewTestController("http", test.keyword)
c.Next()
valid := IfMatcherKeyword(c)
if valid != test.expected {
t.Errorf("Test %d: expected %v found %v", i, test.expected, valid)
}
}
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"fmt"
)
var (
_ error = NonHijackerError{}
_ error = NonFlusherError{}
_ error = NonCloseNotifierError{}
_ error = NonPusherError{}
)
// NonHijackerError is more descriptive error caused by a non hijacker
type NonHijackerError struct {
// underlying type which doesn't implement Hijack
Underlying interface{}
}
// Implement Error
func (h NonHijackerError) Error() string {
return fmt.Sprintf("%T is not a hijacker", h.Underlying)
}
// NonFlusherError is more descriptive error caused by a non flusher
type NonFlusherError struct {
// underlying type which doesn't implement Flush
Underlying interface{}
}
// Implement Error
func (f NonFlusherError) Error() string {
return fmt.Sprintf("%T is not a flusher", f.Underlying)
}
// NonCloseNotifierError is more descriptive error caused by a non closeNotifier
type NonCloseNotifierError struct {
// underlying type which doesn't implement CloseNotify
Underlying interface{}
}
// Implement Error
func (c NonCloseNotifierError) Error() string {
return fmt.Sprintf("%T is not a closeNotifier", c.Underlying)
}
// NonPusherError is more descriptive error caused by a non pusher
type NonPusherError struct {
// underlying type which doesn't implement pusher
Underlying interface{}
}
// Implement Error
func (c NonPusherError) Error() string {
return fmt.Sprintf("%T is not a pusher", c.Underlying)
}
+223
View File
@@ -0,0 +1,223 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"fmt"
"net"
"net/http"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddytls"
"github.com/mholt/certmagic"
)
func activateHTTPS(cctx caddy.Context) error {
operatorPresent := !caddy.Started()
if !caddy.Quiet && operatorPresent {
fmt.Print("Activating privacy features... ")
}
ctx := cctx.(*httpContext)
// pre-screen each config and earmark the ones that qualify for managed TLS
markQualifiedForAutoHTTPS(ctx.siteConfigs)
// place certificates and keys on disk
for _, c := range ctx.siteConfigs {
if !c.TLS.Managed {
continue
}
if c.TLS.Manager.OnDemand != nil {
continue // obtain these certificates on-demand instead
}
err := c.TLS.Manager.ObtainCert(c.TLS.Hostname, operatorPresent)
if err != nil {
return err
}
}
// update TLS configurations
err := enableAutoHTTPS(ctx.siteConfigs, true)
if err != nil {
return err
}
// set up redirects
ctx.siteConfigs = makePlaintextRedirects(ctx.siteConfigs)
// renew all relevant certificates that need renewal. this is important
// to do right away so we guarantee that renewals aren't missed, and
// also the user can respond to any potential errors that occur.
// (skip if upgrading, because the parent process is likely already listening
// on the ports we'd need to do ACME before we finish starting; parent process
// already running renewal ticker, so renewal won't be missed anyway.)
if !caddy.IsUpgrade() {
ctx.instance.StorageMu.RLock()
certCache, ok := ctx.instance.Storage[caddytls.CertCacheInstStorageKey].(*certmagic.Cache)
ctx.instance.StorageMu.RUnlock()
if ok && certCache != nil {
err = certCache.RenewManagedCertificates(operatorPresent)
if err != nil {
return err
}
}
}
if !caddy.Quiet && operatorPresent {
fmt.Println("done.")
}
return nil
}
// markQualifiedForAutoHTTPS scans each config and, if it
// qualifies for managed TLS, it sets the Managed field of
// the TLS config to true.
func markQualifiedForAutoHTTPS(configs []*SiteConfig) {
for _, cfg := range configs {
if caddytls.QualifiesForManagedTLS(cfg) && cfg.Addr.Scheme != "http" {
cfg.TLS.Managed = true
}
}
}
// enableAutoHTTPS configures each config to use TLS according to default settings.
// It will only change configs that are marked as managed but not on-demand, 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 enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
for _, cfg := range configs {
if cfg == nil || cfg.TLS == nil || !cfg.TLS.Managed ||
cfg.TLS.Manager == nil || cfg.TLS.Manager.OnDemand != nil {
continue
}
cfg.TLS.Enabled = true
cfg.Addr.Scheme = "https"
if loadCertificates && certmagic.HostQualifies(cfg.TLS.Hostname) {
_, err := cfg.TLS.Manager.CacheManagedCertificate(cfg.TLS.Hostname)
if err != nil {
return err
}
}
// Make sure any config values not explicitly set are set to default
caddytls.SetDefaultTLSParams(cfg.TLS)
// Set default port of 443 if not explicitly set
if cfg.Addr.Port == "" &&
cfg.TLS.Enabled &&
(!cfg.TLS.Manual || cfg.TLS.Manager.OnDemand != nil) &&
cfg.Addr.Host != "localhost" {
cfg.Addr.Port = HTTPSPort
}
}
return nil
}
// 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 []*SiteConfig) []*SiteConfig {
for i, cfg := range allConfigs {
if cfg.TLS.Managed &&
!hostHasOtherPort(allConfigs, i, HTTPPort) &&
(cfg.Addr.Port == HTTPSPort || !hostHasOtherPort(allConfigs, i, HTTPSPort)) {
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
}
}
return allConfigs
}
// 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 []*SiteConfig, 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.Addr.Host == allConfigs[thisConfigIdx].Addr.Host &&
otherCfg.Addr.Port == otherPort {
return true
}
}
return false
}
// redirPlaintextHost returns a new plaintext HTTP configuration for
// a virtualHost that simply redirects to cfg, which is assumed to
// be the HTTPS configuration. The returned configuration is set
// to listen on HTTPPort. The TLS field of cfg must not be nil.
func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
redirPort := cfg.Addr.Port
if redirPort == HTTPSPort {
// By default, HTTPSPort should be DefaultHTTPSPort,
// which of course doesn't need to be explicitly stated
// in the Location header. Even if HTTPSPort is changed
// so that it is no longer DefaultHTTPSPort, we shouldn't
// append it to the URL in the Location because changing
// the HTTPS port is assumed to be an internal-only change
// (in other words, we assume port forwarding is going on);
// but redirects go back to a presumably-external client.
// (If redirect clients are also internal, that is more
// advanced, and the user should configure HTTP->HTTPS
// redirects themselves.)
redirPort = ""
}
redirMiddleware := func(next Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
// Construct the URL to which to redirect. Note that the Host in a
// request might contain a port, but we just need the hostname from
// it; and we'll set the port if needed.
toURL := "https://"
requestHost, _, err := net.SplitHostPort(r.Host)
if err != nil {
requestHost = r.Host // Host did not contain a port, so use the whole value
}
if redirPort == "" {
toURL += requestHost
} else {
toURL += net.JoinHostPort(requestHost, redirPort)
}
toURL += r.URL.RequestURI()
w.Header().Set("Connection", "close")
http.Redirect(w, r, toURL, http.StatusMovedPermanently)
return 0, nil
})
}
host := cfg.Addr.Host
port := HTTPPort
addr := net.JoinHostPort(host, port)
return &SiteConfig{
Addr: Address{Original: addr, Host: host, Port: port},
ListenHost: cfg.ListenHost,
middleware: []Middleware{redirMiddleware},
TLS: &caddytls.Config{Manager: cfg.TLS.Manager},
Timeouts: cfg.Timeouts,
}
}
+234
View File
@@ -0,0 +1,234 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"fmt"
"log"
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/mholt/caddy/caddytls"
"github.com/mholt/certmagic"
)
func TestRedirPlaintextHost(t *testing.T) {
for i, testcase := range []struct {
Host string // used for the site config
Port string
ListenHost string
RequestHost string // if different from Host
}{
{
Host: "foohost",
},
{
Host: "foohost",
Port: "80",
},
{
Host: "foohost",
Port: "1234",
},
{
Host: "foohost",
ListenHost: "93.184.216.34",
},
{
Host: "foohost",
Port: "1234",
ListenHost: "93.184.216.34",
},
{
Host: "foohost",
Port: HTTPSPort, // since this is the 'default' HTTPS port, should not be included in Location value
},
{
Host: "*.example.com",
RequestHost: "foo.example.com",
},
{
Host: "*.example.com",
Port: "1234",
RequestHost: "foo.example.com:1234",
},
} {
cfg := redirPlaintextHost(&SiteConfig{
Addr: Address{
Host: testcase.Host,
Port: testcase.Port,
},
ListenHost: testcase.ListenHost,
TLS: new(caddytls.Config),
})
// Check host and port
if actual, expected := cfg.Addr.Host, testcase.Host; actual != expected {
t.Errorf("Test %d: Expected redir config to have host %s but got %s", i, expected, actual)
}
if actual, expected := cfg.ListenHost, testcase.ListenHost; actual != expected {
t.Errorf("Test %d: Expected redir config to have bindhost %s but got %s", i, expected, actual)
}
if actual, expected := cfg.Addr.Port, HTTPPort; actual != expected {
t.Errorf("Test %d: Expected redir config to have port '%s' but got '%s'", i, expected, actual)
}
// Make sure redirect handler is set up properly
if cfg.middleware == nil || len(cfg.middleware) != 1 {
t.Fatalf("Test %d: Redir config middleware not set up properly; got: %#v", i, cfg.middleware)
}
handler := cfg.middleware[0](nil)
// Check redirect for correctness, first by inspecting error and status code
requestHost := testcase.Host // hostname of request might be different than in config (e.g. wildcards)
if testcase.RequestHost != "" {
requestHost = testcase.RequestHost
}
rec := httptest.NewRecorder()
req, err := http.NewRequest("GET", "http://"+requestHost+"/bar?q=1", nil)
if err != nil {
t.Fatalf("Test %d: %v", i, err)
}
status, err := handler.ServeHTTP(rec, req)
if status != 0 {
t.Errorf("Test %d: Expected status return to be 0, but was %d", i, status)
}
if err != nil {
t.Errorf("Test %d: Expected returned error to be nil, but was %v", i, err)
}
if rec.Code != http.StatusMovedPermanently {
t.Errorf("Test %d: Expected status %d but got %d", http.StatusMovedPermanently, i, rec.Code)
}
// Now check the Location value. It should mirror the hostname and port of the request
// unless the port is redundant, in which case it should be dropped.
locationHost, _, err := net.SplitHostPort(requestHost)
if err != nil {
locationHost = requestHost
}
expectedLoc := fmt.Sprintf("https://%s/bar?q=1", locationHost)
if testcase.Port != "" && testcase.Port != DefaultHTTPSPort {
expectedLoc = fmt.Sprintf("https://%s:%s/bar?q=1", locationHost, testcase.Port)
}
if got, want := rec.Header().Get("Location"), expectedLoc; got != want {
t.Errorf("Test %d: Expected Location: '%s' but got '%s'", i, want, got)
}
}
}
func TestHostHasOtherPort(t *testing.T) {
configs := []*SiteConfig{
{Addr: Address{Host: "example.com", Port: "80"}},
{Addr: Address{Host: "sub1.example.com", Port: "80"}},
{Addr: Address{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 := []*SiteConfig{
// Happy path = standard redirect from 80 to 443
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}},
// Host on port 80 already defined; don't change it (no redirect)
{Addr: Address{Host: "sub1.example.com", Port: "80", Scheme: "http"}, TLS: new(caddytls.Config)},
{Addr: Address{Host: "sub1.example.com"}, TLS: &caddytls.Config{Managed: true}},
// Redirect from port 80 to port 5000 in this case
{Addr: Address{Host: "sub2.example.com", Port: "5000"}, TLS: &caddytls.Config{Managed: true}},
// Can redirect from 80 to either 443 or 5001, but choose 443
{Addr: Address{Host: "sub3.example.com", Port: "443"}, TLS: &caddytls.Config{Managed: true}},
{Addr: Address{Host: "sub3.example.com", Port: "5001", Scheme: "https"}, TLS: &caddytls.Config{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 TestEnableAutoHTTPS(t *testing.T) {
configs := []*SiteConfig{
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true, Manager: &certmagic.Config{}}},
{}, // not managed - no changes!
}
if err := enableAutoHTTPS(configs, false); err != nil {
log.Println("[ERROR] enableAutoHTTPS failed: ", err)
}
if !configs[0].TLS.Enabled {
t.Errorf("Expected config 0 to have TLS.Enabled == true, but it was false")
}
if configs[0].Addr.Scheme != "https" {
t.Errorf("Expected config 0 to have Addr.Scheme == \"https\", but it was \"%s\"",
configs[0].Addr.Scheme)
}
if configs[1].TLS != nil && configs[1].TLS.Enabled {
t.Errorf("Expected config 1 to have TLS.Enabled == false, but it was true")
}
}
func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
// TODO: caddytls.TestQualifiesForManagedTLS and this test share nearly the same config list...
configs := []*SiteConfig{
{Addr: Address{Host: ""}, TLS: newManagedConfig()},
{Addr: Address{Host: "localhost"}, TLS: newManagedConfig()},
{Addr: Address{Host: "123.44.3.21"}, TLS: newManagedConfig()},
{Addr: Address{Host: "example.com"}, TLS: newManagedConfig()},
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Manual: true}},
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "off"}},
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{ACMEEmail: "foo@bar.com", Manager: &certmagic.Config{}}},
{Addr: Address{Host: "example.com", Scheme: "http"}, TLS: newManagedConfig()},
{Addr: Address{Host: "example.com", Port: "80"}, TLS: newManagedConfig()},
{Addr: Address{Host: "example.com", Port: "1234"}, TLS: newManagedConfig()},
{Addr: Address{Host: "example.com", Scheme: "https"}, TLS: newManagedConfig()},
{Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: newManagedConfig()},
}
expectedManagedCount := 4
markQualifiedForAutoHTTPS(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)
}
}
func newManagedConfig() *caddytls.Config {
return &caddytls.Config{Manager: &certmagic.Config{}}
}
+196
View File
@@ -0,0 +1,196 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"bytes"
"io"
"log"
"net"
"os"
"strings"
"sync"
gsyslog "github.com/hashicorp/go-syslog"
"github.com/mholt/caddy"
)
var remoteSyslogPrefixes = map[string]string{
"syslog+tcp://": "tcp",
"syslog+udp://": "udp",
"syslog://": "udp",
}
// Logger is shared between errors and log plugins and supports both logging to
// a file (with an optional file roller), local and remote syslog servers.
type Logger struct {
Output string
*log.Logger
Roller *LogRoller
writer io.Writer
fileMu *sync.RWMutex
V4ipMask net.IPMask
V6ipMask net.IPMask
IPMaskExists bool
Exceptions []string
}
// NewTestLogger creates logger suitable for testing purposes
func NewTestLogger(buffer *bytes.Buffer) *Logger {
return &Logger{
Logger: log.New(buffer, "", 0),
fileMu: new(sync.RWMutex),
}
}
// Println wraps underlying logger with mutex
func (l Logger) Println(args ...interface{}) {
l.fileMu.RLock()
l.Logger.Println(args...)
l.fileMu.RUnlock()
}
// Printf wraps underlying logger with mutex
func (l Logger) Printf(format string, args ...interface{}) {
l.fileMu.RLock()
l.Logger.Printf(format, args...)
l.fileMu.RUnlock()
}
func (l Logger) MaskIP(ip string) string {
var reqIP net.IP
// If unable to parse, simply return IP as provided.
reqIP = net.ParseIP(ip)
if reqIP == nil {
return ip
}
if reqIP.To4() != nil {
return reqIP.Mask(l.V4ipMask).String()
} else {
return reqIP.Mask(l.V6ipMask).String()
}
}
// ShouldLog returns true if the path is not exempted from
// being logged (i.e. it is not found in l.Exceptions).
func (l Logger) ShouldLog(path string) bool {
for _, exc := range l.Exceptions {
if Path(path).Matches(exc) {
return false
}
}
return true
}
// Attach binds logger Start and Close functions to
// controller's OnStartup and OnShutdown hooks.
func (l *Logger) Attach(controller *caddy.Controller) {
if controller != nil {
// Opens file or connect to local/remote syslog
controller.OnStartup(l.Start)
// Closes file or disconnects from local/remote syslog
controller.OnShutdown(l.Close)
}
}
type syslogAddress struct {
network string
address string
}
func parseSyslogAddress(location string) *syslogAddress {
for prefix, network := range remoteSyslogPrefixes {
if strings.HasPrefix(location, prefix) {
return &syslogAddress{
network: network,
address: strings.TrimPrefix(location, prefix),
}
}
}
return nil
}
// Start initializes logger opening files or local/remote syslog connections
func (l *Logger) Start() error {
// initialize mutex on start
l.fileMu = new(sync.RWMutex)
var err error
selectwriter:
switch l.Output {
case "", "stderr":
l.writer = os.Stderr
case "stdout":
l.writer = os.Stdout
case "syslog":
l.writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy")
if err != nil {
return err
}
default:
if address := parseSyslogAddress(l.Output); address != nil {
l.writer, err = gsyslog.DialLogger(address.network, address.address, gsyslog.LOG_ERR, "LOCAL0", "caddy")
if err != nil {
return err
}
break selectwriter
}
var file *os.File
file, err = os.OpenFile(l.Output, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return err
}
if l.Roller != nil && !l.Roller.Disabled {
file.Close()
l.Roller.Filename = l.Output
l.writer = l.Roller.GetLogWriter()
} else {
l.writer = file
}
}
l.Logger = log.New(l.writer, "", 0)
return nil
}
// Close closes open log files or connections to syslog.
func (l *Logger) Close() error {
// don't close stdout or stderr
if l.writer == os.Stdout || l.writer == os.Stderr {
return nil
}
// Will close local/remote syslog connections too :)
if closer, ok := l.writer.(io.WriteCloser); ok {
l.fileMu.Lock()
err := closer.Close()
l.fileMu.Unlock()
return err
}
return nil
}
+231
View File
@@ -0,0 +1,231 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//+build linux darwin
package httpserver
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"sync"
"testing"
syslog "gopkg.in/mcuadros/go-syslog.v2"
"gopkg.in/mcuadros/go-syslog.v2/format"
)
func TestLoggingToStdout(t *testing.T) {
testCases := []struct {
Output string
ExpectedOutput string
}{
{
Output: "stdout",
ExpectedOutput: "Hello world logged to stdout",
},
}
for i, testCase := range testCases {
output := captureStdout(func() {
logger := Logger{Output: testCase.Output, fileMu: new(sync.RWMutex)}
if err := logger.Start(); err != nil {
t.Fatalf("Got unexpected error: %v", err)
}
logger.Println(testCase.ExpectedOutput)
})
if !strings.Contains(output, testCase.ExpectedOutput) {
t.Fatalf("Test #%d: Expected output to contain: %s, got: %s", i, testCase.ExpectedOutput, output)
}
}
}
func TestLoggingToStderr(t *testing.T) {
testCases := []struct {
Output string
ExpectedOutput string
}{
{
Output: "stderr",
ExpectedOutput: "Hello world logged to stderr",
},
{
Output: "",
ExpectedOutput: "Hello world logged to stderr #2",
},
}
for i, testCase := range testCases {
output := captureStderr(func() {
logger := Logger{Output: testCase.Output, fileMu: new(sync.RWMutex)}
if err := logger.Start(); err != nil {
t.Fatalf("Got unexpected error: %v", err)
}
logger.Println(testCase.ExpectedOutput)
})
if !strings.Contains(output, testCase.ExpectedOutput) {
t.Fatalf("Test #%d: Expected output to contain: %s, got: %s", i, testCase.ExpectedOutput, output)
}
}
}
func TestLoggingToFile(t *testing.T) {
file := filepath.Join(os.TempDir(), "access.log")
expectedOutput := "Hello world written to file"
logger := Logger{Output: file}
if err := logger.Start(); err != nil {
t.Fatalf("Got unexpected error during logger start: %v", err)
}
logger.Print(expectedOutput)
content, err := ioutil.ReadFile(file)
if err != nil {
t.Fatalf("Could not read log file content: %v", err)
}
if !bytes.Contains(content, []byte(expectedOutput)) {
t.Fatalf("Expected log file to contain: %s, got: %s", expectedOutput, string(content))
}
os.Remove(file)
}
func TestLoggingToSyslog(t *testing.T) {
testCases := []struct {
Output string
ExpectedOutput string
}{
{
Output: "syslog://127.0.0.1:5660",
ExpectedOutput: "Hello world! Test #1 over tcp",
},
{
Output: "syslog+tcp://127.0.0.1:5661",
ExpectedOutput: "Hello world! Test #2 over tcp",
},
{
Output: "syslog+udp://127.0.0.1:5662",
ExpectedOutput: "Hello world! Test #3 over udp",
},
}
for i, testCase := range testCases {
ch := make(chan format.LogParts, 256)
server, err := bootServer(testCase.Output, ch)
defer server.Kill()
if err != nil {
t.Errorf("Test #%d: expected no error during syslog server boot, got: %v", i, err)
}
logger := Logger{Output: testCase.Output, fileMu: new(sync.RWMutex)}
if err := logger.Start(); err != nil {
t.Errorf("Test #%d: expected no error during logger start, got: %v", i, err)
}
defer logger.Close()
logger.Print(testCase.ExpectedOutput)
actual := <-ch
if content, ok := actual["content"].(string); ok {
if !strings.Contains(content, testCase.ExpectedOutput) {
t.Errorf("Test #%d: expected server to capture content: %s, but got: %s", i, testCase.ExpectedOutput, content)
}
} else {
t.Errorf("Test #%d: expected server to capture content but got: %v", i, actual)
}
}
}
func bootServer(location string, ch chan format.LogParts) (*syslog.Server, error) {
address := parseSyslogAddress(location)
if address == nil {
return nil, fmt.Errorf("Could not parse syslog address: %s", location)
}
server := syslog.NewServer()
server.SetFormat(syslog.Automatic)
switch address.network {
case "tcp":
if err := server.ListenTCP(address.address); err != nil {
log.Println("[ERROR] server failed to listen on TCP address: ", err)
}
case "udp":
if err := server.ListenUDP(address.address); err != nil {
log.Println("[ERROR] server failed to listen on UDP address: ", err)
}
}
server.SetHandler(syslog.NewChannelHandler(ch))
if err := server.Boot(); err != nil {
return nil, err
}
return server, nil
}
func captureStdout(f func()) string {
original := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
f()
w.Close()
written, _ := ioutil.ReadAll(r)
os.Stdout = original
return string(written)
}
func captureStderr(f func()) string {
original := os.Stderr
r, w, _ := os.Pipe()
os.Stderr = w
f()
w.Close()
written, _ := ioutil.ReadAll(r)
os.Stderr = original
return string(written)
}
+228
View File
@@ -0,0 +1,228 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"fmt"
"net/http"
"os"
"path"
"time"
"github.com/mholt/caddy"
)
func init() {
initCaseSettings()
}
type (
// Middleware is the middle layer which represents the traditional
// idea of middleware: it chains one Handler to the next by being
// passed the next Handler in the chain.
Middleware func(Handler) Handler
// ListenerMiddleware is similar to the Middleware type, except it
// chains one net.Listener to the next.
ListenerMiddleware func(caddy.Listener) caddy.Listener
// Handler is like http.Handler except ServeHTTP may return a status
// code and/or error.
//
// If ServeHTTP writes the response header, it should return a status
// code of 0. This signals to other handlers before it that the response
// is already handled, and that they should not write to it also. Keep
// in mind that writing to the response body writes the header, too.
//
// If ServeHTTP encounters an error, it should return the error value
// so it can be logged by designated error-handling middleware.
//
// If writing a response after calling the next ServeHTTP method, the
// returned status code SHOULD be used when writing the response.
//
// If handling errors after calling the next ServeHTTP method, the
// returned error value SHOULD be logged or handled accordingly.
//
// Otherwise, return values should be propagated down the middleware
// chain by returning them unchanged.
Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request) (int, error)
}
// HandlerFunc is a convenience type like http.HandlerFunc, except
// ServeHTTP returns a status code and an error. See Handler
// documentation for more information.
HandlerFunc func(http.ResponseWriter, *http.Request) (int, error)
// RequestMatcher checks to see if current request should be handled
// by underlying handler.
RequestMatcher interface {
Match(r *http.Request) bool
}
// HandlerConfig is a middleware configuration.
// This makes it possible for middlewares to have a common
// configuration interface.
//
// TODO The long term plan is to get all middleware implement this
// interface for configurations.
HandlerConfig interface {
RequestMatcher
BasePath() string
}
// ConfigSelector selects a configuration.
ConfigSelector []HandlerConfig
)
// ServeHTTP implements the Handler interface.
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
return f(w, r)
}
// Select selects a Config.
// This chooses the config with the longest length.
func (c ConfigSelector) Select(r *http.Request) (config HandlerConfig) {
for i := range c {
if !c[i].Match(r) {
continue
}
if config == nil || len(c[i].BasePath()) > len(config.BasePath()) {
config = c[i]
}
}
return config
}
// IndexFile looks for a file in /root/fpath/indexFile for each string
// in indexFiles. If an index file is found, it returns the root-relative
// path to the file and true. If no index file is found, empty string
// and false is returned. fpath must end in a forward slash '/'
// otherwise no index files will be tried (directory paths must end
// in a forward slash according to HTTP).
//
// All paths passed into and returned from this function use '/' as the
// path separator, just like URLs. IndexFle handles path manipulation
// internally for systems that use different path separators.
func IndexFile(root http.FileSystem, fpath string, indexFiles []string) (string, bool) {
if fpath[len(fpath)-1] != '/' || root == nil {
return "", false
}
for _, indexFile := range indexFiles {
// func (http.FileSystem).Open wants all paths separated by "/",
// regardless of operating system convention, so use
// path.Join instead of filepath.Join
fp := path.Join(fpath, indexFile)
f, err := root.Open(fp)
if err == nil {
f.Close()
return fp, true
}
}
return "", false
}
// SetLastModifiedHeader checks if the provided modTime is valid and if it is sets it
// as a Last-Modified header to the ResponseWriter. If the modTime is in the future
// the current time is used instead.
func SetLastModifiedHeader(w http.ResponseWriter, modTime time.Time) {
if modTime.IsZero() || modTime.Equal(time.Unix(0, 0)) {
// the time does not appear to be valid. Don't put it in the response
return
}
// RFC 2616 - Section 14.29 - Last-Modified:
// An origin server MUST NOT send a Last-Modified date which is later than the
// server's time of message origination. In such cases, where the resource's last
// modification would indicate some time in the future, the server MUST replace
// that date with the message origination date.
now := currentTime()
if modTime.After(now) {
modTime = now
}
w.Header().Set("Last-Modified", modTime.UTC().Format(http.TimeFormat))
}
// CaseSensitivePath determines if paths should be case sensitive.
// This is configurable via CASE_SENSITIVE_PATH environment variable.
var CaseSensitivePath = false
const caseSensitivePathEnv = "CASE_SENSITIVE_PATH"
// initCaseSettings loads case sensitivity config from environment variable.
//
// This could have been in init, but init cannot be called from tests.
func initCaseSettings() {
switch os.Getenv(caseSensitivePathEnv) {
case "1", "true":
CaseSensitivePath = true
default:
CaseSensitivePath = false
}
}
// MergeRequestMatchers merges multiple RequestMatchers into one.
// This allows a middleware to use multiple RequestMatchers.
func MergeRequestMatchers(matchers ...RequestMatcher) RequestMatcher {
return requestMatchers(matchers)
}
type requestMatchers []RequestMatcher
// Match satisfies RequestMatcher interface.
func (m requestMatchers) Match(r *http.Request) bool {
for _, matcher := range m {
if !matcher.Match(r) {
return false
}
}
return true
}
// currentTime, as it is defined here, returns time.Now().
// It's defined as a variable for mocking time in tests.
var currentTime = func() time.Time { return time.Now() }
// EmptyNext is a no-op function that can be passed into
// 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
// plugins can use this as a convenience.
var EmptyNext = 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
// plugins can use this as a convenience.
func SameNext(next1, next2 Handler) bool {
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
}
// Context key constants.
const (
// ReplacerCtxKey is the context key for a per-request replacer.
ReplacerCtxKey caddy.CtxKey = "replacer"
// RemoteUserCtxKey is the key for the remote user of the request, if any (basicauth).
RemoteUserCtxKey caddy.CtxKey = "remote_user"
// MitmCtxKey is the key for the result of MITM detection
MitmCtxKey caddy.CtxKey = "mitm"
// RequestIDCtxKey is the key for the U4 UUID value
RequestIDCtxKey caddy.CtxKey = "request_id"
)
+72
View File
@@ -0,0 +1,72 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"os"
"testing"
)
func TestPathCaseSensitivity(t *testing.T) {
tests := []struct {
basePath string
path string
caseSensitive bool
expected bool
}{
{"/", "/file", true, true},
{"/a", "/file", true, false},
{"/f", "/file", true, true},
{"/f", "/File", true, false},
{"/f", "/File", false, true},
{"/file", "/file", true, true},
{"/file", "/file", false, true},
{"/files", "/file", false, false},
{"/files", "/file", true, false},
{"/folder", "/folder/file.txt", true, true},
{"/folders", "/folder/file.txt", true, false},
{"/folder", "/Folder/file.txt", false, true},
{"/folders", "/Folder/file.txt", false, false},
}
for i, test := range tests {
CaseSensitivePath = test.caseSensitive
valid := Path(test.path).Matches(test.basePath)
if test.expected != valid {
t.Errorf("Test %d: Expected %v, found %v", i, test.expected, valid)
}
}
}
func TestPathCaseSensitiveEnv(t *testing.T) {
tests := []struct {
envValue string
expected bool
}{
{"1", true},
{"0", false},
{"false", false},
{"true", true},
{"", false},
}
for i, test := range tests {
os.Setenv(caseSensitivePathEnv, test.envValue)
initCaseSettings()
if test.expected != CaseSensitivePath {
t.Errorf("Test %d: Expected %v, found %v", i, test.expected, CaseSensitivePath)
}
}
}
+780
View File
@@ -0,0 +1,780 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"bytes"
"context"
"crypto/tls"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"github.com/mholt/caddy/caddytls"
"github.com/mholt/caddy/telemetry"
)
// tlsHandler is a http.Handler that will inject a value
// into the request context indicating if the TLS
// connection is likely being intercepted.
type tlsHandler struct {
next http.Handler
listener *tlsHelloListener
closeOnMITM bool // whether to close connection on MITM; TODO: expose through new directive
}
// ServeHTTP checks the User-Agent. For the four main browsers (Chrome,
// Edge, Firefox, and Safari) indicated by the User-Agent, the properties
// of the TLS Client Hello will be compared. The context value "mitm" will
// be set to a value indicating if it is likely that the underlying TLS
// connection is being intercepted.
//
// Note that due to Microsoft's decision to intentionally make IE/Edge
// user agents obscure (and look like other browsers), this may offer
// less accuracy for IE/Edge clients.
//
// This MITM detection capability is based on research done by Durumeric,
// Halderman, et. al. in "The Security Impact of HTTPS Interception" (NDSS '17):
// https://jhalderm.com/pub/papers/interception-ndss17.pdf
func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// TODO: one request per connection, we should report UA in connection with
// handshake (reported in caddytls package) and our MITM assessment
if h.listener == nil {
h.next.ServeHTTP(w, r)
return
}
h.listener.helloInfosMu.RLock()
info := h.listener.helloInfos[r.RemoteAddr]
h.listener.helloInfosMu.RUnlock()
ua := r.Header.Get("User-Agent")
uaHash := telemetry.FastHash([]byte(ua))
// report this request's UA in connection with this ClientHello
go telemetry.AppendUnique("tls_client_hello_ua:"+caddytls.ClientHelloInfo(info).Key(), uaHash)
var checked, mitm bool
if r.Header.Get("X-BlueCoat-Via") != "" || // Blue Coat (masks User-Agent header to generic values)
r.Header.Get("X-FCCKV2") != "" || // Fortinet
info.advertisesHeartbeatSupport() { // no major browsers have ever implemented Heartbeat
// TODO: Move the heartbeat check into each "looksLike" function...
checked = true
mitm = true
} else if strings.Contains(ua, "Edge") || strings.Contains(ua, "MSIE") ||
strings.Contains(ua, "Trident") {
checked = true
mitm = !info.looksLikeEdge()
} else if strings.Contains(ua, "Chrome") {
checked = true
mitm = !info.looksLikeChrome()
} else if strings.Contains(ua, "CriOS") {
// Chrome on iOS sometimes uses iOS-provided TLS stack (which looks exactly like Safari)
// but for connections that don't render a web page (favicon, etc.) it uses its own...
checked = true
mitm = !info.looksLikeChrome() && !info.looksLikeSafari()
} else if strings.Contains(ua, "Firefox") {
checked = true
if strings.Contains(ua, "Windows") {
ver := getVersion(ua, "Firefox")
if ver == 45.0 || ver == 52.0 {
mitm = !info.looksLikeTor()
} else {
mitm = !info.looksLikeFirefox()
}
} else {
mitm = !info.looksLikeFirefox()
}
} else if strings.Contains(ua, "Safari") {
checked = true
mitm = !info.looksLikeSafari()
}
if checked {
r = r.WithContext(context.WithValue(r.Context(), MitmCtxKey, mitm))
if mitm {
go telemetry.AppendUnique("http_mitm", "likely")
} else {
go telemetry.AppendUnique("http_mitm", "unlikely")
}
} else {
go telemetry.AppendUnique("http_mitm", "unknown")
}
if mitm && h.closeOnMITM {
// TODO: This termination might need to happen later in the middleware
// chain in order to be picked up by the log directive, in case the site
// owner still wants to log this event. It'll probably require a new
// directive. If this feature is useful, we can finish implementing this.
r.Close = true
return
}
h.next.ServeHTTP(w, r)
}
// getVersion returns a (possibly simplified) representation of the version string
// from a UserAgent string. It returns a float, so it can represent major and minor
// versions; the rest of the version is just tacked on behind the decimal point.
// The purpose of this is to stay simple while allowing for basic, fast comparisons.
// If the version for softwareName is not found in ua, -1 is returned.
func getVersion(ua, softwareName string) float64 {
search := softwareName + "/"
start := strings.Index(ua, search)
if start < 0 {
return -1
}
start += len(search)
end := strings.Index(ua[start:], " ")
if end < 0 {
end = len(ua)
} else {
end += start
}
strVer := strings.Replace(ua[start:end], "-", "", -1)
firstDot := strings.Index(strVer, ".")
if firstDot >= 0 {
strVer = strVer[:firstDot+1] + strings.Replace(strVer[firstDot+1:], ".", "", -1)
}
ver, err := strconv.ParseFloat(strVer, 64)
if err != nil {
return -1
}
return ver
}
// clientHelloConn reads the ClientHello
// and stores it in the attached listener.
type clientHelloConn struct {
net.Conn
listener *tlsHelloListener
readHello bool // whether ClientHello has been read
buf *bytes.Buffer
}
// Read reads from c.Conn (by letting the standard library
// do the reading off the wire), with the exception of
// getting a copy of the ClientHello so it can parse it.
func (c *clientHelloConn) Read(b []byte) (n int, err error) {
// if we've already read the ClientHello, pass thru
if c.readHello {
return c.Conn.Read(b)
}
// we let the standard lib read off the wire for us, and
// tee that into our buffer so we can read the ClientHello
tee := io.TeeReader(c.Conn, c.buf)
n, err = tee.Read(b)
if err != nil {
return
}
if c.buf.Len() < 5 {
return // need to read more bytes for header
}
// read the header bytes
hdr := make([]byte, 5)
_, err = io.ReadFull(c.buf, hdr)
if err != nil {
return // this would be highly unusual and sad
}
// get length of the ClientHello message and read it
length := int(uint16(hdr[3])<<8 | uint16(hdr[4]))
if c.buf.Len() < length {
return // need to read more bytes
}
hello := make([]byte, length)
_, err = io.ReadFull(c.buf, hello)
if err != nil {
return
}
bufpool.Put(c.buf) // buffer no longer needed
// parse the ClientHello and store it in the map
rawParsed := parseRawClientHello(hello)
c.listener.helloInfosMu.Lock()
c.listener.helloInfos[c.Conn.RemoteAddr().String()] = rawParsed
c.listener.helloInfosMu.Unlock()
// report this ClientHello to telemetry
chKey := caddytls.ClientHelloInfo(rawParsed).Key()
go telemetry.SetNested("tls_client_hello", chKey, rawParsed)
go telemetry.AppendUnique("tls_client_hello_count", chKey)
c.readHello = true
return
}
// parseRawClientHello parses data which contains the raw
// TLS Client Hello message. It extracts relevant information
// into info. Any error reading the Client Hello (such as
// insufficient length or invalid length values) results in
// a silent error and an incomplete info struct, since there
// is no good way to handle an error like this during Accept().
// The data is expected to contain the whole ClientHello and
// ONLY the ClientHello.
//
// The majority of this code is borrowed from the Go standard
// library, which is (c) The Go Authors. It has been modified
// to fit this use case.
func parseRawClientHello(data []byte) (info rawHelloInfo) {
if len(data) < 42 {
return
}
info.Version = uint16(data[4])<<8 | uint16(data[5])
sessionIDLen := int(data[38])
if sessionIDLen > 32 || len(data) < 39+sessionIDLen {
return
}
data = data[39+sessionIDLen:]
if len(data) < 2 {
return
}
// cipherSuiteLen is the number of bytes of cipher suite numbers. Since
// they are uint16s, the number must be even.
cipherSuiteLen := int(data[0])<<8 | int(data[1])
if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen {
return
}
numCipherSuites := cipherSuiteLen / 2
// read in the cipher suites
info.CipherSuites = make([]uint16, numCipherSuites)
for i := 0; i < numCipherSuites; i++ {
info.CipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i])
}
data = data[2+cipherSuiteLen:]
if len(data) < 1 {
return
}
// read in the compression methods
compressionMethodsLen := int(data[0])
if len(data) < 1+compressionMethodsLen {
return
}
info.CompressionMethods = data[1 : 1+compressionMethodsLen]
data = data[1+compressionMethodsLen:]
// ClientHello is optionally followed by extension data
if len(data) < 2 {
return
}
extensionsLength := int(data[0])<<8 | int(data[1])
data = data[2:]
if extensionsLength != len(data) {
return
}
// read in each extension, and extract any relevant information
// from extensions we care about
for len(data) != 0 {
if len(data) < 4 {
return
}
extension := uint16(data[0])<<8 | uint16(data[1])
length := int(data[2])<<8 | int(data[3])
data = data[4:]
if len(data) < length {
return
}
// record that the client advertised support for this extension
info.Extensions = append(info.Extensions, extension)
switch extension {
case extensionSupportedCurves:
// http://tools.ietf.org/html/rfc4492#section-5.5.1
if length < 2 {
return
}
l := int(data[0])<<8 | int(data[1])
if l%2 == 1 || length != l+2 {
return
}
numCurves := l / 2
info.Curves = make([]tls.CurveID, numCurves)
d := data[2:]
for i := 0; i < numCurves; i++ {
info.Curves[i] = tls.CurveID(d[0])<<8 | tls.CurveID(d[1])
d = d[2:]
}
case extensionSupportedPoints:
// http://tools.ietf.org/html/rfc4492#section-5.5.2
if length < 1 {
return
}
l := int(data[0])
if length != l+1 {
return
}
info.Points = make([]uint8, l)
copy(info.Points, data[1:])
}
data = data[length:]
}
return
}
// newTLSListener returns a new tlsHelloListener that wraps ln.
func newTLSListener(ln net.Listener, config *tls.Config) *tlsHelloListener {
return &tlsHelloListener{
Listener: ln,
config: config,
helloInfos: make(map[string]rawHelloInfo),
}
}
// tlsHelloListener is a TLS listener that is specially designed
// to read the ClientHello manually so we can extract necessary
// information from it. Each ClientHello message is mapped by
// the remote address of the client, which must be removed when
// the connection is closed (use ConnState).
type tlsHelloListener struct {
net.Listener
config *tls.Config
helloInfos map[string]rawHelloInfo
helloInfosMu sync.RWMutex
}
// Accept waits for and returns the next connection to the listener.
// After it accepts the underlying connection, it reads the
// ClientHello message and stores the parsed data into a map on l.
func (l *tlsHelloListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
buf := bufpool.Get().(*bytes.Buffer)
buf.Reset()
helloConn := &clientHelloConn{Conn: conn, listener: l, buf: buf}
return tls.Server(helloConn, l.config), nil
}
// rawHelloInfo contains the "raw" data parsed from the TLS
// Client Hello. No interpretation is done on the raw data.
//
// The methods on this type implement heuristics described
// by Durumeric, Halderman, et. al. in
// "The Security Impact of HTTPS Interception":
// https://jhalderm.com/pub/papers/interception-ndss17.pdf
type rawHelloInfo caddytls.ClientHelloInfo
// advertisesHeartbeatSupport returns true if info indicates
// that the client supports the Heartbeat extension.
func (info rawHelloInfo) advertisesHeartbeatSupport() bool {
for _, ext := range info.Extensions {
if ext == extensionHeartbeat {
return true
}
}
return false
}
// looksLikeFirefox returns true if info looks like a handshake
// from a modern version of Firefox.
func (info rawHelloInfo) looksLikeFirefox() bool {
// "To determine whether a Firefox session has been
// intercepted, we check for the presence and order
// of extensions, cipher suites, elliptic curves,
// EC point formats, and handshake compression methods." (early 2016)
// We check for the presence and order of the extensions.
// Note: Sometimes 0x15 (21, padding) is present, sometimes not.
// Note: Firefox 51+ does not advertise 0x3374 (13172, NPN).
// Note: Firefox doesn't advertise 0x0 (0, SNI) when connecting to IP addresses.
// Note: Firefox 55+ doesn't appear to advertise 0xFF03 (65283, short headers). It used to be between 5 and 13.
// Note: Firefox on Fedora (or RedHat) doesn't include ECC suites because of patent liability.
requiredExtensionsOrder := []uint16{23, 65281, 10, 11, 35, 16, 5, 13}
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) {
return false
}
// We check for both presence of curves and their ordering.
requiredCurves := []tls.CurveID{29, 23, 24, 25}
if len(info.Curves) < len(requiredCurves) {
return false
}
for i := range requiredCurves {
if info.Curves[i] != requiredCurves[i] {
return false
}
}
if len(info.Curves) > len(requiredCurves) {
// newer Firefox (55 Nightly?) may have additional curves at end of list
allowedCurves := []tls.CurveID{256, 257}
for i := range allowedCurves {
if info.Curves[len(requiredCurves)+i] != allowedCurves[i] {
return false
}
}
}
if hasGreaseCiphers(info.CipherSuites) {
return false
}
// We check for order of cipher suites but not presence, since
// according to the paper, cipher suites may be not be added
// or reordered by the user, but they may be disabled.
expectedCipherSuiteOrder := []uint16{
TLS_AES_128_GCM_SHA256, // 0x1301
TLS_CHACHA20_POLY1305_SHA256, // 0x1303
TLS_AES_256_GCM_SHA384, // 0x1302
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // 0xc02f
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // 0xcca9
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, // 0xcca8
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, // 0xc030
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // 0xc00a
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // 0xc009
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // 0xc013
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // 0xc014
TLS_DHE_RSA_WITH_AES_128_CBC_SHA, // 0x33
TLS_DHE_RSA_WITH_AES_256_CBC_SHA, // 0x39
tls.TLS_RSA_WITH_AES_128_CBC_SHA, // 0x2f
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // 0xa
}
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, false)
}
// looksLikeChrome returns true if info looks like a handshake
// from a modern version of Chrome.
func (info rawHelloInfo) looksLikeChrome() bool {
// "We check for ciphers and extensions that Chrome is known
// to not support, but do not check for the inclusion of
// specific ciphers or extensions, nor do we validate their
// order. When appropriate, we check the presence and order
// of elliptic curves, compression methods, and EC point formats." (early 2016)
// Not in Chrome 56, but present in Safari 10 (Feb. 2017):
// TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (0xc024)
// TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (0xc023)
// TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
// TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
// TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (0xc028)
// TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (0xc027)
// TLS_RSA_WITH_AES_256_CBC_SHA256 (0x3d)
// TLS_RSA_WITH_AES_128_CBC_SHA256 (0x3c)
// Not in Chrome 56, but present in Firefox 51 (Feb. 2017):
// TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
// TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
// TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x33)
// TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x39)
// Selected ciphers present in Chrome mobile (Feb. 2017):
// 0xc00a, 0xc014, 0xc009, 0x9c, 0x9d, 0x2f, 0x35, 0xa
chromeCipherExclusions := map[uint16]struct{}{
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384: {}, // 0xc024
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256: {}, // 0xc023
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384: {}, // 0xc028
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256: {}, // 0xc027
TLS_RSA_WITH_AES_256_CBC_SHA256: {}, // 0x3d
tls.TLS_RSA_WITH_AES_128_CBC_SHA256: {}, // 0x3c
TLS_DHE_RSA_WITH_AES_128_CBC_SHA: {}, // 0x33
TLS_DHE_RSA_WITH_AES_256_CBC_SHA: {}, // 0x39
}
for _, ext := range info.CipherSuites {
if _, ok := chromeCipherExclusions[ext]; ok {
return false
}
}
// Chrome does not include curve 25 (CurveP521) (as of Chrome 56, Feb. 2017).
for _, curve := range info.Curves {
if curve == 25 {
return false
}
}
if !hasGreaseCiphers(info.CipherSuites) {
return false
}
return true
}
// looksLikeEdge returns true if info looks like a handshake
// from a modern version of MS Edge.
func (info rawHelloInfo) looksLikeEdge() bool {
// "SChannel connections can by uniquely identified because SChannel
// is the only TLS library we tested that includes the OCSP status
// request extension before the supported groups and EC point formats
// extensions." (early 2016)
//
// More specifically, the OCSP status request extension appears
// *directly* before the other two extensions, which occur in that
// order. (I contacted the authors for clarification and verified it.)
for i, ext := range info.Extensions {
if ext == extensionOCSPStatusRequest {
if len(info.Extensions) <= i+2 {
return false
}
if info.Extensions[i+1] != extensionSupportedCurves ||
info.Extensions[i+2] != extensionSupportedPoints {
return false
}
}
}
for _, cs := range info.CipherSuites {
// As of Feb. 2017, Edge does not have 0xff, but Avast adds it
if cs == scsvRenegotiation {
return false
}
// Edge and modern IE do not have 0x4 or 0x5, but Blue Coat does
if cs == TLS_RSA_WITH_RC4_128_MD5 || cs == tls.TLS_RSA_WITH_RC4_128_SHA {
return false
}
}
if hasGreaseCiphers(info.CipherSuites) {
return false
}
return true
}
// looksLikeSafari returns true if info looks like a handshake
// from a modern version of MS Safari.
func (info rawHelloInfo) looksLikeSafari() bool {
// "One unique aspect of Secure Transport is that it includes
// the TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0xff) cipher first,
// whereas the other libraries we investigated include the
// cipher last. Similar to Microsoft, Apple has changed
// TLS behavior in minor OS updates, which are not indicated
// in the HTTP User-Agent header. We allow for any of the
// updates when validating handshakes, and we check for the
// presence and ordering of ciphers, extensions, elliptic
// curves, and compression methods." (early 2016)
// Note that any C lib (e.g. curl) compiled on macOS
// will probably use Secure Transport which will also
// share the TLS handshake characteristics of Safari.
// We check for the presence and order of the extensions.
requiredExtensionsOrder := []uint16{10, 11, 13, 13172, 16, 5, 18, 23}
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) {
// Safari on iOS 11 (beta) uses different set/ordering of extensions
requiredExtensionsOrderiOS11 := []uint16{65281, 0, 23, 13, 5, 13172, 18, 16, 11, 10}
if !assertPresenceAndOrdering(requiredExtensionsOrderiOS11, info.Extensions, true) {
return false
}
} else {
// For these versions of Safari, expect TLS_EMPTY_RENEGOTIATION_INFO_SCSV first.
if len(info.CipherSuites) < 1 {
return false
}
if info.CipherSuites[0] != scsvRenegotiation {
return false
}
}
if hasGreaseCiphers(info.CipherSuites) {
return false
}
// We check for order and presence of cipher suites
expectedCipherSuiteOrder := []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, // 0xc024
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, // 0xc023
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // 0xc00a
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // 0xc009
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, // 0xc030
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // 0xc02f
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, // 0xc028
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, // 0xc027
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // 0xc014
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // 0xc013
tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // 0x9d
tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // 0x9c
TLS_RSA_WITH_AES_256_CBC_SHA256, // 0x3d
tls.TLS_RSA_WITH_AES_128_CBC_SHA256, // 0x3c
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35
tls.TLS_RSA_WITH_AES_128_CBC_SHA, // 0x2f
}
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, true)
}
// looksLikeTor returns true if the info looks like a ClientHello from Tor browser
// (based on Firefox).
func (info rawHelloInfo) looksLikeTor() bool {
requiredExtensionsOrder := []uint16{10, 11, 16, 5, 13}
if !assertPresenceAndOrdering(requiredExtensionsOrder, info.Extensions, true) {
return false
}
// check for session tickets support; Tor doesn't support them to prevent tracking
for _, ext := range info.Extensions {
if ext == 35 {
return false
}
}
// We check for both presence of curves and their ordering, including
// an optional curve at the beginning (for Tor based on Firefox 52)
infoCurves := info.Curves
if len(info.Curves) == 4 {
if info.Curves[0] != 29 {
return false
}
infoCurves = info.Curves[1:]
}
requiredCurves := []tls.CurveID{23, 24, 25}
if len(infoCurves) < len(requiredCurves) {
return false
}
for i := range requiredCurves {
if infoCurves[i] != requiredCurves[i] {
return false
}
}
if hasGreaseCiphers(info.CipherSuites) {
return false
}
// We check for order of cipher suites but not presence, since
// according to the paper, cipher suites may be not be added
// or reordered by the user, but they may be disabled.
expectedCipherSuiteOrder := []uint16{
TLS_AES_128_GCM_SHA256, // 0x1301
TLS_CHACHA20_POLY1305_SHA256, // 0x1303
TLS_AES_256_GCM_SHA384, // 0x1302
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // 0xc02f
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // 0xcca9
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, // 0xcca8
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, // 0xc030
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // 0xc00a
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // 0xc009
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // 0xc013
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // 0xc014
TLS_DHE_RSA_WITH_AES_128_CBC_SHA, // 0x33
TLS_DHE_RSA_WITH_AES_256_CBC_SHA, // 0x39
tls.TLS_RSA_WITH_AES_128_CBC_SHA, // 0x2f
tls.TLS_RSA_WITH_AES_256_CBC_SHA, // 0x35
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // 0xa
}
return assertPresenceAndOrdering(expectedCipherSuiteOrder, info.CipherSuites, false)
}
// assertPresenceAndOrdering will return true if candidateList contains
// the items in requiredItems in the same order as requiredItems.
//
// If requiredIsSubset is true, then all items in requiredItems must be
// present in candidateList. If requiredIsSubset is false, then requiredItems
// may contain items that are not in candidateList.
//
// In all cases, the order of requiredItems is enforced.
func assertPresenceAndOrdering(requiredItems, candidateList []uint16, requiredIsSubset bool) bool {
superset := requiredItems
subset := candidateList
if requiredIsSubset {
superset = candidateList
subset = requiredItems
}
var j int
for _, item := range subset {
var found bool
for j < len(superset) {
if superset[j] == item {
found = true
break
}
j++
}
if j == len(superset) && !found {
return false
}
}
return true
}
func hasGreaseCiphers(cipherSuites []uint16) bool {
for _, cipher := range cipherSuites {
if _, ok := greaseCiphers[cipher]; ok {
return true
}
}
return false
}
// pool buffers so we can reuse allocations over time
var bufpool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
var greaseCiphers = map[uint16]struct{}{
0x0A0A: {},
0x1A1A: {},
0x2A2A: {},
0x3A3A: {},
0x4A4A: {},
0x5A5A: {},
0x6A6A: {},
0x7A7A: {},
0x8A8A: {},
0x9A9A: {},
0xAAAA: {},
0xBABA: {},
0xCACA: {},
0xDADA: {},
0xEAEA: {},
0xFAFA: {},
}
// Define variables used for TLS communication
const (
extensionOCSPStatusRequest = 5
extensionSupportedCurves = 10 // also called "SupportedGroups"
extensionSupportedPoints = 11
extensionHeartbeat = 15
scsvRenegotiation = 0xff
// cipher suites missing from the crypto/tls package,
// in no particular order here
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xc024
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 = 0xc028
TLS_RSA_WITH_AES_256_CBC_SHA256 = 0x3d
TLS_DHE_RSA_WITH_AES_128_CBC_SHA = 0x33
TLS_DHE_RSA_WITH_AES_256_CBC_SHA = 0x39
TLS_RSA_WITH_RC4_128_MD5 = 0x4
// new PSK ciphers introduced by TLS 1.3, not (yet) in crypto/tls
// https://tlswg.github.io/tls13-spec/#rfc.appendix.A.4)
TLS_AES_128_GCM_SHA256 = 0x1301
TLS_AES_256_GCM_SHA384 = 0x1302
TLS_CHACHA20_POLY1305_SHA256 = 0x1303
TLS_AES_128_CCM_SHA256 = 0x1304
TLS_AES_128_CCM_8_SHA256 = 0x1305
)
+424
View File
@@ -0,0 +1,424 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"crypto/tls"
"encoding/hex"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
func TestParseClientHello(t *testing.T) {
for i, test := range []struct {
inputHex string
expected rawHelloInfo
}{
{
// curl 7.51.0 (x86_64-apple-darwin16.0) libcurl/7.51.0 SecureTransport zlib/1.2.8
inputHex: `010000a6030358a28c73a71bdfc1f09dee13fecdc58805dcce42ac44254df548f14645f7dc2c00004400ffc02cc02bc024c023c00ac009c008c030c02fc028c027c014c013c012009f009e006b0067003900330016009d009c003d003c0035002f000a00af00ae008d008c008b01000039000a00080006001700180019000b00020100000d00120010040102010501060104030203050306030005000501000000000012000000170000`,
expected: rawHelloInfo{
Version: 0x303,
CipherSuites: []uint16{255, 49196, 49195, 49188, 49187, 49162, 49161, 49160, 49200, 49199, 49192, 49191, 49172, 49171, 49170, 159, 158, 107, 103, 57, 51, 22, 157, 156, 61, 60, 53, 47, 10, 175, 174, 141, 140, 139},
Extensions: []uint16{10, 11, 13, 5, 18, 23},
CompressionMethods: []byte{0},
Curves: []tls.CurveID{23, 24, 25},
Points: []uint8{0},
},
},
{
// Chrome 56
inputHex: `010000c003031dae75222dae1433a5a283ddcde8ddabaefbf16d84f250eee6fdff48cdfff8a00000201a1ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010000777a7a0000ff010001000000000e000c0000096c6f63616c686f73740017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a0008aaaa001d001700182a2a000100`,
expected: rawHelloInfo{
Version: 0x303,
CipherSuites: []uint16{6682, 49195, 49199, 49196, 49200, 52393, 52392, 52244, 52243, 49171, 49172, 156, 157, 47, 53, 10},
Extensions: []uint16{31354, 65281, 0, 23, 35, 13, 5, 18, 16, 30032, 11, 10, 10794},
CompressionMethods: []byte{0},
Curves: []tls.CurveID{43690, 29, 23, 24},
Points: []uint8{0},
},
},
{
// Firefox 51
inputHex: `010000bd030375f9022fc3a6562467f3540d68013b2d0b961979de6129e944efe0b35531323500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a010000760000000e000c0000096c6f63616c686f737400170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000ff030000000d0020001e040305030603020308040805080604010501060102010402050206020202`,
expected: rawHelloInfo{
Version: 0x303,
CipherSuites: []uint16{49195, 49199, 52393, 52392, 49196, 49200, 49162, 49161, 49171, 49172, 51, 57, 47, 53, 10},
Extensions: []uint16{0, 23, 65281, 10, 11, 35, 16, 5, 65283, 13},
CompressionMethods: []byte{0},
Curves: []tls.CurveID{29, 23, 24, 25},
Points: []uint8{0},
},
},
{
// openssl s_client (OpenSSL 0.9.8zh 14 Jan 2016)
inputHex: `0100012b03035d385236b8ca7b7946fa0336f164e76bf821ed90e8de26d97cc677671b6f36380000acc030c02cc028c024c014c00a00a500a300a1009f006b006a0069006800390038003700360088008700860085c032c02ec02ac026c00fc005009d003d00350084c02fc02bc027c023c013c00900a400a200a0009e00670040003f003e0033003200310030009a0099009800970045004400430042c031c02dc029c025c00ec004009c003c002f009600410007c011c007c00cc00200050004c012c008001600130010000dc00dc003000a00ff0201000055000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230000000d0020001e060106020603050105020503040104020403030103020303020102020203000f000101`,
expected: rawHelloInfo{
Version: 0x303,
CipherSuites: []uint16{49200, 49196, 49192, 49188, 49172, 49162, 165, 163, 161, 159, 107, 106, 105, 104, 57, 56, 55, 54, 136, 135, 134, 133, 49202, 49198, 49194, 49190, 49167, 49157, 157, 61, 53, 132, 49199, 49195, 49191, 49187, 49171, 49161, 164, 162, 160, 158, 103, 64, 63, 62, 51, 50, 49, 48, 154, 153, 152, 151, 69, 68, 67, 66, 49201, 49197, 49193, 49189, 49166, 49156, 156, 60, 47, 150, 65, 7, 49169, 49159, 49164, 49154, 5, 4, 49170, 49160, 22, 19, 16, 13, 49165, 49155, 10, 255},
Extensions: []uint16{11, 10, 35, 13, 15},
CompressionMethods: []byte{1, 0},
Curves: []tls.CurveID{23, 25, 28, 27, 24, 26, 22, 14, 13, 11, 12, 9, 10},
Points: []uint8{0, 1, 2},
},
},
} {
data, err := hex.DecodeString(test.inputHex)
if err != nil {
t.Fatalf("Test %d: Could not decode hex data: %v", i, err)
}
actual := parseRawClientHello(data)
if !reflect.DeepEqual(test.expected, actual) {
t.Errorf("Test %d: Expected %+v; got %+v", i, test.expected, actual)
}
}
}
func TestHeuristicFunctionsAndHandler(t *testing.T) {
// To test the heuristics, we assemble a collection of real
// ClientHello messages from various TLS clients, both genuine
// and intercepted. Please be sure to hex-encode them and
// document the User-Agent associated with the connection
// as well as any intercepting proxy as thoroughly as possible.
//
// If the TLS client used is not an HTTP client (e.g. s_client),
// you can leave the userAgent blank, but please use a comment
// to document crucial missing information such as client name,
// version, and platform, maybe even the date you collected
// the sample! Please group similar clients together, ordered
// by version for convenience.
// clientHello pairs a User-Agent string to its ClientHello message.
type clientHello struct {
userAgent string
helloHex string // do NOT include the header, just the ClientHello message
interception bool // if test case shows an interception, set to true
reqHeaders http.Header // if the request should set any headers to imitate a browser or proxy
}
// clientHellos groups samples of true (real) ClientHellos by the
// name of the browser that produced them. We limit the set of
// browsers to those we are programmed to protect, as well as a
// category for "Other" which contains real ClientHello messages
// from clients that we do not recognize, which may be used to
// test or imitate interception scenarios.
//
// Please group similar clients and order by version for convenience
// when adding to the test cases.
clientHellos := map[string][]clientHello{
"Chrome": {
{
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
helloHex: `010000c003031dae75222dae1433a5a283ddcde8ddabaefbf16d84f250eee6fdff48cdfff8a00000201a1ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010000777a7a0000ff010001000000000e000c0000096c6f63616c686f73740017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a0008aaaa001d001700182a2a000100`,
interception: false,
},
{
// Chrome on iOS will use iOS' TLS stack for requests that load
// the web page (apparently required by the dev ToS) but will use its
// own TLS stack for everything else, it seems.
// Chrome on iOS
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.79 Mobile/14A456 Safari/602.1",
helloHex: `010000de030358b062c509b21410a6496b5a82bfec74436cdecebe8ea1da29799939bbd3c17200002c00ffc02cc02bc024c023c00ac009c008c030c02fc028c027c014c013c012009d009c003d003c0035002f000a0100008900000014001200000f66696e6572706978656c732e636f6d000a00080006001700180019000b00020100000d00120010040102010501060104030203050306033374000000100030002e0268320568322d31360568322d31350568322d313408737064792f332e3106737064792f3308687474702f312e310005000501000000000012000000170000`,
},
{
// Chrome on iOS (requesting favicon)
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.79 Mobile/14A456 Safari/602.1",
helloHex: `010000c20303863eb64788e3b9638c261300318411cbdd8f09576d58eec1e744b6ce944f574f0000208a8acca9cca8cc14cc13c02bc02fc02cc030c013c014009c009d002f0035000a01000079baba0000ff0100010000000014001200000f66696e6572706978656c732e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e31000b00020100000a000a00083a3a001d001700184a4a000100`,
},
{
userAgent: "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
helloHex: `010000c603036f717a88212c3e9e41940f82c42acb3473e0e4a64e8f52d9af33d34e972e08a30000206a6ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a0100007d7a7a0000ff0100010000000014001200000f66696e6572706978656c732e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00087a7a001d001700188a8a000100`,
interception: false,
},
{
userAgent: "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
helloHex: `010001fc030383141d213d1bf069171843489faf808028d282c9828e1ba87637c863833c730720a67e76e152f4b704523b72317ef4587e231f02e2395e0ecac6be9f28c35e6ce600208a8ac02bc02fc02cc030cca9cca8cc14cc13c013c014009c009d002f0035000a010001931a1a0000ff0100010000000014001200000f66696e6572706978656c732e636f6d00170000002300785e85429bf1764f33111cd3ad5d1c56d765976fd962b49dbecbb6f7865e2a8d8536ad854f1fa99a8bbbf998814fee54a63a0bf162869d2bba37e9778304e7c4140825718e191b574c6246a0611de6447bdd80417f83ff9d9b7124069a9f74b90394ecb89bec5f6a1a67c1b89e50b8674782f53dd51807651a000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00081a1a001d001700182a2a0001000015009a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`,
interception: false,
},
{
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
helloHex: `010000c203034166c97e2016046e0c88ad867c410d0aee470f4d9b4ec8fe41a751d2a6348e3100001c4a4ac02bc02fc02cc030cca9cca8c013c014009c009d002f0035000a0100007dcaca0000ff0100010000000014001200000f66696e6572706978656c732e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00086a6a001d001700187a7a000100`,
interception: false,
},
{
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
helloHex: `010000c203037741795e73cd5b4949f79a0dc9cccc8b006e4c0ec324f965c6fe9f0833909f0100001c7a7ac02bc02fc02cc030cca9cca8c013c014009c009d002f0035000a0100007d7a7a0000ff0100010000000014001200000f66696e6572706978656c732e636f6d0017000000230000000d00140012040308040401050308050501080606010201000500050100000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a000a00084a4a001d001700185a5a000100`,
interception: false,
},
},
"Firefox": {
{
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:51.0) Gecko/20100101 Firefox/51.0",
helloHex: `010000bd030375f9022fc3a6562467f3540d68013b2d0b961979de6129e944efe0b35531323500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a010000760000000e000c0000096c6f63616c686f737400170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000ff030000000d0020001e040305030603020308040805080604010501060102010402050206020202`,
interception: false,
},
{
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0",
helloHex: `010001fc0303c99d54ae0628bbb9fea3833a4244c6a712cac9d7738f4930b8b9d8e2f6bd578220f7936cedb48907981c9292fb08ceee6f59bd6fddb3d4271ccd7c12380c5038ab001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a01000195001500af000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e000c0000096c6f63616c686f737400170000ff01000100000a000a0008001d001700180019000b000201000023007886da2d41843ff42131b856982c19a545837b70e604325423a817d925e9d95bd084737682cea6b804dfb7cbe336a3b27b8d520d57520c29cfe5f4f3d3236183b84b05c18f0ca30bf598111e390086fea00d9631f1f78527277eb7838b86e73c4e5d15b55d086b1a4a8aa29f12a55126c6274bcd499bbeb23a0010000e000c02683208687474702f312e31000500050100000000000d0018001604030503060308040805080604010501060102030201`,
interception: false,
},
{
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:53.0) Gecko/20100101 Firefox/53.0",
helloHex: `010000b1030365d899820b999245d571c2f7d6b850f63ad931d3c68ceb9cf5a508421a871dc500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100006a0000000e000c0000096c6f63616c686f737400170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0018001604030503060308040805080604010501060102030201`,
interception: false,
},
{
// this was a Nightly release at the time
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:55.0) Gecko/20100101 Firefox/55.0",
helloHex: `010001fc030331e380b7d12018e1202ef3327607203df5c5732b4fa5ab5abaf0b60034c2fb662070c836b9b89123e37f4f1074d152df438fa8ee8a0f89b036fd952f4fcc0b994f001c130113031302c02bc02fcca9cca8c02cc030c013c014002f0035000a0100019700000014001200000f63616464797365727665722e636f6d00170000ff01000100000a000e000c001d00170018001901000101000b0002010000230078c97e7716a041e2ea824571bef26a3dff2bf50a883cd15d904ab2d17deb514f6e0a079ee7c212c000178387ffafc2e530b6df6662f570aae134330f13c458a0eaad5a96a9696f572110918740b15db1143d19aaaa706942030b433a7e6150f62b443c0564e5b8f7ee9577bf3bf7faec8c67425b648ab54d880010000e000c02683208687474702f312e310005000501000000000028006b0069001d0020aee6e596155ee6f79f943e81ceabe0979d27fbbb8b9189ccb2ebc75226351f32001700410421875a44e510decac11ef1d7cfddd4dfe105d5cd3a2d42fba03ebde23e51e8ce65bda1b48be82d4848d1db2bfce68e94092e925a9ce0dbf5df35479558108489002b0009087f12030303020301000d0018001604030503060308040805080604010501060102030201002d000201010015002500000000000000000000000000000000000000000000000000000000000000000000000000`,
interception: false,
},
{
// Firefox on Fedora (RedHat) doesn't include ECC ciphers because of patent liabilities
userAgent: "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0",
helloHex: `010000b70303f5280b74d617d42e39fd77b78a2b537b1d7787ce4fcbcf3604c9fbcd677c6c5500001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100007000000014001200000f66696e6572706978656c732e636f6d00170000ff01000100000a000a0008001d001700180019000b00020100002300000010000e000c02683208687474702f312e31000500050100000000000d0018001604030503060308040805080604010501060102030201`,
interception: false,
},
},
"Edge": {
{
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
helloHex: `010000bd030358a3c9bf05f734842e189fb6ce653b67b846e990bc1fc5fb8c397874d06020f1000038c02cc02bc030c02f009f009ec024c023c028c027c00ac009c014c01300390033009d009c003d003c0035002f000a006a00400038003200130100005c000500050100000000000a00080006001d00170018000b00020100000d00140012040105010201040305030203020206010603002300000010000e000c02683208687474702f312e310017000055000006000100020002ff01000100`,
interception: false,
},
},
"Safari": {
{
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8",
helloHex: `010000d2030358a295b513c8140c6ff880f4a8a73cc830ed2dab2c4f2068eb365228d828732e00002600ffc02cc02bc024c023c00ac009c030c02fc028c027c014c013009d009c003d003c0035002f010000830000000e000c0000096c6f63616c686f7374000a00080006001700180019000b00020100000d00120010040102010501060104030203050306033374000000100030002e0268320568322d31360568322d31350568322d313408737064792f332e3106737064792f3308687474702f312e310005000501000000000012000000170000`,
interception: false,
},
{
// I think this was iOS 11 beta
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.28 (KHTML, like Gecko) Version/11.0 Mobile/15A5318g Safari/604.1",
helloHex: `010000e10303be294e11847ba01301e0bb6129f4a0d66344602141a8f0a1ab0750a1db145755000028c02cc02bc024c023cca9c00ac009c030c02fc028c027cca8c014c013009d009c003d003c0035002f01000090ff0100010000000014001200000f66696e6572706978656c732e636f6d00170000000d00140012040308040401050308050501080606010201000500050100000000337400000012000000100030002e0268320568322d31360568322d31350568322d313408737064792f332e3106737064792f3308687474702f312e31000b00020100000a00080006001d00170018`,
interception: false,
},
{
// iOS 11 stable
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1",
helloHex: `010000dc030327fafb16708fcbe489fda332260d32b1a22bea6672a72b5e61d7b9963df1b10d000028c02cc02bc024c023c00ac009cca9c030c02fc028c027c014c013cca8009d009c003d003c0035002f0100008bff010001000000000f000d00000a6d69746d2e776174636800170000000d00140012040308040401050308050501080606010201000500050100000000337400000012000000100030002e0268320568322d31360568322d31350568322d313408737064792f332e3106737064792f3308687474702f312e31000b00020100000a00080006001d00170018`,
interception: false,
},
},
"Tor": {
{
userAgent: "Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0",
helloHex: `010000a40303137f05d4151f2d9095aee4254416d9dce73d6a1d857e8097ea20d021c04a7a81000016c02bc02fc00ac009c013c01400330039002f0035000a0100006500000014001200000f66696e6572706978656c732e636f6dff01000100000a00080006001700180019000b00020100337400000010000b000908687474702f312e31000500050100000000000d001600140401050106010201040305030603020304020202`,
interception: false,
},
{
userAgent: "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0",
helloHex: `010000b4030322e1f3aff4c37caba303c2ce53ba1689b3e70117a46f413d44f70a74cb6a496100001ec02bc02fcca9cca8c02cc030c00ac009c013c01400330039002f0035000a0100006d00000014001200000f66696e6572706978656c732e636f6d00170000ff01000100000a000a0008001d001700180019000b000201000010000b000908687474702f312e31000500050100000000ff030000000d0018001604030503060308040805080604010501060102030201`,
interception: false,
},
},
"Other": { // these are either non-browser clients or intercepted client hellos
{
// openssl s_client (OpenSSL 0.9.8zh 14 Jan 2016) - NOT an interception, but not a browser either
helloHex: `0100012b03035d385236b8ca7b7946fa0336f164e76bf821ed90e8de26d97cc677671b6f36380000acc030c02cc028c024c014c00a00a500a300a1009f006b006a0069006800390038003700360088008700860085c032c02ec02ac026c00fc005009d003d00350084c02fc02bc027c023c013c00900a400a200a0009e00670040003f003e0033003200310030009a0099009800970045004400430042c031c02dc029c025c00ec004009c003c002f009600410007c011c007c00cc00200050004c012c008001600130010000dc00dc003000a00ff0201000055000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230000000d0020001e060106020603050105020503040104020403030103020303020102020203000f000101`,
// NOTE: This test case is not actually an interception, but s_client is not a browser
// or any client we support MITM checking for, either. Since it advertises heartbeat,
// our heuristics still flag it as a MITM.
interception: true,
},
{
// curl 7.51.0 (x86_64-apple-darwin16.0) libcurl/7.51.0 SecureTransport zlib/1.2.8
userAgent: "curl/7.51.0",
helloHex: `010000a6030358a28c73a71bdfc1f09dee13fecdc58805dcce42ac44254df548f14645f7dc2c00004400ffc02cc02bc024c023c00ac009c008c030c02fc028c027c014c013c012009f009e006b0067003900330016009d009c003d003c0035002f000a00af00ae008d008c008b01000039000a00080006001700180019000b00020100000d00120010040102010501060104030203050306030005000501000000000012000000170000`,
interception: false,
},
{
// Avast 17.1.2286 (Feb. 2017) on Windows 10 x64 build 14393, intercepting Edge
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
helloHex: `010000ce0303b418fdc4b6cf6436a5e2bfb06b96ed5faa7285c20c7b49341a78be962a9dc40000003ac02cc02bc030c02f009f009ec024c023c028c027c00ac009c014c01300390033009d009c003d003c0035002f000a006a004000380032001300ff0100006b00000014001200000f66696e6572706978656c732e636f6d000b000403000102000a00080006001d0017001800230000000d001400120401050102010403050302030202060106030005000501000000000010000e000c02683208687474702f312e310016000000170000`,
interception: true,
},
{
// Kaspersky Internet Security 17.0.0.611 on Windows 10 x64 build 14393, intercepting Edge
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
helloHex: `010000eb030361ce302bf4b0d5adf1ff30b2cf433c4a4b68f33e07b2651695e7ae6ec3cf126400003ac02cc02bc030c02f009f009ec024c023c028c027c00ac009c014c01300390033009d009c003d003c0035002f000a006a004000380032001300ff0100008800000014001200000f66696e6572706978656c732e636f6d000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230000000d0020001e060106020603050105020503040104020403030103020303020102020203000500050100000000000f0001010010000e000c02683208687474702f312e31`,
interception: true,
},
{
// Kaspersky Internet Security 17.0.0.611 on Windows 10 x64 build 14393, intercepting Firefox 51
userAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0",
helloHex: `010001fc0303768e3f9ea75194c7cb03d23e8e6371b95fb696d339b797be57a634309ec98a42200f2a7554098364b7f05d21a8c7f43f31a893a4fc5670051020408c8e4dc234dd001cc02bc02fc02cc030c00ac009c013c01400330039002f0035000a00ff0100019700000014001200000f66696e6572706978656c732e636f6d000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230078bf4e244d4de3d53c6331edda9672dfc4a17aae92b671e86da1368b1b5ae5324372817d8f3b7ffe1a7a1537a5049b86cd7c44863978c1e615b005942755da20fc3a4e34a16f78034aa3b1cffcef95f81a0995c522a53b0e95a4f98db84c43359d93d8647b2de2a69f3ebdcfc6bca452730cbd00179226dedf000d0020001e060106020603050105020503040104020403030103020303020102020203000500050100000000000f0001010010000e000c02683208687474702f312e3100150093000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000`,
interception: true,
},
{
// Kaspersky Internet Security 17.0.0.611 on Windows 10 x64 build 14393, intercepting Chrome 56
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
helloHex: `010000c903033481e7af24e647ba5a79ec97e9264c1a1f990cf842f50effe22be52130d5af82000018c02bc02fc02cc030c013c014009c009d002f0035000a00ff0100008800000014001200000f66696e6572706978656c732e636f6d000b000403000102000a001c001a00170019001c001b0018001a0016000e000d000b000c0009000a00230000000d0020001e060106020603050105020503040104020403030103020303020102020203000500050100000000000f0001010010000e000c02683208687474702f312e31`,
interception: true,
},
{
// AVG 17.1.3006 (build 17.1.3354.20) on Windows 10 x64 build 14393, intercepting Edge
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
helloHex: `010000ca0303fd83091207161eca6b4887db50587109c50e463beb190362736b1fcf9e05f807000036c02cc02bc030c02f009f009ec024c023c028c027c00ac009c014c01300390033009d009c003d003c0035002f006a00400038003200ff0100006b00000014001200000f66696e6572706978656c732e636f6d000b000403000102000a00080006001d0017001800230000000d001400120401050102010403050302030202060106030005000501000000000010000e000c02683208687474702f312e310016000000170000`,
interception: true,
},
{
// IE 11 on Windows 7, this connection was intercepted by Blue Coat
// no sensible User-Agent value, since Blue Coat changes it to something super generic
// By the way, here's another reason we hate Blue Coat: they break TLS 1.3:
// https://twitter.com/FiloSottile/status/835269932929667072
helloHex: `010000b1030358a3f3bae627f464da8cb35976b88e9119640032d41e62a107d608ed8d3e62b9000034c028c027c014c013009f009e009d009cc02cc02bc024c023c00ac009003d003c0035002f006a004000380032000a0013000500040100005400000014001200000f66696e6572706978656c732e636f6d000500050100000000000a00080006001700180019000b00020100000d0014001206010603040105010201040305030203020200170000ff01000100`,
interception: true,
reqHeaders: http.Header{"X-Bluecoat-Via": {"66808702E9A2CF4"}}, // actual field name would be "X-BlueCoat-Via" but Go canonicalizes field names
},
{
// Firefox 51.0.1 being intercepted by burp 1.7.17
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:51.0) Gecko/20100101 Firefox/51.0",
helloHex: `010000d8030358a92f4daca95acc2f6a10a9c50d736135eae39406d3090238464540d482677600003ac023c027003cc025c02900670040c009c013002fc004c00e00330032c02bc02f009cc02dc031009e00a2c008c012000ac003c00d0016001300ff01000075000a0034003200170001000300130015000600070009000a0018000b000c0019000d000e000f001000110002001200040005001400080016000b00020100000d00180016060306010503050104030401040202030201020201010000001700150000126a61677561722e6b796877616e612e6f7267`,
interception: true,
},
{
// Chrome 56 on Windows 10 being intercepted by Fortigate (on some public school network); note: I had to enable TLS 1.0 for this test (proxy was issuing a SHA-1 cert to client)
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
helloHex: `010000e5030158ac612125c83bae95282113b2a4c572cf613c160d234350fb6d0ddce879ffec000064003300320039003800160013c013c009c014c00ac012c008002f0035000a00150012003d003c00670040006b006ac011c0070096009a009900410084004500440088008700ba00be00bd00c000c400c3c03cc044c042c03dc045c04300090005000400ff01000058000a003600340000000100020003000400050006000700080009000a000b000c000d000e000f0010001100120013001400150016001700180019000b0002010000000014001200000f66696e6572706978656c732e636f6d`,
interception: true,
},
{
// IE 11 on Windows 10, intercepted by Fortigate (same firewall as above)
userAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko",
helloHex: `010000e5030158ac634c5278d7b17421f23a64cc91d68c470c6b247322fe867ba035b373d05c000064003300320039003800160013c013c009c014c00ac012c008002f0035000a00150012003d003c00670040006b006ac011c0070096009a009900410084004500440088008700ba00be00bd00c000c400c3c03cc044c042c03dc045c04300090005000400ff01000058000a003600340000000100020003000400050006000700080009000a000b000c000d000e000f0010001100120013001400150016001700180019000b0002010000000014001200000f66696e6572706978656c732e636f6d`,
interception: true,
},
{
// Edge 38.14393.0.0 on Windows 10, intercepted by Fortigate (same as above)
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
helloHex: `010000e5030158ac6421a45794b8ade6a0ac6c910cde0f99c49bb1ba737b88638ec8dcf0d077000064003300320039003800160013c013c009c014c00ac012c008002f0035000a00150012003d003c00670040006b006ac011c0070096009a009900410084004500440088008700ba00be00bd00c000c400c3c03cc044c042c03dc045c04300090005000400ff01000058000a003600340000000100020003000400050006000700080009000a000b000c000d000e000f0010001100120013001400150016001700180019000b0002010000000014001200000f66696e6572706978656c732e636f6d`,
interception: true,
},
{
// Firefox 50.0.1 on Windows 10, intercepted by Fortigate (same as above)
userAgent: "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0",
helloHex: `010000e5030158ac64e40495e77b7baf2031281451620bfe354b0c37521ebc0a40f5dc0c0cb6000064003300320039003800160013c013c009c014c00ac012c008002f0035000a00150012003d003c00670040006b006ac011c0070096009a009900410084004500440088008700ba00be00bd00c000c400c3c03cc044c042c03dc045c04300090005000400ff01000058000a003600340000000100020003000400050006000700080009000a000b000c000d000e000f0010001100120013001400150016001700180019000b0002010000000014001200000f66696e6572706978656c732e636f6d`,
interception: true,
},
},
}
for client, chs := range clientHellos {
for i, ch := range chs {
hello, err := hex.DecodeString(ch.helloHex)
if err != nil {
t.Errorf("[%s] Test %d: Error decoding ClientHello: %v", client, i, err)
continue
}
parsed := parseRawClientHello(hello)
isChrome := parsed.looksLikeChrome()
isFirefox := parsed.looksLikeFirefox()
isSafari := parsed.looksLikeSafari()
isEdge := parsed.looksLikeEdge()
isTor := parsed.looksLikeTor()
// we want each of the heuristic functions to be as
// exclusive but as low-maintenance as possible;
// in other words, if one returns true, the others
// should return false, with as little logic as possible,
// but with enough logic to force TLS proxies to do a
// good job preserving characteristics of the handshake.
if (isChrome && (isFirefox || isSafari || isEdge || isTor)) ||
(isFirefox && (isChrome || isSafari || isEdge || isTor)) ||
(isSafari && (isChrome || isFirefox || isEdge || isTor)) ||
(isEdge && (isChrome || isFirefox || isSafari || isTor)) ||
(isTor && (isChrome || isFirefox || isSafari || isEdge)) {
t.Errorf("[%s] Test %d: Multiple fingerprinting functions matched: "+
"Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n",
client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed)
}
// test the handler and detection results
var got, checked bool
want := ch.interception
handler := &tlsHandler{
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got, checked = r.Context().Value(MitmCtxKey).(bool)
}),
listener: newTLSListener(nil, nil),
}
handler.listener.helloInfos[""] = parsed
w := httptest.NewRecorder()
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
r.Header.Set("User-Agent", ch.userAgent)
if ch.reqHeaders != nil {
for field, values := range ch.reqHeaders {
r.Header[field] = values // NOTE: field names not standardized when setting directly like this!
}
}
handler.ServeHTTP(w, r)
if got != want {
t.Errorf("[%s] Test %d: Expected MITM=%v but got %v (type assertion OK (checked)=%v)",
client, i, want, got, checked)
t.Errorf("[%s] Test %d: Looks like Chrome=%v Firefox=%v Safari=%v Edge=%v Tor=%v\n\tparsed hello dec: %+v\n",
client, i, isChrome, isFirefox, isSafari, isEdge, isTor, parsed)
}
}
}
}
func TestGetVersion(t *testing.T) {
for i, test := range []struct {
UserAgent string
SoftwareName string
Version float64
}{
{
UserAgent: "Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0",
SoftwareName: "Firefox",
Version: 45.0,
},
{
UserAgent: "Mozilla/5.0 (Windows NT 6.1; rv:45.0) Gecko/20100101 Firefox/45.0 more_stuff_here",
SoftwareName: "Firefox",
Version: 45.0,
},
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
SoftwareName: "Safari",
Version: 537.36,
},
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
SoftwareName: "Chrome",
Version: 51.0270479,
},
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
SoftwareName: "Mozilla",
Version: 5.0,
},
{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
SoftwareName: "curl",
Version: -1,
},
} {
actual := getVersion(test.UserAgent, test.SoftwareName)
if actual != test.Version {
t.Errorf("Test [%d]: Expected version=%f, got version=%f for %s in '%s'",
i, test.Version, actual, test.SoftwareName, test.UserAgent)
}
}
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"net/http"
"path"
"strings"
)
// Path represents a URI path. It should usually be
// set to the value of a request path.
type Path string
// Matches checks to see if base matches p. The correct
// usage of this method sets p as the request path, and
// base as a Caddyfile (user-defined) rule path.
//
// Path matching will probably not always be a direct
// comparison; this method assures that paths can be
// easily and consistently matched.
//
// Multiple slashes are collapsed/merged. See issue #1859.
func (p Path) Matches(base string) bool {
if base == "/" || base == "" {
return true
}
// sanitize the paths for comparison, very important
// (slightly lossy if the base path requires multiple
// consecutive forward slashes, since those will be merged)
pHasTrailingSlash := strings.HasSuffix(string(p), "/")
baseHasTrailingSlash := strings.HasSuffix(base, "/")
p = Path(path.Clean(string(p)))
base = path.Clean(base)
if pHasTrailingSlash {
p += "/"
}
if baseHasTrailingSlash {
base += "/"
}
if CaseSensitivePath {
return strings.HasPrefix(string(p), base)
}
return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(base))
}
// PathMatcher is a Path RequestMatcher.
type PathMatcher string
// Match satisfies RequestMatcher.
func (p PathMatcher) Match(r *http.Request) bool {
return Path(r.URL.Path).Matches(string(p))
}
+146
View File
@@ -0,0 +1,146 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import "testing"
func TestPathMatches(t *testing.T) {
for i, testcase := range []struct {
reqPath Path
rulePath string // or "base path" as in Caddyfile docs
shouldMatch bool
caseInsensitive bool
}{
{
reqPath: "/",
rulePath: "/",
shouldMatch: true,
},
{
reqPath: "/foo/bar",
rulePath: "/foo",
shouldMatch: true,
},
{
reqPath: "/foobar",
rulePath: "/foo/",
shouldMatch: false,
},
{
reqPath: "/foobar",
rulePath: "/foo/bar",
shouldMatch: false,
},
{
reqPath: "/foo/",
rulePath: "/foo/",
shouldMatch: true,
},
{
reqPath: "/Foobar",
rulePath: "/Foo",
shouldMatch: true,
},
{
reqPath: "/FooBar",
rulePath: "/Foo",
shouldMatch: true,
},
{
reqPath: "/foobar",
rulePath: "/FooBar",
shouldMatch: true,
caseInsensitive: true,
},
{
reqPath: "",
rulePath: "/", // a lone forward slash means to match all requests (see issue #1645) - many future test cases related to this issue
shouldMatch: true,
},
{
reqPath: "foobar.php",
rulePath: "/",
shouldMatch: true,
},
{
reqPath: "",
rulePath: "",
shouldMatch: true,
},
{
reqPath: "/foo/bar",
rulePath: "",
shouldMatch: true,
},
{
reqPath: "/foo/bar",
rulePath: "",
shouldMatch: true,
},
{
reqPath: "no/leading/slash",
rulePath: "/",
shouldMatch: true,
},
{
reqPath: "no/leading/slash",
rulePath: "/no/leading/slash",
shouldMatch: false,
},
{
reqPath: "no/leading/slash",
rulePath: "",
shouldMatch: true,
},
{
// see issue #1859
reqPath: "//double-slash",
rulePath: "/double-slash",
shouldMatch: true,
},
{
reqPath: "/double//slash",
rulePath: "/double/slash",
shouldMatch: true,
},
{
reqPath: "//more/double//slashes",
rulePath: "/more/double/slashes",
shouldMatch: true,
},
{
reqPath: "/path/../traversal",
rulePath: "/traversal",
shouldMatch: true,
},
{
reqPath: "/path/../traversal",
rulePath: "/path",
shouldMatch: false,
},
{
reqPath: "/keep-slashes/http://something/foo/bar",
rulePath: "/keep-slashes/http://something",
shouldMatch: true,
},
} {
CaseSensitivePath = !testcase.caseInsensitive
if got, want := testcase.reqPath.Matches(testcase.rulePath), testcase.shouldMatch; got != want {
t.Errorf("Test %d: For request path '%s' and base path '%s': expected %v, got %v",
i, testcase.reqPath, testcase.rulePath, want, got)
}
}
}
+730
View File
@@ -0,0 +1,730 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"crypto/tls"
"flag"
"fmt"
"log"
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/caddyhttp/staticfiles"
"github.com/mholt/caddy/caddytls"
"github.com/mholt/caddy/telemetry"
"github.com/mholt/certmagic"
)
const serverType = "http"
func init() {
flag.StringVar(&HTTPPort, "http-port", HTTPPort, "Default port to use for HTTP")
flag.StringVar(&HTTPSPort, "https-port", HTTPSPort, "Default port to use for HTTPS")
flag.StringVar(&Host, "host", DefaultHost, "Default host")
flag.StringVar(&Port, "port", DefaultPort, "Default port")
flag.StringVar(&Root, "root", DefaultRoot, "Root path of default site")
flag.DurationVar(&GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown")
flag.BoolVar(&HTTP2, "http2", true, "Use HTTP/2")
flag.BoolVar(&QUIC, "quic", false, "Use experimental QUIC")
caddy.RegisterServerType(serverType, caddy.ServerType{
Directives: func() []string { return directives },
DefaultInput: func() caddy.Input {
if Port == DefaultPort && Host != "" {
// by leaving the port blank in this case we give auto HTTPS
// a chance to set the port to 443 for us
return caddy.CaddyfileInput{
Contents: []byte(fmt.Sprintf("%s\nroot %s", Host, Root)),
ServerTypeName: serverType,
}
}
return caddy.CaddyfileInput{
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)),
ServerTypeName: serverType,
}
},
NewContext: newContext,
})
caddy.RegisterCaddyfileLoader("short", caddy.LoaderFunc(shortCaddyfileLoader))
caddy.RegisterParsingCallback(serverType, "root", hideCaddyfile)
caddy.RegisterParsingCallback(serverType, "tls", activateHTTPS)
caddytls.RegisterConfigGetter(serverType, func(c *caddy.Controller) *caddytls.Config { return GetConfig(c).TLS })
// disable the caddytls package reporting ClientHellos
// to telemetry, since our MITM detector does this but
// with more information than the standard lib provides
// (as of May 2018)
caddytls.ClientHelloTelemetry = false
}
// hideCaddyfile hides the source/origin Caddyfile if it is within the
// site root. This function should be run after parsing the root directive.
func hideCaddyfile(cctx caddy.Context) error {
ctx := cctx.(*httpContext)
for _, cfg := range ctx.siteConfigs {
// if no Caddyfile exists exit.
if cfg.originCaddyfile == "" {
return nil
}
absRoot, err := filepath.Abs(cfg.Root)
if err != nil {
return err
}
absOriginCaddyfile, err := filepath.Abs(cfg.originCaddyfile)
if err != nil {
return err
}
if strings.HasPrefix(absOriginCaddyfile, absRoot) {
cfg.HiddenFiles = append(cfg.HiddenFiles, filepath.ToSlash(strings.TrimPrefix(absOriginCaddyfile, absRoot)))
}
}
return nil
}
func newContext(inst *caddy.Instance) caddy.Context {
return &httpContext{instance: inst, keysToSiteConfigs: make(map[string]*SiteConfig)}
}
type httpContext struct {
instance *caddy.Instance
// keysToSiteConfigs maps an address at the top of a
// server block (a "key") to its SiteConfig. Not all
// SiteConfigs will be represented here, only ones
// that appeared in the Caddyfile.
keysToSiteConfigs map[string]*SiteConfig
// siteConfigs is the master list of all site configs.
siteConfigs []*SiteConfig
}
func (h *httpContext) saveConfig(key string, cfg *SiteConfig) {
h.siteConfigs = append(h.siteConfigs, cfg)
h.keysToSiteConfigs[key] = cfg
}
// InspectServerBlocks make sure that everything checks out before
// executing directives and otherwise prepares the directives to
// be parsed and executed.
func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
siteAddrs := make(map[string]string)
// For each address in each server block, make a new config
for _, sb := range serverBlocks {
for _, key := range sb.Keys {
addr, err := standardizeAddress(key)
if err != nil {
return serverBlocks, err
}
addr = addr.Normalize()
key = addr.Key()
if _, dup := h.keysToSiteConfigs[key]; dup {
return serverBlocks, fmt.Errorf("duplicate site key: %s", key)
}
// Fill in address components from command line so that middleware
// have access to the correct information during setup
if addr.Host == "" && Host != DefaultHost {
addr.Host = Host
}
if addr.Port == "" && Port != DefaultPort {
addr.Port = Port
}
// Make sure the adjusted site address is distinct
addrCopy := addr // make copy so we don't disturb the original, carefully-parsed address struct
if addrCopy.Port == "" && Port == DefaultPort {
addrCopy.Port = Port
}
addrStr := addrCopy.String()
if otherSiteKey, dup := siteAddrs[addrStr]; dup {
err := fmt.Errorf("duplicate site address: %s", addrStr)
if (addrCopy.Host == Host && Host != DefaultHost) ||
(addrCopy.Port == Port && Port != DefaultPort) {
err = fmt.Errorf("site defined as %s is a duplicate of %s because of modified "+
"default host and/or port values (usually via -host or -port flags)", key, otherSiteKey)
}
return serverBlocks, err
}
siteAddrs[addrStr] = key
// If default HTTP or HTTPS ports have been customized,
// make sure the ACME challenge ports match
var altHTTPPort, altTLSALPNPort int
if HTTPPort != DefaultHTTPPort {
portInt, err := strconv.Atoi(HTTPPort)
if err != nil {
return nil, err
}
altHTTPPort = portInt
}
if HTTPSPort != DefaultHTTPSPort {
portInt, err := strconv.Atoi(HTTPSPort)
if err != nil {
return nil, err
}
altTLSALPNPort = portInt
}
// Make our caddytls.Config, which has a pointer to the
// instance's certificate cache and enough information
// to use automatic HTTPS when the time comes
caddytlsConfig, err := caddytls.NewConfig(h.instance)
if err != nil {
return nil, fmt.Errorf("creating new caddytls configuration: %v", err)
}
caddytlsConfig.Hostname = addr.Host
caddytlsConfig.Manager.AltHTTPPort = altHTTPPort
caddytlsConfig.Manager.AltTLSALPNPort = altTLSALPNPort
// Save the config to our master list, and key it for lookups
cfg := &SiteConfig{
Addr: addr,
Root: Root,
TLS: caddytlsConfig,
originCaddyfile: sourceFile,
IndexPages: staticfiles.DefaultIndexPages,
}
h.saveConfig(key, cfg)
}
}
// For sites that have gzip (which gets chained in
// before the error handler) we should ensure that the
// errors directive also appears so error pages aren't
// written after the gzip writer is closed. See #616.
for _, sb := range serverBlocks {
_, hasGzip := sb.Tokens["gzip"]
_, hasErrors := sb.Tokens["errors"]
if hasGzip && !hasErrors {
sb.Tokens["errors"] = []caddyfile.Token{{Text: "errors"}}
}
}
return serverBlocks, nil
}
// MakeServers uses the newly-created siteConfigs to
// create and return a list of server instances.
func (h *httpContext) MakeServers() ([]caddy.Server, error) {
// make a rough estimate as to whether we're in a "production
// environment/system" - start by assuming that most production
// servers will set their default CA endpoint to a public,
// trusted CA (obviously not a perfect heuristic)
var looksLikeProductionCA bool
for _, publicCAEndpoint := range caddytls.KnownACMECAs {
if strings.Contains(certmagic.Default.CA, publicCAEndpoint) {
looksLikeProductionCA = true
break
}
}
// Iterate each site configuration and make sure that:
// 1) TLS is disabled for explicitly-HTTP sites (necessary
// when an HTTP address shares a block containing tls)
// 2) if QUIC is enabled, TLS ClientAuth is not, because
// currently, QUIC does not support ClientAuth (TODO:
// revisit this when our QUIC implementation supports it)
// 3) if TLS ClientAuth is used, StrictHostMatching is on
var atLeastOneSiteLooksLikeProduction bool
for _, cfg := range h.siteConfigs {
// see if all the addresses (both sites and
// listeners) are loopback to help us determine
// if this is a "production" instance or not
if !atLeastOneSiteLooksLikeProduction {
if !caddy.IsLoopback(cfg.Addr.Host) &&
!caddy.IsLoopback(cfg.ListenHost) &&
(caddytls.QualifiesForManagedTLS(cfg) ||
certmagic.HostQualifies(cfg.Addr.Host)) {
atLeastOneSiteLooksLikeProduction = true
}
}
// make sure TLS is disabled for explicitly-HTTP sites
// (necessary when HTTP address shares a block containing tls)
if !cfg.TLS.Enabled {
continue
}
if cfg.Addr.Port == HTTPPort || cfg.Addr.Scheme == "http" {
cfg.TLS.Enabled = false
log.Printf("[WARNING] TLS disabled for %s", cfg.Addr)
} else if cfg.Addr.Scheme == "" {
// set scheme to https ourselves, since TLS is enabled
// and it was not explicitly set to something else. this
// makes it appear as "https" when we print the list of
// running sites; otherwise "http" would be assumed which
// is incorrect for this site.
cfg.Addr.Scheme = "https"
}
if cfg.Addr.Port == "" && ((!cfg.TLS.Manual && !cfg.TLS.SelfSigned) || cfg.TLS.Manager.OnDemand != nil) {
// this is vital, otherwise the function call below that
// sets the listener address will use the default port
// instead of 443 because it doesn't know about TLS.
cfg.Addr.Port = HTTPSPort
}
if cfg.TLS.ClientAuth != tls.NoClientCert {
if QUIC {
return nil, fmt.Errorf("cannot enable TLS client authentication with QUIC, because QUIC does not yet support it")
}
// this must be enabled so that a client cannot connect
// using SNI for another site on this listener that
// does NOT require ClientAuth, and then send HTTP
// requests with the Host header of this site which DOES
// require client auth, thus bypassing it...
cfg.StrictHostMatching = true
}
}
// we must map (group) each config to a bind address
groups, err := groupSiteConfigsByListenAddr(h.siteConfigs)
if err != nil {
return nil, err
}
// then we create a server for each group
var servers []caddy.Server
for addr, group := range groups {
s, err := NewServer(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
}
// NOTE: This value is only a "good guess". Quite often, development
// environments will use internal DNS or a local hosts file to serve
// real-looking domains in local development. We can't easily tell
// which without doing a DNS lookup, so this guess is definitely naive,
// and if we ever want a better guess, we will have to do DNS lookups.
deploymentGuess := "dev"
if looksLikeProductionCA && atLeastOneSiteLooksLikeProduction {
deploymentGuess = "prod"
}
telemetry.Set("http_deployment_guess", deploymentGuess)
telemetry.Set("http_num_sites", len(h.siteConfigs))
return servers, nil
}
// normalizedKey returns "normalized" key representation:
// scheme and host names are lowered, everything else stays the same
func normalizedKey(key string) string {
addr, err := standardizeAddress(key)
if err != nil {
return key
}
return addr.Normalize().Key()
}
// GetConfig gets the SiteConfig that corresponds to c.
// If none exist (should only happen in tests), then a
// new, empty one will be created.
func GetConfig(c *caddy.Controller) *SiteConfig {
ctx := c.Context().(*httpContext)
key := normalizedKey(c.Key)
if cfg, ok := ctx.keysToSiteConfigs[key]; ok {
return cfg
}
// we should only get here during tests because directive
// actions typically skip the server blocks where we make
// the configs
cfg := &SiteConfig{
Root: Root,
TLS: &caddytls.Config{Manager: certmagic.NewDefault()},
IndexPages: staticfiles.DefaultIndexPages,
}
ctx.saveConfig(key, cfg)
return cfg
}
// shortCaddyfileLoader loads a Caddyfile if positional arguments are
// detected, or, in other words, if un-named arguments are provided to
// the program. A "short Caddyfile" is one in which each argument
// is a line of the Caddyfile. The default host and port are prepended
// according to the Host and Port values.
func shortCaddyfileLoader(serverType string) (caddy.Input, error) {
if flag.NArg() > 0 && serverType == "http" {
confBody := fmt.Sprintf("%s:%s\n%s", Host, Port, strings.Join(flag.Args(), "\n"))
return caddy.CaddyfileInput{
Contents: []byte(confBody),
Filepath: "args",
ServerTypeName: serverType,
}, nil
}
return nil, nil
}
// groupSiteConfigsByListenAddr groups site configs by their listen
// (bind) address, so sites that use the same listener can be served
// on the same server instance. The return value maps the listen
// address (what you pass into net.Listen) to the list of site configs.
// This function does NOT vet the configs to ensure they are compatible.
func groupSiteConfigsByListenAddr(configs []*SiteConfig) (map[string][]*SiteConfig, error) {
groups := make(map[string][]*SiteConfig)
for _, conf := range configs {
// We would add a special case here so that localhost addresses
// bind to 127.0.0.1 if conf.ListenHost is not already set, which
// would prevent outsiders from even connecting; but that was problematic:
// https://caddy.community/t/wildcard-virtual-domains-with-wildcard-roots/221/5?u=matt
if conf.Addr.Port == "" {
conf.Addr.Port = Port
}
addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.ListenHost, conf.Addr.Port))
if err != nil {
return nil, err
}
addrstr := addr.String()
groups[addrstr] = append(groups[addrstr], conf)
}
return groups, nil
}
// Address represents a site address. It contains
// the original input value, and the component
// parts of an address. The component parts may be
// updated to the correct values as setup proceeds,
// but the original value should never be changed.
//
// The Host field must be in a normalized form.
type Address struct {
Original, Scheme, Host, Port, Path string
}
// String returns a human-friendly print of the address.
func (a Address) String() string {
if a.Host == "" && a.Port == "" {
return ""
}
scheme := a.Scheme
if scheme == "" {
if a.Port == HTTPSPort {
scheme = "https"
} else {
scheme = "http"
}
}
s := scheme
if s != "" {
s += "://"
}
if a.Port != "" &&
((scheme == "https" && a.Port != DefaultHTTPSPort) ||
(scheme == "http" && a.Port != DefaultHTTPPort)) {
s += net.JoinHostPort(a.Host, a.Port)
} else {
s += a.Host
}
if a.Path != "" {
s += a.Path
}
return s
}
// VHost returns a sensible concatenation of Host:Port/Path from a.
// It's basically the a.Original but without the scheme.
func (a Address) VHost() string {
if idx := strings.Index(a.Original, "://"); idx > -1 {
return a.Original[idx+3:]
}
return a.Original
}
// Normalize normalizes URL: turn scheme and host names into lower case
func (a Address) Normalize() Address {
path := a.Path
if !CaseSensitivePath {
path = strings.ToLower(path)
}
// ensure host is normalized if it's an IP address
host := a.Host
if ip := net.ParseIP(host); ip != nil {
host = ip.String()
}
return Address{
Original: a.Original,
Scheme: strings.ToLower(a.Scheme),
Host: strings.ToLower(host),
Port: a.Port,
Path: path,
}
}
// Key is similar to String, just replaces scheme and host values with modified values.
// Unlike String it doesn't add anything default (scheme, port, etc)
func (a Address) Key() string {
res := ""
if a.Scheme != "" {
res += a.Scheme + "://"
}
if a.Host != "" {
res += a.Host
}
if a.Port != "" {
if strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
// insert port only if the original has its own explicit port
res += ":" + a.Port
}
}
if a.Path != "" {
res += a.Path
}
return res
}
// standardizeAddress parses an address string into a structured format with separate
// scheme, host, port, and path portions, as well as the original input string.
func standardizeAddress(str string) (Address, error) {
input := str
// Split input into components (prepend with // to assert host by default)
if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") {
str = "//" + str
}
u, err := url.Parse(str)
if err != nil {
return Address{}, err
}
// separate host and port
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
host, port, err = net.SplitHostPort(u.Host + ":")
if err != nil {
host = u.Host
}
}
// see if we can set port based off scheme
if port == "" {
if u.Scheme == "http" {
port = HTTPPort
} else if u.Scheme == "https" {
port = HTTPSPort
}
}
// repeated or conflicting scheme is confusing, so error
if u.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 (u.Scheme == "http" && port == HTTPSPort) || (u.Scheme == "https" && port == HTTPPort) {
return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input)
}
// standardize http and https ports to their respective port numbers
if port == "http" {
u.Scheme = "http"
port = HTTPPort
} else if port == "https" {
u.Scheme = "https"
port = HTTPSPort
}
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err
}
// RegisterDevDirective splices name into the list of directives
// immediately before another directive. This function is ONLY
// for plugin development purposes! NEVER use it for a plugin
// that you are not currently building. If before is empty,
// the directive will be appended to the end of the list.
//
// It is imperative that directives execute in the proper
// order, and hard-coding the list of directives guarantees
// a correct, absolute order every time. This function is
// convenient when developing a plugin, but it does not
// guarantee absolute ordering. Multiple plugins registering
// directives with this function will lead to non-
// deterministic builds and buggy software.
//
// Directive names must be lower-cased and unique. Any errors
// here are fatal, and even successful calls print a message
// to stdout as a reminder to use it only in development.
func RegisterDevDirective(name, before string) {
if name == "" {
fmt.Println("[FATAL] Cannot register empty directive name")
os.Exit(1)
}
if strings.ToLower(name) != name {
fmt.Printf("[FATAL] %s: directive name must be lowercase\n", name)
os.Exit(1)
}
for _, dir := range directives {
if dir == name {
fmt.Printf("[FATAL] %s: directive name already exists\n", name)
os.Exit(1)
}
}
if before == "" {
directives = append(directives, name)
} else {
var found bool
for i, dir := range directives {
if dir == before {
directives = append(directives[:i], append([]string{name}, directives[i:]...)...)
found = true
break
}
}
if !found {
fmt.Printf("[FATAL] %s: directive not found\n", before)
os.Exit(1)
}
}
msg := fmt.Sprintf("Registered directive '%s' ", name)
if before == "" {
msg += "at end of list"
} else {
msg += fmt.Sprintf("before '%s'", before)
}
fmt.Printf("[DEV NOTICE] %s\n", msg)
}
// directives is the list of all directives known to exist for the
// http server type, including non-standard (3rd-party) directives.
// The ordering of this list is important.
var directives = []string{
// primitive actions that set up the fundamental vitals of each config
"root",
"index",
"bind",
"limits",
"timeouts",
"tls",
// services/utilities, or other directives that don't necessarily inject handlers
"startup", // TODO: Deprecate this directive
"shutdown", // TODO: Deprecate this directive
"on",
"supervisor", // github.com/lucaslorentz/caddy-supervisor
"request_id",
"realip", // github.com/captncraig/caddy-realip
"git", // github.com/abiosoft/caddy-git
// directives that add listener middleware to the stack
"proxyprotocol", // github.com/mastercactapus/caddy-proxyprotocol
// directives that add middleware to the stack
"locale", // github.com/simia-tech/caddy-locale
"log",
"cache", // github.com/nicolasazrak/caddy-cache
"rewrite",
"ext",
"minify", // github.com/hacdias/caddy-minify
"gzip",
"header",
"geoip", // github.com/kodnaplakal/caddy-geoip
"errors",
"authz", // github.com/casbin/caddy-authz
"filter", // github.com/echocat/caddy-filter
"ipfilter", // github.com/pyed/ipfilter
"ratelimit", // github.com/xuqingfeng/caddy-rate-limit
"expires", // github.com/epicagency/caddy-expires
"forwardproxy", // github.com/caddyserver/forwardproxy
"basicauth",
"redir",
"status",
"cors", // github.com/captncraig/cors/caddy
"s3browser", // github.com/techknowlogick/caddy-s3browser
"nobots", // github.com/Xumeiquer/nobots
"mime",
"login", // github.com/tarent/loginsrv/caddy
"reauth", // github.com/freman/caddy-reauth
"extauth", // github.com/BTBurke/caddy-extauth
"jwt", // github.com/BTBurke/caddy-jwt
"jsonp", // github.com/pschlump/caddy-jsonp
"upload", // blitznote.com/src/caddy.upload
"multipass", // github.com/namsral/multipass/caddy
"internal",
"pprof",
"expvar",
"push",
"datadog", // github.com/payintech/caddy-datadog
"prometheus", // github.com/miekg/caddy-prometheus
"templates",
"proxy",
"fastcgi",
"cgi", // github.com/jung-kurt/caddy-cgi
"websocket",
"filebrowser", // github.com/filebrowser/caddy
"webdav", // github.com/hacdias/caddy-webdav
"markdown",
"browse",
"mailout", // github.com/SchumacherFM/mailout
"awses", // github.com/miquella/caddy-awses
"awslambda", // github.com/coopernurse/caddy-awslambda
"grpc", // github.com/pieterlouw/caddy-grpc
"gopkg", // github.com/zikes/gopkg
"restic", // github.com/restic/caddy
"wkd", // github.com/emersion/caddy-wkd
"dyndns", // github.com/linkonoid/caddy-dyndns
}
const (
// DefaultHost is the default host.
DefaultHost = ""
// DefaultPort is the default port.
DefaultPort = "2015"
// DefaultRoot is the default root folder.
DefaultRoot = "."
// DefaultHTTPPort is the default port for HTTP.
DefaultHTTPPort = "80"
// DefaultHTTPSPort is the default port for HTTPS.
DefaultHTTPSPort = "443"
)
// These "soft defaults" are configurable by
// command line flags, etc.
var (
// Root is the site root
Root = DefaultRoot
// Host is the site host
Host = DefaultHost
// Port is the site port
Port = DefaultPort
// GracefulTimeout is the maximum duration of a graceful shutdown.
GracefulTimeout time.Duration
// HTTP2 indicates whether HTTP2 is enabled or not.
HTTP2 bool
// QUIC indicates whether QUIC is enabled or not.
QUIC bool
// HTTPPort is the port to use for HTTP.
HTTPPort = DefaultHTTPPort
// HTTPSPort is the port to use for HTTPS.
HTTPSPort = DefaultHTTPSPort
)
+349
View File
@@ -0,0 +1,349 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"strings"
"testing"
"sort"
"fmt"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyfile"
)
func TestStandardizeAddress(t *testing.T) {
for i, test := range []struct {
input string
scheme, host, port, path 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},
{`host/path`, "", "host", "", "/path", false},
{`http://host/`, "http", "host", "80", "/", false},
{`//asdf`, "", "asdf", "", "", false},
{`:1234/asdf`, "", "", "1234", "/asdf", false},
{`http://host/path`, "http", "host", "80", "/path", false},
{`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false},
{`host:80/path`, "", "host", "80", "/path", false},
{`host:https/path`, "https", "host", "443", "/path", false},
{`/path`, "", "", "", "/path", false},
} {
actual, err := standardizeAddress(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 !test.shouldErr && actual.Original != test.input {
t.Errorf("Test %d (%s): Expected original '%s', got '%s'", i, test.input, test.input, actual.Original)
}
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)
}
if actual.Path != test.path {
t.Errorf("Test %d (%s): Expected path '%s', got '%s'", i, test.input, test.path, actual.Path)
}
}
}
func TestAddressVHost(t *testing.T) {
for i, test := range []struct {
addr Address
expected string
}{
{Address{Original: "host:1234"}, "host:1234"},
{Address{Original: "host:1234/foo"}, "host:1234/foo"},
{Address{Original: "host/foo"}, "host/foo"},
{Address{Original: "http://host/foo"}, "host/foo"},
{Address{Original: "https://host/foo"}, "host/foo"},
} {
actual := test.addr.VHost()
if actual != test.expected {
t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual)
}
}
}
func TestAddressString(t *testing.T) {
for i, test := range []struct {
addr Address
expected string
}{
{Address{Scheme: "http", Host: "host", Port: "1234", Path: "/path"}, "http://host:1234/path"},
{Address{Scheme: "", Host: "host", Port: "", Path: ""}, "http://host"},
{Address{Scheme: "", Host: "host", Port: "80", Path: ""}, "http://host"},
{Address{Scheme: "", Host: "host", Port: "443", Path: ""}, "https://host"},
{Address{Scheme: "https", Host: "host", Port: "443", Path: ""}, "https://host"},
{Address{Scheme: "https", Host: "host", Port: "", Path: ""}, "https://host"},
{Address{Scheme: "", Host: "host", Port: "80", Path: "/path"}, "http://host/path"},
{Address{Scheme: "http", Host: "", Port: "1234", Path: ""}, "http://:1234"},
{Address{Scheme: "", Host: "", Port: "", Path: ""}, ""},
} {
actual := test.addr.String()
if actual != test.expected {
t.Errorf("Test %d: expected '%s' but got '%s'", i, test.expected, actual)
}
}
}
func TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {
Port = "9999"
filename := "Testfile"
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
input := strings.NewReader(`localhost`)
sblocks, err := caddyfile.Parse(filename, input, nil)
if err != nil {
t.Fatalf("Expected no error setting up test, got: %v", err)
}
_, err = ctx.InspectServerBlocks(filename, sblocks)
if err != nil {
t.Fatalf("Didn't expect an error, but got: %v", err)
}
localhostKey := "localhost"
item, ok := ctx.keysToSiteConfigs[localhostKey]
if !ok {
availableKeys := make(sort.StringSlice, len(ctx.keysToSiteConfigs))
i := 0
for key := range ctx.keysToSiteConfigs {
availableKeys[i] = fmt.Sprintf("'%s'", key)
i++
}
availableKeys.Sort()
t.Errorf("`%s` not found within registered keys, only these are available: %s", localhostKey, strings.Join(availableKeys, ", "))
return
}
addr := item.Addr
if addr.Port != Port {
t.Errorf("Expected the port on the address to be set, but got: %#v", addr)
}
}
// See discussion on PR #2015
func TestInspectServerBlocksWithAdjustedAddress(t *testing.T) {
Port = DefaultPort
Host = "example.com"
filename := "Testfile"
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
input := strings.NewReader("example.com {\n}\n:2015 {\n}")
sblocks, err := caddyfile.Parse(filename, input, nil)
if err != nil {
t.Fatalf("Expected no error setting up test, got: %v", err)
}
_, err = ctx.InspectServerBlocks(filename, sblocks)
if err == nil {
t.Fatalf("Expected an error because site definitions should overlap, got: %v", err)
}
}
func TestInspectServerBlocksCaseInsensitiveKey(t *testing.T) {
filename := "Testfile"
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
input := strings.NewReader("localhost {\n}\nLOCALHOST {\n}")
sblocks, err := caddyfile.Parse(filename, input, nil)
if err != nil {
t.Fatalf("Expected no error setting up test, got: %v", err)
}
_, err = ctx.InspectServerBlocks(filename, sblocks)
if err == nil {
t.Error("Expected an error because keys on this server type are case-insensitive (so these are duplicated), but didn't get an error")
}
}
func TestKeyNormalization(t *testing.T) {
originalCaseSensitivePath := CaseSensitivePath
defer func() {
CaseSensitivePath = originalCaseSensitivePath
}()
CaseSensitivePath = true
caseSensitiveData := []struct {
orig string
res string
}{
{
orig: "HTTP://A/ABCDEF",
res: "http://a/ABCDEF",
},
{
orig: "A/ABCDEF",
res: "a/ABCDEF",
},
{
orig: "A:2015/Port",
res: "a:2015/Port",
},
}
for _, item := range caseSensitiveData {
v := normalizedKey(item.orig)
if v != item.res {
t.Errorf("Normalization of `%s` with CaseSensitivePath option set to true must be equal to `%s`, got `%s` instead", item.orig, item.res, v)
}
}
CaseSensitivePath = false
caseInsensitiveData := []struct {
orig string
res string
}{
{
orig: "HTTP://A/ABCDEF",
res: "http://a/abcdef",
},
{
orig: "A/ABCDEF",
res: "a/abcdef",
},
{
orig: "A:2015/Port",
res: "a:2015/port",
},
}
for _, item := range caseInsensitiveData {
v := normalizedKey(item.orig)
if v != item.res {
t.Errorf("Normalization of `%s` with CaseSensitivePath option set to false must be equal to `%s`, got `%s` instead", item.orig, item.res, v)
}
}
}
func TestGetConfig(t *testing.T) {
// case insensitivity for key
con := caddy.NewTestController("http", "")
con.Key = "foo"
cfg := GetConfig(con)
con.Key = "FOO"
cfg2 := GetConfig(con)
if cfg != cfg2 {
t.Errorf("Expected same config using same key with different case; got %p and %p", cfg, cfg2)
}
// make sure different key returns different config
con.Key = "foobar"
cfg3 := GetConfig(con)
if cfg == cfg3 {
t.Errorf("Expected different configs using when key is different; got %p and %p", cfg, cfg3)
}
con.Key = "foo/foobar"
cfg4 := GetConfig(con)
con.Key = "foo/Foobar"
cfg5 := GetConfig(con)
if cfg4 == cfg5 {
t.Errorf("Expected different cases in path to differentiate keys in general")
}
}
func TestDirectivesList(t *testing.T) {
for i, dir1 := range directives {
if dir1 == "" {
t.Errorf("directives[%d]: empty directive name", i)
continue
}
if got, want := dir1, strings.ToLower(dir1); got != want {
t.Errorf("directives[%d]: %s should be lower-cased", i, dir1)
continue
}
for j := i + 1; j < len(directives); j++ {
dir2 := directives[j]
if dir1 == dir2 {
t.Errorf("directives[%d] (%s) is a duplicate of directives[%d] (%s)",
j, dir2, i, dir1)
}
}
}
}
func TestContextSaveConfig(t *testing.T) {
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
ctx.saveConfig("foo", new(SiteConfig))
if _, ok := ctx.keysToSiteConfigs["foo"]; !ok {
t.Error("Expected config to be saved, but it wasn't")
}
if got, want := len(ctx.siteConfigs), 1; got != want {
t.Errorf("Expected len(siteConfigs) == %d, but was %d", want, got)
}
ctx.saveConfig("Foobar", new(SiteConfig))
if _, ok := ctx.keysToSiteConfigs["foobar"]; ok {
t.Error("Did not expect to get config with case-insensitive key, but did")
}
if got, want := len(ctx.siteConfigs), 2; got != want {
t.Errorf("Expected len(siteConfigs) == %d, but was %d", want, got)
}
}
// Test to make sure we are correctly hiding the Caddyfile
func TestHideCaddyfile(t *testing.T) {
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
ctx.saveConfig("test", &SiteConfig{
Root: Root,
originCaddyfile: "Testfile",
})
err := hideCaddyfile(ctx)
if err != nil {
t.Fatalf("Failed to hide Caddyfile, got: %v", err)
return
}
if len(ctx.siteConfigs[0].HiddenFiles) == 0 {
t.Fatal("Failed to add Caddyfile to HiddenFiles.")
return
}
for _, file := range ctx.siteConfigs[0].HiddenFiles {
if file == "/Testfile" {
return
}
}
t.Fatal("Caddyfile missing from HiddenFiles")
}
+261
View File
@@ -0,0 +1,261 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"bytes"
"io"
"net/http"
"sync"
"time"
)
// ResponseRecorder is a type of http.ResponseWriter that captures
// the status code written to it and also the size of the body
// written in the response. A status code does not have
// to be written, however, in which case 200 must be assumed.
// It is best to have the constructor initialize this type
// with that default status code.
//
// Setting the Replacer field allows middlewares to type-assert
// the http.ResponseWriter to ResponseRecorder and set their own
// placeholder values for logging utilities to use.
//
// Beware when accessing the Replacer value; it may be nil!
type ResponseRecorder struct {
*ResponseWriterWrapper
Replacer Replacer
status int
size int
start time.Time
}
// NewResponseRecorder makes and returns a new ResponseRecorder.
// Because a status is not set unless WriteHeader is called
// explicitly, this constructor initializes with a status code
// of 200 to cover the default case.
func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder {
return &ResponseRecorder{
ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
status: http.StatusOK,
start: time.Now(),
}
}
// WriteHeader records the status code and calls the
// underlying ResponseWriter's WriteHeader method.
func (r *ResponseRecorder) WriteHeader(status int) {
r.status = status
r.ResponseWriterWrapper.WriteHeader(status)
}
// Write is a wrapper that records the size of the body
// that gets written.
func (r *ResponseRecorder) Write(buf []byte) (int, error) {
n, err := r.ResponseWriterWrapper.Write(buf)
if err == nil {
r.size += n
}
return n, err
}
// Size returns the size of the recorded response body.
func (r *ResponseRecorder) Size() int {
return r.size
}
// Status returns the recorded response status code.
func (r *ResponseRecorder) Status() int {
return r.status
}
// ResponseBuffer is a type that conditionally buffers the
// response in memory. It implements http.ResponseWriter so
// that it can stream the response if it is not buffering.
// Whether it buffers is decided by a func passed into the
// constructor, NewResponseBuffer.
//
// This type implements http.ResponseWriter, so you can pass
// this to the Next() middleware in the chain and record its
// response. However, since the entire response body will be
// buffered in memory, only use this when explicitly configured
// and required for some specific reason. For example, the
// text/template package only parses templates out of []byte
// and not io.Reader, so the templates directive uses this
// type to obtain the entire template text, but only on certain
// requests that match the right Content-Type, etc.
//
// ResponseBuffer also implements io.ReaderFrom for performance
// reasons. The standard lib's http.response type (unexported)
// uses io.Copy to write the body. io.Copy makes an allocation
// if the destination does not have a ReadFrom method (or if
// the source does not have a WriteTo method, but that's
// irrelevant here). Our ReadFrom is smart: if buffering, it
// calls the buffer's ReadFrom, which makes no allocs because
// it is already a buffer! If we're streaming the response
// instead, ReadFrom uses io.CopyBuffer with a pooled buffer
// that is managed within this package.
type ResponseBuffer struct {
*ResponseWriterWrapper
Buffer *bytes.Buffer
header http.Header
status int
shouldBuffer func(status int, header http.Header) bool
stream bool
rw http.ResponseWriter
wroteHeader bool
}
// NewResponseBuffer returns a new ResponseBuffer that will
// use buf to store the full body of the response if shouldBuffer
// returns true. If shouldBuffer returns false, then the response
// body will be streamed directly to rw.
//
// shouldBuffer will be passed the status code and header fields of
// the response. With that information, the function should decide
// whether to buffer the response in memory. For example: the templates
// directive uses this to determine whether the response is the
// right Content-Type (according to user config) for a template.
//
// For performance, the buf you pass in should probably be obtained
// from a sync.Pool in order to reuse allocated space.
func NewResponseBuffer(buf *bytes.Buffer, rw http.ResponseWriter,
shouldBuffer func(status int, header http.Header) bool) *ResponseBuffer {
rb := &ResponseBuffer{
Buffer: buf,
header: make(http.Header),
status: http.StatusOK, // default status code
shouldBuffer: shouldBuffer,
rw: rw,
}
rb.ResponseWriterWrapper = &ResponseWriterWrapper{ResponseWriter: rw}
return rb
}
// Header returns the response header map.
func (rb *ResponseBuffer) Header() http.Header {
return rb.header
}
// WriteHeader calls shouldBuffer to decide whether the
// upcoming body should be buffered, and then writes
// the header to the response.
func (rb *ResponseBuffer) WriteHeader(status int) {
if rb.wroteHeader {
return
}
rb.wroteHeader = true
rb.status = status
rb.stream = !rb.shouldBuffer(status, rb.header)
if rb.stream {
rb.CopyHeader()
rb.ResponseWriterWrapper.WriteHeader(status)
}
}
// Write writes buf to rb.Buffer if buffering, otherwise
// to the ResponseWriter directly if streaming.
func (rb *ResponseBuffer) Write(buf []byte) (int, error) {
if !rb.wroteHeader {
rb.WriteHeader(http.StatusOK)
}
if rb.stream {
return rb.ResponseWriterWrapper.Write(buf)
}
return rb.Buffer.Write(buf)
}
// Buffered returns whether rb has decided to buffer the response.
func (rb *ResponseBuffer) Buffered() bool {
return !rb.stream
}
// CopyHeader copies the buffered header in rb to the ResponseWriter,
// but it does not write the header out.
func (rb *ResponseBuffer) CopyHeader() {
for field, val := range rb.header {
rb.ResponseWriterWrapper.Header()[field] = val
}
}
// ReadFrom avoids allocations when writing to the buffer (if buffering),
// and reduces allocations when writing to the ResponseWriter directly
// (if streaming).
//
// In local testing with the templates directive, req/sec were improved
// from ~8,200 to ~9,600 on templated files by ensuring that this type
// implements io.ReaderFrom.
func (rb *ResponseBuffer) ReadFrom(src io.Reader) (int64, error) {
if !rb.wroteHeader {
rb.WriteHeader(http.StatusOK)
}
if rb.stream {
// first see if we can avoid any allocations at all
if wt, ok := src.(io.WriterTo); ok {
return wt.WriteTo(rb.ResponseWriterWrapper)
}
// if not, use a pooled copy buffer to reduce allocs
// (this improved req/sec from ~25,300 to ~27,000 on
// static files served directly with the fileserver,
// but results fluctuated a little on each run).
// a note of caution:
// https://go-review.googlesource.com/c/22134#message-ff351762308fe05f6b72a487d6842e3988916486
buf := respBufPool.Get().([]byte)
n, err := io.CopyBuffer(rb.ResponseWriterWrapper, src, buf)
respBufPool.Put(buf) // deferring this slowed down benchmarks a smidgin, I think
return n, err
}
return rb.Buffer.ReadFrom(src)
}
// StatusCodeWriter returns an http.ResponseWriter that always
// writes the status code stored in rb from when a response
// was buffered to it.
func (rb *ResponseBuffer) StatusCodeWriter(w http.ResponseWriter) http.ResponseWriter {
return forcedStatusCodeWriter{w, rb}
}
// forcedStatusCodeWriter is used to force a status code when
// writing the header. It uses the status code saved on rb.
// This is useful if passing a http.ResponseWriter into
// http.ServeContent because ServeContent hard-codes 2xx status
// codes. If we buffered the response, we force that status code
// instead.
type forcedStatusCodeWriter struct {
http.ResponseWriter
rb *ResponseBuffer
}
func (fscw forcedStatusCodeWriter) WriteHeader(int) {
fscw.ResponseWriter.WriteHeader(fscw.rb.status)
}
// respBufPool is used for io.CopyBuffer when ResponseBuffer
// is configured to stream a response.
var respBufPool = &sync.Pool{
New: func() interface{} {
return make([]byte, 32*1024)
},
}
// Interface guards
var (
_ HTTPInterfaces = (*ResponseRecorder)(nil)
_ HTTPInterfaces = (*ResponseBuffer)(nil)
_ io.ReaderFrom = (*ResponseBuffer)(nil)
)
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestNewResponseRecorder(t *testing.T) {
w := httptest.NewRecorder()
recordRequest := NewResponseRecorder(w)
if !(recordRequest.ResponseWriter == w) {
t.Fatalf("Expected Response writer in the Recording to be same as the one sent\n")
}
if recordRequest.status != http.StatusOK {
t.Fatalf("Expected recorded status to be http.StatusOK (%d) , but found %d\n ", http.StatusOK, recordRequest.status)
}
}
func TestWriteHeader(t *testing.T) {
w := httptest.NewRecorder()
recordRequest := NewResponseRecorder(w)
recordRequest.WriteHeader(401)
if w.Code != 401 || recordRequest.status != 401 {
t.Fatalf("Expected Response status to be set to 401, but found %d\n", recordRequest.status)
}
}
func TestWrite(t *testing.T) {
w := httptest.NewRecorder()
responseTestString := "test"
recordRequest := NewResponseRecorder(w)
buf := []byte(responseTestString)
_, _ = recordRequest.Write(buf)
if recordRequest.size != len(buf) {
t.Fatalf("Expected the bytes written counter to be %d, but instead found %d\n", len(buf), recordRequest.size)
}
if w.Body.String() != responseTestString {
t.Fatalf("Expected Response Body to be %s , but found %s\n", responseTestString, w.Body.String())
}
}
+561
View File
@@ -0,0 +1,561 @@
// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package httpserver
import (
"bytes"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddytls"
)
// requestReplacer is a strings.Replacer which is used to
// encode literal \r and \n characters and keep everything
// on one line
var requestReplacer = strings.NewReplacer(
"\r", "\\r",
"\n", "\\n",
)
var now = time.Now
// Replacer is a type which can replace placeholder
// substrings in a string with actual values from a
// http.Request and ResponseRecorder. Always use
// NewReplacer to get one of these. Any placeholders
// made with Set() should overwrite existing values if
// the key is already used.
type Replacer interface {
Replace(string) string
Set(key, value string)
}
// replacer implements Replacer. customReplacements
// is used to store custom replacements created with
// Set() until the time of replacement, at which point
// they will be used to overwrite other replacements
// if there is a name conflict.
type replacer struct {
customReplacements map[string]string
emptyValue string
responseRecorder *ResponseRecorder
request *http.Request
requestBody *limitWriter
}
type limitWriter struct {
w bytes.Buffer
remain int
}
func newLimitWriter(max int) *limitWriter {
return &limitWriter{
w: bytes.Buffer{},
remain: max,
}
}
func (lw *limitWriter) Write(p []byte) (int, error) {
// skip if we are full
if lw.remain <= 0 {
return len(p), nil
}
if n := len(p); n > lw.remain {
p = p[:lw.remain]
}
n, err := lw.w.Write(p)
lw.remain -= n
return n, err
}
func (lw *limitWriter) String() string {
return lw.w.String()
}
// NewReplacer makes a new replacer based on r and rr which
// are used for request and response placeholders, respectively.
// Request placeholders are created immediately, whereas
// response placeholders are not created until Replace()
// is invoked. rr may be nil if it is not available.
// emptyValue should be the string that is used in place
// of empty string (can still be empty string).
func NewReplacer(r *http.Request, rr *ResponseRecorder, emptyValue string) Replacer {
repl := &replacer{
request: r,
responseRecorder: rr,
emptyValue: emptyValue,
}
// extract customReplacements from a request replacer when present.
if existing, ok := r.Context().Value(ReplacerCtxKey).(*replacer); ok {
repl.requestBody = existing.requestBody
repl.customReplacements = existing.customReplacements
} else {
// if there is no existing replacer, build one from scratch.
rb := newLimitWriter(MaxLogBodySize)
if r.Body != nil {
r.Body = struct {
io.Reader
io.Closer
}{io.TeeReader(r.Body, rb), io.Closer(r.Body)}
}
repl.requestBody = rb
repl.customReplacements = make(map[string]string)
}
return repl
}
func canLogRequest(r *http.Request) bool {
if r.Method == "POST" || r.Method == "PUT" {
for _, cType := range r.Header[headerContentType] {
// the cType could have charset and other info
if strings.Contains(cType, contentTypeJSON) || strings.Contains(cType, contentTypeXML) {
return true
}
}
}
return false
}
// unescapeBraces finds escaped braces in s and returns
// a string with those braces unescaped.
func unescapeBraces(s string) string {
s = strings.Replace(s, "\\{", "{", -1)
s = strings.Replace(s, "\\}", "}", -1)
return s
}
// Replace performs a replacement of values on s and returns
// the string with the replaced values.
func (r *replacer) Replace(s string) string {
// Do not attempt replacements if no placeholder is found.
if !strings.ContainsAny(s, "{}") {
return s
}
result := ""
Placeholders: // process each placeholder in sequence
for {
var idxStart, idxEnd int
idxOffset := 0
for { // find first unescaped opening brace
searchSpace := s[idxOffset:]
idxStart = strings.Index(searchSpace, "{")
if idxStart == -1 {
// no more placeholders
break Placeholders
}
if idxStart == 0 || searchSpace[idxStart-1] != '\\' {
// preceding character is not an escape
idxStart += idxOffset
break
}
// the brace we found was escaped
// search the rest of the string next
idxOffset += idxStart + 1
}
idxOffset = 0
for { // find first unescaped closing brace
searchSpace := s[idxStart+idxOffset:]
idxEnd = strings.Index(searchSpace, "}")
if idxEnd == -1 {
// unpaired placeholder
break Placeholders
}
if idxEnd == 0 || searchSpace[idxEnd-1] != '\\' {
// preceding character is not an escape
idxEnd += idxOffset + idxStart
break
}
// the brace we found was escaped
// search the rest of the string next
idxOffset += idxEnd + 1
}
// get a replacement for the unescaped placeholder
placeholder := unescapeBraces(s[idxStart : idxEnd+1])
replacement := r.getSubstitution(placeholder)
// append unescaped prefix + replacement
result += strings.TrimPrefix(unescapeBraces(s[:idxStart]), "\\") + replacement
// strip out scanned parts
s = s[idxEnd+1:]
}
// append unscanned parts
return result + unescapeBraces(s)
}
func roundDuration(d time.Duration) time.Duration {
if d >= time.Millisecond {
return round(d, time.Millisecond)
} else if d >= time.Microsecond {
return round(d, time.Microsecond)
}
return d
}
// round rounds d to the nearest r
func round(d, r time.Duration) time.Duration {
if r <= 0 {
return d
}
neg := d < 0
if neg {
d = -d
}
if m := d % r; m+m < r {
d = d - m
} else {
d = d + r - m
}
if neg {
return -d
}
return d
}
// getPeerCert returns peer certificate
func (r *replacer) getPeerCert() *x509.Certificate {
if r.request.TLS != nil && len(r.request.TLS.PeerCertificates) > 0 {
return r.request.TLS.PeerCertificates[0]
}
return nil
}
// getSubstitution retrieves value from corresponding key
func (r *replacer) getSubstitution(key string) string {
// search custom replacements first
if value, ok := r.customReplacements[key]; ok {
return value
}
// search request headers then
if key[1] == '>' {
want := key[2 : len(key)-1]
for key, values := range r.request.Header {
// Header placeholders (case-insensitive)
if strings.EqualFold(key, want) {
return strings.Join(values, ",")
}
}
}
// search response headers then
if r.responseRecorder != nil && key[1] == '<' {
want := key[2 : len(key)-1]
for key, values := range r.responseRecorder.Header() {
// Header placeholders (case-insensitive)
if strings.EqualFold(key, want) {
return strings.Join(values, ",")
}
}
}
// next check for cookies
if key[1] == '~' {
name := key[2 : len(key)-1]
if cookie, err := r.request.Cookie(name); err == nil {
return cookie.Value
}
}
// next check for query argument
if key[1] == '?' {
query := r.request.URL.Query()
name := key[2 : len(key)-1]
return query.Get(name)
}
// search default replacements in the end
switch key {
case "{method}":
return r.request.Method
case "{scheme}":
if r.request.TLS != nil {
return "https"
}
return "http"
case "{hostname}":
name, err := os.Hostname()
if err != nil {
return r.emptyValue
}
return name
case "{host}":
return r.request.Host
case "{hostonly}":
host, _, err := net.SplitHostPort(r.request.Host)
if err != nil {
return r.request.Host
}
return host
case "{path}":
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return u.Path
case "{path_escaped}":
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return url.QueryEscape(u.Path)
case "{request_id}":
reqid, _ := r.request.Context().Value(RequestIDCtxKey).(string)
return reqid
case "{rewrite_path}":
return r.request.URL.Path
case "{rewrite_path_escaped}":
return url.QueryEscape(r.request.URL.Path)
case "{query}":
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return u.RawQuery
case "{query_escaped}":
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return url.QueryEscape(u.RawQuery)
case "{fragment}":
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return u.Fragment
case "{proto}":
return r.request.Proto
case "{remote}":
host, _, err := net.SplitHostPort(r.request.RemoteAddr)
if err != nil {
return r.request.RemoteAddr
}
return host
case "{port}":
_, port, err := net.SplitHostPort(r.request.RemoteAddr)
if err != nil {
return r.emptyValue
}
return port
case "{uri}":
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return u.RequestURI()
case "{uri_escaped}":
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return url.QueryEscape(u.RequestURI())
case "{rewrite_uri}":
return r.request.URL.RequestURI()
case "{rewrite_uri_escaped}":
return url.QueryEscape(r.request.URL.RequestURI())
case "{when}":
return now().Format(timeFormat)
case "{when_iso_local}":
return now().Format(timeFormatISO)
case "{when_iso}":
return now().UTC().Format(timeFormatISOUTC)
case "{when_unix}":
return strconv.FormatInt(now().Unix(), 10)
case "{when_unix_ms}":
return strconv.FormatInt(nanoToMilliseconds(now().UnixNano()), 10)
case "{file}":
_, file := path.Split(r.request.URL.Path)
return file
case "{dir}":
dir, _ := path.Split(r.request.URL.Path)
return dir
case "{request}":
dump, err := httputil.DumpRequest(r.request, false)
if err != nil {
return r.emptyValue
}
return requestReplacer.Replace(string(dump))
case "{request_body}":
if !canLogRequest(r.request) {
return r.emptyValue
}
_, err := ioutil.ReadAll(r.request.Body)
if err != nil {
if err == ErrMaxBytesExceeded {
return r.emptyValue
}
}
return requestReplacer.Replace(r.requestBody.String())
case "{mitm}":
if val, ok := r.request.Context().Value(caddy.CtxKey("mitm")).(bool); ok {
if val {
return "likely"
}
return "unlikely"
}
return "unknown"
case "{status}":
if r.responseRecorder == nil {
return r.emptyValue
}
return strconv.Itoa(r.responseRecorder.status)
case "{size}":
if r.responseRecorder == nil {
return r.emptyValue
}
return strconv.Itoa(r.responseRecorder.size)
case "{latency}":
if r.responseRecorder == nil {
return r.emptyValue
}
return roundDuration(time.Since(r.responseRecorder.start)).String()
case "{latency_ms}":
if r.responseRecorder == nil {
return r.emptyValue
}
elapsedDuration := time.Since(r.responseRecorder.start)
return strconv.FormatInt(convertToMilliseconds(elapsedDuration), 10)
case "{tls_protocol}":
if r.request.TLS != nil {
if name, err := caddytls.GetSupportedProtocolName(r.request.TLS.Version); err == nil {
return name
} else {
return "tls" // this should never happen, but guard in case
}
}
return r.emptyValue // because not using a secure channel
case "{tls_cipher}":
if r.request.TLS != nil {
if name, err := caddytls.GetSupportedCipherName(r.request.TLS.CipherSuite); err == nil {
return name
} else {
return "UNKNOWN" // this should never happen, but guard in case
}
}
return r.emptyValue
case "{tls_client_escaped_cert}":
cert := r.getPeerCert()
if cert != nil {
pemBlock := pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}
return url.QueryEscape(string(pem.EncodeToMemory(&pemBlock)))
}
return r.emptyValue
case "{tls_client_fingerprint}":
cert := r.getPeerCert()
if cert != nil {
return fmt.Sprintf("%x", sha256.Sum256(cert.Raw))
}
return r.emptyValue
case "{tls_client_i_dn}":
cert := r.getPeerCert()
if cert != nil {
return cert.Issuer.String()
}
return r.emptyValue
case "{tls_client_raw_cert}":
cert := r.getPeerCert()
if cert != nil {
return string(cert.Raw)
}
return r.emptyValue
case "{tls_client_s_dn}":
cert := r.getPeerCert()
if cert != nil {
return cert.Subject.String()
}
return r.emptyValue
case "{tls_client_serial}":
cert := r.getPeerCert()
if cert != nil {
return fmt.Sprintf("%x", cert.SerialNumber)
}
return r.emptyValue
case "{tls_client_v_end}":
cert := r.getPeerCert()
if cert != nil {
return cert.NotAfter.In(time.UTC).Format("Jan 02 15:04:05 2006 MST")
}
return r.emptyValue
case "{tls_client_v_remain}":
cert := r.getPeerCert()
if cert != nil {
now := time.Now().In(time.UTC)
days := int64(cert.NotAfter.Sub(now).Seconds() / 86400)
return strconv.FormatInt(days, 10)
}
return r.emptyValue
case "{tls_client_v_start}":
cert := r.getPeerCert()
if cert != nil {
return cert.NotBefore.Format("Jan 02 15:04:05 2006 MST")
}
return r.emptyValue
case "{server_port}":
_, port, err := net.SplitHostPort(r.request.Host)
if err != nil {
if r.request.TLS != nil {
return "443"
} else {
return "80"
}
}
return port
default:
// {labelN}
if strings.HasPrefix(key, "{label") {
nStr := key[6 : len(key)-1] // get the integer N in "{labelN}"
n, err := strconv.Atoi(nStr)
if err != nil || n < 1 {
return r.emptyValue
}
labels := strings.Split(r.request.Host, ".")
if n > len(labels) {
return r.emptyValue
}
return labels[n-1]
}
}
return r.emptyValue
}
func nanoToMilliseconds(d int64) int64 {
return d / 1e6
}
// convertToMilliseconds returns the number of milliseconds in the given duration
func convertToMilliseconds(d time.Duration) int64 {
return nanoToMilliseconds(d.Nanoseconds())
}
// Set sets key to value in the r.customReplacements map.
func (r *replacer) Set(key, value string) {
r.customReplacements["{"+key+"}"] = value
}
const (
timeFormat = "02/Jan/2006:15:04:05 -0700"
timeFormatISO = "2006-01-02T15:04:05" // ISO 8601 with timezone to be assumed as local
timeFormatISOUTC = "2006-01-02T15:04:05Z" // ISO 8601 with timezone to be assumed as UTC
headerContentType = "Content-Type"
contentTypeJSON = "application/json"
contentTypeXML = "application/xml"
// MaxLogBodySize limits the size of logged request's body
MaxLogBodySize = 100 * 1024
)

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