Compare commits

...

719 Commits

Author SHA1 Message Date
Billie Cleek 240de5a5dd v1: Allow empty files to be imported (#3421)
* add tests for an empty file

* return nil from lexer.load when the file is empty
2020-05-16 11:14:03 -06:00
Aapeli.Smith 3b4f7a3c81 httpserver: support more file types of custom error page (#3196)
Co-authored-by: Aapeli.Smith <aapelismith@users.noreply.github.com>
2020-04-13 09:58:56 -06:00
Alexander ADAM 891446d063 fix features link in README.md (#3115) 2020-03-03 09:34:06 -07:00
Daniel Santos fa7322365a Only enforces SNI matching if ClientAuth is enabled (#3096) 2020-02-27 19:37:19 -07:00
Daniel Santos 4fbdd23283 tls: Add insecure_disable_sni_matching subdirective (#3075)
* Disable StrictHostMatching for single server configs

* Add the insecure_disable_sni_matching directive

* Do not override insecure_disable_sni_matching

* Remove comment
2020-02-25 16:04:59 -07:00
Hu Chen c953d17bcc dist: Update mac README.md (#3074) 2020-02-25 15:36:12 -07:00
Levi.Lu 904d41e51a update lego to v3.3.0 to fix dnspod json error (#3030) 2020-02-09 08:55:53 -07:00
Jeremy Lin a2d71bdd94 browse: allow filter init via filter query param (#3026)
This allows creating links that display only a subset of files in a directory.
2020-02-08 12:41:16 -07:00
Matthew Holt 11ae1aa6b8 Prepare v1.0.5 release 2020-01-15 09:57:41 -07:00
Matthew Holt 3c8837163d errors: Fix test 2020-01-15 09:57:23 -07:00
Matt Holt b6ca782c99 Don't use default log settings on custom logs (#2973) (#2976)
Attempt partial fix/rollback of #2781.
2020-01-15 09:40:32 -07:00
Dmitri Goutnik a976629174 go.mod: Add support for freebsd/arm64 (#2956) 2020-01-12 14:01:25 -07:00
Dozer cd66b62083 gzip: Avoid duplicated Vary header (#2939) 2020-01-12 14:00:18 -07:00
Maxime Soulé 4b68de8418 FreeBSD init: PID file should not be executable (#2945) 2019-12-28 11:54:28 -07:00
MisterDuval 008415f206 fastcgi: Trim dot and space suffix for Windows sake (#2917) 2019-12-14 11:21:27 -07:00
Vanessa Nicole Naff f0eae39cb2 readme: Update sponsor link 2019-12-13 09:42:46 -07:00
Gábor Lipták 7fa90f08ae Correct golint warning (#2915) 2019-12-04 16:35:56 -07:00
evtr 5ec503386c proxy: ability to use client certs in reverse proxy (#2914)
* ability to use client certs in reverse proxy

* changed to http3.RoundTripper after review
2019-12-04 15:35:36 -07:00
Matthew Holt 6f9a39525a github: Remove issue and PR templates; update contributing guidelines 2019-12-04 15:13:29 -07:00
Rustin cf611796c6 update readme to remove GO111MODULE env (#2886) 2019-11-18 12:02:08 -07:00
Matthew Holt aadda6e34e Prepare v1.0.4 tag 2019-11-15 13:11:32 -07:00
Matthew Holt 6c4cb5006a go.mod: lego v3.2.0, certmagic v0.8.3, and quic-go v0.13.1 2019-11-15 13:08:28 -07:00
Yuqian Ma 12107f035c dist: update caddy.service on Ubuntu 18 (#2866)
move `StartLimitIntervalSec` and `StartLimitBurst` from [Service] to [Unit]
ref: https://lists.freedesktop.org/archives/systemd-devel/2017-July/039255.html
2019-11-15 12:59:03 -07:00
MisterDuval 832df649c1 fastcgi: Case-insensitive extension comparison 2019-11-15 12:54:39 -07:00
Julian Kornberger cc63eca0c8 Add flag to disable timestamps for process log (#2781)
* Add flag to enable/disable timstamps for process log.

solves #2615

* Remove timestamp argument from log.Printf()

* Add log-timestamps=false to systemd unit

* Copy log flags

* Fix argument list
2019-11-06 13:06:34 -07:00
Marten Seemann aa94f2b802 update quic-go to v0.13.0 (#2862)
This version supports IETF QUIC draft-23.
2019-11-06 00:30:29 -07:00
Matt Holt 3f9a431100 Create FUNDING.yml 2019-10-30 09:56:43 -06:00
Adam Daniels 8c860641b9 dist: Update FreeBSD Init script to support custom flags (#2796)
Clear rc_flags in start precmd. If these flags are still present during
start command, they are passed to daemon(8) instead of caddy(8).

Extract all options into $caddy_options environment variable.
2019-10-15 15:38:20 -06:00
Jonathan Rudenberg 99914d2204 Move certmagic import out of caddy package (#2807) 2019-10-14 10:07:36 -06:00
Jacob Hoffman-Andrews 24b2e02ee5 init/systemd: Re-add ReadWriteDirectories. (#2798)
In systemd 231
(https://github.com/systemd/systemd/blob/4f10b80786e8baa1399b6de6111d5b3a16bf99ba/NEWS#L3558-L3565),
ReadWriteDirectories was renamed ReadWritePaths.

In https://github.com/caddyserver/caddy/pull/2620/files, @aspeteRakete
renamed the directive in Caddy's example systemd unit.

However, this means that if anyone runs this sytemd unit on a version of
systemd older than 231, Caddy will go into a crash loop that hammers
Let's Encrypt's servers. That's because the ProtectSystem=full directive
prevents writes to all paths that aren't explicitly permitted, and older
systemd doesn't see any paths being permitted.

To maximize compatibility, I re-add the original ReadWriteDirectories
directive. Older systemd will read that; newer systemd will read the
newer directive. Both should ignore the directive they do not recognize.

Another approach to solve this problem would be to remove
ProtectSystem=true, originally introduced in da8ae9e5. That would reduce
the risk of similar breakages in the future. It would make for a slightly
less "exemplary" systemd unit, but I think it would still be adequate,
given that this unit runs caddy as "www-data", a user the presumably has
low privileges.
2019-10-09 19:04:28 -06:00
Adil H be2fdb6af6 httpserver: no SetKeepAlivePeriod in openbsd (#2787)
* no SetKeepAlivePeriod on openbsd

* fix tcpKeepAliveListener.Accept signature
2019-10-08 09:49:40 -06:00
Matt Holt 16b296c97e systemd: Prevent excessive restarts in tight loop 2019-10-07 11:28:21 -06:00
Jannis Andrija Schnitzer 11eee95222 staticfiles: Signal that redirection headers have been written (#2792)
The Handler interface expects a first return value of 0 if headers have
already been written.
(cf. https://godoc.org/github.com/caddyserver/caddy/caddyhttp/httpserver#Handler)

When using http.Redirect, this is the case as http.Redirect does write
headers. When using Caddy with nested handlers, returning
http.StatusMovedPermanently could cause a wrong (200) response on a
non-slashy request on a directory name. Returning 0 ensures the
redirection will always take place.
2019-10-07 10:22:53 -06:00
Tobias Mühl 1de4a99ec3 Fix typo (#2763) 2019-09-23 22:01:44 -06:00
linquize 96579b97f6 gitattributes: go.mod, go.sum always eol=lf (#2753) 2019-09-22 14:17:16 -06:00
Ashish Acharya 8cc2f770fa Change mholt references to caddyserver (#2751)
mholt/caddy.git references are changed to caddyserver/caddy.git in the link titles
2019-09-15 11:41:20 -06:00
Jack Brown a23f707268 Update build docs to clarify usage of .go extension (#2726)
Issue #2716 was caused by a misunderstanding regarding the filename to use when creating a custom build from source. These changes aim to make the instructions more explicit.
2019-08-26 19:07:07 +01:00
Alexander Danilov ed4c2775e4 main: log caddy version on start (#2717) 2019-08-21 11:13:34 -06:00
Matthew Holt bff2469d9d Version 1.0.3 notes 2019-08-14 13:45:42 -06:00
Matthew Holt a08ab0c007 Fix slice bounds when getting key of address (fixes #2706) 2019-08-14 10:41:25 -06:00
Matthew Holt 28e1f7c562 Version 1.0.2 2019-08-13 14:37:58 -06:00
Matthew Holt 914f39d784 Adjust address parsing for Go 1.12.8's breaking changes
See https://github.com/golang/go/commit/3226f2d492963d361af9dfc6714ef141ba606713
and https://github.com/golang/go/issues/29098
2019-08-13 14:37:45 -06:00
linquize 0ba427a6f4 websocket: Enhancements, message types, and tests (#2359)
* websocket: Should reset respawn parameter when processing next config entry

* websocket: add message types: lines, text, binary

* websocket: Add unit test

* Add websocket sample files
2019-07-19 13:29:49 -06:00
Matthew Holt 7fab1b15c8 readme: Fix tests badge 2019-07-19 11:28:59 -06:00
Christian Muehlhaeuser 3856ad03b0 Used goimports to fix import order (#2682)
Keeps the list sorted and prevents future merge conflicts.
2019-07-18 22:05:49 -06:00
George Hartzell d411b7d087 Add doc re running as non-root user on FreeBSD (#2655)
Add a README.md in `dist/init/freebsd` that describes how to configure
the system so that `caddy` can be run without root privileges.
2019-07-18 15:19:21 -06:00
George Hartzell 580f7677ad Use syslog to manage caddy std{out,err} on FreeBSD (#2652)
* Use syslog to manage caddy std{out,err} on FreeBSD

There is no good way to rotate the logfile created by the previous
FreeBSD rc.d script (it's the result of redirecting std{out,err} and
is held open by the shell).

This solves the problem by sending caddy's std{out,err} stream to
syslog, using the daemon command's builtin functionality.

It replaces the old `caddy_logfile` rc.conf variable with
`caddy_syslog_facility` (which defaults to 'local7') and
`caddy_syslog_level` (which defaults to 'notice').

By default, these messages will end up in /var/log/messages but can
be redirected as documented in the script's comments.

* Add info about rotating log with newsyslog

If you create a caddy specific logfile in `/var/log`, you should
rotate it.

This adds a bit of info to the dist/init/freebsd/README.md about
rotating that log file with newsyslog.
2019-07-18 13:58:40 -06:00
rouzier 120811e7f7 staticfiles: Support pre-compressed zstd, make etag content-encoding-aware (#2626)
* Add support for precompressed zstd files (rfc8478)

* Avoid the hash lookup for the file extension.

* Only calculate Etag once
2019-07-18 13:50:01 -06:00
aspeteRakete 43458bda46 Updated systemd caddy.service (#2620)
According to https://github.com/systemd/systemd/blob/v241/NEWS#L2799
The Directive ReadWriteDirectories= has been renamed to ReadWritePaths=
in 241.
2019-07-18 13:46:00 -06:00
William Wang a9ccaa1ae5 add recaptcha plugin (#2664) 2019-07-11 13:37:27 -06:00
Matthew Holt f6ee100bae Update notes for v1.0.1 2019-07-02 13:08:31 -06:00
Matthew Holt f5720fecd6 Change all import paths: mholt/caddy -> caddyserver/caddy
Includes updating go.mod to use new module path
2019-07-02 12:49:20 -06:00
Matthew Holt 0b2e054839 tls: Deprecate 'max_certs' in favor of 'ask'; use latest CertMagic 2019-07-01 11:43:27 -06:00
Damir Vandic 6f01928512 Fix graceful shutdown (#2618)
Currently, the instance waitgroup is decremented twice in `startServers()`: once when `Serve()` is finished and once when `ServePacket()` is finished. However, with a graceful shutdown, `Serve()` returns before the server has actually finished shutting down all active connections. This patch increases the wait group by one when the server is shut down so that the program only exits when all the server instances have finished serving their connections.
2019-06-23 16:24:13 -06:00
Matthew Holt 6115a462c7 mod: Use CertMagic v0.6.1 2019-06-21 08:03:17 -06:00
Matthew Holt 5f9cba0f19 caddyfile: Move metrics into caddy package 2019-06-21 08:02:53 -06:00
Matthew Holt 05b3938556 Minor fixes to tests 2019-06-19 17:02:34 -06:00
Matthew Holt 62b4553f7d tls: Disable on-demand TLS when random config is chosen
A random config is intended to be used only for solving TLS-ALPN
challenges; so we have to be sure to disable on-demand TLS so that
arbitrary names can't request certificates with another name's
on-demand config.
2019-06-19 16:57:45 -06:00
Matthew Holt ad20323b52 Refactor clustering setup code 2019-06-19 16:57:45 -06:00
Matthew Holt 721c100bb0 Use CertMagic's HTTP and HTTPS port variable
Slightly inconvenient because it uses int type and we use string, but
oh well. This fixes a bug related to setting -http-port and -https-port
flags which weren't being used by CertMagic in some cases.
2019-06-19 16:57:45 -06:00
Matthew Holt 6720bdfb55 Clean up certmagic locks on signaled process exit
This should help prevent hanging in some cases when the process is
restarted and tries to obtain or renew a certificate, for example, but
the lock remains from the previous shutdown (which was during the same
operation). Only works if the process is cleanly shut down with a signal
it can capture.
2019-06-19 16:57:45 -06:00
shouya 0c626fbc2e tls: Allow client auth configs if CA filenames match (#2648)
* verify client certs

* move client cert compatible checker to an independent function

* unexport client cert compatible checker

* rename functions and add comment

* gofmt code

* add test

* add back the comment
2019-06-19 11:25:56 -06:00
Daniel af82141808 caddyhttp: Add 'permission' plugin directive (#2639) 2019-06-12 10:15:17 -06:00
Jared Ririe d11b648137 caddytls: Fix goroutine leak when restarting Caddy (#2644)
Each time the Caddyfile reloads and Caddy is restarted,
caddytls.NewConfig starts a goroutine for cleaning the
certificate storage. This goroutine ranges over a time.Ticker
channel; although Stop is called on this ticker, Stop does
not close the underlying channel so the goroutine never exits.

This change adds an additional channel that is listened to
in the certificate cleaning goroutine so it can exit
on restarts.
2019-06-11 15:24:35 -06:00
Matthew Holt 14a8ffedd8 Fix panic serving index file if HTTP request is malformed 2019-05-27 08:12:19 -06:00
Matthew Holt b5906135c7 Move PR template in attempt to fix (sigh)
https://github.community/t5/How-to-use-Git-and-GitHub/Our-pull-request-templates-aren-t-showing-up-for-any-PRs/m-p/22958
2019-05-27 08:11:33 -06:00
AndreKR 4bad5c79be Simple rewrite regex captures (#2592)
* More informative rewrite test output

When running rewrite tests, the output in case of a test failure now
includes not only the rewritten URLs but also the from URL.

* Move re-escaping to regexpMatches

This commit moves the code to post-process the match replacements from
ComplexRule to regexpMatches, so it can later be re-used for SimpleRule.

Also changes the comment in an attempt to better explain the reasoning
behind that code.

The required strings.Replacer is now built only once.

* Support regex captures in simple rewrite rules

Closes #2586
2019-05-24 12:00:27 -06:00
Łukasz Nowak 81430e4aff gzip: Add .wasm (WebAssembly files) (#2624) 2019-05-24 09:59:06 -06:00
Anthony Plunkett c238b72d5d readme: clarify about Go file to build Caddy (#2611) 2019-05-13 22:17:36 -06:00
Kurt Jung a2ed91bc45 httpserver: Add pubsub plugin (#2589) 2019-04-26 12:32:43 -06:00
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
Tw eeb23a2469 redirect: determine the FromScheme at runtime (#1297)
Signed-off-by: Tw <tw19881113@gmail.com>
2017-01-21 09:53:44 +08:00
Matthew Holt c6ba43f888 Set session ticket keys properly (fixed in Go 1.8) 2017-01-15 09:30:02 -07:00
274 changed files with 22610 additions and 9793 deletions
+7
View File
@@ -9,6 +9,13 @@
# 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
go.mod text eol=lf
go.sum text eol=lf
*.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
+185
View File
@@ -0,0 +1,185 @@
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 huge impact on the project by helping with its code. To contribute code to Caddy, open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy/-/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. :blue_heart: 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:
- **Open an issue to propose your change first.** This way we can avoid confusion, coordinate what everyone is working on, and ensure that any changes are in-line with the project's goals and the best interests of its users. We can also discuss the best possible implementation. If there's already an issue about it, comment on the existing issue to claim it.
- **Keep pull requests small.** Smaller PRs are more likely to be merged because they are easier to review! We might ask you to break up large PRs into smaller ones. [An example of what we want to avoid.](https://twitter.com/iamdevloper/status/397664295875805184)
- **Keep related commits together in a PR.** We do want pull requests to be small, but you should also keep multiple related commits in the same PR if they rely on each other.
- **Write tests.** 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/caddyserver/caddy). This makes a copy of the code you can write to.
2. If you don't already have this repo (caddyserver/caddy.git) repo on your computer, get it with `go get github.com/caddyserver/caddy/caddy`.
3. Tell git that it can push the caddyserver/caddy.git repo to your fork by adding a remote: `git remote add myfork https://github.com/you/caddy.git`
4. Make your changes in the caddyserver/caddy.git repo on your computer.
5. Push your changes to your fork: `git push myfork`
6. [Create a pull request](https://github.com/caddyserver/caddy/pull/new/master) to merge your changes into caddyserver/caddy @ master. (Click "compare across forks" and change the head fork.)
This workflow is nice because you don't have to change import paths. You can get fancier by using different branches if you want.
### Writing a 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/caddyserver/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/caddyserver/caddy/issues) to see if it has already been reported. If not, [open a new issue](https://github.com/caddyserver/caddy/issues/new) and describe the bug, and somebody will look into it! (This repository is only for Caddy and its standard modules.)
**You can help 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!
#### Bug reporting requirements
Maintainers---or more generally, developers---need three things to act on bugs:
1. To agree or be convinced that it's a bug (reporter's responsibility).
- A bug is undesired or surprising behavior which violates documentation or the spec.
2. To be able to understand what is happening (mostly reporter's responsibility).
- If the reporter can provide satisfactory instructions such that a developer can reproduce the bug, the developer will likely be able to understand the bug, write a test case, and implement a fix.
- Otherwise, the burden is on the reporter to test possible solutions. This is discouraged because it loosens the feedback loop, slows down debugging efforts, obscures the true nature of the problem from the developers, and is unlikely to result in new test cases.
3. A solution, or ideas toward a solution (mostly maintainer's responsibility).
- Sometimes the best solution is a documentation change.
- Usually the developers have the best domain knowledge for inventing a solution, but reporters may have ideas or preferences for how they would like the software to work.
- Security, correctness, and project goals/vision all take priority over a user's preferences.
- It's simply good business to yield a solution that satisfies the users, and it's even better business to leave them impressed.
Thus, at the very least, the reporter is expected to:
1. Convince the reader that it's a bug (if it's not obvious).
2. Reduce the problem down to the minimum specific steps required to reproduce it.
The maintainer is usually able to do the rest; but of course the reporter may invest additional effort to speed up the process.
### Suggesting features
First, [search to see if your feature has already been requested](https://github.com/caddyserver/caddy/issues). If it has, you can add a :+1: reaction to vote for it. If your feature idea is new, open an issue to request the feature. Please describe your idea thoroughly so that we know how to implement it! Really vague requests may not be helpful or actionable and, without clarification, will have to be closed.
While we really do value your requests and implement many of them, not all features are a good fit for Caddy. Most of those [make good plugins](https://github.com/caddyserver/caddy/wiki), 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/v1/docs](https://caddyserver.com/v1/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.
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [mholt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+6 -1
View File
@@ -13,5 +13,10 @@ access.log
/*.conf
Caddyfile
!caddyfile/
og_static/
og_static/
.vscode/
*.bat
-34
View File
@@ -1,34 +0,0 @@
language: go
go:
- 1.7.4
- tip
matrix:
allow_failures:
- go: tip
fast_finish: true
before_install:
# Decrypts a script that installs an authenticated cookie
# for git to use when cloning from googlesource.com.
# Bypasses "bandwidth limit exceeded" errors.
# See github.com/golang/go/issues/12933
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then openssl aes-256-cbc -K $encrypted_3df18f9af81d_key -iv $encrypted_3df18f9af81d_iv -in dist/gitcookie.sh.enc -out dist/gitcookie.sh -d; fi
install:
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash dist/gitcookie.sh; fi
- go get -t ./...
- go get github.com/golang/lint/golint
- go get github.com/gordonklaus/ineffassign
- go get github.com/client9/misspell/cmd/misspell
script:
- diff <(echo -n) <(gofmt -s -d .)
- ineffassign .
- misspell -error .
- go vet ./...
- go test -race ./...
after_script:
- golint ./...
-115
View File
@@ -1,115 +0,0 @@
## Contributing to Caddy
Welcome! Our community focuses on helping others and making Caddy the best it
can be. We gladly accept contributions and encourage you to get involved!
### Join us in the forum
The [Caddy forum](https://forum.caddyserver.com) is the place for all discussion
that doesn't belong in issues or pull requests. Feel free to participate with us!
If you want to file a bug report or make an improvement to Caddy, however, you
should submit an issue or pull request.
### Bug reports
Please [search this repository](https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93)
with a variety of keywords to ensure your bug is not already reported.
If unique, [open an issue](https://github.com/mholt/caddy/issues) and answer the
questions so we can understand and reproduce the problematic behavior.
The burden is on you to convince us that it is actually a bug in Caddy. This is
easiest to do when you write clear, concise instructions so we can reproduce
the behavior (even if it seems obvious). The more detailed and specific you are,
the faster we will be able to help you. Check out
[How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
Please be kind. :smile: Remember that Caddy comes at no cost to you, and you're
getting free help. If we helped you, please consider
[donating](https://caddyserver.com/donate) - it keeps us motivated!
### Minor improvements and new tests
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time for
minor changes or new tests. Make sure to write tests to assert your change is
working properly and is thoroughly covered. We'll ask most pull requests to be
[squashed](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html),
especially with small commits.
Your pull request may be thoroughly reviewed. This is because if we accept the
PR, we also assume responsibility for it, although we would prefer you to
help maintain your code after it gets merged.
### Proposals, suggestions, ideas, new features
First, please [search](https://github.com/mholt/caddy/search?q=&type=Issues&utf8=%E2%9C%93)
with a variety of keywords to ensure your suggestion/proposal is new.
If so, you may open either an issue or a pull request for discussion and
feedback.
The advantage of issues is that you don't have to spend time implementing your
idea, but you should still describe it thoroughly as if someone reading it would
implement the whole thing starting from scratch.
The advantage of pull requests is that we can immediately see the impact the
change will have on the project, what the code will look like, and how to
improve it. The disadvantage of pull requests is that they are unlikely to get
accepted without significant changes first, or it may be rejected entirely.
Don't worry, that won't happen without an open discussion first.
If you are going to spend significant time writing code for a new pull request,
best to open an issue to "claim" it and get feedback before you invest a lot of
time. Not all pull requests are merged, and that's okay,
[Read why.](https://github.com/turbolinks/turbolinks/pull/124#issuecomment-239826060)
Remember: pull requests should always be thoroughly documented both via godoc
and with at least a rough draft of documentation that might go on the website
for users to read.
### Collaborator status
If your pull request is merged, congratulations! You're technically a
collaborator. We may also grant you "Collaborator status" which means you can
push to the repository and merge other pull requests. We hope that you will
stay involved by reviewing pull requests, submitting more of your own, and
resolving issues as you are able to. Thanks for making Caddy amazing!
We ask that collaborators will conduct thorough code reviews and be nice to
new contributors. Before merging a PR, it's best to get the approval of
at least one or two other collaborators and/or the project owner. We prefer
squashed commits instead of many little, semantically-unimportant commits. Also,
CI and other post-commit hooks must pass before being merged except in certain
unusual circumstances.
Collaborator status may be removed for inactive users from time to time as
we see fit; this is not an insult, just a basic security precaution in case
the account becomes inactive or abandoned. Privileges can always be restored
later.
**Reviewing pull requests:** Please help submit and review pull requests as
you are able! We would ask that every pull request be reviewed by at least
one collaborator who did not open the pull request before merging. This will
help ensure high code quality as new collaborators are added to the project.
Read [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments)
on the Go wiki for an idea of what we look for in good, clean Go code.
### Vulnerabilities
If you've found a vulnerability that is serious, please email me: Matthew dot
Holt at Gmail. If it's not a big deal, a pull request will probably be faster.
## Thank you
Thanks for your help! Caddy would not be what it is today without your
contributions.
-24
View File
@@ -1,24 +0,0 @@
(Are you asking for help with using Caddy? Please use our forum instead: https://forum.caddyserver.com. If you are filing a bug report, please answer the following questions. If your issue is not a bug report, you do not need to use this template. Either way, please consider donating if we've helped you. Thanks!)
### 1. What version of Caddy are you running (`caddy -version`)?
### 2. What are you trying to do?
### 3. What is your entire Caddyfile?
```text
(Put Caddyfile here)
```
### 4. How did you run Caddy (give the full command and describe the execution environment)?
### 5. What did you expect to see?
### 6. What did you see instead (give full error messages and/or log)?
### 7. How can someone who is starting from scratch reproduce this behavior as minimally as possible?
+150 -95
View File
@@ -1,68 +1,151 @@
<a href="https://caddyserver.com"><img src="https://caddyserver.com/resources/images/caddy-lower.png" alt="Caddy" width="350"></a>
<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=5"><img src="https://img.shields.io/azure-devops/build/mholt-dev/afec6074-9842-457f-98cf-69df6adbbf2e/5/master.svg?label=cross-platform%20tests"></a>
<a href="https://godoc.org/github.com/caddyserver/caddy"><img src="https://img.shields.io/badge/godoc-reference-blue.svg"></a>
<a href="https://goreportcard.com/report/caddyserver/caddy"><img src="https://goreportcard.com/badge/github.com/caddyserver/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/caddyserver/caddy?badge" title="Caddy on Sourcegraph"><img src="https://sourcegraph.com/github.com/caddyserver/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>
[![community](https://img.shields.io/badge/community-forum-ff69b4.svg?style=flat-square)](https://forum.caddyserver.com) [![twitter](https://img.shields.io/badge/twitter-@caddyserver-55acee.svg?style=flat-square)](https://twitter.com/caddyserver) [![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/mholt/caddy) [![Linux Build Status](https://img.shields.io/travis/mholt/caddy.svg?style=flat-square&label=linux+build)](https://travis-ci.org/mholt/caddy) [![Windows Build Status](https://img.shields.io/appveyor/ci/mholt/caddy.svg?style=flat-square&label=windows+build)](https://ci.appveyor.com/project/mholt/caddy)
[![Go Report Card](https://goreportcard.com/badge/github.com/mholt/caddy?style=flat-square)](https://goreportcard.com/report/mholt/caddy)
---
Caddy is a **production-ready** open-source web server that is fast, easy to use, and makes you more productive.
Caddy is a general-purpose web server for Windows, Mac, Linux, BSD, and
[Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android). It is
a capable but easier alternative to other popular web servers.
Available for Windows, Mac, Linux, BSD, Solaris, and [Android](https://github.com/caddyserver/caddy/wiki/Running-Caddy-on-Android).
[Releases](https://github.com/mholt/caddy/releases) ·
[User Guide](https://caddyserver.com/docs) ·
[Community](https://forum.caddyserver.com)
Try browsing [the code on Sourcegraph](https://sourcegraph.com/github.com/mholt/caddy)!
<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
- [Features](#features)
- [Install](#install)
- [Quick Start](#quick-start)
- [Running from Source](#running-from-source)
- [Running in Production](#running-in-production)
- [Contributing](#contributing)
- [Donors](#donors)
- [About the Project](#about-the-project)
## Features
- **Easy configuration** with Caddyfile
- **Automatic HTTPS** via [Let's Encrypt](https://letsencrypt.org); Caddy
obtains and manages all cryptographic assets for you
- **HTTP/2** enabled by default (powered by Go standard library)
- **Virtual hosting** for hundreds of sites per server instance, including TLS
SNI
- Experimental **QUIC support** for those that like speed
- **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
- **Brilliant extensibility** so Caddy can be customized for your needs
- **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!
<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>
## Install
Caddy binaries have no dependencies and are available for every platform. Get Caddy any of these ways:
- **[Download page](https://caddyserver.com/download)** (RECOMMENDED) allows you to customize your build in the browser
- **[Latest release](https://github.com/caddyserver/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
To build from source you need **[Git](https://git-scm.com/downloads)** and **[Go](https://golang.org/doc/install)** (1.13 or newer).
**To build Caddy without plugins:**
- Run `go get github.com/caddyserver/caddy/caddy`
Caddy will be installed to your `$GOPATH/bin` folder.
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.
**To build Caddy with plugins (and with version information):**
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.
- Create a new folder anywhere and within create a Go file (with an extension of `.go`, such as `main.go`) with the contents below, adjusting to import the plugins you want to include:
```go
package main
import (
"github.com/caddyserver/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/caddyserver/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:**
- Run `git clone https://github.com/caddyserver/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
Caddy binaries have no dependencies and are available for every platform.
Install Caddy any one of these ways:
To serve static files from the current working directory, run:
- **[Download page](https://caddyserver.com/download)** allows you to
customize your build in the browser
- **[Latest release](https://github.com/mholt/caddy/releases/latest)** for
pre-built binaries
- **curl [getcaddy.com](https://getcaddy.com)** for auto install:
`curl https://getcaddy.com | bash`
```
caddy
```
Once `caddy` is in your PATH, you can `cd` to your website's folder and run
`caddy` to serve it. By default, Caddy serves the current directory at
[localhost:2015](http://localhost:2015).
Caddy's default port is 2015, so open your browser to [http://localhost:2015](http://localhost:2015).
To customize how your site is served, create a file named Caddyfile by your
site and paste this into it:
### 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
websocket /echo cat
ext .html
@@ -71,93 +154,65 @@ proxy /api 127.0.0.1:7005
header /api Access-Control-Allow-Origin *
```
When you run `caddy` in that directory, it will automatically find and use
that Caddyfile to configure itself.
When you run `caddy` in that directory, it will automatically find and use that Caddyfile.
This simple file enables compression, allows directory browsing (for folders
without an index file), hosts a WebSocket echo server at /echo, serves clean
URLs, logs requests to access.log, proxies all API requests to a backend on
port 7005, and adds the coveted `Access-Control-Allow-Origin: *` header for
all responses from the API.
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.
To host multiple sites and do more with the Caddyfile, please see the
[Caddyfile documentation](https://caddyserver.com/docs/caddyfile).
### Doing more with Caddy
Note that production sites are served over
[HTTPS by default](https://caddyserver.com/docs/automatic-https).
To host multiple sites and do more with the Caddyfile, please see the [Caddyfile tutorial](https://caddyserver.com/tutorial/caddyfile).
Caddy has a command line interface. Run `caddy -h` to view basic help or see
the [CLI documentation](https://caddyserver.com/docs/cli) for details.
**Running as root:** We advise against this. You can still listen on ports
< 1024 using setcap like so: `sudo setcap cap_net_bind_service=+ep ./caddy`
## Running from Source
Note: You will need **[Go 1.7](https://golang.org/dl/)** or newer.
1. `go get github.com/mholt/caddy/caddy`
2. `cd` into your website's directory
3. Run `caddy` (assuming `$GOPATH/bin` is in your `$PATH`)
Caddy's `main()` is in the caddy subfolder. To recompile Caddy, use
`build.bash` found in that folder.
Sites with qualifying hostnames are served over [HTTPS by default](https://caddyserver.com/docs/automatic-https).
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
The Caddy project does not officially maintain any system-specific
integrations, but your download file includes
[unofficial resources](https://github.com/mholt/caddy/tree/master/dist/init)
contributed by the community that you may find helpful for running Caddy in
production.
Caddy is production-ready if you find it to be a good fit for your site and workflow.
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.
**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/caddyserver/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 our community](https://forum.caddyserver.com) where you can chat with
other Caddy users and developers!**
**[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/caddyserver/caddy/)!
Please see our [contributing guidelines](https://github.com/mholt/caddy/blob/master/CONTRIBUTING.md)
and check out the [developer wiki](https://github.com/mholt/caddy/wiki).
Please see our [contributing guidelines](https://github.com/caddyserver/caddy/blob/master/.github/CONTRIBUTING.md) for instructions. If you want to write a plugin, check out the [developer wiki](https://github.com/caddyserver/caddy/wiki).
We use GitHub issues and pull requests only for discussing bug reports and
the development of specific changes. We welcome all other topics on the
[forum](https://forum.caddyserver.com)!
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 pull requests to [caddyserver/caddyserver.com](https://github.com/caddyserver/caddyserver.com).
If you want to contribute to the documentation, please [submit an issue](https://github.com/caddyserver/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/caddyserver/caddy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) tag
Thanks for making Caddy -- and the Web -- better!
Special thanks to
[![DigitalOcean](https://i.imgur.com/sfGr0eY.png)](https://www.digitalocean.com)
for hosting the Caddy project.
## 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://github.com/sponsors/mholt)!**
## 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 [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.
**The name "Caddy":** The name of the software is "Caddy", not "Caddy Server"
or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the
Caddy web server". See [brand guidelines](https://caddyserver.com/brand).
**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.
*Author on Twitter: [@mholt6](https://twitter.com/mholt6)*
-31
View File
@@ -1,31 +0,0 @@
version: "{build}"
os: Windows Server 2012 R2
clone_folder: c:\gopath\src\github.com\mholt\caddy
environment:
GOPATH: c:\gopath
install:
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go1.7.4.windows-amd64.zip
- 7z x go1.7.4.windows-amd64.zip -y -oC:\ > NUL
- go version
- go env
- go get -t ./...
- go get github.com/golang/lint/golint
- go get github.com/gordonklaus/ineffassign
- set PATH=%GOPATH%\bin;%PATH%
build: off
test_script:
- go vet ./...
- go test -race ./...
- ineffassign .
after_test:
- golint ./...
deploy: off
+14
View File
@@ -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.
package caddy
import (
+22 -2
View File
@@ -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.
package caddy
import (
@@ -11,9 +25,15 @@ func TestAssetsPath(t *testing.T) {
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
}
os.Setenv("CADDYPATH", "testpath")
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)
}
os.Setenv("CADDYPATH", "")
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
+286 -119
View File
@@ -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.
// Package caddy implements the Caddy server manager.
//
// To use this package:
@@ -5,6 +19,8 @@
// 1. Set the AppName and AppVersion variables.
// 2. Call LoadCaddyfile() to get the Caddyfile.
// Pass in the name of the server type (like "http").
// Make sure the server type's package is imported
// (import _ "github.com/caddyserver/caddy/caddyhttp").
// 3. Call caddy.Start() to start Caddy. You get back
// an Instance, on which you can call Restart() to
// restart it or Stop() to stop it.
@@ -15,6 +31,7 @@ package caddy
import (
"bytes"
"encoding/gob"
"fmt"
"io"
"io/ioutil"
@@ -26,7 +43,8 @@ import (
"sync"
"time"
"github.com/mholt/caddy/caddyfile"
"github.com/caddyserver/caddy/caddyfile"
"github.com/caddyserver/caddy/telemetry"
)
// Configurable application parameters
@@ -49,7 +67,7 @@ var (
// isUpgrade will be set to true if this process
// was started as part of an upgrade, where a parent
// Caddy process started this one.
isUpgrade bool
isUpgrade = os.Getenv("CADDY__UPGRADE") == "1"
// started will be set to true when the first
// instance is started; it never gets set to
@@ -60,8 +78,18 @@ var (
mu sync.Mutex
)
func init() {
OnProcessExit = append(OnProcessExit, func() {
if PidFile != "" {
os.Remove(PidFile)
}
})
}
// Instance contains the state of servers created as a result of
// calling Start and can be used to access or control those servers.
// It is literally an instance of a server type. Instance values
// should NOT be copied. Use *Instance for safety.
type Instance struct {
// serverType is the name of the instance's server type
serverType string
@@ -72,18 +100,33 @@ type Instance struct {
// wg is used to wait for all servers to shut down
wg *sync.WaitGroup
// context is the context created for this instance.
// context is the context created for this instance,
// used to coordinate the setting up of the server type
context Context
// servers is the list of servers with their listeners.
// servers is the list of servers with their listeners
servers []ServerListener
// these callbacks execute when certain events occur
onFirstStartup []func() error // starting, not as part of a restart
onStartup []func() error // starting, even as part of a restart
onRestart []func() error // before restart commences
onShutdown []func() error // stopping, even as part of a restart
onFinalShutdown []func() error // stopping, not as part of a restart
OnFirstStartup []func() error // starting, not as part of a restart
OnStartup []func() error // starting, even as part of a restart
OnRestart []func() error // before restart commences
OnRestartFailed []func() error // if restart failed
OnShutdown []func() error // stopping, even as part of a restart
OnFinalShutdown []func() error // stopping, not as part of a restart
// storing values on an instance is preferable to
// global state because these will get garbage-
// collected after in-process reloads when the
// old instances are destroyed; use StorageMu
// to access this value safely
Storage map[interface{}]interface{}
StorageMu sync.RWMutex
}
// Instances returns the list of instances.
func Instances() []*Instance {
return instances
}
// Servers returns the ServerListeners in i.
@@ -120,13 +163,13 @@ func (i *Instance) Stop() error {
// the rest. All the non-nil errors will be returned.
func (i *Instance) ShutdownCallbacks() []error {
var errs []error
for _, shutdownFunc := range i.onShutdown {
for _, shutdownFunc := range i.OnShutdown {
err := shutdownFunc()
if err != nil {
errs = append(errs, err)
}
}
for _, finalShutdownFunc := range i.onFinalShutdown {
for _, finalShutdownFunc := range i.OnFinalShutdown {
err := finalShutdownFunc()
if err != nil {
errs = append(errs, err)
@@ -144,9 +187,28 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
i.wg.Add(1)
defer i.wg.Done()
var err error
// if something went wrong on restart then run onRestartFailed callbacks
defer func() {
r := recover()
if err != nil || r != nil {
for _, fn := range i.OnRestartFailed {
if err := fn(); err != nil {
log.Printf("[ERROR] Restart failed callback returned error: %v", err)
}
}
if err != nil {
log.Printf("[ERROR] Restart failed: %v", err)
}
if r != nil {
log.Printf("[PANIC] Restart: %v", r)
}
}
}()
// run restart callbacks
for _, fn := range i.onRestart {
err := fn()
for _, fn := range i.OnRestart {
err = fn()
if err != nil {
return i, err
}
@@ -179,22 +241,28 @@ func (i *Instance) Restart(newCaddyfile Input) (*Instance, error) {
}
// create new instance; if the restart fails, it is simply discarded
newInst := &Instance{serverType: newCaddyfile.ServerType(), wg: i.wg}
newInst := &Instance{serverType: newCaddyfile.ServerType(), wg: i.wg, Storage: make(map[interface{}]interface{})}
// attempt to start new instance
err := startWithListenerFds(newCaddyfile, newInst, restartFds)
err = startWithListenerFds(newCaddyfile, newInst, restartFds)
if err != nil {
return i, err
return i, fmt.Errorf("starting with listener file descriptors: %v", err)
}
// success! stop the old instance
for _, shutdownFunc := range i.onShutdown {
err := shutdownFunc()
err = i.Stop()
if err != nil {
return i, err
}
for _, shutdownFunc := range i.OnShutdown {
err = shutdownFunc()
if err != nil {
return i, err
}
}
i.Stop()
// Execute instantiation events
EmitEvent(InstanceStartupEvent, newInst)
log.Println("[INFO] Reloading complete")
@@ -208,42 +276,6 @@ func (i *Instance) SaveServer(s Server, ln net.Listener) {
i.servers = append(i.servers, ServerListener{server: s, listener: ln})
}
// HasListenerWithAddress returns whether this package is
// tracking a server using a listener with the address
// addr.
func HasListenerWithAddress(addr string) bool {
instancesMu.Lock()
defer instancesMu.Unlock()
for _, inst := range instances {
for _, sln := range inst.servers {
if listenerAddrEqual(sln.listener, addr) {
return true
}
}
}
return false
}
// listenerAddrEqual compares a listener's address with
// addr. Extra care is taken to match addresses with an
// empty hostname portion, as listeners tend to report
// [::]:80, for example, when the matching address that
// created the listener might be simply :80.
func listenerAddrEqual(ln net.Listener, addr string) bool {
lnAddr := ln.Addr().String()
hostname, port, err := net.SplitHostPort(addr)
if err != nil {
return lnAddr == addr
}
if lnAddr == net.JoinHostPort("::", port) {
return true
}
if lnAddr == net.JoinHostPort("0.0.0.0", port) {
return true
}
return hostname != "" && lnAddr == addr
}
// TCPServer is a type that can listen and serve connections.
// A TCPServer must associate with exactly zero or one net.Listeners.
type TCPServer interface {
@@ -322,6 +354,11 @@ type GracefulServer interface {
// address; you must store the address the
// server is to serve on some other way.
Address() string
// WrapListener wraps a listener with the
// listener middlewares configured for this
// server, if any.
WrapListener(net.Listener) net.Listener
}
// Listener is a net.Listener with an underlying file descriptor.
@@ -358,6 +395,16 @@ type AfterStartup interface {
// is returned. Consequently, this function never returns a nil
// value as long as there are no errors.
func LoadCaddyfile(serverType string) (Input, error) {
// If we are finishing an upgrade, we must obtain the Caddyfile
// from our parent process, regardless of configured loaders.
if IsUpgrade() {
err := gob.NewDecoder(os.Stdin).Decode(&loadedGob)
if err != nil {
return nil, err
}
return loadedGob.Caddyfile, nil
}
// Ask plugged-in loaders for a Caddyfile
cdyfile, err := loadCaddyfileInput(serverType)
if err != nil {
@@ -422,17 +469,51 @@ func (i *Instance) Caddyfile() Input {
//
// This function blocks until all the servers are listening.
func Start(cdyfile Input) (*Instance, error) {
writePidFile()
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup)}
return inst, startWithListenerFds(cdyfile, inst, nil)
inst := &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
err := startWithListenerFds(cdyfile, inst, nil)
if err != nil {
return inst, err
}
signalSuccessToParent()
if pidErr := writePidFile(); pidErr != nil {
log.Printf("[ERROR] Could not write pidfile: %v", pidErr)
}
// Execute instantiation events
EmitEvent(InstanceStartupEvent, inst)
return inst, nil
}
func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]restartTriple) error {
// save this instance in the list now so that
// plugins can access it if need be, for example
// the caddytls package, so it can perform cert
// renewals while starting up; we just have to
// remove the instance from the list later if
// it fails
instancesMu.Lock()
instances = append(instances, inst)
instancesMu.Unlock()
var err error
defer func() {
if err != nil {
instancesMu.Lock()
for i, otherInst := range instances {
if otherInst == inst {
instances = append(instances[:i], instances[i+1:]...)
break
}
}
instancesMu.Unlock()
}
}()
if cdyfile == nil {
cdyfile = CaddyfileInput{}
}
err := ValidateAndExecuteDirectives(cdyfile, inst, false)
err = ValidateAndExecuteDirectives(cdyfile, inst, false)
if err != nil {
return err
}
@@ -443,16 +524,17 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
}
// run startup callbacks
if restartFds == nil {
for _, firstStartupFunc := range inst.onFirstStartup {
err := firstStartupFunc()
if !IsUpgrade() && restartFds == nil {
// first startup means not a restart or upgrade
for _, firstStartupFunc := range inst.OnFirstStartup {
err = firstStartupFunc()
if err != nil {
return err
}
}
}
for _, startupFunc := range inst.onStartup {
err := startupFunc()
for _, startupFunc := range inst.OnStartup {
err = startupFunc()
if err != nil {
return err
}
@@ -463,10 +545,6 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
return err
}
instancesMu.Lock()
instances = append(instances, inst)
instancesMu.Unlock()
// run any AfterStartup callbacks if this is not
// part of a restart; then show file descriptor notice
if restartFds == nil {
@@ -477,6 +555,11 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
}
if !Quiet {
for _, srvln := range inst.servers {
// only show FD notice if the listener is not nil.
// This can happen when only serving UDP or TCP
if srvln.listener == nil {
continue
}
if !IsLoopback(srvln.listener.Addr().String()) {
checkFdlimit()
break
@@ -498,10 +581,9 @@ func startWithListenerFds(cdyfile Input, inst *Instance, restartFds map[string]r
// callbacks will not be executed between directives, since the purpose
// is only to check the input for valid syntax.
func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bool) error {
// If parsing only inst will be nil, create an instance for this function call only.
if justValidate {
inst = &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup)}
inst = &Instance{serverType: cdyfile.ServerType(), wg: new(sync.WaitGroup), Storage: make(map[interface{}]interface{})}
}
stypeName := cdyfile.ServerType()
@@ -518,23 +600,25 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo
return err
}
inst.context = stype.NewContext()
for _, sb := range sblocks {
for dir := range sb.Tokens {
telemetry.AppendUnique("directives", dir)
}
}
inst.context = stype.NewContext(inst)
if inst.context == nil {
return fmt.Errorf("server type %s produced a nil Context", stypeName)
}
sblocks, err = inst.context.InspectServerBlocks(cdyfile.Path(), sblocks)
if err != nil {
return err
return fmt.Errorf("error inspecting server blocks: %v", err)
}
err = executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate)
if err != nil {
return err
}
return nil
telemetry.Set("num_server_blocks", len(sblocks))
return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate)
}
func executeDirectives(inst *Instance, filename string,
@@ -607,6 +691,11 @@ func executeDirectives(inst *Instance, filename string,
func startServers(serverList []Server, inst *Instance, restartFds map[string]restartTriple) error {
errChan := make(chan error, len(serverList))
// used for signaling to error logging goroutine to terminate
stopChan := make(chan struct{})
// used to track termination of servers
stopWg := &sync.WaitGroup{}
for _, s := range serverList {
var (
ln net.Listener
@@ -614,6 +703,37 @@ func startServers(serverList []Server, inst *Instance, restartFds map[string]res
err error
)
// if performing an upgrade, obtain listener file descriptors
// from parent process
if IsUpgrade() {
if gs, ok := s.(GracefulServer); ok {
addr := gs.Address()
if fdIndex, ok := loadedGob.ListenerFds["tcp"+addr]; ok {
file := os.NewFile(fdIndex, "")
ln, err = net.FileListener(file)
if err != nil {
return fmt.Errorf("making listener from file: %v", err)
}
err = file.Close()
if err != nil {
return fmt.Errorf("closing copy of listener file: %v", err)
}
}
if fdIndex, ok := loadedGob.ListenerFds["udp"+addr]; ok {
file := os.NewFile(fdIndex, "")
pc, err = net.FilePacketConn(file)
if err != nil {
return fmt.Errorf("making packet connection from file: %v", err)
}
err = file.Close()
if err != nil {
return fmt.Errorf("closing copy of packet connection file: %v", err)
}
}
ln = gs.WrapListener(ln)
}
}
// If this is a reload and s is a GracefulServer,
// reuse the listener for a graceful restart.
if gs, ok := s.(GracefulServer); ok && restartFds != nil {
@@ -623,71 +743,97 @@ func startServers(serverList []Server, inst *Instance, restartFds map[string]res
if old.listener != nil {
file, err := old.listener.File()
if err != nil {
return err
return fmt.Errorf("getting old listener file: %v", err)
}
ln, err = net.FileListener(file)
if err != nil {
return err
return fmt.Errorf("getting file listener: %v", err)
}
err = file.Close()
if err != nil {
return fmt.Errorf("closing copy of listener file: %v", err)
}
file.Close()
}
// packetconn
if old.packet != nil {
file, err := old.packet.File()
if err != nil {
return err
return fmt.Errorf("getting old packet file: %v", err)
}
pc, err = net.FilePacketConn(file)
if err != nil {
return err
return fmt.Errorf("getting file packet connection: %v", err)
}
err = file.Close()
if err != nil {
return fmt.Errorf("close copy of packet file: %v", err)
}
file.Close()
}
ln = gs.WrapListener(ln)
}
}
if ln == nil {
ln, err = s.Listen()
if err != nil {
return err
return fmt.Errorf("Listen: %v", err)
}
}
if pc == nil {
pc, err = s.ListenPacket()
if err != nil {
return err
return fmt.Errorf("ListenPacket: %v", err)
}
}
inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc})
}
for _, s := range inst.servers {
inst.wg.Add(2)
go func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
defer inst.wg.Done()
stopWg.Add(2)
func(s Server, ln net.Listener, pc net.PacketConn, inst *Instance) {
go func() {
defer func() {
inst.wg.Done()
stopWg.Done()
}()
errChan <- s.Serve(ln)
}()
go func() {
errChan <- s.Serve(ln)
defer inst.wg.Done()
defer func() {
inst.wg.Done()
stopWg.Done()
}()
errChan <- s.ServePacket(pc)
}()
errChan <- s.ServePacket(pc)
}(s, ln, pc, inst)
inst.servers = append(inst.servers, ServerListener{server: s, listener: ln, packet: pc})
}(s.server, s.listener, s.packet, inst)
}
// Log errors that may be returned from Serve() calls,
// these errors should only be occurring in the server loop.
go func() {
for err := range errChan {
if err == nil {
continue
for {
select {
case err := <-errChan:
if err != nil {
if !strings.Contains(err.Error(), "use of closed network connection") {
// this error is normal when closing the listener; see https://github.com/golang/go/issues/4373
log.Println(err)
}
}
case <-stopChan:
return
}
if strings.Contains(err.Error(), "use of closed network connection") {
// this error is normal when closing the listener
continue
}
log.Println(err)
}
}()
go func() {
stopWg.Wait()
stopChan <- struct{}{}
}()
return nil
}
@@ -737,10 +883,15 @@ func Stop() error {
for {
instancesMu.Lock()
if len(instances) == 0 {
instancesMu.Unlock()
break
}
inst := instances[0]
instancesMu.Unlock()
// Increase the instance waitgroup so that the last wait() call in
// caddymain/run.go blocks until this server instance has shut down
inst.wg.Add(1)
defer inst.wg.Done()
if err := inst.Stop(); err != nil {
log.Printf("[ERROR] Stopping %s: %v", inst.serverType, err)
}
@@ -752,7 +903,7 @@ func Stop() error {
// explicitly like a common local hostname. addr must only
// be a host or a host:port combination.
func IsLoopback(addr string) bool {
host, _, err := net.SplitHostPort(addr)
host, _, err := net.SplitHostPort(strings.ToLower(addr))
if err != nil {
host = addr // happens if the addr is just a hostname
}
@@ -761,23 +912,36 @@ func IsLoopback(addr string) bool {
strings.HasPrefix(host, "127.")
}
// Upgrade re-launches the process, preserving the listeners
// for a graceful restart. It does NOT load new configuration;
// it only starts the process anew with a fresh binary.
//
// TODO: This is not yet implemented
func Upgrade() error {
return fmt.Errorf("not implemented")
// TODO: have child process set isUpgrade = true
}
// IsInternal returns true if the IP of addr
// belongs to a private network IP range. addr must only
// be an IP or an IP:port combination.
// Loopback addresses are considered false.
func IsInternal(addr string) bool {
privateNetworks := []string{
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"fc00::/7",
}
// IsUpgrade returns true if this process is part of an upgrade
// where a parent caddy process spawned this one to upgrade
// the binary.
func IsUpgrade() bool {
mu.Lock()
defer mu.Unlock()
return isUpgrade
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr // happens if the addr is just a hostname, missing port
// if we encounter an error, the brackets need to be stripped
// because SplitHostPort didn't do it for us
host = strings.Trim(host, "[]")
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
for _, privateNetwork := range privateNetworks {
_, ipnet, _ := net.ParseCIDR(privateNetwork)
if ipnet.Contains(ip) {
return true
}
}
return false
}
// Started returns true if at least one instance has been
@@ -867,3 +1031,6 @@ var (
// by default if no other file is specified.
DefaultConfigFile = "Caddyfile"
)
// CtxKey is a value type for use with context.WithValue.
type CtxKey string
-56
View File
@@ -1,56 +0,0 @@
#!/usr/bin/env bash
#
# Caddy build script. Automates proper versioning.
#
# Usage:
#
# $ ./build.bash [output_filename] [git_repo]
#
# Outputs compiled program in current directory.
# Default git repo is current directory.
# Builds always take place from current directory.
set -euo pipefail
: ${output_filename:="${1:-}"}
: ${output_filename:=""}
: ${git_repo:="${2:-}"}
: ${git_repo:="."}
pkg=github.com/mholt/caddy/caddy/caddymain
ldflags=()
# Timestamp of build
name="${pkg}.buildDate"
value=$(date -u +"%a %b %d %H:%M:%S %Z %Y")
ldflags+=("-X" "\"${name}=${value}\"")
# Current tag, if HEAD is on a tag
name="${pkg}.gitTag"
set +e
value="$(git -C "${git_repo}" describe --exact-match HEAD 2>/dev/null)"
set -e
ldflags+=("-X" "\"${name}=${value}\"")
# Nearest tag on branch
name="${pkg}.gitNearestTag"
value="$(git -C "${git_repo}" describe --abbrev=0 --tags HEAD)"
ldflags+=("-X" "\"${name}=${value}\"")
# Commit SHA
name="${pkg}.gitCommit"
value="$(git -C "${git_repo}" rev-parse --short HEAD)"
ldflags+=("-X" "\"${name}=${value}\"")
# Summary of uncommitted changes
name="${pkg}.gitShortStat"
value="$(git -C "${git_repo}" diff-index --shortstat HEAD)"
ldflags+=("-X" "\"${name}=${value}\"")
# List of modified files
name="${pkg}.gitFilesModified"
value="$(git -C "${git_repo}" diff-index --name-only HEAD)"
ldflags+=("-X" "\"${name}=${value}\"")
go build -ldflags "${ldflags[*]}" -o "${output_filename}"
+416 -65
View File
@@ -1,44 +1,73 @@
// 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"
"gopkg.in/natefinch/lumberjack.v2"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyfile"
"github.com/caddyserver/caddy/caddytls"
"github.com/caddyserver/caddy/telemetry"
"github.com/google/uuid"
"github.com/klauspost/cpuid"
"github.com/mholt/certmagic"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/xenolf/lego/acme"
"github.com/mholt/caddy"
// plug in the HTTP server type
_ "github.com/mholt/caddy/caddyhttp"
"github.com/mholt/caddy/caddytls"
_ "github.com/caddyserver/caddy/caddyhttp" // plug in the HTTP server type
// This is where other plugins get plugged in (imported)
)
func init() {
caddy.TrapSignals()
setVersion()
flag.BoolVar(&caddytls.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
flag.StringVar(&caddytls.DefaultCAUrl, "ca", "https://acme-v01.api.letsencrypt.org/directory", "URL to certificate authority's ACME server directory")
flag.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(&caddytls.DefaultEmail, "email", "", "Default ACME CA account email address")
flag.DurationVar(&acme.HTTPClient.Timeout, "catimeout", acme.HTTPClient.Timeout, "Default ACME CA HTTP timeout")
flag.StringVar(&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.BoolVar(&logTimestamps, "log-timestamps", true, "Enable timestamps for the process log")
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")
@@ -50,9 +79,18 @@ func init() {
func Run() {
flag.Parse()
module := getBuildModule()
cleanModVersion := strings.TrimPrefix(module.Version, "v")
caddy.AppName = appName
caddy.AppVersion = appVersion
acme.UserAgent = appName + "/" + appVersion
caddy.AppVersion = module.Version
caddy.OnProcessExit = append(caddy.OnProcessExit, certmagic.CleanUpOwnLocks)
certmagic.UserAgent = appName + "/" + cleanModVersion
if !logTimestamps {
// Disable timestamps for logging
log.SetFlags(0)
}
// Set up process log before anything bad happens
switch logfile {
@@ -63,12 +101,47 @@ func Run() {
case "":
log.SetOutput(ioutil.Discard)
default:
log.SetOutput(&lumberjack.Logger{
Filename: logfile,
MaxSize: 100,
MaxAge: 14,
MaxBackups: 10,
})
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
@@ -81,9 +154,11 @@ func Run() {
os.Exit(0)
}
if version {
fmt.Printf("%s %s\n", appName, appVersion)
if devBuild && gitShortStat != "" {
fmt.Printf("%s\n%s\n", gitShortStat, gitFilesModified)
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)
}
@@ -92,17 +167,17 @@ func Run() {
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)
}
// Execute plugins that are registered to run as the process starts
err = caddy.StartupHooks(serverType)
if err != nil {
mustLogFatalf("%v", err)
}
// Executes Startup events
caddy.EmitEvent(caddy.StartupEvent, nil)
// Get Caddyfile input
caddyfileinput, err := caddy.LoadCaddyfile(serverType)
@@ -121,12 +196,35 @@ func Run() {
os.Exit(0)
}
// Log Caddy version before start
log.Printf("[INFO] Caddy version: %s", module.Version)
// 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()
}
@@ -154,10 +252,18 @@ func confLoader(serverType string) (caddy.Input, error) {
return caddy.CaddyfileFromPipe(os.Stdin, serverType)
}
contents, err := ioutil.ReadFile(conf)
if err != nil {
return nil, err
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,
@@ -181,26 +287,63 @@ func defaultLoader(serverType string) (caddy.Input, error) {
}, nil
}
// setVersion figures out the version information
// based on variables set by -ldflags.
func setVersion() {
// A development build is one that's not at a tag or has uncommitted changes
devBuild = gitTag == "" || gitShortStat != ""
// Only set the appVersion if -ldflags was used
if gitNearestTag != "" || gitTag != "" {
if devBuild && gitNearestTag != "" {
appVersion = fmt.Sprintf("%s (+%s %s)",
strings.TrimPrefix(gitNearestTag, "v"), gitCommit, buildDate)
} else if gitTag != "" {
appVersion = strings.TrimPrefix(gitTag, "v")
// 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/caddyserver/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
@@ -216,6 +359,9 @@ func setCPU(cpu string) error {
}
percent = float32(pctInt) / 100
numCPU = int(float32(availCPU) * percent)
if numCPU < 1 {
numCPU = 1
}
} else {
// Number
num, err := strconv.Atoi(cpu)
@@ -233,29 +379,234 @@ func setCPU(cpu string) error {
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
logfile string
revoke string
version bool
plugins bool
validate bool
serverType string
conf string
cpu string
envFile string
fromJSON bool
logfile string
logTimestamps bool
logRollMB int
logRollCompress bool
revoke string
toJSON bool
version bool
plugins bool
printEnv bool
validate bool
disabledMetrics string
)
// Build information obtained with the help of -ldflags
var (
appVersion = "(untracked dev build)" // inferred at startup
devBuild = true // inferred at startup
buildDate string // date -u
gitTag string // git describe --exact-match HEAD 2> /dev/null
gitNearestTag string // git describe --abbrev=0 --tags HEAD
gitCommit string // git rev-parse HEAD
gitShortStat string // git diff-index --shortstat
gitFilesModified string // git diff-index --name-only HEAD
)
// EnableTelemetry defines whether telemetry is enabled in Run.
var EnableTelemetry = true
+74
View File
@@ -1,7 +1,23 @@
// 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"
)
@@ -27,6 +43,7 @@ func TestSetCPU(t *testing.T) {
{"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 {
@@ -42,3 +59,60 @@ func TestSetCPU(t *testing.T) {
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)
}
})
}
}
+16 -2
View File
@@ -1,11 +1,25 @@
// 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://forum.caddyserver.com/t/my-wish-for-0-9-go-gettable-custom-builds/59?u=matt
// 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"
import "github.com/caddyserver/caddy/caddy/caddymain"
var run = caddymain.Run // replaced for tests
+14
View File
@@ -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.
package main
import "testing"
+144 -30
View File
@@ -1,9 +1,27 @@
// 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 (
"net"
"strconv"
"fmt"
"log"
"reflect"
"sync"
"testing"
"github.com/caddyserver/caddy/caddyfile"
)
/*
@@ -34,6 +52,77 @@ func TestCaddyStartStop(t *testing.T) {
}
*/
// 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
@@ -61,38 +150,63 @@ func TestIsLoopback(t *testing.T) {
}
}
func TestListenerAddrEqual(t *testing.T) {
ln1, err := net.Listen("tcp", "[::]:0")
if err != nil {
t.Fatal(err)
}
defer ln1.Close()
ln1port := strconv.Itoa(ln1.Addr().(*net.TCPAddr).Port)
ln2, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln2.Close()
ln2port := strconv.Itoa(ln2.Addr().(*net.TCPAddr).Port)
func TestIsInternal(t *testing.T) {
for i, test := range []struct {
ln net.Listener
addr string
input string
expect bool
}{
{ln1, ":1234", false},
{ln1, "0.0.0.0:1234", false},
{ln1, "0.0.0.0", false},
{ln1, ":" + ln1port, true},
{ln1, "0.0.0.0:" + ln1port, true},
{ln2, ":" + ln2port, false},
{ln2, "127.0.0.1:1234", false},
{ln2, "127.0.0.1", false},
{ln2, "127.0.0.1:" + ln2port, true},
{"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 := listenerAddrEqual(test.ln, test.addr), test.expect; got != want {
t.Errorf("Test %d (%s == %s): expected %v but was %v", i, test.addr, test.ln.Addr().String(), want, got)
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)
}
}
}
+16 -8
View File
@@ -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.
package caddyfile
import (
@@ -120,12 +134,6 @@ func (d *Dispenser) NextBlock() bool {
return true
}
// IncrNest adds a level of nesting to the dispenser.
func (d *Dispenser) IncrNest() {
d.nesting++
return
}
// Val gets the text of the current token. If there is no token
// loaded, it returns empty string.
func (d *Dispenser) Val() string {
@@ -216,9 +224,9 @@ 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.File(), d.Line(), msg)
msg = fmt.Sprintf("%s:%d - Error during parsing: %s", d.File(), d.Line(), msg)
return errors.New(msg)
}
+14
View File
@@ -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.
package caddyfile
import (
+14
View File
@@ -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.
package caddyfile
import (
+14
View File
@@ -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.
package caddyfile
import "testing"
+17
View File
@@ -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.
package caddyfile
import (
@@ -34,6 +48,9 @@ func (l *lexer) load(input io.Reader) error {
// discard byte order mark, if present
firstCh, _, err := l.reader.ReadRune()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
if firstCh != 0xFEFF {
+18 -1
View File
@@ -1,6 +1,21 @@
// 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"
)
@@ -144,7 +159,9 @@ func TestLexer(t *testing.T) {
func tokenize(input string) (tokens []Token) {
l := lexer{}
l.load(strings.NewReader(input))
if err := l.load(strings.NewReader(input)); err != nil {
log.Printf("[ERROR] load failed: %v", err)
}
for l.next() {
tokens = append(tokens, l.token)
}
+134 -60
View File
@@ -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.
package caddyfile
import (
@@ -40,6 +54,7 @@ type parser struct {
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) {
@@ -81,6 +96,24 @@ func (p *parser) begin() error {
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()
}
@@ -207,69 +240,57 @@ func (p *parser) doImport() error {
if p.NextArg() {
return p.Err("Import takes only one argument (glob pattern or file)")
}
// make path relative to Caddyfile rather than current working directory (issue #867)
// and then use glob to get list of matching filenames
absFile, err := filepath.Abs(p.Dispenser.filename)
if err != nil {
return p.Errf("Failed to get absolute path of file: %s", p.Dispenser.filename)
}
var matches []string
var globPattern string
if !filepath.IsAbs(importPattern) {
globPattern = filepath.Join(filepath.Dir(absFile), importPattern)
} else {
globPattern = importPattern
}
matches, err = filepath.Glob(globPattern)
if err != nil {
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
}
if len(matches) == 0 {
if strings.Contains(globPattern, "*") {
log.Printf("[WARNING] No files matching import pattern: %s", importPattern)
} else {
return p.Errf("File to import not found: %s", importPattern)
}
}
// splice out the import directive and its argument (2 tokens total)
tokensBefore := p.tokens[:p.cursor-1]
tokensAfter := p.tokens[p.cursor+1:]
// collect all the imported tokens
var importedTokens []Token
for _, importFile := range matches {
newTokens, err := p.doSingleImport(importFile)
// 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 err
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.filename, err)
}
var importLine int
importDir := filepath.Dir(importFile)
for i, token := range newTokens {
if token.Text == "import" {
importLine = token.Line
continue
}
if token.Line == importLine {
var abs string
if filepath.IsAbs(token.Text) {
abs = token.Text
} else if !filepath.IsAbs(importFile) {
abs = filepath.Join(filepath.Dir(absFile), token.Text)
} else {
abs = filepath.Join(importDir, token.Text)
}
newTokens[i] = Token{
Text: abs,
Line: token.Line,
File: token.File,
}
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)
}
}
importedTokens = append(importedTokens, newTokens...)
// 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
@@ -300,8 +321,12 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
return nil, p.Errf("Could not read tokens while importing %s: %v", importFile, err)
}
// Tack the filename onto these tokens so errors show the imported file's name
filename := filepath.Base(importFile)
// 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
}
@@ -316,7 +341,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
// are loaded into the current server block for later use
// by directive setup functions.
func (p *parser) directive() error {
dir := p.Val()
dir := replaceEnvVars(p.Val())
nesting := 0
// TODO: More helpful error message ("did you mean..." or "maybe you need to install its server type")
@@ -337,6 +362,12 @@ func (p *parser) directive() error {
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])
@@ -396,8 +427,13 @@ func replaceEnvVars(s string) string {
func replaceEnvReferences(s, refStart, refEnd string) string {
index := strings.Index(s, refStart)
for index != -1 {
endIndex := strings.Index(s, refEnd)
if endIndex != -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 {
@@ -414,3 +450,41 @@ 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
}
+257 -13
View File
@@ -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.
package caddyfile
import (
@@ -9,21 +23,47 @@ import (
)
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))
tests := []struct {
name string
input string
expected []string
}{
{
name: "not-empty",
input: "a b c\nd e",
expected: []string{"a", "b", "c", "d", "e"},
}, {
name: "empty",
input: "",
}, {
name: "newline",
input: "\n",
}, {
name: "space",
input: " ",
}, {
name: "tab and newline",
input: "\t\n",
},
}
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)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tokens, err := allTokens(strings.NewReader(tt.input))
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(tokens) != len(tt.expected) {
t.Fatalf("Expected %d tokens, got %d", len(tt.expected), len(tokens))
}
for i, val := range tt.expected {
if tokens[i].Text != val {
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].Text)
}
}
})
}
}
@@ -214,6 +254,17 @@ func TestParseOneAndImport(t *testing.T) {
{`""`, 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)
@@ -346,6 +397,68 @@ func TestRecursiveImport(t *testing.T) {
}
}
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
@@ -427,6 +540,7 @@ 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}`)
@@ -435,6 +549,13 @@ func TestEnvironmentReplacement(t *testing.T) {
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()
@@ -493,6 +614,13 @@ func TestEnvironmentReplacement(t *testing.T) {
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 {
@@ -500,3 +628,119 @@ func testParser(input string) parser {
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)
}
}
+55 -8
View File
@@ -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.
// Package basicauth implements HTTP Basic Authentication for Caddy.
//
// This is useful for simple protections on a website, like requiring
@@ -7,18 +21,20 @@ package basicauth
import (
"bufio"
"context"
"crypto/sha1"
"crypto/subtle"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/jimstudt/http-authentication/basic"
"github.com/mholt/caddy/caddyhttp/httpserver"
)
// BasicAuth is middleware to protect resources with a username and password.
@@ -35,6 +51,16 @@ type BasicAuth struct {
// 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 {
@@ -44,9 +70,10 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// path matches; this endpoint is protected
protected = true
realm = rule.Realm
// parse auth header
username, password, ok := r.BasicAuth()
username, password, ok = r.BasicAuth()
// check credentials
if !ok ||
@@ -58,8 +85,14 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// by this point, authentication was successful
isAuthenticated = true
// remove credentials from request to avoid leaking upstream
r.Header.Del("Authorization")
// 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)
}
}
@@ -67,8 +100,17 @@ func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
// browsers show a message that says something like:
// "The website says: <realm>"
// which is kinda dumb, but whatever.
w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"")
return http.StatusUnauthorized, nil
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
@@ -82,6 +124,7 @@ 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.
@@ -151,11 +194,15 @@ func PlainMatcher(passw string) PasswordMatcher {
// compare hashes of equal length instead of actual password
// to avoid leaking password length
passwHash := sha1.New()
passwHash.Write([]byte(passw))
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()
pwHash.Write([]byte(pw))
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
}
+128 -50
View File
@@ -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.
package basicauth
import (
@@ -8,63 +22,97 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestBasicAuth(t *testing.T) {
rw := BasicAuth{
Next: httpserver.HandlerFunc(contentHandler),
Rules: []Rule{
{Username: "okuser", Password: PlainMatcher("okpass"), Resources: []string{"/testing"}},
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"}},
},
},
}
tests := []struct {
type testType struct {
from string
result int
user string
password string
}{
{"/testing", http.StatusOK, "okuser", "okpass"},
{"/testing", http.StatusUnauthorized, "baduser", "okpass"},
{"/testing", http.StatusUnauthorized, "okuser", "badpass"},
{"/testing", http.StatusUnauthorized, "OKuser", "okpass"},
{"/testing", http.StatusUnauthorized, "OKuser", "badPASS"},
{"/testing", http.StatusUnauthorized, "", "okpass"},
{"/testing", http.StatusUnauthorized, "okuser", ""},
{"/testing", http.StatusUnauthorized, "", ""},
haserror bool
}
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)
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},
}
rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req)
if err != nil {
t.Fatalf("Test %d: Could not ServeHTTP: %v", i, err)
var test testType
for _, rw := range rws {
expectRealm := rw.Rules[0].Realm
if expectRealm == "" {
expectRealm = "Restricted" // Default if Realm not specified in rule
}
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=\"Restricted\""; got != want {
t.Errorf("Test %d: Www-Authenticate header should be '%s', got: '%s'", i, want, got)
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 {
t.Errorf("Test %d: response should have a 'Www-Authenticate' header", i)
}
} else {
if got, want := req.Header.Get("Authorization"), ""; got != want {
t.Errorf("Test %d: Expected Authorization header to be stripped from request after successful authentication, but is: %s", i, got)
if req.Header.Get("Authorization") == "" {
// see issue #1508: https://github.com/caddyserver/caddy/issues/1508
t.Errorf("Test %d: Expected Authorization header to be retained after successful auth, but was empty", i)
}
}
}
}
@@ -80,16 +128,17 @@ func TestMultipleOverlappingRules(t *testing.T) {
}
tests := []struct {
from string
result int
cred string
from string
result int
cred string
haserror bool
}{
{"/t", http.StatusOK, "t:p1"},
{"/t/t", http.StatusOK, "t:p1"},
{"/t/t", http.StatusOK, "t1:p2"},
{"/a", http.StatusOK, "t1:p2"},
{"/t/t", http.StatusUnauthorized, "t1:p3"},
{"/t", http.StatusUnauthorized, "t1:p2"},
{"/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 {
@@ -104,7 +153,9 @@ func TestMultipleOverlappingRules(t *testing.T) {
rec := httptest.NewRecorder()
result, err := rw.ServeHTTP(rec, req)
if err != nil {
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
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'",
@@ -127,7 +178,7 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
htfh, err := ioutil.TempFile("", "basicauth-")
if err != nil {
t.Skipf("Error creating temp file (%v), will skip htpassword test")
t.Skip("Error creating temp file, will skip htpassword test")
return
}
defer os.Remove(htfh.Name())
@@ -150,3 +201,30 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
}
}
}
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)
}
}
+42 -11
View File
@@ -1,10 +1,24 @@
// 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"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
@@ -51,13 +65,6 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
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)
}
for c.NextBlock() {
rule.Resources = append(rule.Resources, c.Val())
if c.NextArg() {
return rules, c.Errf("Expecting only one resource per line (extra '%s')", c.Val())
}
}
case 3:
rule.Resources = append(rule.Resources, args[0])
rule.Username = args[1]
@@ -68,6 +75,29 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
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)
}
@@ -75,8 +105,9 @@ func basicAuthParse(c *caddy.Controller) ([]Rule, error) {
}
func passwordMatcher(username, passw, siteRoot string) (PasswordMatcher, error) {
if !strings.HasPrefix(passw, "htpasswd=") {
htpasswdPrefix := "htpasswd="
if !strings.HasPrefix(passw, htpasswdPrefix) {
return PlainMatcher(passw), nil
}
return GetHtpasswdMatcher(passw[9:], username, siteRoot)
return GetHtpasswdMatcher(passw[len(htpasswdPrefix):], username, siteRoot)
}
+48 -2
View File
@@ -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.
package basicauth
import (
@@ -7,8 +21,8 @@ import (
"strings"
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
@@ -64,12 +78,39 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
}`, 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"}},
}},
@@ -109,6 +150,11 @@ md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
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
}
+17 -3
View File
@@ -1,8 +1,22 @@
// 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"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
@@ -18,7 +32,7 @@ func setupBind(c *caddy.Controller) error {
if !c.Args(&config.ListenHost) {
return c.ArgErr()
}
config.TLS.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
config.TLS.Manager.ListenHost = config.ListenHost // necessary for ACME challenges, see issue #309
}
return nil
}
+17 -3
View File
@@ -1,10 +1,24 @@
// 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"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetupBind(t *testing.T) {
@@ -18,7 +32,7 @@ func TestSetupBind(t *testing.T) {
if got, want := cfg.ListenHost, "1.2.3.4"; got != want {
t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got)
}
if got, want := cfg.TLS.ListenHost, "1.2.3.4"; got != want {
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)
}
}
+137 -53
View File
@@ -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.
// Package browse provides middleware for listing files in a directory
// when directory path is requested instead of a specific file.
package browse
@@ -15,15 +29,16 @@ import (
"text/template"
"time"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
"github.com/dustin/go-humanize"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddyhttp/staticfiles"
)
const (
sortByName = "name"
sortBySize = "size"
sortByTime = "time"
sortByName = "name"
sortByNameDirFirst = "namedirfirst"
sortBySize = "size"
sortByTime = "time"
)
// Browse is an http.Handler that can show a file listing when
@@ -36,7 +51,7 @@ type Browse struct {
// Config is a configuration for browsing in a particular path.
type Config struct {
PathScope string
PathScope string // the base path the URL must match to enable browsing
Fs staticfiles.FileServer
Variables interface{}
Template *template.Template
@@ -44,43 +59,48 @@ type Config struct {
// 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)
// The name of the directory (the last element of the path).
Name string
// The full path of the request
// The full path of the request.
Path string
// Whether the parent directory is browsable
// Whether the parent directory is browse-able.
CanGoUp bool
// The items (files and folders) in the path
// The items (files and folders) in the path.
Items []FileInfo
// The number of directories in the listing
// The number of directories in the listing.
NumDirs int
// The number of files (items that aren't directories) in the listing
// The number of files (items that aren't directories) in the listing.
NumFiles int
// Which sorting order is used
// Which sorting order is used.
Sort string
// And which order
// And which order.
Order string
// If ≠0 then Items have been limited to that many elements
// If ≠0 then Items have been limited to that many elements.
ItemsLimitedTo int
// Optional custom variables for use in browse templates
// Optional custom variables for use in browse templates.
User interface{}
httpserver.Context
}
// BreadcrumbMap returns l.Path where every element is a map
// of URLs and path segment names.
func (l Listing) BreadcrumbMap() map[string]string {
result := map[string]string{}
// 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
@@ -93,13 +113,12 @@ func (l Listing) BreadcrumbMap() map[string]string {
}
parts := strings.Split(lpath, "/")
for i, part := range parts {
if i == 0 && part == "" {
// Leading slash (root)
result["/"] = "/"
continue
for i := range parts {
txt := parts[i]
if i == 0 && parts[i] == "" {
txt = "/"
}
result[strings.Join(parts[:i+1], "/")] = part
result = append(result, Crumb{Link: strings.Repeat("../", len(parts)-i-1), Text: txt})
}
return result
@@ -107,12 +126,13 @@ func (l Listing) BreadcrumbMap() map[string]string {
// 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
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
@@ -128,6 +148,7 @@ func (fi FileInfo) HumanModTime(format string) string {
// Implement sorting for Listing
type byName Listing
type byNameDirFirst Listing
type bySize Listing
type byTime Listing
@@ -140,6 +161,22 @@ 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] }
@@ -147,12 +184,21 @@ 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 + iSize
iSize = directoryOffset
}
if l.Items[j].IsDir {
jSize = directoryOffset + jSize
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
}
@@ -169,6 +215,8 @@ func (l Listing) applySort() {
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:
@@ -181,6 +229,8 @@ func (l Listing) applySort() {
switch l.Sort {
case sortByName:
sort.Sort(byName(l))
case sortByNameDirFirst:
sort.Sort(byNameDirFirst(l))
case sortBySize:
sort.Sort(bySize(l))
case sortByTime:
@@ -194,7 +244,7 @@ func (l Listing) applySort() {
func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config *Config) (Listing, bool) {
var (
fileinfos []FileInfo
fileInfos []FileInfo
dirCount, fileCount int
hasIndexFile bool
)
@@ -202,33 +252,36 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config
for _, f := range files {
name := f.Name()
for _, indexName := range staticfiles.IndexPages {
for _, indexName := range config.Fs.IndexPages {
if name == indexName {
hasIndexFile = true
break
}
}
if f.IsDir() {
isDir := f.IsDir() || isSymlinkTargetDir(f, urlPath, config)
if isDir {
name += "/"
dirCount++
} else {
fileCount++
}
url := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
if config.Fs.IsHidden(f) {
continue
}
fileinfos = append(fileinfos, FileInfo{
IsDir: f.IsDir(),
Name: f.Name(),
Size: f.Size(),
URL: url.String(),
ModTime: f.ModTime().UTC(),
Mode: f.Mode(),
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(),
})
}
@@ -236,12 +289,38 @@ func directoryListing(files []os.FileInfo, canGoUp bool, urlPath string, config
Name: path.Base(urlPath),
Path: urlPath,
CanGoUp: canGoUp,
Items: fileinfos,
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) {
@@ -298,9 +377,14 @@ func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
// Browsing navigation gets messed up if browsing a directory
// that doesn't end in "/" (which it should, anyway)
if !strings.HasSuffix(r.URL.Path, "/") {
staticfiles.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
return 0, nil
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)
@@ -338,11 +422,11 @@ func (b Browse) handleSortOrder(w http.ResponseWriter, r *http.Request, scope st
// If the query 'sort' or 'order' is empty, use defaults or any values previously saved in Cookies
switch sort {
case "":
sort = sortByName
sort = sortByNameDirFirst
if sortCookie, sortErr := r.Cookie("sort"); sortErr == nil {
sort = sortCookie.Value
}
case sortByName, sortBySize, sortByTime:
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sort, Path: scope, Secure: r.TLS != nil})
}
@@ -420,7 +504,7 @@ func (b Browse) ServeListing(w http.ResponseWriter, r *http.Request, requestedFi
}
buf.WriteTo(w)
_, _ = buf.WriteTo(w)
return http.StatusOK, nil
}
+288 -27
View File
@@ -1,21 +1,42 @@
// 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"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/caddyserver/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"
@@ -69,6 +90,13 @@ func TestSort(t *testing.T) {
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"
@@ -92,6 +120,14 @@ func TestSort(t *testing.T) {
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) {
@@ -126,6 +162,8 @@ func TestBrowseHTTPMethods(t *testing.T) {
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 {
@@ -161,6 +199,8 @@ func TestBrowseTemplate(t *testing.T) {
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()
@@ -180,6 +220,8 @@ func TestBrowseTemplate(t *testing.T) {
<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>
@@ -191,7 +233,7 @@ func TestBrowseTemplate(t *testing.T) {
`
if respBody != expectedBody {
t.Fatalf("Expected body: %v got: %v", expectedBody, respBody)
t.Fatalf("Expected body: '%v' got: '%v'", expectedBody, respBody)
}
}
@@ -199,7 +241,7 @@ func TestBrowseTemplate(t *testing.T) {
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")
t.Fatalf("Next shouldn't be called: %s", r.URL)
return 0, nil
}),
Configs: []Config{
@@ -254,6 +296,9 @@ func TestBrowseJson(t *testing.T) {
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 {
@@ -266,33 +311,33 @@ func TestBrowseJson(t *testing.T) {
}{
//test case 1: testing for default sort and order and without the limit parameter, default sort is by name and the default order is ascending
//without the limit query entire listing will be produced
{"/", "", "", -1, false, listing.Items},
{"/?sort=name", "", "", -1, false, listing.Items},
//test case 2: limit is set to 1, orderBy and sortBy is default
{"/?limit=1", "", "", 1, false, listing.Items[:1]},
{"/?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", "", "", 100000000, false, listing.Items},
{"/?limit=100000000&sort=name", "", "", 100000000, false, listing.Items},
//test case 4 : testing for negative limit
{"/?limit=-1", "", "", -1, false, listing.Items},
{"/?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", "", "desc", -1, false, listing.Items},
{"/?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", "", "desc", 2, false, listing.Items},
{"/?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", "", "desc", 3, false, listing.Items},
{"/?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", "", "asc", 3, false, listing.Items},
{"/?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", "", "asc", 1111111, false, listing.Items},
{"/?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", "size", "asc", -1, false, listing.Items},
{"/?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", "time", "asc", -1, false, listing.Items},
{"/?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", "time", "asc", 1, false, listing.Items},
{"/?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", "time", "asc", -100, false, listing.Items},
{"/?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", "size", "asc", -100, false, listing.Items},
{"/?order=asc&sort=size&limit=-100&sort=name", "size", "asc", -100, false, listing.Items},
}
for i, test := range tests {
@@ -301,6 +346,8 @@ func TestBrowseJson(t *testing.T) {
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()
@@ -319,25 +366,25 @@ func TestBrowseJson(t *testing.T) {
}
actualJSONResponse := rec.Body.String()
copyOflisting := listing
copyOfListing := listing
if test.SortBy == "" {
copyOflisting.Sort = "name"
copyOfListing.Sort = "name"
} else {
copyOflisting.Sort = test.SortBy
copyOfListing.Sort = test.SortBy
}
if test.OrderBy == "" {
copyOflisting.Order = "asc"
copyOfListing.Order = "asc"
} else {
copyOflisting.Order = test.OrderBy
copyOfListing.Order = test.OrderBy
}
copyOflisting.applySort()
copyOfListing.applySort()
limit := test.Limit
if limit <= len(copyOflisting.Items) && limit > 0 {
marsh, err = json.Marshal(copyOflisting.Items[: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)
marsh, err = json.Marshal(copyOfListing.Items)
}
if err != nil {
@@ -362,3 +409,217 @@ func isReversed(data sort.Interface) bool {
}
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)
}
}()
}
}
+136 -60
View File
@@ -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.
package browse
import (
@@ -6,9 +20,9 @@ import (
"net/http"
"text/template"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddyhttp/staticfiles"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
)
func init() {
@@ -64,8 +78,9 @@ func browseParse(c *caddy.Controller) ([]Config, error) {
}
bc.Fs = staticfiles.FileServer{
Root: http.Dir(cfg.Root),
Hide: httpserver.GetConfig(c).HiddenFiles,
Root: http.Dir(cfg.Root),
Hide: cfg.HiddenFiles,
IndexPages: cfg.IndexPages,
}
// Second argument would be the template file to use
@@ -110,6 +125,7 @@ const defaultTemplate = `<!DOCTYPE html>
body {
font-family: sans-serif;
text-rendering: optimizespeed;
background-color: #ffffff;
}
a {
@@ -130,12 +146,12 @@ header,
th:first-child,
td:first-child {
padding-left: 5%;
width: 5%;
}
th:last-child,
td:last-child {
padding-right: 5%;
width: 5%;
}
header {
@@ -150,16 +166,22 @@ h1 {
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
color: #999;
}
h1 a {
color: inherit;
color: #000;
margin: 0 4px;
}
h1 a:hover {
text-decoration: underline;
}
h1 a:first-child {
margin: 0;
}
main {
display: block;
}
@@ -216,19 +238,24 @@ th svg {
}
td {
white-space: nowrap;
font-size: 14px;
}
td:first-child {
width: 50%;
td:nth-child(2) {
width: 80%;
}
th:last-child,
td:last-child {
td:nth-child(3) {
padding: 0 20px 0 20px;
}
th:nth-child(4),
td:nth-child(4) {
text-align: right;
}
td:first-child svg {
td:nth-child(2) svg {
position: absolute;
}
@@ -240,6 +267,30 @@ td .goup {
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;
@@ -251,61 +302,60 @@ footer {
display: none;
}
td:first-child {
td:nth-child(2) {
width: auto;
}
th:nth-child(2),
td:nth-child(2) {
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>
<body onload='initFilter()'>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="0" width="0" style="position: absolute;">
<defs>
<!-- Folder -->
<linearGradient id="f" y2="640" gradientUnits="userSpaceOnUse" x2="244.84" gradientTransform="matrix(.97319 0 0 1.0135 -.50695 -13.679)" y1="415.75" x1="244.84">
<stop stop-color="#b3ddfd" offset="0"/>
<stop stop-color="#69c" offset="1"/>
</linearGradient>
<linearGradient id="e" y2="571.06" gradientUnits="userSpaceOnUse" x2="238.03" gradientTransform="translate(0,2)" y1="346.05" x1="236.26">
<stop stop-color="#ace" offset="0"/>
<stop stop-color="#369" offset="1"/>
</linearGradient>
<g id="folder" transform="translate(-266.06 -193.36)">
<g transform="matrix(.066019 0 0 .066019 264.2 170.93)">
<g transform="matrix(1.4738 0 0 1.4738 -52.053 -166.93)">
<path fill="#69c" d="m98.424 343.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="409.69" x="54.428" fill="#369"/>
<path fill="url(#e)" d="m98.424 345.78c-11.08 0-20 8.92-20 20v48.5 33.719 105.06c0 11.08 8.92 20 20 20h279.22c11.08 0 20-8.92 20-20v-138.78c0-11.08-8.92-20-20-20h-117.12c-7.5478-1.1844-9.7958-6.8483-10.375-11.312v-5.625-11.562c0-11.08-8.92-20-20-20h-131.72z"/>
<rect rx="12.885" ry="12.199" height="227.28" width="366.69" y="407.69" x="54.428" fill="url(#f)"/>
<g 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 -->
<linearGradient id="a">
<stop stop-color="#cbcbcb" offset="0"/>
<stop stop-color="#f0f0f0" offset=".34923"/>
<stop stop-color="#e2e2e2" offset="1"/>
</linearGradient>
<linearGradient id="d" y2="686.15" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="207.83" gradientTransform="matrix(.28346 0 0 .31053 -608.52 485.11)" x2="380.1" x1="749.25"/>
<linearGradient id="c" y2="287.74" xlink:href="#a" gradientUnits="userSpaceOnUse" y1="169.44" gradientTransform="matrix(.28342 0 0 .31057 -608.52 485.11)" x2="622.33" x1="741.64"/>
<linearGradient id="b" y2="418.54" gradientUnits="userSpaceOnUse" y1="236.13" gradientTransform="matrix(.29343 0 0 .29999 -608.52 485.11)" x2="330.88" x1="687.96">
<stop stop-color="#fff" offset="0"/>
<stop stop-color="#fff" stop-opacity="0" offset="1"/>
</linearGradient>
<g id="file" transform="translate(-278.15 -216.59)">
<g fill-rule="evenodd" transform="matrix(.19775 0 0 .19775 381.05 112.68)">
<path d="m-520.17 525.5v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke-width=".42649" fill="#fff"/>
<g>
<path d="m-520.11 525.68v36.739 36.739 36.739 36.739h33.528 33.528 33.528 33.528v-36.739-36.739-36.739l-33.528-36.739h-33.528-33.528-33.528z" stroke-opacity=".36478" stroke="#000" stroke-width=".42649" fill="url(#d)"/>
<path d="m-386 562.42c-10.108-2.9925-23.206-2.5682-33.101-0.86253 1.7084-10.962 1.922-24.701-0.4271-35.877l33.528 36.739z" stroke-width=".95407pt" fill="url(#c)"/>
<path d="m-519.13 537-0.60402 134.7h131.68l0.0755-33.296c-2.9446 1.1325-32.692-40.998-70.141-39.186-37.483 1.8137-27.785-56.777-61.006-62.214z" stroke-width="1pt" fill="url(#b)"/>
<g 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>
@@ -323,7 +373,7 @@ footer {
<header>
<h1>
{{range $url, $name := .BreadcrumbMap}}<a href="{{html $url}}">{{html $name}}</a>{{if ne $url "/"}}/{{end}}{{end}}
{{range $i, $crumb := .Breadcrumbs}}<a href="{{html $crumb.Link}}">{{html $crumb.Text}}</a>{{if ne $i 0}}/{{end}}{{end}}
</h1>
</header>
<main>
@@ -341,38 +391,49 @@ footer {
<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=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
<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=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
<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=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
<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=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
<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=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#up-arrow"></use></svg></a>
<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=".4em" version="1.1" viewBox="0 0 12.922194 6.0358899"><use xlink:href="#down-arrow"></use></svg></a>
<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>
@@ -380,16 +441,18 @@ footer {
</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 35.678803 28.527945"><use xlink:href="#folder"></use></svg>
<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 26.604381 29.144726"><use xlink:href="#file"></use></svg>
<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>
@@ -400,6 +463,7 @@ footer {
<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>
@@ -411,6 +475,18 @@ footer {
</footer>
<script>
var filterEl = document.getElementById('filter');
filterEl.focus();
function initFilter() {
if (!filterEl.value) {
var filterParam = new URL(window.location.href).searchParams.get('filter');
if (filterParam) {
filterEl.value = filterParam;
}
}
filter();
}
function filter() {
var q = filterEl.value.trim().toLowerCase();
var elems = document.querySelectorAll('tr.file');
@@ -440,7 +516,7 @@ footer {
return;
}
}
e.textContent = d.toLocaleString();
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);
+17 -3
View File
@@ -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.
package browse
import (
@@ -8,8 +22,8 @@ import (
"testing"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
@@ -39,7 +53,7 @@ func TestSetup(t *testing.T) {
// test case #1 tests instantiation of Config with default values
{"browse /", []string{"/"}, false},
// test case #2 tests detectaction of custom template
// test case #2 tests detection of custom template
{"browse . " + tempTemplatePath, []string{"."}, false},
// test case #3 tests detection of non-existent template
+8
View File
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
</head>
<body>
</body>
</html>
+42 -25
View File
@@ -1,32 +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"
_ "github.com/caddyserver/caddy/caddyhttp/httpserver"
// plug in the standard directives
_ "github.com/mholt/caddy/caddyhttp/basicauth"
_ "github.com/mholt/caddy/caddyhttp/bind"
_ "github.com/mholt/caddy/caddyhttp/browse"
_ "github.com/mholt/caddy/caddyhttp/errors"
_ "github.com/mholt/caddy/caddyhttp/expvar"
_ "github.com/mholt/caddy/caddyhttp/extensions"
_ "github.com/mholt/caddy/caddyhttp/fastcgi"
_ "github.com/mholt/caddy/caddyhttp/gzip"
_ "github.com/mholt/caddy/caddyhttp/header"
_ "github.com/mholt/caddy/caddyhttp/internalsrv"
_ "github.com/mholt/caddy/caddyhttp/log"
_ "github.com/mholt/caddy/caddyhttp/markdown"
_ "github.com/mholt/caddy/caddyhttp/maxrequestbody"
_ "github.com/mholt/caddy/caddyhttp/mime"
_ "github.com/mholt/caddy/caddyhttp/pprof"
_ "github.com/mholt/caddy/caddyhttp/proxy"
_ "github.com/mholt/caddy/caddyhttp/redirect"
_ "github.com/mholt/caddy/caddyhttp/rewrite"
_ "github.com/mholt/caddy/caddyhttp/root"
_ "github.com/mholt/caddy/caddyhttp/status"
_ "github.com/mholt/caddy/caddyhttp/templates"
_ "github.com/mholt/caddy/caddyhttp/timeouts"
_ "github.com/mholt/caddy/caddyhttp/websocket"
_ "github.com/mholt/caddy/startupshutdown"
_ "github.com/caddyserver/caddy/caddyhttp/basicauth"
_ "github.com/caddyserver/caddy/caddyhttp/bind"
_ "github.com/caddyserver/caddy/caddyhttp/browse"
_ "github.com/caddyserver/caddy/caddyhttp/errors"
_ "github.com/caddyserver/caddy/caddyhttp/expvar"
_ "github.com/caddyserver/caddy/caddyhttp/extensions"
_ "github.com/caddyserver/caddy/caddyhttp/fastcgi"
_ "github.com/caddyserver/caddy/caddyhttp/gzip"
_ "github.com/caddyserver/caddy/caddyhttp/header"
_ "github.com/caddyserver/caddy/caddyhttp/index"
_ "github.com/caddyserver/caddy/caddyhttp/internalsrv"
_ "github.com/caddyserver/caddy/caddyhttp/limits"
_ "github.com/caddyserver/caddy/caddyhttp/log"
_ "github.com/caddyserver/caddy/caddyhttp/markdown"
_ "github.com/caddyserver/caddy/caddyhttp/mime"
_ "github.com/caddyserver/caddy/caddyhttp/pprof"
_ "github.com/caddyserver/caddy/caddyhttp/proxy"
_ "github.com/caddyserver/caddy/caddyhttp/push"
_ "github.com/caddyserver/caddy/caddyhttp/redirect"
_ "github.com/caddyserver/caddy/caddyhttp/requestid"
_ "github.com/caddyserver/caddy/caddyhttp/rewrite"
_ "github.com/caddyserver/caddy/caddyhttp/root"
_ "github.com/caddyserver/caddy/caddyhttp/status"
_ "github.com/caddyserver/caddy/caddyhttp/templates"
_ "github.com/caddyserver/caddy/caddyhttp/timeouts"
_ "github.com/caddyserver/caddy/caddyhttp/websocket"
_ "github.com/caddyserver/caddy/onevent"
)
+17 -3
View File
@@ -1,19 +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"
"github.com/caddyserver/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 := 29 // importing caddyhttp plugs in this many plugins
numStandardPlugins := 32 // importing caddyhttp plugs in this many plugins
s := caddy.DescribePlugins()
if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want {
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)
}
}
+31 -20
View File
@@ -1,18 +1,32 @@
// 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"
"log"
"mime"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
@@ -27,11 +41,8 @@ type ErrorHandler struct {
Next httpserver.Handler
GenericErrorPage string // default error page filename
ErrorPages map[int]string // map of status code to filename
LogFile string
Log *log.Logger
LogRoller *httpserver.LogRoller
Debug bool // if true, errors are written out to client rather than to a log
file *os.File // a log file to close when done
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) {
@@ -40,7 +51,7 @@ func (h ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, er
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)
errMsg := fmt.Sprintf("[ERROR %d %s] %v", 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")
@@ -69,22 +80,24 @@ func (h ErrorHandler) errorPage(w http.ResponseWriter, r *http.Request, code int
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)
h.Log.Printf("[NOTICE %d %s] could not load error page: %v", code, r.URL.String(), err)
httpserver.DefaultErrorFunc(w, r, code)
return
}
defer errorPage.Close()
// Get content type by extension
contentType := mime.TypeByExtension(filepath.Ext(pagePath))
if contentType == "" {
contentType = "text/html; charset=utf-8"
}
// Copy the page body into the response
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Content-Type", contentType)
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)
h.Log.Printf("[NOTICE %d %s] could not respond with %s: %v", code, r.URL.String(), pagePath, err)
httpserver.DefaultErrorFunc(w, r, code)
}
@@ -132,13 +145,13 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
}
// Trim file path
delim := "/caddy/"
delim := "/github.com/caddyserver/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)
panicMsg := fmt.Sprintf("[PANIC %s] %s:%d - %v", 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
@@ -150,5 +163,3 @@ func (h ErrorHandler) recovery(w http.ResponseWriter, r *http.Request) {
h.errorPage(w, r, http.StatusInternalServerError)
}
}
const timeFormat = "02/Jan/2006:15:04:05 -0700"
+41 -5
View File
@@ -1,10 +1,23 @@
// 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"
"log"
"net/http"
"net/http/httptest"
"os"
@@ -13,7 +26,7 @@ import (
"strings"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestErrors(t *testing.T) {
@@ -32,7 +45,7 @@ func TestErrors(t *testing.T) {
http.StatusNotFound: path,
http.StatusForbidden: "not_exist_file",
},
Log: log.New(&buf, "", 0),
Log: httpserver.NewTestLogger(&buf),
}
_, notExistErr := os.Open("not_exist_file")
@@ -140,7 +153,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
body := rec.Body.String()
if !strings.Contains(body, "[PANIC /] caddyhttp/errors/errors_test.go") {
if !strings.Contains(body, "[PANIC /]") {
t.Errorf("Expected response body to contain error log line, but it didn't:\n%s", body)
}
if !strings.Contains(body, panicMsg) {
@@ -154,6 +167,7 @@ func TestVisibleErrorWithPanic(t *testing.T) {
func TestGenericErrorPage(t *testing.T) {
// create temporary generic error page
const genericErrorContent = "This is a generic error page"
const badRequestErrorJSONContent = `{"message":"This is a error json message"}`
genericErrorPagePath, err := createErrorPageFile("generic_error_test.html", genericErrorContent)
if err != nil {
@@ -170,13 +184,20 @@ func TestGenericErrorPage(t *testing.T) {
}
defer os.Remove(notFoundErrorPagePath)
badRequestErrorJSONPath, err := createErrorPageFile("not_found.json", badRequestErrorJSONContent)
if err != nil {
t.Fatal(err)
}
defer os.Remove(badRequestErrorJSONPath)
buf := bytes.Buffer{}
em := ErrorHandler{
GenericErrorPage: genericErrorPagePath,
ErrorPages: map[int]string{
http.StatusNotFound: notFoundErrorPagePath,
http.StatusBadRequest:badRequestErrorJSONPath,
},
Log: log.New(&buf, "", 0),
Log: httpserver.NewTestLogger(&buf),
}
tests := []struct {
@@ -185,6 +206,7 @@ func TestGenericErrorPage(t *testing.T) {
expectedBody string
expectedLog string
expectedErr error
expectedContentType string
}{
{
next: genErrorHandler(http.StatusNotFound, nil, ""),
@@ -192,6 +214,15 @@ func TestGenericErrorPage(t *testing.T) {
expectedBody: notFoundErrorContent,
expectedLog: "",
expectedErr: nil,
expectedContentType: "text/html; charset=utf-8",
},
{
next: genErrorHandler(http.StatusBadRequest, nil, ""),
expectedCode: 0,
expectedBody: badRequestErrorJSONContent,
expectedLog: "",
expectedErr: nil,
expectedContentType: "application/json",
},
{
next: genErrorHandler(http.StatusInternalServerError, nil, ""),
@@ -199,6 +230,7 @@ func TestGenericErrorPage(t *testing.T) {
expectedBody: genericErrorContent,
expectedLog: "",
expectedErr: nil,
expectedContentType:"text/html; charset=utf-8",
},
}
@@ -225,6 +257,10 @@ func TestGenericErrorPage(t *testing.T) {
t.Errorf("Test %d: Expected body %q, but got %q",
i, test.expectedBody, body)
}
if contentType := rec.Header().Get("Content-Type"); contentType != test.expectedContentType{
t.Errorf("Test %d: Expected Content-Type %s, but got %s",
i, test.expectedContentType, contentType)
}
if log := buf.String(); !strings.Contains(log, test.expectedLog) {
t.Errorf("Test %d: Expected log %q, but got %q",
i, test.expectedLog, log)
+59 -94
View File
@@ -1,73 +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 errors
import (
"io"
"log"
"os"
"path/filepath"
"strconv"
"github.com/hashicorp/go-syslog"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy"
"github.com/caddyserver/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
}
// Open the log file for writing when the server starts
c.OnStartup(func() error {
var err error
var writer io.Writer
switch handler.LogFile {
case "visible":
handler.Debug = true
case "stdout":
writer = os.Stdout
case "stderr":
writer = os.Stderr
case "syslog":
writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "caddy")
if err != nil {
return err
}
default:
if handler.LogFile == "" {
writer = os.Stderr // default
break
}
var file *os.File
file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
return err
}
if handler.LogRoller != nil {
file.Close()
handler.LogRoller.Filename = handler.LogFile
writer = handler.LogRoller.GetLogWriter()
} else {
handler.file = file
writer = file
}
}
handler.Log = log.New(writer, "", 0)
return nil
})
// When server stops, close any open log file
c.OnShutdown(func() error {
if handler.file != nil {
handler.file.Close()
}
return nil
})
handler.Log.Attach(c)
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
handler.Next = next
@@ -78,46 +43,40 @@ func setup(c *caddy.Controller) error {
}
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)}
handler := &ErrorHandler{
ErrorPages: make(map[int]string),
Log: &httpserver.Logger{},
}
cfg := httpserver.GetConfig(c)
optionalBlock := func() (bool, error) {
var hadBlock bool
optionalBlock := func() error {
for c.NextBlock() {
hadBlock = true
what := c.Val()
if !c.NextArg() {
return hadBlock, c.ArgErr()
}
where := c.Val()
where := c.RemainingArgs()
if what == "log" {
if where == "visible" {
handler.Debug = true
} else {
handler.LogFile = where
if c.NextArg() {
if c.Val() == "{" {
c.IncrNest()
logRoller, err := httpserver.ParseRoller(c)
if err != nil {
return hadBlock, err
}
handler.LogRoller = logRoller
}
}
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)
@@ -126,24 +85,24 @@ func errorsParse(c *caddy.Controller) (*ErrorHandler, error) {
if what == "*" {
if handler.GenericErrorPage != "" {
return hadBlock, c.Errf("Duplicate status code entry: %s", what)
return c.Errf("Duplicate status code entry: %s", what)
}
handler.GenericErrorPage = where
} else {
whatInt, err := strconv.Atoi(what)
if err != nil {
return hadBlock, c.Err("Expecting a numeric status code or '*', got '" + what + "'")
return c.Err("Expecting a numeric status code or '*', got '" + what + "'")
}
if _, exists := handler.ErrorPages[whatInt]; exists {
return hadBlock, c.Errf("Duplicate status code entry: %s", what)
return c.Errf("Duplicate status code entry: %s", what)
}
handler.ErrorPages[whatInt] = where
}
}
}
return hadBlock, nil
return nil
}
for c.Next() {
@@ -151,21 +110,27 @@ func errorsParse(c *caddy.Controller) (*ErrorHandler, error) {
if c.Val() == "}" {
continue
}
// Configuration may be in a block
hadBlock, err := optionalBlock()
if err != nil {
return handler, err
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()
}
}
// Otherwise, the only argument would be an error log file name or 'visible'
if !hadBlock {
if c.NextArg() {
if c.Val() == "visible" {
handler.Debug = true
} else {
handler.LogFile = c.Val()
}
}
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
}
}
+87 -43
View File
@@ -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.
package errors
import (
@@ -5,8 +19,8 @@ import (
"reflect"
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
@@ -26,12 +40,12 @@ func TestSetup(t *testing.T) {
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
}
if myHandler.LogFile != "" {
t.Errorf("Expected '%s' as the default LogFile", "")
}
if myHandler.LogRoller != nil {
t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller)
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")
}
@@ -58,65 +72,81 @@ func TestErrorsParse(t *testing.T) {
}{
{`errors`, false, ErrorHandler{
ErrorPages: map[int]string{},
Log: &httpserver.Logger{},
}},
{`errors errors.txt`, false, ErrorHandler{
ErrorPages: map[int]string{},
LogFile: "errors.txt",
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: httpserver.DefaultLogRoller(),
},
}},
{`errors visible`, false, ErrorHandler{
ErrorPages: map[int]string{},
Debug: true,
Log: &httpserver.Logger{},
}},
{`errors { log visible }`, false, ErrorHandler{
ErrorPages: map[int]string{},
Debug: true,
}},
{`errors { log errors.txt
{`errors errors.txt {
404 404.html
500 500.html
}`, false, ErrorHandler{
LogFile: "errors.txt",
ErrorPages: map[int]string{
404: "404.html",
500: "500.html",
},
}},
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, ErrorHandler{
LogFile: "errors.txt",
LogRoller: &httpserver.LogRoller{
MaxSize: 2,
MaxAge: 10,
MaxBackups: 3,
LocalTime: true,
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: httpserver.DefaultLogRoller(),
},
ErrorPages: map[int]string{},
}},
{`errors { log errors.txt {
size 3
age 11
keep 5
}
{`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{
LogFile: "errors.txt",
ErrorPages: map[int]string{
404: "404.html",
503: "503.html",
},
LogRoller: &httpserver.LogRoller{
MaxSize: 3,
MaxAge: 11,
MaxBackups: 5,
LocalTime: true,
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: &httpserver.LogRoller{
MaxSize: 3,
MaxAge: 11,
MaxBackups: 5,
Compress: false,
LocalTime: true,
},
},
}},
{`errors { log errors.txt
{`errors errors.txt {
* generic_error.html
404 404.html
503 503.html
}`, false, ErrorHandler{
LogFile: "errors.txt",
Log: &httpserver.Logger{
Output: "errors.txt",
Roller: httpserver.DefaultLogRoller(),
},
GenericErrorPage: "generic_error.html",
ErrorPages: map[int]string{
404: "404.html",
@@ -131,17 +161,31 @@ func TestErrorsParse(t *testing.T) {
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{}}},
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{}}},
* 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))
@@ -154,7 +198,7 @@ func TestErrorsParse(t *testing.T) {
}
if !reflect.DeepEqual(actualErrorsRule, &test.expectedErrorHandler) {
t.Errorf("Test %d expect %v, but got %v", i,
actualErrorsRule, test.expectedErrorHandler)
test.expectedErrorHandler, actualErrorsRule)
}
}
}
+15 -1
View File
@@ -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.
package expvar
import (
@@ -5,7 +19,7 @@ import (
"fmt"
"net/http"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// ExpVar is a simple struct to hold expvar's configuration
+15 -1
View File
@@ -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.
package expvar
import (
@@ -6,7 +20,7 @@ import (
"net/http/httptest"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestExpVar(t *testing.T) {
+16 -2
View File
@@ -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.
package expvar
import (
@@ -5,8 +19,8 @@ import (
"runtime"
"sync"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
+16 -2
View File
@@ -1,10 +1,24 @@
// 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"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
+18 -12
View File
@@ -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.
// Package extensions contains middleware for clean URLs.
//
// The root path of the site is passed in as well as possible extensions
@@ -12,7 +26,7 @@ import (
"path"
"strings"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// Ext can assume an extension from clean URLs.
@@ -31,9 +45,10 @@ type Ext struct {
// 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 path.Ext(urlpath) == "" && len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] != '/' {
if len(r.URL.Path) > 0 && path.Ext(urlpath) == "" && r.URL.Path[len(r.URL.Path)-1] != '/' {
for _, ext := range e.Extensions {
if resourceExists(e.Root, urlpath+ext) {
_, err := os.Stat(httpserver.SafePath(e.Root, urlpath) + ext)
if err == nil {
r.URL.Path = urlpath + ext
break
}
@@ -41,12 +56,3 @@ func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
}
return e.Next.ServeHTTP(w, r)
}
// resourceExists returns true if the file specified at
// root + path exists; false otherwise.
func resourceExists(root, path string) bool {
_, err := os.Stat(root + path)
// technically we should use os.IsNotExist(err)
// but we don't handle any other kinds of errors anyway
return err == nil
}
+16 -2
View File
@@ -1,8 +1,22 @@
// 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"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
+16 -2
View File
@@ -1,10 +1,24 @@
// 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"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
-102
View File
@@ -1,102 +0,0 @@
package fastcgi
import (
"errors"
"sync"
"sync/atomic"
"time"
)
type dialer interface {
Dial() (Client, error)
Close(Client) error
}
// basicDialer is a basic dialer that wraps default fcgi functions.
type basicDialer struct {
network string
address string
timeout time.Duration
}
func (b basicDialer) Dial() (Client, error) {
return DialTimeout(b.network, b.address, b.timeout)
}
func (b basicDialer) Close(c Client) error { return c.Close() }
// persistentDialer keeps a pool of fcgi connections.
// connections are not closed after use, rather added back to the pool for reuse.
type persistentDialer struct {
size int
network string
address string
timeout time.Duration
pool []Client
sync.Mutex
}
func (p *persistentDialer) Dial() (Client, error) {
p.Lock()
// connection is available, return first one.
if len(p.pool) > 0 {
client := p.pool[0]
p.pool = p.pool[1:]
p.Unlock()
return client, nil
}
p.Unlock()
// no connection available, create new one
return DialTimeout(p.network, p.address, p.timeout)
}
func (p *persistentDialer) Close(client Client) error {
p.Lock()
if len(p.pool) < p.size {
// pool is not full yet, add connection for reuse
p.pool = append(p.pool, client)
p.Unlock()
return nil
}
p.Unlock()
// otherwise, close the connection.
return client.Close()
}
type loadBalancingDialer struct {
current int64
dialers []dialer
}
func (m *loadBalancingDialer) Dial() (Client, error) {
nextDialerIndex := atomic.AddInt64(&m.current, 1) % int64(len(m.dialers))
currentDialer := m.dialers[nextDialerIndex]
client, err := currentDialer.Dial()
if err != nil {
return nil, err
}
return &dialerAwareClient{Client: client, dialer: currentDialer}, nil
}
func (m *loadBalancingDialer) Close(c Client) error {
// Close the client according to dialer behaviour
if da, ok := c.(*dialerAwareClient); ok {
return da.dialer.Close(c)
}
return errors.New("Cannot close client")
}
type dialerAwareClient struct {
Client
dialer dialer
}
-126
View File
@@ -1,126 +0,0 @@
package fastcgi
import (
"errors"
"testing"
)
func TestLoadbalancingDialer(t *testing.T) {
// given
runs := 100
mockDialer1 := new(mockDialer)
mockDialer2 := new(mockDialer)
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1, mockDialer2}}
// when
for i := 0; i < runs; i++ {
client, err := dialer.Dial()
dialer.Close(client)
if err != nil {
t.Errorf("Expected error to be nil")
}
}
// then
if mockDialer1.dialCalled != mockDialer2.dialCalled && mockDialer1.dialCalled != 50 {
t.Errorf("Expected dialer to call Dial() on multiple backend dialers %d times [actual: %d, %d]", 50, mockDialer1.dialCalled, mockDialer2.dialCalled)
}
if mockDialer1.closeCalled != mockDialer2.closeCalled && mockDialer1.closeCalled != 50 {
t.Errorf("Expected dialer to call Close() on multiple backend dialers %d times [actual: %d, %d]", 50, mockDialer1.closeCalled, mockDialer2.closeCalled)
}
}
func TestLoadBalancingDialerShouldReturnDialerAwareClient(t *testing.T) {
// given
mockDialer1 := new(mockDialer)
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1}}
// when
client, err := dialer.Dial()
// then
if err != nil {
t.Errorf("Expected error to be nil")
}
if awareClient, ok := client.(*dialerAwareClient); !ok {
t.Error("Expected dialer to wrap client")
} else {
if awareClient.dialer != mockDialer1 {
t.Error("Expected wrapped client to have reference to dialer")
}
}
}
func TestLoadBalancingDialerShouldUnderlyingReturnDialerError(t *testing.T) {
// given
mockDialer1 := new(errorReturningDialer)
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1}}
// when
_, err := dialer.Dial()
// then
if err.Error() != "Error during dial" {
t.Errorf("Expected 'Error during dial', got: '%s'", err.Error())
}
}
func TestLoadBalancingDialerShouldCloseClient(t *testing.T) {
// given
mockDialer1 := new(mockDialer)
mockDialer2 := new(mockDialer)
dialer := &loadBalancingDialer{dialers: []dialer{mockDialer1, mockDialer2}}
client, _ := dialer.Dial()
// when
err := dialer.Close(client)
// then
if err != nil {
t.Error("Expected error not to occur")
}
// load balancing starts from index 1
if mockDialer2.client != client {
t.Errorf("Expected Close() to be called on referenced dialer")
}
}
type mockDialer struct {
dialCalled int
closeCalled int
client Client
}
type mockClient struct {
Client
}
func (m *mockDialer) Dial() (Client, error) {
m.dialCalled++
return mockClient{Client: &FCGIClient{}}, nil
}
func (m *mockDialer) Close(c Client) error {
m.client = c
m.closeCalled++
return nil
}
type errorReturningDialer struct {
client Client
}
func (m *errorReturningDialer) Dial() (Client, error) {
return mockClient{Client: &FCGIClient{}}, errors.New("Error during dial")
}
func (m *errorReturningDialer) Close(c Client) error {
m.client = c
return errors.New("Error during close")
}
+182 -46
View File
@@ -1,21 +1,43 @@
// 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"
"github.com/mholt/caddy/caddyhttp/httpserver"
"crypto/tls"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddytls"
)
// Handler is a middleware type that can handle requests as a FastCGI client.
@@ -32,17 +54,28 @@ type Handler struct {
ServerPort string
}
// When a rewrite is performed, a header field of this name
// is added to the request
// It contains the original request URI before the rewrite.
const internalRewriteFieldName = "Caddy-Rewrite-Original-URI"
// 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 and the path must be allowed.
if !httpserver.Path(r.URL.Path).Matches(rule.Path) || !rule.AllowedPath(r.URL.Path) {
// 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
}
@@ -52,6 +85,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
// but we also want to be flexible for the script we proxy to.
fpath := r.URL.Path
// We trim those characters because they are served as plain text if appended after .php on Windows
fpath = strings.TrimRight(fpath, " .")
if idx, ok := httpserver.IndexFile(h.FileSys, fpath, rule.IndexFiles); ok {
fpath = idx
@@ -69,7 +104,8 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
}
// These criteria work well in this order for PHP sites
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(fpath, rule.Ext) {
// We lower path and Ext as on Windows, the system is case insensitive, so .PHP is served as .php
if !h.exists(fpath) || fpath[len(fpath)-1] == '/' || strings.HasSuffix(strings.ToLower(fpath), strings.ToLower(rule.Ext)) {
// Create environment for CGI script
env, err := h.buildEnv(r, rule, fpath)
@@ -78,30 +114,57 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
}
// Connect to FastCGI gateway
fcgiBackend, err := rule.dialer.Dial()
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 {
if err, ok := err.(net.Error); ok && err.Timeout() {
return http.StatusGatewayTimeout, err
}
return http.StatusBadGateway, err
}
defer fcgiBackend.Close()
fcgiBackend.SetReadTimeout(rule.ReadTimeout)
fcgiBackend.SetSendTimeout(rule.SendTimeout)
// 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
contentLength, _ := strconv.Atoi(r.Header.Get("Content-Length"))
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)
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
@@ -120,9 +183,9 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
}
// Log any stderr output from upstream
if stderr := fcgiBackend.StdErr(); stderr.Len() != 0 {
if fcgiBackend.stderr.Len() != 0 {
// Remove trailing newline, error logger already does this.
err = LogError(strings.TrimSuffix(stderr.String(), "\n"))
err = LogError(strings.TrimSuffix(fcgiBackend.stderr.String(), "\n"))
}
// Normally we would return the status code if it is an error status (>= 400),
@@ -182,9 +245,6 @@ func (h Handler) exists(path string) bool {
func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) {
var env map[string]string
// Get absolute path of requested resource
absPath := filepath.Join(rule.Root, fpath)
// Separate remote IP and port; more lenient than net.SplitHostPort
var ip, port string
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 {
@@ -206,24 +266,36 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
docURI := fpath[:splitPos+len(rule.SplitPath)]
pathInfo := fpath[splitPos+len(rule.SplitPath):]
scriptName := fpath
scriptFilename := absPath
// Strip PATH_INFO from SCRIPT_NAME
scriptName = strings.TrimSuffix(scriptName, pathInfo)
// Get the request URI. The request URI might be as it came in over the wire,
// or it might have been rewritten internally by the rewrite middleware (see issue #256).
// If it was rewritten, there will be a header indicating the original URL,
// which is needed to get the correct RequestURI value for PHP apps.
reqURI := r.URL.RequestURI()
if origURI := r.Header.Get(internalRewriteFieldName); origURI != "" {
reqURI = origURI
// 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"),
@@ -235,8 +307,9 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
"REMOTE_HOST": ip, // For speed, remote host lookups disabled
"REMOTE_PORT": port,
"REMOTE_IDENT": "", // Not used
"REMOTE_USER": "", // Not used
"REMOTE_USER": remoteUser,
"REQUEST_METHOD": r.Method,
"REQUEST_SCHEME": requestScheme,
"SERVER_NAME": h.ServerName,
"SERVER_PORT": h.ServerPort,
"SERVER_PROTOCOL": r.Proto,
@@ -246,13 +319,13 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
"DOCUMENT_ROOT": rule.Root,
"DOCUMENT_URI": docURI,
"HTTP_HOST": r.Host, // added here, since not always part of headers
"REQUEST_URI": reqURI,
"REQUEST_URI": reqURL.RequestURI(),
"SCRIPT_FILENAME": scriptFilename,
"SCRIPT_NAME": scriptName,
}
// compliance with the CGI specification that PATH_TRANSLATED
// should only exist if PATH_INFO is defined.
// 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
@@ -261,20 +334,29 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]
// 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, "")
// Add env variables from config
for _, envVar := range rule.EnvVars {
// replace request placeholders in environment variables
env[envVar[0]] = replacer.Replace(envVar[1])
}
// Add all HTTP headers (except Caddy-Rewrite-Original-URI ) to env variables
// Add all HTTP headers to env variables
for field, val := range r.Header {
if strings.ToLower(field) == strings.ToLower(internalRewriteFieldName) {
continue
}
header := strings.ToUpper(field)
header = headerNameReplacer.Replace(header)
env["HTTP_"+header] = strings.Join(val, ", ")
@@ -288,8 +370,8 @@ type Rule struct {
// The base path to match. Required.
Path string
// The address of the FastCGI server. Required.
Address string
// upstream load balancer
balancer
// Always process files with this extension with fastcgi.
Ext string
@@ -314,14 +396,60 @@ type Rule struct {
// 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
}
// FCGI dialer
dialer dialer
// 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.
@@ -361,3 +489,11 @@ type LogError string
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",
}
+131 -157
View File
@@ -1,16 +1,35 @@
// 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"
"strings"
"sync"
"testing"
"time"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestServeHTTP(t *testing.T) {
@@ -21,22 +40,24 @@ func TestServeHTTP(t *testing.T) {
if err != nil {
t.Fatalf("Unable to create listener for test: %v", err)
}
defer listener.Close()
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", bodyLenStr)
w.Write([]byte(body))
}))
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)
}
}()
network, address := parseAddress(listener.Addr().String())
handler := Handler{
Next: nil,
Rules: []Rule{
{
Path: "/",
Address: listener.Addr().String(),
dialer: basicDialer{network: network, address: address},
},
},
Next: nil,
Rules: []Rule{{Path: "/", balancer: address(listener.Addr().String())}},
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
@@ -60,120 +81,29 @@ func TestServeHTTP(t *testing.T) {
}
}
// connectionCounter in fact is a listener with an added counter to keep track
// of the number of accepted connections.
type connectionCounter struct {
net.Listener
sync.Mutex
counter int
}
func (l *connectionCounter) Accept() (net.Conn, error) {
l.Lock()
l.counter++
l.Unlock()
return l.Listener.Accept()
}
// TestPersistent ensures that persistent
// as well as the non-persistent fastCGI servers
// send the answers corresnponding to the correct request.
// It also checks the number of tcp connections used.
func TestPersistent(t *testing.T) {
numberOfRequests := 32
for _, poolsize := range []int{0, 1, 5, numberOfRequests} {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Unable to create listener for test: %v", err)
}
listener := &connectionCounter{l, *new(sync.Mutex), 0}
// this fcgi server replies with the request URL
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body := "This answers a request to " + r.URL.Path
bodyLenStr := strconv.Itoa(len(body))
w.Header().Set("Content-Length", bodyLenStr)
w.Write([]byte(body))
}))
network, address := parseAddress(listener.Addr().String())
handler := Handler{
Next: nil,
Rules: []Rule{{Path: "/", Address: listener.Addr().String(), dialer: &persistentDialer{size: poolsize, network: network, address: address}}},
}
var semaphore sync.WaitGroup
serialMutex := new(sync.Mutex)
serialCounter := 0
parallelCounter := 0
// make some serial followed by some
// parallel requests to challenge the handler
for _, serialize := range []bool{true, false, false, false} {
if serialize {
serialCounter++
} else {
parallelCounter++
}
semaphore.Add(numberOfRequests)
for i := 0; i < numberOfRequests; i++ {
go func(i int, serialize bool) {
defer semaphore.Done()
if serialize {
serialMutex.Lock()
defer serialMutex.Unlock()
}
r, err := http.NewRequest("GET", "/"+strconv.Itoa(i), nil)
if err != nil {
t.Errorf("Unable to create request: %v", err)
}
w := httptest.NewRecorder()
status, err := handler.ServeHTTP(w, r)
if status != 0 {
t.Errorf("Handler(pool: %v) return status %v", poolsize, status)
}
if err != nil {
t.Errorf("Handler(pool: %v) Error: %v", poolsize, err)
}
want := "This answers a request to /" + strconv.Itoa(i)
if got := w.Body.String(); got != want {
t.Errorf("Expected response from handler(pool: %v) to be '%s', got: '%s'", poolsize, want, got)
}
}(i, serialize)
} //next request
semaphore.Wait()
} // next set of requests (serial/parallel)
listener.Close()
t.Logf("The pool: %v test used %v tcp connections to answer %v * %v serial and %v * %v parallel requests.", poolsize, listener.counter, serialCounter, numberOfRequests, parallelCounter, numberOfRequests)
} // next handler (persistent/non-persistent)
}
func TestRuleParseAddress(t *testing.T) {
getClientTestTable := []struct {
rule *Rule
expectednetwork string
expectedaddress string
}{
{&Rule{Address: "tcp://172.17.0.1:9000"}, "tcp", "172.17.0.1:9000"},
{&Rule{Address: "fastcgi://localhost:9000"}, "tcp", "localhost:9000"},
{&Rule{Address: "172.17.0.15"}, "tcp", "172.17.0.15"},
{&Rule{Address: "/my/unix/socket"}, "unix", "/my/unix/socket"},
{&Rule{Address: "unix:/second/unix/socket"}, "unix", "/second/unix/socket"},
{&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 {
if actualnetwork, _ := parseAddress(entry.rule.Address); actualnetwork != entry.expectednetwork {
t.Errorf("Unexpected network for address string %v. Got %v, expected %v", entry.rule.Address, actualnetwork, entry.expectednetwork)
addr, err := entry.rule.Address()
if err != nil {
t.Errorf("Unexpected error in retrieving address: %s", err.Error())
}
if _, actualaddress := parseAddress(entry.rule.Address); actualaddress != entry.expectedaddress {
t.Errorf("Unexpected parsed address for address string %v. Got %v, expected %v", entry.rule.Address, actualaddress, entry.expectedaddress)
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)
}
}
}
@@ -222,16 +152,20 @@ func TestBuildEnv(t *testing.T) {
}
}
rule := Rule{}
url, err := url.Parse("http://localhost:2015/fgci_test.php?test=blabla")
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 {
return &http.Request{
r := http.Request{
Method: "GET",
URL: url,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
@@ -242,6 +176,8 @@ func TestBuildEnv(t *testing.T) {
"Foo": {"Bar", "two"},
},
}
ctx := context.WithValue(r.Context(), httpserver.OriginalURLCtxKey, *r.URL)
return r.WithContext(ctx)
}
fpath := "/fgci_test.php"
@@ -251,9 +187,10 @@ func TestBuildEnv(t *testing.T) {
"REMOTE_ADDR": "2b02:1810:4f2d:9400:70ab:f822:be8a:9093",
"REMOTE_PORT": "51688",
"SERVER_PROTOCOL": "HTTP/1.1",
"QUERY_STRING": "test=blabla",
"QUERY_STRING": "test=foobar",
"REQUEST_METHOD": "GET",
"HTTP_HOST": "localhost:2015",
"SCRIPT_NAME": "/fgci_test.php",
}
}
@@ -301,29 +238,32 @@ func TestBuildEnv(t *testing.T) {
}
envExpected = newEnv()
envExpected["HTTP_HOST"] = "localhost:2015"
envExpected["CUSTOM_URI"] = "custom_uri/fgci_test.php?test=blabla"
envExpected["CUSTOM_QUERY"] = "custom=true&test=blabla"
envExpected["CUSTOM_URI"] = "custom_uri/fgci_test.php?test=foobar"
envExpected["CUSTOM_QUERY"] = "custom=true&test=foobar"
testBuildEnv(r, rule, fpath, envExpected)
// 6. Test Caddy-Rewrite-Original-URI header is not removed
// 6. Test SCRIPT_NAME includes path prefix
r = newReq()
rule.EnvVars = [][2]string{
{"HTTP_HOST", "{host}"},
{"CUSTOM_URI", "custom_uri{uri}"},
{"CUSTOM_QUERY", "custom=true&{query}"},
}
ctx := context.WithValue(r.Context(), caddy.CtxKey("path_prefix"), "/test")
r = r.WithContext(ctx)
envExpected = newEnv()
envExpected["HTTP_HOST"] = "localhost:2015"
envExpected["CUSTOM_URI"] = "custom_uri/fgci_test.php?test=blabla"
envExpected["CUSTOM_QUERY"] = "custom=true&test=blabla"
httpFieldName := strings.ToUpper(internalRewriteFieldName)
envExpected["HTTP_"+httpFieldName] = ""
r.Header.Add(internalRewriteFieldName, "/apath/torewrite/index.php")
envExpected["SCRIPT_NAME"] = "/test/fgci_test.php"
testBuildEnv(r, rule, fpath, envExpected)
if r.Header.Get(internalRewriteFieldName) == "" {
t.Errorf("Error: Header Expected %v", internalRewriteFieldName)
}
// 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) {
@@ -344,16 +284,14 @@ func TestReadTimeout(t *testing.T) {
if err != nil {
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
}
defer listener.Close()
defer func() { _ = listener.Close() }()
network, address := parseAddress(listener.Addr().String())
handler := Handler{
Next: nil,
Rules: []Rule{
{
Path: "/",
Address: listener.Addr().String(),
dialer: basicDialer{network: network, address: address},
balancer: address(listener.Addr().String()),
ReadTimeout: test.readTimeout,
},
},
@@ -365,11 +303,16 @@ func TestReadTimeout(t *testing.T) {
w := httptest.NewRecorder()
wg.Add(1)
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(test.sleep)
w.WriteHeader(http.StatusOK)
wg.Done()
}))
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 {
@@ -406,16 +349,14 @@ func TestSendTimeout(t *testing.T) {
if err != nil {
t.Fatalf("Test %d: Unable to create listener for test: %v", i, err)
}
defer listener.Close()
defer func() { _ = listener.Close() }()
network, address := parseAddress(listener.Addr().String())
handler := Handler{
Next: nil,
Rules: []Rule{
{
Path: "/",
Address: listener.Addr().String(),
dialer: basicDialer{network: network, address: address},
balancer: address(listener.Addr().String()),
SendTimeout: test.sendTimeout,
},
},
@@ -426,9 +367,14 @@ func TestSendTimeout(t *testing.T) {
}
w := httptest.NewRecorder()
go fcgi.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
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 {
@@ -448,3 +394,31 @@ func TestSendTimeout(t *testing.T) {
}
}
}
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,
}
}
+71 -76
View File
@@ -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"
@@ -107,18 +122,6 @@ const (
maxPad = 255
)
// Client interface
type Client interface {
Get(pair map[string]string) (response *http.Response, err error)
Head(pair map[string]string) (response *http.Response, err error)
Options(pairs map[string]string) (response *http.Response, err error)
Post(pairs map[string]string, method string, bodyType string, body io.Reader, contentLength int) (response *http.Response, err error)
Close() error
StdErr() bytes.Buffer
SetReadTimeout(time.Duration) error
SetSendTimeout(time.Duration) error
}
type header struct {
Version uint8
Type uint8
@@ -150,7 +153,7 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
return
}
if rec.h.Version != 1 {
err = errInvalidHeaderVersion
err = errors.New("fcgi: invalid header version")
return
}
if rec.h.Type == EndRequest {
@@ -172,64 +175,66 @@ func (rec *record) read(r io.Reader) (buf []byte, err error) {
// FCGIClient implements a FastCGI client, which is a standard for
// interfacing external applications with Web servers.
type FCGIClient struct {
mutex sync.Mutex
conn net.Conn
h header
buf bytes.Buffer
stderr bytes.Buffer
keepAlive bool
reqID uint16
readTimeout time.Duration
sendTimeout time.Duration
mutex sync.Mutex
rwc io.ReadWriteCloser
h header
buf bytes.Buffer
stderr bytes.Buffer
keepAlive bool
reqID uint16
}
// DialTimeout connects to the fcgi responder at the specified network address, using default net.Dialer.
// 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 DialTimeout(network string, address string, timeout time.Duration) (fcgi *FCGIClient, err error) {
conn, err := net.DialTimeout(network, address, timeout)
func DialWithDialerContext(ctx context.Context, network, address string, dialer net.Dialer) (fcgi *FCGIClient, err error) {
var conn net.Conn
conn, err = dialer.DialContext(ctx, network, address)
if err != nil {
return
}
fcgi = &FCGIClient{conn: conn, keepAlive: false, reqID: 1}
fcgi = &FCGIClient{
rwc: conn,
keepAlive: false,
reqID: 1,
}
return fcgi, nil
return
}
// Close closes fcgi connnection.
func (c *FCGIClient) Close() error {
return c.conn.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 (c *FCGIClient) writeRecord(recType uint8, content []byte) error {
// 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 := c.buf.Write(content); err != nil {
return err
}
if _, err := c.buf.Write(pad[:c.h.PaddingLength]); err != nil {
return err
}
if c.sendTimeout != 0 {
if err := c.conn.SetWriteDeadline(time.Now().Add(c.sendTimeout)); err != nil {
return err
}
}
if _, err := c.conn.Write(c.buf.Bytes()); err != nil {
return err
}
return nil
_, err = c.rwc.Write(c.buf.Bytes())
return err
}
func (c *FCGIClient) writeBeginRequest(role uint16, flags uint8) error {
@@ -345,14 +350,13 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
if len(p) > 0 {
if len(w.buf) == 0 {
// filter outputs for error log
for {
rec := &record{}
var buf []byte
buf, err = rec.read(w.c.conn)
if err == errInvalidHeaderVersion {
continue
} else if err != nil {
buf, err = rec.read(w.c.rwc)
if err != nil {
return
}
// standard error output
@@ -376,15 +380,10 @@ func (w *streamReader) Read(p []byte) (n int, err error) {
return
}
// StdErr returns stderr stream
func (c *FCGIClient) StdErr() bytes.Buffer {
return c.stderr
}
// Do made the request and returns a io.Reader that translates the data read
// from fcgi responder out of fcgi packet before returning it.
func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
err = c.writeBeginRequest(uint16(Responder), FCGIKeepConn)
err = c.writeBeginRequest(uint16(Responder), 0)
if err != nil {
return
}
@@ -396,7 +395,7 @@ func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er
body := newWriter(c, Stdin)
if req != nil {
io.Copy(body, req)
_, _ = io.Copy(body, req)
}
body.Close()
@@ -407,11 +406,11 @@ func (c *FCGIClient) Do(p map[string]string, req io.Reader) (r io.Reader, err er
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer
// that closes FCGIClient connection.
type clientCloser struct {
f *FCGIClient
*FCGIClient
io.Reader
}
func (c clientCloser) Close() error { return c.f.Close() }
func (f clientCloser) Close() error { return f.rwc.Close() }
// Request returns a HTTP Response with Header and Body
// from fcgi responder
@@ -425,12 +424,6 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
tp := textproto.NewReader(rb)
resp = new(http.Response)
if c.readTimeout != 0 {
if err = c.conn.SetReadDeadline(time.Now().Add(c.readTimeout)); err != nil {
return
}
}
// Parse the response headers.
mimeHeader, err := tp.ReadMIMEHeader()
if err != nil && err != io.EOF {
@@ -465,12 +458,12 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
}
// Get issues a GET request to the fcgi responder.
func (c *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 c.Request(p, nil)
return c.Request(p, body)
}
// Head issues a HEAD request to the fcgi responder.
@@ -493,7 +486,7 @@ func (c *FCGIClient) Options(p map[string]string) (resp *http.Response, err erro
// Post issues a POST request to the fcgi responder. with request body
// in the format that bodyType specified
func (c *FCGIClient) Post(p map[string]string, method 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)
}
@@ -504,7 +497,7 @@ func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, b
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 {
@@ -518,7 +511,7 @@ func (c *FCGIClient) Post(p map[string]string, method string, bodyType string, b
// as a string key to a list values (url.Values)
func (c *FCGIClient) PostForm(p map[string]string, data url.Values) (resp *http.Response, err error) {
body := bytes.NewReader([]byte(data.Encode()))
return c.Post(p, "POST", "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,
@@ -560,24 +553,26 @@ func (c *FCGIClient) PostFile(p map[string]string, data url.Values, file map[str
return
}
return c.Post(p, "POST", 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 {
c.readTimeout = t
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 {
c.sendTimeout = t
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
func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" }
var errInvalidHeaderVersion = errors.New("fcgi: invalid header version")
+44 -18
View File
@@ -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,7 +44,7 @@ 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 (
@@ -45,7 +59,9 @@ 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, "-")
@@ -54,15 +70,15 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
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"
}
}
@@ -103,7 +119,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
}
func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[string]string, files map[string]string) (content []byte) {
fcgi, err := DialTimeout("tcp", ipPort, 0)
fcgi, err := Dial("tcp", ipPort)
if err != nil {
log.Println("err:", err)
return
@@ -117,7 +133,7 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
if len(data) > 0 {
length = len(data)
rd := bytes.NewReader(data)
resp, err = fcgi.Post(fcgiParams, "", "", 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 {
@@ -126,7 +142,8 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
}
resp, err = fcgi.PostForm(fcgiParams, values)
} else {
resp, err = fcgi.Get(fcgiParams)
rd := bytes.NewReader(data)
resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len()))
}
default:
@@ -182,8 +199,12 @@ 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
@@ -199,12 +220,13 @@ func DisabledTest(t *testing.T) {
go func() {
listener, err := net.Listen("tcp", ipPort)
if err != nil {
// handle error
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)
@@ -229,7 +251,7 @@ func DisabledTest(t *testing.T) {
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))
data += k0 + "=" + url.QueryEscape(v0) + "&"
}
@@ -246,7 +268,7 @@ func DisabledTest(t *testing.T) {
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
}
@@ -270,6 +292,10 @@ func DisabledTest(t *testing.T) {
delete(f0, "m0")
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)
}
}
+54 -43
View File
@@ -1,17 +1,34 @@
// 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"
"strconv"
"strings"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
var defaultTimeout = 60 * time.Second
func init() {
caddy.RegisterPlugin("fastcgi", caddy.Plugin{
ServerType: "http",
@@ -61,13 +78,20 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
}
rule := Rule{
Root: absRoot,
Path: args[0],
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
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
@@ -75,10 +99,6 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
}
var err error
var pool int
var connectTimeout = 60 * time.Second
var dialers []dialer
var poolSize = -1
for c.NextBlock() {
switch c.Val() {
@@ -106,6 +126,10 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
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 {
@@ -126,24 +150,11 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
}
rule.IgnoredSubPaths = ignoredPaths
case "pool":
if !c.NextArg() {
return rules, c.ArgErr()
}
pool, err = strconv.Atoi(c.Val())
if err != nil {
return rules, err
}
if pool >= 0 {
poolSize = pool
} else {
return rules, c.Errf("positive integer expected, found %d", pool)
}
case "connect_timeout":
if !c.NextArg() {
return rules, c.ArgErr()
}
connectTimeout, err = time.ParseDuration(c.Val())
rule.ConnectTimeout, err = time.ParseDuration(c.Val())
if err != nil {
return rules, err
}
@@ -168,32 +179,32 @@ func fastcgiParse(c *caddy.Controller) ([]Rule, error) {
}
}
for _, rawAddress := range upstreams {
network, address := parseAddress(rawAddress)
if poolSize >= 0 {
dialers = append(dialers, &persistentDialer{
size: poolSize,
network: network,
address: address,
timeout: connectTimeout,
})
} else {
dialers = append(dialers, basicDialer{
network: network,
address: address,
timeout: connectTimeout,
})
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}
}
rule.dialer = &loadBalancingDialer{dialers: dialers}
rule.Address = strings.Join(upstreams, ",")
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 {
+124 -236
View File
@@ -1,14 +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.
package fastcgi
import (
"context"
"fmt"
"os"
"reflect"
"net"
"testing"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
@@ -32,45 +46,29 @@ func TestSetup(t *testing.T) {
if myHandler.Rules[0].Path != "/" {
t.Errorf("Expected / as the Path")
}
if myHandler.Rules[0].Address != "127.0.0.1:9000" {
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")
}
}
func (p *persistentDialer) Equals(q *persistentDialer) bool {
if p.size != q.size {
return false
}
if p.network != q.network {
return false
}
if p.address != q.address {
return false
if myHandler.Rules[0].ConnectTimeout != 60*time.Second {
t.Errorf("Expected default value of 60 seconds")
}
if len(p.pool) != len(q.pool) {
return false
if myHandler.Rules[0].ReadTimeout != 60*time.Second {
t.Errorf("Expected default value of 60 seconds")
}
for i, client := range p.pool {
if client != q.pool[i] {
return false
}
if myHandler.Rules[0].SendTimeout != 60*time.Second {
t.Errorf("Expected default value of 60 seconds")
}
// ignore mutex state
return true
}
func TestFastcgiParse(t *testing.T) {
rootPath, err := os.Getwd()
if err != nil {
t.Errorf("Can't determine current working directory; got '%v'", err)
}
defaultAddress := "127.0.0.1:9001"
network, address := parseAddress(defaultAddress)
t.Logf("Address '%v' was parsed to network '%v' and address '%v'", defaultAddress, network, address)
tests := []struct {
inputFastcgiConfig string
shouldErr bool
@@ -79,193 +77,47 @@ func TestFastcgiParse(t *testing.T) {
{`fastcgi /blog 127.0.0.1:9000 php`,
false, []Rule{{
Root: rootPath,
Path: "/blog",
Address: "127.0.0.1:9000",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9000"}},
Ext: ".php",
SplitPath: ".php",
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}}},
IndexFiles: []string{"index.php"},
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
}}},
{`fastcgi /blog 127.0.0.1:9000 php {
root /tmp
}`,
false, []Rule{{
Root: "/tmp",
Path: "/blog",
Address: "127.0.0.1:9000",
Ext: ".php",
SplitPath: ".php",
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}}},
IndexFiles: []string{"index.php"},
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
}}},
{`fastcgi /blog 127.0.0.1:9000 php {
upstream 127.0.0.1:9001
}`,
false, []Rule{{
Root: rootPath,
Path: "/blog",
Address: "127.0.0.1:9000,127.0.0.1:9001",
Ext: ".php",
SplitPath: ".php",
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}, basicDialer{network: "tcp", address: "127.0.0.1:9001", timeout: 60 * time.Second}}},
IndexFiles: []string{"index.php"},
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
}}},
{`fastcgi /blog 127.0.0.1:9000 {
upstream 127.0.0.1:9001
}`,
false, []Rule{{
Root: rootPath,
Path: "/blog",
Address: "127.0.0.1:9000,127.0.0.1:9001",
Ext: "",
SplitPath: "",
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}, basicDialer{network: "tcp", address: "127.0.0.1:9001", timeout: 60 * time.Second}}},
IndexFiles: []string{},
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / ` + defaultAddress + ` {
{`fastcgi / 127.0.0.1:9001 {
split .html
}`,
false, []Rule{{
Root: rootPath,
Path: "/",
Address: defaultAddress,
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
SplitPath: ".html",
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
IndexFiles: []string{},
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / ` + defaultAddress + ` {
{`fastcgi / 127.0.0.1:9001 {
split .html
except /admin /user
}`,
false, []Rule{{
Root: rootPath,
Path: "/",
Address: "127.0.0.1:9001",
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
SplitPath: ".html",
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
IndexFiles: []string{},
IgnoredSubPaths: []string{"/admin", "/user"},
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / ` + defaultAddress + ` {
pool 0
}`,
{`fastcgi / 127.0.0.1:9001 {
send_timeout 30s
}`,
false, []Rule{{
Root: rootPath,
Path: "/",
Address: defaultAddress,
balancer: &roundRobin{addresses: []string{"127.0.0.1:9001"}},
Ext: "",
SplitPath: "",
dialer: &loadBalancingDialer{dialers: []dialer{&persistentDialer{size: 0, network: network, address: address, timeout: 60 * time.Second}}},
IndexFiles: []string{},
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
SendTimeout: 30 * time.Second,
}}},
{`fastcgi / 127.0.0.1:8080 {
upstream 127.0.0.1:9000
pool 5
}`,
false, []Rule{{
Root: rootPath,
Path: "/",
Address: "127.0.0.1:8080,127.0.0.1:9000",
Ext: "",
SplitPath: "",
dialer: &loadBalancingDialer{dialers: []dialer{&persistentDialer{size: 5, network: "tcp", address: "127.0.0.1:8080", timeout: 60 * time.Second}, &persistentDialer{size: 5, network: "tcp", address: "127.0.0.1:9000", timeout: 60 * time.Second}}},
IndexFiles: []string{},
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / ` + defaultAddress + ` {
split .php
}`,
false, []Rule{{
Root: rootPath,
Path: "/",
Address: defaultAddress,
Ext: "",
SplitPath: ".php",
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
IndexFiles: []string{},
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
}}},
{`fastcgi / ` + defaultAddress + ` {
connect_timeout 5s
}`,
false, []Rule{{
Root: rootPath,
Path: "/",
Address: defaultAddress,
Ext: "",
SplitPath: "",
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 5 * time.Second}}},
IndexFiles: []string{},
ReadTimeout: 60 * time.Second,
SendTimeout: 60 * time.Second,
}}},
{
`fastcgi / ` + defaultAddress + ` { connect_timeout BADVALUE }`,
true,
[]Rule{},
},
{`fastcgi / ` + defaultAddress + ` {
read_timeout 5s
}`,
false, []Rule{{
Root: rootPath,
Path: "/",
Address: defaultAddress,
Ext: "",
SplitPath: "",
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
IndexFiles: []string{},
ReadTimeout: 5 * time.Second,
SendTimeout: 60 * time.Second,
}}},
{
`fastcgi / ` + defaultAddress + ` { read_timeout BADVALUE }`,
true,
[]Rule{},
},
{`fastcgi / ` + defaultAddress + ` {
send_timeout 5s
}`,
false, []Rule{{
Root: rootPath,
Path: "/",
Address: defaultAddress,
Ext: "",
SplitPath: "",
dialer: &loadBalancingDialer{dialers: []dialer{basicDialer{network: network, address: address, timeout: 60 * time.Second}}},
IndexFiles: []string{},
ReadTimeout: 60 * time.Second,
SendTimeout: 5 * time.Second,
}}},
{
`fastcgi / ` + defaultAddress + ` { send_timeout BADVALUE }`,
true,
[]Rule{},
},
{`fastcgi / {
}`,
true, []Rule{},
},
}
for i, test := range tests {
actualFastcgiConfigs, err := fastcgiParse(caddy.NewTestController("http", test.inputFastcgiConfig))
@@ -281,19 +133,24 @@ func TestFastcgiParse(t *testing.T) {
}
for j, actualFastcgiConfig := range actualFastcgiConfigs {
if actualFastcgiConfig.Root != test.expectedFastcgiConfig[j].Root {
t.Errorf("Test %d expected %dth FastCGI Root to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Root, actualFastcgiConfig.Root)
}
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
}
if actualFastcgiConfig.Address != test.expectedFastcgiConfig[j].Address {
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, test.expectedFastcgiConfig[j].Address, actualFastcgiConfig.Address)
i, j, expectedAddr, actualAddr)
}
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
@@ -306,16 +163,6 @@ func TestFastcgiParse(t *testing.T) {
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
}
if reflect.TypeOf(actualFastcgiConfig.dialer) != reflect.TypeOf(test.expectedFastcgiConfig[j].dialer) {
t.Errorf("Test %d expected %dth FastCGI dialer to be of type %T, but got %T",
i, j, test.expectedFastcgiConfig[j].dialer, actualFastcgiConfig.dialer)
} else {
if !areDialersEqual(actualFastcgiConfig.dialer, test.expectedFastcgiConfig[j].dialer, t) {
t.Errorf("Test %d expected %dth FastCGI dialer to be %v, but got %v",
i, j, test.expectedFastcgiConfig[j].dialer, actualFastcgiConfig.dialer)
}
}
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)
@@ -326,42 +173,83 @@ func TestFastcgiParse(t *testing.T) {
i, j, test.expectedFastcgiConfig[j].IgnoredSubPaths, actualFastcgiConfig.IgnoredSubPaths)
}
if fmt.Sprint(actualFastcgiConfig.ReadTimeout) != fmt.Sprint(test.expectedFastcgiConfig[j].ReadTimeout) {
t.Errorf("Test %d expected %dth FastCGI ReadTimeout to be %s , but got %s",
i, j, test.expectedFastcgiConfig[j].ReadTimeout, actualFastcgiConfig.ReadTimeout)
}
if fmt.Sprint(actualFastcgiConfig.SendTimeout) != fmt.Sprint(test.expectedFastcgiConfig[j].SendTimeout) {
t.Errorf("Test %d expected %dth FastCGI SendTimeout to be %s , but got %s",
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 areDialersEqual(current, expected dialer, t *testing.T) bool {
switch actual := current.(type) {
case *loadBalancingDialer:
if expected, ok := expected.(*loadBalancingDialer); ok {
for i := 0; i < len(actual.dialers); i++ {
if !areDialersEqual(actual.dialers[i], expected.dialers[i], t) {
return false
}
}
return true
}
case basicDialer:
return current == expected
case *persistentDialer:
if expected, ok := expected.(*persistentDialer); ok {
return actual.Equals(expected)
}
default:
t.Errorf("Unknown dialer type %T", current)
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,
},
}
return 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
}
+67 -55
View File
@@ -1,18 +1,29 @@
// 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 (
"bufio"
"compress/gzip"
"io"
"io/ioutil"
"net"
"net/http"
"strings"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
@@ -20,6 +31,8 @@ func init() {
ServerType: "http",
Action: setup,
})
initWriterPool()
}
// Gzip is a middleware type which gzips HTTP responses. It is
@@ -53,22 +66,31 @@ outer:
}
}
// gzipWriter modifies underlying writer at init,
// use a discard writer instead to leave ResponseWriter in
// original form.
gzipWriter, err := newWriter(c, ioutil.Discard)
if err != nil {
// should not happen
return http.StatusInternalServerError, err
// In order to avoid unused memory allocation, gzip.putWriter only be called when gzip compression happened.
// see https://github.com/caddyserver/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 gzipWriter.Close()
gz := &gzipResponseWriter{Writer: gzipWriter, ResponseWriter: w}
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
gzipWriter.Reset(w)
if gzWriter, ok := gz.Writer().(*gzip.Writer); ok {
gzWriter.Reset(w)
}
rw = gz
} else {
// wrap gzip writer with ResponseFilterWriter
@@ -92,22 +114,13 @@ outer:
return g.Next.ServeHTTP(w, r)
}
// newWriter create a new Gzip Writer based on the compression level.
// If the level is valid (i.e. between 1 and 9), it uses the level.
// Otherwise, it uses default compression level.
func newWriter(c Config, w io.Writer) (*gzip.Writer, error) {
if c.Level >= gzip.BestSpeed && c.Level <= gzip.BestCompression {
return gzip.NewWriterLevel(w, c.Level)
}
return gzip.NewWriter(w), nil
}
// gzipResponeWriter wraps the underlying Write method
// gzipResponseWriter wraps the underlying Write method
// with a gzip.Writer to compress the output.
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
internalWriter io.Writer
*httpserver.ResponseWriterWrapper
statusCodeWritten bool
newWriter func() io.Writer
}
// WriteHeader wraps the underlying WriteHeader method to prevent
@@ -117,8 +130,24 @@ type gzipResponseWriter struct {
func (w *gzipResponseWriter) WriteHeader(code int) {
w.Header().Del("Content-Length")
w.Header().Set("Content-Encoding", "gzip")
w.Header().Add("Vary", "Accept-Encoding")
w.ResponseWriter.WriteHeader(code)
varyList, exist := w.Header()["Vary"]
shouldAddVary := true
if exist {
for _, vary := range varyList {
if vary == "Accept-Encoding" {
shouldAddVary = false
break
}
}
}
if shouldAddVary {
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
}
@@ -130,34 +159,17 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) {
if !w.statusCodeWritten {
w.WriteHeader(http.StatusOK)
}
n, err := w.Writer.Write(b)
n, err := w.Writer().Write(b)
return n, err
}
// Hijack implements http.Hijacker. It simply wraps the underlying
// ResponseWriter's Hijack method if there is one, or returns an error.
func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := w.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
//Writer use a lazy way to initialize Writer
func (w *gzipResponseWriter) Writer() io.Writer {
if w.internalWriter == nil {
w.internalWriter = w.newWriter()
}
return nil, nil, httpserver.NonHijackerError{Underlying: w.ResponseWriter}
return w.internalWriter
}
// Flush implements http.Flusher. It simply wraps the underlying
// ResponseWriter's Flush method if there is one, or panics.
func (w *gzipResponseWriter) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
} else {
panic(httpserver.NonFlusherError{Underlying: w.ResponseWriter}) // should be recovered at the beginning of middleware stack
}
}
// CloseNotify implements http.CloseNotifier.
// It just inherits the underlying ResponseWriter's CloseNotify method.
func (w *gzipResponseWriter) CloseNotify() <-chan bool {
if cn, ok := w.ResponseWriter.(http.CloseNotifier); ok {
return cn.CloseNotify()
}
panic(httpserver.NonCloseNotifierError{Underlying: w.ResponseWriter})
}
// Interface guards
var _ httpserver.HTTPInterfaces = (*gzipResponseWriter)(nil)
+80 -3
View File
@@ -1,6 +1,21 @@
// 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"
@@ -8,7 +23,7 @@ import (
"strings"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestGzipHandler(t *testing.T) {
@@ -37,6 +52,14 @@ func TestGzipHandler(t *testing.T) {
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)
@@ -77,6 +100,22 @@ func TestGzipHandler(t *testing.T) {
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 {
@@ -92,10 +131,14 @@ func nextFunc(shouldGzip bool) httpserver.Handler {
if shouldGzip {
if w.Header().Get("Content-Encoding") != "gzip" {
return 0, fmt.Errorf("Content-Encoding must be gzip, found %v", r.Header.Get("Content-Encoding"))
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", r.Header.Get("Vary"))
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)
@@ -117,3 +160,37 @@ func nextFunc(shouldGzip bool) httpserver.Handler {
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)
}
}
}
+16 -2
View File
@@ -1,10 +1,24 @@
// 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"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// RequestFilter determines if a request should be gzipped.
@@ -16,7 +30,7 @@ type RequestFilter interface {
// 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"}
".md", ".mdown", ".xml", ".svg", ".go", ".cgi", ".py", ".pl", ".aspx", ".asp", ".m3u", ".m3u8", ".wasm"}
// DefaultExtFilter creates an ExtFilter with default extensions.
func DefaultExtFilter() ExtFilter {
+14
View File
@@ -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.
package gzip
import (
+15 -1
View File
@@ -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.
package gzip
import (
@@ -68,7 +82,7 @@ func (r *ResponseFilterWriter) WriteHeader(code int) {
if r.shouldCompress {
// replace discard writer with ResponseWriter
if gzWriter, ok := r.gzipResponseWriter.Writer.(*gzip.Writer); ok {
if gzWriter, ok := r.gzipResponseWriter.Writer().(*gzip.Writer); ok {
gzWriter.Reset(r.ResponseWriter)
}
// use gzip WriteHeader to include and delete
+29 -6
View File
@@ -1,13 +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.
package gzip
import (
"compress/gzip"
"fmt"
"log"
"net/http"
"net/http/httptest"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestLengthFilter(t *testing.T) {
@@ -33,7 +48,7 @@ func TestLengthFilter(t *testing.T) {
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), r, false})
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))
}
@@ -63,7 +78,9 @@ func TestResponseFilterWriter(t *testing.T) {
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)))
w.Write([]byte(ts.body))
if _, err := w.Write([]byte(ts.body)); err != nil {
log.Println("[ERROR] failed to write response: ", err)
}
return 200, nil
})
@@ -72,7 +89,9 @@ func TestResponseFilterWriter(t *testing.T) {
w := httptest.NewRecorder()
server.ServeHTTP(w, r)
if _, err := server.ServeHTTP(w, r); err != nil {
log.Println("[ERROR] unable to serve a gzipped response: ", err)
}
resp := w.Body.String()
@@ -95,7 +114,9 @@ func TestResponseGzippedOutput(t *testing.T) {
server.Next = httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
w.Header().Set("Content-Encoding", "gzip")
w.Write([]byte("gzipped"))
if _, err := w.Write([]byte("gzipped")); err != nil {
log.Println("[ERROR] failed to write response: ", err)
}
return 200, nil
})
@@ -103,7 +124,9 @@ func TestResponseGzippedOutput(t *testing.T) {
r.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
server.ServeHTTP(w, r)
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" {
+64 -2
View File
@@ -1,12 +1,29 @@
// 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"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// setup configures a new gzip middleware instance.
@@ -119,3 +136,48 @@ func gzipParse(c *caddy.Controller) ([]Config, error) {
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)
}
+16 -2
View File
@@ -1,10 +1,24 @@
// 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"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
+24 -36
View File
@@ -1,15 +1,27 @@
// 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 (
"bufio"
"net"
"net/http"
"strings"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// Headers is middleware that adds headers to the responses
@@ -23,7 +35,9 @@ type Headers struct {
// 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{w: w}
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 {
@@ -62,20 +76,20 @@ type headerOperation func(http.Header)
// responseWriterWrapper wraps the real ResponseWriter.
// It defers header operations until writeHeader
type responseWriterWrapper struct {
w http.ResponseWriter
*httpserver.ResponseWriterWrapper
ops []headerOperation
wroteHeader bool
}
func (rww *responseWriterWrapper) Header() http.Header {
return rww.w.Header()
return rww.ResponseWriterWrapper.Header()
}
func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
if !rww.wroteHeader {
rww.WriteHeader(http.StatusOK)
}
return rww.w.Write(d)
return rww.ResponseWriterWrapper.Write(d)
}
func (rww *responseWriterWrapper) WriteHeader(status int) {
@@ -91,7 +105,7 @@ func (rww *responseWriterWrapper) WriteHeader(status int) {
op(h)
}
rww.w.WriteHeader(status)
rww.ResponseWriterWrapper.WriteHeader(status)
}
// delHeader deletes the existing header according to the key
@@ -106,31 +120,5 @@ func (rww *responseWriterWrapper) delHeader(key string) {
})
}
// Hijack implements http.Hijacker. It simply wraps the underlying
// ResponseWriter's Hijack method if there is one, or returns an error.
func (rww *responseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := rww.w.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, httpserver.NonHijackerError{Underlying: rww.w}
}
// Flush implements http.Flusher. It simply wraps the underlying
// ResponseWriter's Flush method if there is one, or panics.
func (rww *responseWriterWrapper) Flush() {
if f, ok := rww.w.(http.Flusher); ok {
f.Flush()
} else {
panic(httpserver.NonFlusherError{Underlying: rww.w}) // should be recovered at the beginning of middleware stack
}
}
// CloseNotify implements http.CloseNotifier.
// It just inherits the underlying ResponseWriter's CloseNotify method.
// It panics if the underlying ResponseWriter is not a CloseNotifier.
func (rww *responseWriterWrapper) CloseNotify() <-chan bool {
if cn, ok := rww.w.(http.CloseNotifier); ok {
return cn.CloseNotify()
}
panic(httpserver.NonCloseNotifierError{Underlying: rww.w})
}
// Interface guards
var _ httpserver.HTTPInterfaces = (*responseWriterWrapper)(nil)
+25 -4
View File
@@ -1,7 +1,22 @@
// 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"
@@ -9,7 +24,7 @@ import (
"sort"
"testing"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestHeader(t *testing.T) {
@@ -55,7 +70,9 @@ func TestHeader(t *testing.T) {
// preset header
rec.Header().Set("Server", "Caddy")
he.ServeHTTP(rec, req)
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",
@@ -67,7 +84,9 @@ func TestHeader(t *testing.T) {
func TestMultipleHeaders(t *testing.T) {
he := Headers{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
fmt.Fprint(w, "This is a test")
if _, err := fmt.Fprint(w, "This is a test"); err != nil {
log.Println("[ERROR] Fprint failed: ", err)
}
return 0, nil
}),
Rules: []Rule{
@@ -83,7 +102,9 @@ func TestMultipleHeaders(t *testing.T) {
}
rec := httptest.NewRecorder()
he.ServeHTTP(rec, req)
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")]
+16 -2
View File
@@ -1,10 +1,24 @@
// 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"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func init() {
+16 -2
View File
@@ -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.
package header
import (
@@ -6,8 +20,8 @@ import (
"reflect"
"testing"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
func TestSetup(t *testing.T) {
+79 -83
View File
@@ -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.
package httpserver
import (
@@ -6,7 +20,7 @@ import (
"regexp"
"strings"
"github.com/mholt/caddy"
"github.com/caddyserver/caddy"
)
// SetupIfMatcher parses `if` or `if_op` in the current dispenser block.
@@ -26,6 +40,7 @@ func SetupIfMatcher(controller *caddy.Controller) (RequestMatcher, error) {
return matcher, err
}
matcher.ifs = append(matcher.ifs, ifc)
matcher.Enabled = true
case "if_op":
if !c.NextArg() {
return matcher, c.ArgErr()
@@ -48,121 +63,102 @@ const (
isOp = "is"
notOp = "not"
hasOp = "has"
notHasOp = "not_has"
startsWithOp = "starts_with"
endsWithOp = "ends_with"
matchOp = "match"
notMatchOp = "not_match"
)
func operatorError(operator string) error {
return fmt.Errorf("Invalid operator %v", operator)
// 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
}
// ifCondition is a 'if' condition.
type ifCondition func(string, string) bool
// 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:]
}
var ifConditions = map[string]ifCondition{
isOp: isFunc,
notOp: notFunc,
hasOp: hasFunc,
notHasOp: notHasFunc,
startsWithOp: startsWithFunc,
endsWithOp: endsWithFunc,
matchOp: matchFunc,
notMatchOp: notMatchFunc,
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.
// It checks for equality.
func isFunc(a, b string) bool {
func (i ifCond) isFunc(a, b string) bool {
return a == b
}
// notFunc is condition for Not operator.
// It checks for inequality.
func notFunc(a, b string) bool {
func (i ifCond) notFunc(a, b string) bool {
return a != b
}
// hasFunc is condition for Has operator.
// It checks if b is a substring of a.
func hasFunc(a, b string) bool {
return strings.Contains(a, b)
}
// notHasFunc is condition for NotHas operator.
// It checks if b is not a substring of a.
func notHasFunc(a, b string) bool {
return !strings.Contains(a, b)
}
// startsWithFunc is condition for StartsWith operator.
// It checks if b is a prefix of a.
func startsWithFunc(a, b string) bool {
return strings.HasPrefix(a, b)
}
// endsWithFunc is condition for EndsWith operator.
// It checks if b is a suffix of a.
func endsWithFunc(a, b string) bool {
return strings.HasSuffix(a, b)
}
// matchFunc is condition for Match operator.
// It does regexp matching of a against pattern in b
// and returns if they match.
func matchFunc(a, b string) bool {
matched, _ := regexp.MatchString(b, a)
return matched
}
// notMatchFunc is condition for NotMatch operator.
// It does regexp matching of a against pattern in b
// and returns if they do not match.
func notMatchFunc(a, b string) bool {
matched, _ := regexp.MatchString(b, a)
return !matched
}
// ifCond is statement for a IfMatcher condition.
type ifCond struct {
a string
op string
b string
}
// newIfCond creates a new If condition.
func newIfCond(a, operator, b string) (ifCond, error) {
if _, ok := ifConditions[operator]; !ok {
return ifCond{}, operatorError(operator)
}
return ifCond{
a: a,
op: operator,
b: b,
}, nil
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 c, ok := ifConditions[i.op]; ok {
if i.f != nil {
a, b := i.a, i.b
if r != nil {
replacer := NewReplacer(r, nil, "")
a = replacer.Replace(i.a)
b = replacer.Replace(i.b)
if i.op != matchOp {
b = replacer.Replace(i.b)
}
}
return c(a, b)
if i.neg {
return !i.f(a, b)
}
return i.f(a, b)
}
return false
return i.neg // false if not negated, true otherwise
}
// IfMatcher is a RequestMatcher for 'if' conditions.
type IfMatcher struct {
ifs []ifCond // list of If
isOr bool // if true, conditions are 'or' instead of 'and'
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.
@@ -195,7 +191,7 @@ func (m IfMatcher) Or(r *http.Request) bool {
}
// IfMatcherKeyword checks if the next value in the dispenser is a keyword for 'if' config block.
// If true, remaining arguments in the dispinser are cleard to keep the dispenser valid for use.
// 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
+140 -61
View File
@@ -1,72 +1,99 @@
// 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"
"context"
"net/http"
"regexp"
"strings"
"testing"
"github.com/mholt/caddy"
"github.com/caddyserver/caddy"
)
func TestConditions(t *testing.T) {
tests := []struct {
condition string
isTrue bool
shouldErr bool
}{
{"a is b", false},
{"a is a", true},
{"a not b", true},
{"a not a", false},
{"a has a", true},
{"a has b", false},
{"ba has b", true},
{"bab has b", true},
{"bab has bb", false},
{"a not_has a", false},
{"a not_has b", true},
{"ba not_has b", false},
{"bab not_has b", false},
{"bab not_has bb", true},
{"bab starts_with bb", false},
{"bab starts_with ba", true},
{"bab starts_with bab", true},
{"bab ends_with bb", false},
{"bab ends_with bab", true},
{"bab ends_with ab", true},
{"a match *", false},
{"a match a", true},
{"a match .*", true},
{"a match a.*", true},
{"a match b.*", false},
{"ba match b.*", true},
{"ba match b[a-z]", true},
{"b0 match b[a-z]", false},
{"b0a match b[a-z]", false},
{"b0a match b[a-z]+", false},
{"b0a match b[a-z0-9]+", true},
{"a not_match *", true},
{"a not_match a", false},
{"a not_match .*", false},
{"a not_match a.*", false},
{"a not_match b.*", true},
{"ba not_match b.*", false},
{"ba not_match b[a-z]", false},
{"b0 not_match b[a-z]", true},
{"b0a not_match b[a-z]", true},
{"b0a not_match b[a-z]+", true},
{"b0a not_match b[a-z0-9]+", false},
{"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 {
t.Error(err)
if !test.shouldErr {
t.Error(err)
}
continue
}
isTrue := ifCond.True(nil)
if isTrue != test.isTrue {
t.Errorf("Test %d: expected %v found %v", i, test.isTrue, isTrue)
t.Errorf("Test %d: '%s' expected %v found %v", i, test.condition, test.isTrue, isTrue)
}
}
@@ -94,16 +121,21 @@ func TestConditions(t *testing.T) {
for i, test := range replaceTests {
r, err := http.NewRequest("GET", test.url, nil)
if err != nil {
t.Error(err)
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.Error(err)
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
}
}
}
@@ -180,6 +212,7 @@ func TestIfMatcher(t *testing.T) {
}
func TestSetupIfMatcher(t *testing.T) {
rex_b, _ := regexp.Compile("b")
tests := []struct {
input string
shouldErr bool
@@ -189,7 +222,7 @@ func TestSetupIfMatcher(t *testing.T) {
if a match b
}`, false, IfMatcher{
ifs: []ifCond{
{a: "a", op: "match", b: "b"},
{a: "a", op: "match", b: "b", neg: false, rex: rex_b},
},
}},
{`test {
@@ -197,7 +230,7 @@ func TestSetupIfMatcher(t *testing.T) {
if_op or
}`, false, IfMatcher{
ifs: []ifCond{
{a: "a", op: "match", b: "b"},
{a: "a", op: "match", b: "b", neg: false, rex: rex_b},
},
isOr: true,
}},
@@ -215,26 +248,26 @@ func TestSetupIfMatcher(t *testing.T) {
},
{`test {
if goal has go
if cook not_has go
if cook not_has go
}`, false, IfMatcher{
ifs: []ifCond{
{a: "goal", op: "has", b: "go"},
{a: "cook", op: "not_has", b: "go"},
{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 cook not_has go
if_op and
}`, false, IfMatcher{
ifs: []ifCond{
{a: "goal", op: "has", b: "go"},
{a: "cook", op: "not_has", b: "go"},
{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 cook not_has go
if_op not
}`, true, IfMatcher{},
},
@@ -243,6 +276,7 @@ func TestSetupIfMatcher(t *testing.T) {
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)
@@ -251,15 +285,60 @@ func TestSetupIfMatcher(t *testing.T) {
} else if err != nil && test.shouldErr {
continue
}
if _, ok := matcher.(IfMatcher); !ok {
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 fmt.Sprint(matcher) != fmt.Sprint(test.expected) {
t.Errorf("Test %v: Expected %v, found %v", i,
fmt.Sprint(test.expected), fmt.Sprint(matcher))
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)
}
}
}
}
}
+26
View File
@@ -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.
package httpserver
import (
@@ -8,6 +22,7 @@ var (
_ error = NonHijackerError{}
_ error = NonFlusherError{}
_ error = NonCloseNotifierError{}
_ error = NonPusherError{}
)
// NonHijackerError is more descriptive error caused by a non hijacker
@@ -42,3 +57,14 @@ type NonCloseNotifierError struct {
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)
}
-80
View File
@@ -1,80 +0,0 @@
package httpserver
import (
"net"
"sync"
"syscall"
)
// TODO: Should this be a generic graceful listener available in its own package or something?
// Also, passing in a WaitGroup is a little awkward. Why can't this listener just keep
// the waitgroup internal to itself?
// newGracefulListener returns a gracefulListener that wraps l and
// uses wg (stored in the host server) to count connections.
func newGracefulListener(l net.Listener, wg *sync.WaitGroup) *gracefulListener {
gl := &gracefulListener{Listener: l, stop: make(chan error), connWg: wg}
go func() {
<-gl.stop
gl.Lock()
gl.stopped = true
gl.Unlock()
gl.stop <- gl.Listener.Close()
}()
return gl
}
// gracefuListener is a net.Listener which can
// count the number of connections on it. Its
// methods mainly wrap net.Listener to be graceful.
type gracefulListener struct {
net.Listener
stop chan error
stopped bool
sync.Mutex // protects the stopped flag
connWg *sync.WaitGroup // pointer to the host's wg used for counting connections
}
// Accept accepts a connection.
func (gl *gracefulListener) Accept() (c net.Conn, err error) {
c, err = gl.Listener.Accept()
if err != nil {
return
}
c = gracefulConn{Conn: c, connWg: gl.connWg}
gl.connWg.Add(1)
return
}
// Close immediately closes the listener.
func (gl *gracefulListener) Close() error {
gl.Lock()
if gl.stopped {
gl.Unlock()
return syscall.EINVAL
}
gl.Unlock()
gl.stop <- nil
return <-gl.stop
}
// gracefulConn represents a connection on a
// gracefulListener so that we can keep track
// of the number of connections, thus facilitating
// a graceful shutdown.
type gracefulConn struct {
net.Conn
connWg *sync.WaitGroup // pointer to the host server's connection waitgroup
}
// Close closes c's underlying connection while updating the wg count.
func (c gracefulConn) Close() error {
err := c.Conn.Close()
if err != nil {
return err
}
// close can fail on http2 connections (as of Oct. 2015, before http2 in std lib)
// so don't decrement count unless close succeeds
c.connWg.Done()
return nil
}
+89 -29
View File
@@ -1,19 +1,35 @@
// 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"
"strconv"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddytls"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddytls"
"github.com/mholt/certmagic"
)
func activateHTTPS(cctx caddy.Context) error {
operatorPresent := !caddy.Started()
if !caddy.Quiet && operatorPresent {
fmt.Print("Activating privacy features...")
fmt.Print("Activating privacy features... ")
}
ctx := cctx.(*httpContext)
@@ -23,7 +39,13 @@ func activateHTTPS(cctx caddy.Context) error {
// place certificates and keys on disk
for _, c := range ctx.siteConfigs {
err := c.TLS.ObtainCert(c.TLS.Hostname, operatorPresent)
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
}
@@ -41,13 +63,23 @@ func activateHTTPS(cctx caddy.Context) error {
// 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.
err = caddytls.RenewManagedCertificates(true)
if err != nil {
return err
// (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()
if err != nil {
return err
}
}
}
if !caddy.Quiet && operatorPresent {
fmt.Println(" done.")
fmt.Println("done.")
}
return nil
@@ -65,21 +97,22 @@ func markQualifiedForAutoHTTPS(configs []*SiteConfig) {
}
// enableAutoHTTPS configures each config to use TLS according to default settings.
// It will only change configs that are marked as managed, and assumes that
// certificates and keys are already on disk. If loadCertificates is true,
// the certificates will be loaded from disk into the cache for this process
// to use. If false, TLS will still be enabled and configured with default
// settings, but no certificates will be parsed loaded into the cache, and
// the returned error value will always be nil.
// 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 {
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 && caddytls.HostQualifies(cfg.Addr.Host) {
_, err := caddytls.CacheManagedCertificate(cfg.Addr.Host, cfg.TLS)
if loadCertificates && certmagic.HostQualifies(cfg.TLS.Hostname) {
_, err := cfg.TLS.Manager.CacheManagedCertificate(cfg.TLS.Hostname)
if err != nil {
return err
}
@@ -91,9 +124,9 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
// Set default port of 443 if not explicitly set
if cfg.Addr.Port == "" &&
cfg.TLS.Enabled &&
(!cfg.TLS.Manual || cfg.TLS.OnDemand) &&
(!cfg.TLS.Manual || cfg.TLS.Manager.OnDemand != nil) &&
cfg.Addr.Host != "localhost" {
cfg.Addr.Port = "443"
cfg.Addr.Port = strconv.Itoa(certmagic.HTTPSPort)
}
}
return nil
@@ -106,10 +139,12 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
// only set up redirects for configs that qualify. It returns the updated list of
// all configs.
func makePlaintextRedirects(allConfigs []*SiteConfig) []*SiteConfig {
httpPort := strconv.Itoa(certmagic.HTTPPort)
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
for i, cfg := range allConfigs {
if cfg.TLS.Managed &&
!hostHasOtherPort(allConfigs, i, "80") &&
(cfg.Addr.Port == "443" || !hostHasOtherPort(allConfigs, i, "443")) {
!hostHasOtherPort(allConfigs, i, httpPort) &&
(cfg.Addr.Port == httpsPort || !hostHasOtherPort(allConfigs, i, httpsPort)) {
allConfigs = append(allConfigs, redirPlaintextHost(cfg))
}
}
@@ -135,32 +170,57 @@ func hostHasOtherPort(allConfigs []*SiteConfig, thisConfigIdx int, otherPort str
// redirPlaintextHost returns a new plaintext HTTP configuration for
// a virtualHost that simply redirects to cfg, which is assumed to
// be the HTTPS configuration. The returned configuration is set
// to listen on port 80. The TLS field of cfg must not be nil.
// to listen on certmagic.HTTPPort. The TLS field of cfg must not be nil.
func redirPlaintextHost(cfg *SiteConfig) *SiteConfig {
redirPort := cfg.Addr.Port
if redirPort == "443" {
// default port is redundant
if redirPort == strconv.Itoa(certmagic.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) {
toURL := "https://" + r.Host
if redirPort != "" {
toURL += ":" + redirPort
// 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 := "80"
port := strconv.Itoa(certmagic.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{AltHTTPPort: cfg.TLS.AltHTTPPort},
TLS: &caddytls.Config{Manager: cfg.TLS.Manager},
Timeouts: cfg.Timeouts,
}
}
+132 -75
View File
@@ -1,83 +1,134 @@
// 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"
"strconv"
"testing"
"github.com/mholt/caddy/caddytls"
"github.com/caddyserver/caddy/caddytls"
"github.com/mholt/certmagic"
)
func TestRedirPlaintextHost(t *testing.T) {
cfg := redirPlaintextHost(&SiteConfig{
Addr: Address{
Host: "example.com",
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",
},
ListenHost: "93.184.216.34",
TLS: new(caddytls.Config),
})
{
Host: "foohost",
ListenHost: "93.184.216.34",
},
{
Host: "foohost",
Port: "1234",
ListenHost: "93.184.216.34",
},
{
Host: "foohost",
Port: strconv.Itoa(certmagic.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, "example.com"; actual != expected {
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
}
if actual, expected := cfg.ListenHost, "93.184.216.34"; actual != expected {
t.Errorf("Expected redir config to have bindhost %s but got %s", expected, actual)
}
if actual, expected := cfg.Addr.Port, "80"; actual != expected {
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
}
// 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, strconv.Itoa(certmagic.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("Redir config middleware not set up properly; got: %#v", cfg.middleware)
}
// 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)
handler := cfg.middleware[0](nil)
// Check redirect for correctness
rec := httptest.NewRecorder()
req, err := http.NewRequest("GET", "http://foo/bar?q=1", nil)
if err != nil {
t.Fatal(err)
}
status, err := handler.ServeHTTP(rec, req)
if status != 0 {
t.Errorf("Expected status return to be 0, but was %d", status)
}
if err != nil {
t.Errorf("Expected returned error to be nil, but was %v", err)
}
if rec.Code != http.StatusMovedPermanently {
t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code)
}
if got, want := rec.Header().Get("Location"), "https://foo:1234/bar?q=1"; got != want {
t.Errorf("Expected Location: '%s' but got '%s'", want, got)
}
// 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)
}
// browsers can infer a default port from scheme, so make sure the port
// doesn't get added in explicitly for default ports like 443 for https.
cfg = redirPlaintextHost(&SiteConfig{Addr: Address{Host: "example.com", Port: "443"}, TLS: new(caddytls.Config)})
handler = cfg.middleware[0](nil)
rec = httptest.NewRecorder()
req, err = http.NewRequest("GET", "http://foo/bar?q=1", nil)
if err != nil {
t.Fatal(err)
}
status, err = handler.ServeHTTP(rec, req)
if status != 0 {
t.Errorf("Expected status return to be 0, but was %d", status)
}
if err != nil {
t.Errorf("Expected returned error to be nil, but was %v", err)
}
if rec.Code != http.StatusMovedPermanently {
t.Errorf("Expected status %d but got %d", http.StatusMovedPermanently, rec.Code)
}
if got, want := rec.Header().Get("Location"), "https://foo/bar?q=1"; got != want {
t.Errorf("Expected Location: '%s' but got '%s'", want, got)
// 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)
}
}
}
@@ -127,11 +178,13 @@ func TestMakePlaintextRedirects(t *testing.T) {
func TestEnableAutoHTTPS(t *testing.T) {
configs := []*SiteConfig{
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true}},
{Addr: Address{Host: "example.com"}, TLS: &caddytls.Config{Managed: true, Manager: &certmagic.Config{}}},
{}, // not managed - no changes!
}
enableAutoHTTPS(configs, false)
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")
@@ -148,18 +201,18 @@ func TestEnableAutoHTTPS(t *testing.T) {
func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
// TODO: caddytls.TestQualifiesForManagedTLS and this test share nearly the same config list...
configs := []*SiteConfig{
{Addr: Address{Host: ""}, TLS: new(caddytls.Config)},
{Addr: Address{Host: "localhost"}, TLS: new(caddytls.Config)},
{Addr: Address{Host: "123.44.3.21"}, TLS: new(caddytls.Config)},
{Addr: Address{Host: "example.com"}, TLS: new(caddytls.Config)},
{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"}},
{Addr: Address{Host: "example.com", Scheme: "http"}, TLS: new(caddytls.Config)},
{Addr: Address{Host: "example.com", Port: "80"}, TLS: new(caddytls.Config)},
{Addr: Address{Host: "example.com", Port: "1234"}, TLS: new(caddytls.Config)},
{Addr: Address{Host: "example.com", Scheme: "https"}, TLS: new(caddytls.Config)},
{Addr: Address{Host: "example.com", Port: "80", Scheme: "https"}, TLS: new(caddytls.Config)},
{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
@@ -176,3 +229,7 @@ func TestMarkQualifiedForAutoHTTPS(t *testing.T) {
t.Errorf("Expected %d managed configs, but got %d", expectedManagedCount, count)
}
}
func newManagedConfig() *caddytls.Config {
return &caddytls.Config{Manager: &certmagic.Config{}}
}
+194
View File
@@ -0,0 +1,194 @@
// 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"
"github.com/caddyserver/caddy"
gsyslog "github.com/hashicorp/go-syslog"
)
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()
}
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)
}
+43 -4
View File
@@ -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.
package httpserver
import (
@@ -6,6 +20,8 @@ import (
"os"
"path"
"time"
"github.com/caddyserver/caddy"
)
func init() {
@@ -18,6 +34,10 @@ type (
// 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.
//
@@ -97,6 +117,10 @@ func (c ConfigSelector) Select(r *http.Request) (config HandlerConfig) {
// 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 len(fpath) == 0 {
// https://caddy.community/t/panic-runtime-error-index-out-of-range/5781
fpath = "/"
}
if fpath[len(fpath)-1] != '/' || root == nil {
return "", false
}
@@ -138,7 +162,7 @@ func SetLastModifiedHeader(w http.ResponseWriter, modTime time.Time) {
// CaseSensitivePath determines if paths should be case sensitive.
// This is configurable via CASE_SENSITIVE_PATH environment variable.
var CaseSensitivePath = true
var CaseSensitivePath = false
const caseSensitivePathEnv = "CASE_SENSITIVE_PATH"
@@ -147,10 +171,10 @@ const caseSensitivePathEnv = "CASE_SENSITIVE_PATH"
// This could have been in init, but init cannot be called from tests.
func initCaseSettings() {
switch os.Getenv(caseSensitivePathEnv) {
case "0", "false":
CaseSensitivePath = false
default:
case "1", "true":
CaseSensitivePath = true
default:
CaseSensitivePath = false
}
}
@@ -191,3 +215,18 @@ var EmptyNext = HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, e
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"
)
+15 -1
View File
@@ -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.
package httpserver
import (
@@ -45,7 +59,7 @@ func TestPathCaseSensitiveEnv(t *testing.T) {
{"0", false},
{"false", false},
{"true", true},
{"", true},
{"", false},
}
for i, test := range tests {
+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/caddyserver/caddy/caddytls"
"github.com/caddyserver/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)
}
}
}
+44 -6
View File
@@ -1,23 +1,61 @@
// 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.
// 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 other matches p.
// 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.
func (p Path) Matches(other string) bool {
if CaseSensitivePath {
return strings.HasPrefix(string(p), other)
//
// Multiple slashes are collapsed/merged. See issue #1859.
func (p Path) Matches(base string) bool {
if base == "/" || base == "" {
return true
}
return strings.HasPrefix(strings.ToLower(string(p)), strings.ToLower(other))
// 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.
+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)
}
}
}
-76
View File
@@ -1,76 +0,0 @@
package httpserver
import (
"math/rand"
"path"
"strings"
"time"
)
// CleanMaskedPath prevents one or more of the path cleanup operations:
// - collapse multiple slashes into one
// - eliminate "/." (current directory)
// - eliminate "<parent_directory>/.."
// by masking certain patterns in the path with a temporary random string.
// This could be helpful when certain patterns in the path are desired to be preserved
// that would otherwise be changed by path.Clean().
// One such use case is the presence of the double slashes as protocol separator
// (e.g., /api/endpoint/http://example.com).
// This is a common pattern in many applications to allow passing URIs as path argument.
func CleanMaskedPath(reqPath string, masks ...string) string {
var replacerVal string
maskMap := make(map[string]string)
// Iterate over supplied masks and create temporary replacement strings
// only for the masks that are present in the path, then replace all occurrences
for _, mask := range masks {
if strings.Index(reqPath, mask) >= 0 {
replacerVal = "/_caddy" + generateRandomString() + "__"
maskMap[mask] = replacerVal
reqPath = strings.Replace(reqPath, mask, replacerVal, -1)
}
}
reqPath = path.Clean(reqPath)
// Revert the replaced masks after path cleanup
for mask, replacerVal := range maskMap {
reqPath = strings.Replace(reqPath, replacerVal, mask, -1)
}
return reqPath
}
// CleanPath calls CleanMaskedPath() with the default mask of "://"
// to preserve double slashes of protocols
// such as "http://", "https://", and "ftp://" etc.
func CleanPath(reqPath string) string {
return CleanMaskedPath(reqPath, "://")
}
// An efficient and fast method for random string generation.
// Inspired by http://stackoverflow.com/a/31832326.
const randomStringLength = 4
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
letterIdxBits = 6
letterIdxMask = 1<<letterIdxBits - 1
letterIdxMax = 63 / letterIdxBits
)
var src = rand.NewSource(time.Now().UnixNano())
func generateRandomString() string {
b := make([]byte, randomStringLength)
for i, cache, remain := randomStringLength-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}
-120
View File
@@ -1,120 +0,0 @@
package httpserver
import (
"path"
"testing"
)
var paths = map[string]map[string]string{
"/../a/b/../././/c": {
"preserve_all": "/../a/b/../././/c",
"preserve_protocol": "/a/c",
"preserve_slashes": "/a//c",
"preserve_dots": "/../a/b/../././c",
"clean_all": "/a/c",
},
"/path/https://www.google.com": {
"preserve_all": "/path/https://www.google.com",
"preserve_protocol": "/path/https://www.google.com",
"preserve_slashes": "/path/https://www.google.com",
"preserve_dots": "/path/https:/www.google.com",
"clean_all": "/path/https:/www.google.com",
},
"/a/b/../././/c/http://example.com/foo//bar/../blah": {
"preserve_all": "/a/b/../././/c/http://example.com/foo//bar/../blah",
"preserve_protocol": "/a/c/http://example.com/foo/blah",
"preserve_slashes": "/a//c/http://example.com/foo/blah",
"preserve_dots": "/a/b/../././c/http:/example.com/foo/bar/../blah",
"clean_all": "/a/c/http:/example.com/foo/blah",
},
}
func assertEqual(t *testing.T, expected, received string) {
if expected != received {
t.Errorf("\tExpected: %s\n\t\t\tRecieved: %s", expected, received)
}
}
func maskedTestRunner(t *testing.T, variation string, masks ...string) {
for reqPath, transformation := range paths {
assertEqual(t, transformation[variation], CleanMaskedPath(reqPath, masks...))
}
}
// No need to test the built-in path.Clean() function.
// However, it could be useful to cross-examine the test dataset.
func TestPathClean(t *testing.T) {
for reqPath, transformation := range paths {
assertEqual(t, transformation["clean_all"], path.Clean(reqPath))
}
}
func TestCleanAll(t *testing.T) {
maskedTestRunner(t, "clean_all")
}
func TestPreserveAll(t *testing.T) {
maskedTestRunner(t, "preserve_all", "//", "/..", "/.")
}
func TestPreserveProtocol(t *testing.T) {
maskedTestRunner(t, "preserve_protocol", "://")
}
func TestPreserveSlashes(t *testing.T) {
maskedTestRunner(t, "preserve_slashes", "//")
}
func TestPreserveDots(t *testing.T) {
maskedTestRunner(t, "preserve_dots", "/..", "/.")
}
func TestDefaultMask(t *testing.T) {
for reqPath, transformation := range paths {
assertEqual(t, transformation["preserve_protocol"], CleanPath(reqPath))
}
}
func maskedBenchmarkRunner(b *testing.B, masks ...string) {
for n := 0; n < b.N; n++ {
for reqPath := range paths {
CleanMaskedPath(reqPath, masks...)
}
}
}
func BenchmarkPathClean(b *testing.B) {
for n := 0; n < b.N; n++ {
for reqPath := range paths {
path.Clean(reqPath)
}
}
}
func BenchmarkCleanAll(b *testing.B) {
maskedBenchmarkRunner(b)
}
func BenchmarkPreserveAll(b *testing.B) {
maskedBenchmarkRunner(b, "//", "/..", "/.")
}
func BenchmarkPreserveProtocol(b *testing.B) {
maskedBenchmarkRunner(b, "://")
}
func BenchmarkPreserveSlashes(b *testing.B) {
maskedBenchmarkRunner(b, "//")
}
func BenchmarkPreserveDots(b *testing.B) {
maskedBenchmarkRunner(b, "/..", "/.")
}
func BenchmarkDefaultMask(b *testing.B) {
for n := 0; n < b.N; n++ {
for reqPath := range paths {
CleanPath(reqPath)
}
}
}
+295 -59
View File
@@ -1,6 +1,21 @@
// 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"
@@ -8,17 +23,23 @@ import (
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/caddytls"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyfile"
"github.com/caddyserver/caddy/caddyhttp/staticfiles"
"github.com/caddyserver/caddy/caddytls"
"github.com/caddyserver/caddy/telemetry"
"github.com/mholt/certmagic"
)
const serverType = "http"
func init() {
flag.IntVar(&certmagic.HTTPPort, "http-port", certmagic.HTTPPort, "Default port to use for HTTP")
flag.IntVar(&certmagic.HTTPSPort, "https-port", certmagic.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")
@@ -48,6 +69,12 @@ func init() {
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
@@ -55,6 +82,10 @@ func init() {
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
@@ -64,17 +95,19 @@ func hideCaddyfile(cctx caddy.Context) error {
return err
}
if strings.HasPrefix(absOriginCaddyfile, absRoot) {
cfg.HiddenFiles = append(cfg.HiddenFiles, strings.TrimPrefix(absOriginCaddyfile, absRoot))
cfg.HiddenFiles = append(cfg.HiddenFiles, filepath.ToSlash(strings.TrimPrefix(absOriginCaddyfile, absRoot)))
}
}
return nil
}
func newContext() caddy.Context {
return &httpContext{keysToSiteConfigs: make(map[string]*SiteConfig)}
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
@@ -94,18 +127,24 @@ func (h *httpContext) saveConfig(key string, cfg *SiteConfig) {
// 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)
httpPort := strconv.Itoa(certmagic.HTTPPort)
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
// For each address in each server block, make a new config
for _, sb := range serverBlocks {
for _, key := range sb.Keys {
key = strings.ToLower(key)
if _, dup := h.keysToSiteConfigs[key]; dup {
return serverBlocks, fmt.Errorf("duplicate site address: %s", key)
}
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 {
@@ -115,12 +154,59 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
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: &caddytls.Config{Hostname: addr.Host},
TLS: caddytlsConfig,
originCaddyfile: sourceFile,
IndexPages: staticfiles.DefaultIndexPages,
}
h.saveConfig(key, cfg)
}
@@ -144,13 +230,47 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
// MakeServers uses the newly-created siteConfigs to
// create and return a list of server instances.
func (h *httpContext) MakeServers() ([]caddy.Server, error) {
// make sure TLS is disabled for explicitly-HTTP sites
// (necessary when HTTP address shares a block containing tls)
httpPort := strconv.Itoa(certmagic.HTTPPort)
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
// 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)
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 == "80" || cfg.Addr.Scheme == "http" {
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 == "" {
@@ -161,11 +281,16 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
// is incorrect for this site.
cfg.Addr.Scheme = "https"
}
if cfg.Addr.Port == "" && ((!cfg.TLS.Manual && !cfg.TLS.SelfSigned) || cfg.TLS.OnDemand) {
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 = "443"
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")
}
}
}
@@ -185,22 +310,48 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
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 := strings.ToLower(c.Key)
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: new(caddytls.Config)}
cfg := &SiteConfig{
Root: Root,
TLS: &caddytls.Config{Manager: certmagic.NewDefault()},
IndexPages: staticfiles.DefaultIndexPages,
}
ctx.saveConfig(key, cfg)
return cfg
}
@@ -234,7 +385,7 @@ func groupSiteConfigsByListenAddr(configs []*SiteConfig) (map[string][]*SiteConf
// 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://forum.caddyserver.com/t/wildcard-virtual-domains-with-wildcard-roots/221/5?u=matt
// https://caddy.community/t/wildcard-virtual-domains-with-wildcard-roots/221/5?u=matt
if conf.Addr.Port == "" {
conf.Addr.Port = Port
@@ -255,6 +406,8 @@ func groupSiteConfigsByListenAddr(configs []*SiteConfig) (map[string][]*SiteConf
// 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
}
@@ -266,7 +419,7 @@ func (a Address) String() string {
}
scheme := a.Scheme
if scheme == "" {
if a.Port == "443" {
if a.Port == strconv.Itoa(certmagic.HTTPSPort) {
scheme = "https"
} else {
scheme = "http"
@@ -276,11 +429,12 @@ func (a Address) String() string {
if s != "" {
s += "://"
}
s += a.Host
if a.Port != "" &&
((scheme == "https" && a.Port != "443") ||
(scheme == "http" && a.Port != "80")) {
s += ":" + 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
@@ -297,11 +451,66 @@ func (a Address) VHost() string {
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
}
// insert port only if the original has its own explicit port
if a.Port != "" && len(a.Original) >= len(res) &&
strings.HasPrefix(a.Original[len(res):], ":"+a.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
httpPort := strconv.Itoa(certmagic.HTTPPort)
httpsPort := strconv.Itoa(certmagic.HTTPSPort)
// As of Go 1.12.8 (Aug 2019), ports that are service names such
// as ":http" and ":https" are no longer parsed as they were
// before, which is a breaking change for us. Attempt to smooth
// this over for now by replacing those strings with their port
// equivalents. See
// https://github.com/golang/go/commit/3226f2d492963d361af9dfc6714ef141ba606713
str = strings.Replace(str, ":https", ":"+httpsPort, 1)
str = strings.Replace(str, ":http", ":"+httpPort, 1)
// Split input into components (prepend with // to assert host by default)
if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") {
str = "//" + str
@@ -323,32 +532,28 @@ func standardizeAddress(str string) (Address, error) {
// see if we can set port based off scheme
if port == "" {
if u.Scheme == "http" {
port = "80"
port = httpPort
} else if u.Scheme == "https" {
port = "443"
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 == "443") || (u.Scheme == "https" && port == "80") {
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 = "80"
} else if port == "https" {
u.Scheme = "https"
port = "443"
// (this behavior changed in Go 1.12.8)
if u.Scheme == "" {
if port == httpPort {
u.Scheme = "http"
} else if port == httpsPort {
u.Scheme = "https"
}
}
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err
return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, nil
}
// RegisterDevDirective splices name into the list of directives
@@ -414,54 +619,81 @@ func RegisterDevDirective(name, before string) {
var directives = []string{
// primitive actions that set up the fundamental vitals of each config
"root",
"index",
"bind",
"maxrequestbody", // TODO: 'limits'
"limits",
"timeouts",
"tls",
// services/utilities, or other directives that don't necessarily inject handlers
"startup",
"shutdown",
"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",
"filter", // github.com/echocat/caddy-filter
"minify", // github.com/hacdias/caddy-minify
"ipfilter", // github.com/pyed/ipfilter
"ratelimit", // github.com/xuqingfeng/caddy-rate-limit
"search", // github.com/pedronasser/caddy-search
"expires", // github.com/epicagency/caddy-expires
"authz", // github.com/casbin/caddy-authz
"filter", // github.com/echocat/caddy-filter
"ipfilter", // github.com/pyed/ipfilter
"ratelimit", // github.com/xuqingfeng/caddy-rate-limit
"recaptcha", // github.com/defund/caddy-recaptcha
"expires", // github.com/epicagency/caddy-expires
"forwardproxy", // github.com/caddyserver/forwardproxy
"basicauth",
"redir",
"status",
"cors", // github.com/captncraig/cors/caddy
"cors", // github.com/captncraig/cors/caddy
"s3browser", // github.com/techknowlogick/caddy-s3browser
"nobots", // github.com/Xumeiquer/nobots
"mime",
"jwt", // github.com/BTBurke/caddy-jwt
"jsonp", // github.com/pschlump/caddy-jsonp
"upload", // blitznote.com/src/caddy.upload
"multipass", // github.com/namsral/multipass/caddy
"login", // github.com/tarent/loginsrv/caddy
"reauth", // github.com/freman/caddy-reauth
"extauth", // github.com/BTBurke/caddy-extauth
"jwt", // github.com/BTBurke/caddy-jwt
"permission", // github.com/dhaavi/caddy-permission
"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
"proxy",
"fastcgi",
"websocket",
"filemanager", // github.com/hacdias/caddy-filemanager
"markdown",
"templates",
"proxy",
"pubsub", // github.com/jung-kurt/caddy-pubsub
"fastcgi",
"cgi", // github.com/jung-kurt/caddy-cgi
"websocket",
"filebrowser", // github.com/filebrowser/caddy
"webdav", // github.com/hacdias/caddy-webdav
"markdown",
"browse",
"hugo", // github.com/hacdias/caddy-hugo
"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 (
@@ -471,6 +703,10 @@ const (
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
+169 -15
View File
@@ -1,11 +1,29 @@
// 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"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyfile"
"sort"
"fmt"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyfile"
)
func TestStandardizeAddress(t *testing.T) {
@@ -25,12 +43,12 @@ func TestStandardizeAddress(t *testing.T) {
{`:`, "", "", "", "", 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`, "http", "", "80", "", false}, // as of Go 1.12.8, service name in port is no longer supported
{`:https`, "https", "", "443", "", false}, // as of Go 1.12.8, service name in port is no longer supported
{`http://localhost:https`, "", "", "", "", true}, // conflict
{`http://localhost:http`, "http", "localhost", "80", "", false}, // repeated scheme -- test adjusted for Go 1.12.8 (expect no error)
{`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},
@@ -40,8 +58,8 @@ func TestStandardizeAddress(t *testing.T) {
{`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},
{`::1`, "", "::1", "", "", false}, // test adjusted for Go 1.12.8 (expect no error)
{`localhost::`, "", "localhost::", "", "", false}, // test adjusted for Go 1.12.8 (expect no error)
{`#$%@`, "", "", "", "", true},
{`host/path`, "", "host", "", "/path", false},
{`http://host/`, "http", "host", "80", "/", false},
@@ -49,7 +67,7 @@ func TestStandardizeAddress(t *testing.T) {
{`: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:80/path`, "http", "host", "80", "/path", false}, // test adjusted for Go 1.12.8 (expect "http" scheme)
{`host:https/path`, "https", "host", "443", "/path", false},
{`/path`, "", "", "", "/path", false},
} {
@@ -123,7 +141,7 @@ func TestAddressString(t *testing.T) {
func TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {
Port = "9999"
filename := "Testfile"
ctx := newContext().(*httpContext)
ctx := newContext(&caddy.Instance{Storage: make(map[interface{}]interface{})}).(*httpContext)
input := strings.NewReader(`localhost`)
sblocks, err := caddyfile.Parse(filename, input, nil)
if err != nil {
@@ -133,15 +151,45 @@ func TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {
if err != nil {
t.Fatalf("Didn't expect an error, but got: %v", err)
}
addr := ctx.keysToSiteConfigs["localhost"].Addr
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().(*httpContext)
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 {
@@ -153,6 +201,80 @@ func TestInspectServerBlocksCaseInsensitiveKey(t *testing.T) {
}
}
func TestKeyNormalization(t *testing.T) {
originalCaseSensitivePath := CaseSensitivePath
defer func() {
CaseSensitivePath = originalCaseSensitivePath
}()
CaseSensitivePath = true
caseSensitiveData := []struct {
orig string
res string
}{
{
orig: "http://host:1234/path",
res: "http://host:1234/path",
},
{
orig: "HTTP://A/ABCDEF",
res: "http://a/ABCDEF",
},
{
orig: "A/ABCDEF",
res: "a/ABCDEF",
},
{
orig: "A:2015/Path",
res: "a:2015/Path",
},
{
orig: ":80",
res: "http://",
},
{
orig: ":443",
res: "https://",
},
{
orig: ":1234",
res: ":1234",
},
}
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", "")
@@ -170,6 +292,14 @@ func TestGetConfig(t *testing.T) {
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) {
@@ -193,7 +323,7 @@ func TestDirectivesList(t *testing.T) {
}
func TestContextSaveConfig(t *testing.T) {
ctx := newContext().(*httpContext)
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")
@@ -209,3 +339,27 @@ func TestContextSaveConfig(t *testing.T) {
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")
}
+197 -33
View File
@@ -1,9 +1,24 @@
// 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 (
"bufio"
"net"
"bytes"
"io"
"net/http"
"sync"
"time"
)
@@ -20,24 +35,22 @@ import (
//
// Beware when accessing the Replacer value; it may be nil!
type ResponseRecorder struct {
http.ResponseWriter
*ResponseWriterWrapper
Replacer Replacer
status int
size int
start time.Time
}
// NewResponseRecorder makes and returns a new responseRecorder,
// which captures the HTTP Status code from the ResponseWriter
// and also the length of the response body written through it.
// 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{
ResponseWriter: w,
status: http.StatusOK,
start: time.Now(),
ResponseWriterWrapper: &ResponseWriterWrapper{ResponseWriter: w},
status: http.StatusOK,
start: time.Now(),
}
}
@@ -45,53 +58,204 @@ func NewResponseRecorder(w http.ResponseWriter) *ResponseRecorder {
// underlying ResponseWriter's WriteHeader method.
func (r *ResponseRecorder) WriteHeader(status int) {
r.status = status
r.ResponseWriter.WriteHeader(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.ResponseWriter.Write(buf)
n, err := r.ResponseWriterWrapper.Write(buf)
if err == nil {
r.size += n
}
return n, err
}
// Size is a Getter to size property
// Size returns the size of the recorded response body.
func (r *ResponseRecorder) Size() int {
return r.size
}
// Status is a Getter to status property
// Status returns the recorded response status code.
func (r *ResponseRecorder) Status() int {
return r.status
}
// Hijack implements http.Hijacker. It simply wraps the underlying
// ResponseWriter's Hijack method if there is one, or returns an error.
func (r *ResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := r.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, NonHijackerError{Underlying: r.ResponseWriter}
// 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
}
// Flush implements http.Flusher. It simply wraps the underlying
// ResponseWriter's Flush method if there is one, or does nothing.
func (r *ResponseRecorder) Flush() {
if f, ok := r.ResponseWriter.(http.Flusher); ok {
f.Flush()
} else {
panic(NonFlusherError{Underlying: r.ResponseWriter}) // should be recovered at the beginning of middleware stack
// 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)
}
}
// CloseNotify implements http.CloseNotifier.
// It just inherits the underlying ResponseWriter's CloseNotify method.
func (r *ResponseRecorder) CloseNotify() <-chan bool {
if cn, ok := r.ResponseWriter.(http.CloseNotifier); ok {
return cn.CloseNotify()
// 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)
}
panic(NonCloseNotifierError{Underlying: r.ResponseWriter})
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)
)
+15 -1
View File
@@ -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.
package httpserver
import (
@@ -30,7 +44,7 @@ func TestWrite(t *testing.T) {
responseTestString := "test"
recordRequest := NewResponseRecorder(w)
buf := []byte(responseTestString)
recordRequest.Write(buf)
_, _ = 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)
}
+270 -47
View File
@@ -1,7 +1,25 @@
// 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"
@@ -13,6 +31,9 @@ import (
"strconv"
"strings"
"time"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddytls"
)
// requestReplacer is a strings.Replacer which is used to
@@ -86,20 +107,30 @@ func (lw *limitWriter) String() string {
// 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 {
rb := newLimitWriter(MaxLogBodySize)
if r.Body != nil {
r.Body = struct {
io.Reader
io.Closer
}{io.TeeReader(r.Body, rb), io.Closer(r.Body)}
repl := &replacer{
request: r,
responseRecorder: rr,
emptyValue: emptyValue,
}
return &replacer{
request: r,
requestBody: rb,
responseRecorder: rr,
customReplacements: make(map[string]string),
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 {
@@ -114,6 +145,14 @@ func canLogRequest(r *http.Request) bool {
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 {
@@ -123,32 +162,59 @@ func (r *replacer) Replace(s string) string {
}
result := ""
Placeholders: // process each placeholder in sequence
for {
idxStart := strings.Index(s, "{")
if idxStart == -1 {
// no placeholder anymore
break
}
idxEnd := strings.Index(s[idxStart:], "}")
if idxEnd == -1 {
// unpaired placeholder
break
}
idxEnd += idxStart
var idxStart, idxEnd int
// get a replacement
placeholder := s[idxStart : idxEnd+1]
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 prefix + replacement
result += s[:idxStart] + replacement
// append unescaped prefix + replacement
result += strings.TrimPrefix(unescapeBraces(s[:idxStart]), "\\") + replacement
// strip out scanned parts
s = s[idxEnd+1:]
}
// append unscanned parts
return result + s
return result + unescapeBraces(s)
}
func roundDuration(d time.Duration) time.Duration {
@@ -181,6 +247,15 @@ func round(d, r time.Duration) time.Duration {
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
@@ -198,6 +273,29 @@ func (r *replacer) getSubstitution(key string) string {
}
}
}
// 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 {
@@ -223,29 +321,27 @@ func (r *replacer) getSubstitution(key string) string {
}
return host
case "{path}":
// if a rewrite has happened, the original URI should be used as the path
// rather than the rewritten URI
path := r.request.Header.Get("Caddy-Rewrite-Original-URI")
if path == "" {
path = r.request.URL.Path
}
return path
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return u.Path
case "{path_escaped}":
path := r.request.Header.Get("Caddy-Rewrite-Original-URI")
if path == "" {
path = r.request.URL.Path
}
return url.QueryEscape(path)
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}":
return r.request.URL.RawQuery
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return u.RawQuery
case "{query_escaped}":
return url.QueryEscape(r.request.URL.RawQuery)
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return url.QueryEscape(u.RawQuery)
case "{fragment}":
return r.request.URL.Fragment
u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL)
return u.Fragment
case "{proto}":
return r.request.Proto
case "{remote}":
@@ -261,13 +357,25 @@ func (r *replacer) getSubstitution(key string) string {
}
return port
case "{uri}":
return r.request.URL.RequestURI()
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
@@ -286,11 +394,19 @@ func (r *replacer) getSubstitution(key string) string {
}
_, err := ioutil.ReadAll(r.request.Body)
if err != nil {
if _, ok := err.(MaxBytesExceeded); ok {
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
@@ -312,14 +428,120 @@ func (r *replacer) getSubstitution(key string) string {
}
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
}
//convertToMilliseconds returns the number of milliseconds in the given duration
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 d.Nanoseconds() / 1e6
return nanoToMilliseconds(d.Nanoseconds())
}
// Set sets key to value in the r.customReplacements map.
@@ -329,6 +551,7 @@ func (r *replacer) Set(key, value string) {
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"
+320 -7
View File
@@ -1,12 +1,36 @@
// 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"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/caddytls"
)
func TestNewReplacer(t *testing.T) {
@@ -38,24 +62,32 @@ func TestReplace(t *testing.T) {
recordRequest := NewResponseRecorder(w)
reader := strings.NewReader(`{"username": "dennis"}`)
request, err := http.NewRequest("POST", "http://localhost", reader)
request, err := http.NewRequest("POST", "http://localhost.local/?foo=bar", reader)
if err != nil {
t.Fatal("Request Formation Failed\n")
t.Fatalf("Failed to make request: %v", err)
}
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
request = request.WithContext(ctx)
request.Header.Set("Custom", "foobarbaz")
request.Header.Set("ShorterVal", "1")
repl := NewReplacer(request, recordRequest, "-")
// add some headers after creating replacer
request.Header.Set("CustomAdd", "caddy")
request.Header.Set("Cookie", "foo=bar; taste=delicious")
// add some response headers
recordRequest.Header().Set("Custom", "CustomResponseHeader")
hostname, err := os.Hostname()
if err != nil {
t.Fatal("Failed to determine hostname\n")
t.Fatalf("Failed to determine hostname: %v", err)
}
old := now
now = func() time.Time {
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
// Note that the `-7` is seconds, not hours.
return time.Date(2006, 1, 2, 15, 4, 5, 99999999, time.FixedZone("hardcoded", -7))
}
defer func() {
now = old
@@ -65,19 +97,37 @@ func TestReplace(t *testing.T) {
expect string
}{
{"This hostname is {hostname}", "This hostname is " + hostname},
{"This host is {host}.", "This host is localhost."},
{"This host is {host}.", "This host is localhost.local."},
{"This request method is {method}.", "This request method is POST."},
{"The response status is {status}.", "The response status is 200."},
{"{when}", "02/Jan/2006:15:04:05 +0000"},
{"{when_iso}", "2006-01-02T15:04:12Z"},
{"{when_iso_local}", "2006-01-02T15:04:05"},
{"{when_unix}", "1136214252"},
{"{when_unix_ms}", "1136214252099"},
{"The Custom header is {>Custom}.", "The Custom header is foobarbaz."},
{"The CustomAdd header is {>CustomAdd}.", "The CustomAdd header is caddy."},
{"The request is {request}.", "The request is POST / HTTP/1.1\\r\\nHost: localhost\\r\\nCustom: foobarbaz\\r\\nCustomadd: caddy\\r\\nShorterval: 1\\r\\n\\r\\n."},
{"The Custom response header is {<Custom}.", "The Custom response header is CustomResponseHeader."},
{"Bad {>Custom placeholder", "Bad {>Custom placeholder"},
{"The request is {request}.", "The request is POST /?foo=bar HTTP/1.1\\r\\nHost: localhost.local\\r\\n" +
"Cookie: foo=bar; taste=delicious\\r\\nCustom: foobarbaz\\r\\nCustomadd: caddy\\r\\n" +
"Shorterval: 1\\r\\n\\r\\n."},
{"The cUsToM header is {>cUsToM}...", "The cUsToM header is foobarbaz..."},
{"The cUsToM response header is {<CuSTom}.", "The cUsToM response header is CustomResponseHeader."},
{"The Non-Existent header is {>Non-Existent}.", "The Non-Existent header is -."},
{"Bad {host placeholder...", "Bad {host placeholder..."},
{"Bad {>Custom placeholder", "Bad {>Custom placeholder"},
{"Bad {>Custom placeholder {>ShorterVal}", "Bad -"},
{"Bad {}", "Bad -"},
{"Cookies are {~taste}", "Cookies are delicious"},
{"Missing cookie is {~missing}", "Missing cookie is -"},
{"Query string is {query}", "Query string is foo=bar"},
{"Query string value for foo is {?foo}", "Query string value for foo is bar"},
{"Missing query string argument is {?missing}", "Missing query string argument is "},
{"{label1} {label2} {label3} {label4}", "localhost local - -"},
{"Label with missing number is {label} or {labelQQ}", "Label with missing number is - or -"},
{"\\{ 'hostname': '{hostname}' \\}", "{ 'hostname': '" + hostname + "' }"},
{"{server_port}", "80"},
}
for _, c := range testCases {
@@ -110,6 +160,234 @@ func TestReplace(t *testing.T) {
}
}
func TestCustomServerPort(t *testing.T) {
w := httptest.NewRecorder()
recordRequest := NewResponseRecorder(w)
reader := strings.NewReader(`{"username": "dennis"}`)
request, err := http.NewRequest("POST", "http://localhost.local:8000/?foo=bar", reader)
if err != nil {
t.Fatalf("Failed to make request: %v", err)
}
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
request = request.WithContext(ctx)
repl := NewReplacer(request, recordRequest, "-")
testCase := struct {
template string
expect string
}{
template: "{server_port}",
expect: "8000",
}
if expected, actual := testCase.expect, repl.Replace(testCase.template); expected != actual {
t.Errorf("for template '%s', expected '%s', got '%s'", testCase.template, expected, actual)
}
}
func TestTlsReplace(t *testing.T) {
w := httptest.NewRecorder()
recordRequest := NewResponseRecorder(w)
clientCertText := []byte(`-----BEGIN CERTIFICATE-----
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
-----END CERTIFICATE-----`)
block, _ := pem.Decode(clientCertText)
if block == nil {
t.Fatalf("failed to decode PEM certificate")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to decode PEM certificate: %v", err)
}
request := &http.Request{
Method: "GET",
Host: "foo.com",
URL: &url.URL{
Scheme: "https",
Path: "/path/",
Host: "foo.com",
},
Header: http.Header{},
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
RemoteAddr: "192.0.2.1:1234",
RequestURI: "https://foo.com/path/",
TLS: &tls.ConnectionState{
Version: tls.VersionTLS12,
HandshakeComplete: true,
ServerName: "foo.com",
CipherSuite: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
PeerCertificates: []*x509.Certificate{cert},
},
}
repl := NewReplacer(request, recordRequest, "-")
now := time.Now().In(time.UTC)
days := int64(cert.NotAfter.Sub(now).Seconds() / 86400)
pemBlock := pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
}
protocol, _ := caddytls.GetSupportedProtocolName(request.TLS.Version)
cipher, _ := caddytls.GetSupportedCipherName(request.TLS.CipherSuite)
cEscapedCert := url.QueryEscape(string(pem.EncodeToMemory(&pemBlock)))
cFingerprint := fmt.Sprintf("%x", sha256.Sum256(cert.Raw))
cIDn := cert.Issuer.String()
cRawCert := string(cert.Raw)
cSDn := cert.Subject.String()
cSerial := fmt.Sprintf("%x", cert.SerialNumber)
cVEnd := cert.NotAfter.In(time.UTC).Format("Jan 02 15:04:05 2006 MST")
cVRemain := strconv.FormatInt(days, 10)
cVStart := cert.NotBefore.Format("Jan 02 15:04:05 2006 MST")
testCases := []struct {
template string
expect string
}{
{"{tls_protocol}", protocol},
{"{tls_cipher}", cipher},
{"{tls_client_escaped_cert}", cEscapedCert},
{"{tls_client_fingerprint}", cFingerprint},
{"{tls_client_i_dn}", cIDn},
{"{tls_client_raw_cert}", cRawCert},
{"{tls_client_s_dn}", cSDn},
{"{tls_client_serial}", cSerial},
{"{tls_client_v_end}", cVEnd},
{"{tls_client_v_remain}", cVRemain},
{"{tls_client_v_start}", cVStart},
{"{server_port}", "443"},
}
for _, c := range testCases {
if expected, actual := c.expect, repl.Replace(c.template); expected != actual {
t.Errorf("for template '%s', expected '%s', got '%s'", c.template, expected, actual)
}
}
}
func BenchmarkReplace(b *testing.B) {
w := httptest.NewRecorder()
recordRequest := NewResponseRecorder(w)
reader := strings.NewReader(`{"username": "dennis"}`)
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
if err != nil {
b.Fatalf("Failed to make request: %v", err)
}
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
request = request.WithContext(ctx)
request.Header.Set("Custom", "foobarbaz")
request.Header.Set("ShorterVal", "1")
repl := NewReplacer(request, recordRequest, "-")
// add some headers after creating replacer
request.Header.Set("CustomAdd", "caddy")
request.Header.Set("Cookie", "foo=bar; taste=delicious")
// add some response headers
recordRequest.Header().Set("Custom", "CustomResponseHeader")
now = func() time.Time {
// Note that the `-7` is seconds, not hours.
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
repl.Replace("This hostname is {hostname}")
}
}
func BenchmarkReplaceEscaped(b *testing.B) {
w := httptest.NewRecorder()
recordRequest := NewResponseRecorder(w)
reader := strings.NewReader(`{"username": "dennis"}`)
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
if err != nil {
b.Fatalf("Failed to make request: %v", err)
}
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL)
request = request.WithContext(ctx)
request.Header.Set("Custom", "foobarbaz")
request.Header.Set("ShorterVal", "1")
repl := NewReplacer(request, recordRequest, "-")
// add some headers after creating replacer
request.Header.Set("CustomAdd", "caddy")
request.Header.Set("Cookie", "foo=bar; taste=delicious")
// add some response headers
recordRequest.Header().Set("Custom", "CustomResponseHeader")
now = func() time.Time {
// Note that the `-7` is seconds, not hours.
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
repl.Replace("\\{ 'hostname': '{hostname}' \\}")
}
}
func TestResponseRecorderNil(t *testing.T) {
reader := strings.NewReader(`{"username": "dennis"}`)
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
if err != nil {
t.Fatalf("Failed to make request: %v", err)
}
request.Header.Set("Custom", "foobarbaz")
repl := NewReplacer(request, nil, "-")
// add some headers after creating replacer
request.Header.Set("CustomAdd", "caddy")
request.Header.Set("Cookie", "foo=bar; taste=delicious")
old := now
now = func() time.Time {
// Note that the `-7` is seconds, not hours.
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
}
defer func() {
now = old
}()
testCases := []struct {
template string
expect string
}{
{"The Custom response header is {<Custom}.", "The Custom response header is -."},
}
for _, c := range testCases {
if expected, actual := c.expect, repl.Replace(c.template); expected != actual {
t.Errorf("for template '%s', expected '%s', got '%s'", c.template, expected, actual)
}
}
}
func TestSet(t *testing.T) {
w := httptest.NewRecorder()
recordRequest := NewResponseRecorder(w)
@@ -140,6 +418,41 @@ func TestSet(t *testing.T) {
}
}
// Test function to test that various placeholders hold correct values after a rewrite
// has been performed. The NewRequest actually contains the rewritten value.
func TestPathRewrite(t *testing.T) {
w := httptest.NewRecorder()
recordRequest := NewResponseRecorder(w)
reader := strings.NewReader(`{"username": "dennis"}`)
request, err := http.NewRequest("POST", "http://getcaddy.com/index.php?key=value", reader)
if err != nil {
t.Fatalf("Request Formation Failed: %s\n", err.Error())
}
urlCopy := *request.URL
urlCopy.Path = "a/custom/path.php"
ctx := context.WithValue(request.Context(), OriginalURLCtxKey, urlCopy)
request = request.WithContext(ctx)
repl := NewReplacer(request, recordRequest, "")
if got, want := repl.Replace("This path is '{path}'"), "This path is 'a/custom/path.php'"; got != want {
t.Errorf("{path} replacement failed; got '%s', want '%s'", got, want)
}
if got, want := repl.Replace("This path is {rewrite_path}"), "This path is /index.php"; got != want {
t.Errorf("{rewrite_path} replacement failed; got '%s', want '%s'", got, want)
}
if got, want := repl.Replace("This path is '{uri}'"), "This path is 'a/custom/path.php?key=value'"; got != want {
t.Errorf("{uri} replacement failed; got '%s', want '%s'", got, want)
}
if got, want := repl.Replace("This path is {rewrite_uri}"), "This path is /index.php?key=value"; got != want {
t.Errorf("{rewrite_uri} replacement failed; got '%s', want '%s'", got, want)
}
}
func TestRound(t *testing.T) {
var tests = map[time.Duration]time.Duration{
// 599.935µs -> 560µs
@@ -160,7 +473,7 @@ func TestRound(t *testing.T) {
}
}
func TestMillisecondConverstion(t *testing.T) {
func TestMillisecondConversion(t *testing.T) {
var testCases = map[time.Duration]int64{
2 * time.Second: 2000,
9039492 * time.Nanosecond: 9,
@@ -0,0 +1,79 @@
// 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 (
"bufio"
"net"
"net/http"
)
// ResponseWriterWrapper wrappers underlying ResponseWriter
// and inherits its Hijacker/Pusher/CloseNotifier/Flusher as well.
type ResponseWriterWrapper struct {
http.ResponseWriter
}
// Hijack implements http.Hijacker. It simply wraps the underlying
// ResponseWriter's Hijack method if there is one, or returns an error.
func (rww *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := rww.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, NonHijackerError{Underlying: rww.ResponseWriter}
}
// Flush implements http.Flusher. It simply wraps the underlying
// ResponseWriter's Flush method if there is one, or panics.
func (rww *ResponseWriterWrapper) Flush() {
if f, ok := rww.ResponseWriter.(http.Flusher); ok {
f.Flush()
} else {
panic(NonFlusherError{Underlying: rww.ResponseWriter})
}
}
// CloseNotify implements http.CloseNotifier.
// It just inherits the underlying ResponseWriter's CloseNotify method.
// It panics if the underlying ResponseWriter is not a CloseNotifier.
func (rww *ResponseWriterWrapper) CloseNotify() <-chan bool {
if cn, ok := rww.ResponseWriter.(http.CloseNotifier); ok {
return cn.CloseNotify()
}
panic(NonCloseNotifierError{Underlying: rww.ResponseWriter})
}
// Push implements http.Pusher.
// It just inherits the underlying ResponseWriter's Push method.
// It panics if the underlying ResponseWriter is not a Pusher.
func (rww *ResponseWriterWrapper) Push(target string, opts *http.PushOptions) error {
if pusher, hasPusher := rww.ResponseWriter.(http.Pusher); hasPusher {
return pusher.Push(target, opts)
}
return NonPusherError{Underlying: rww.ResponseWriter}
}
// HTTPInterfaces mix all the interfaces that middleware ResponseWriters need to support.
type HTTPInterfaces interface {
http.ResponseWriter
http.Pusher
http.Flusher
http.CloseNotifier
http.Hijacker
}
// Interface guards
var _ HTTPInterfaces = (*ResponseWriterWrapper)(nil)
+121 -42
View File
@@ -1,64 +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 httpserver
import (
"errors"
"io"
"path/filepath"
"strconv"
"github.com/mholt/caddy"
"gopkg.in/natefinch/lumberjack.v2"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
)
// LogRoller implements a type that provides a rolling logger.
type LogRoller struct {
Disabled bool
Filename string
MaxSize int
MaxAge int
MaxBackups int
Compress bool
LocalTime bool
}
// GetLogWriter returns an io.Writer that writes to a rolling logger.
// This should be called only from the main goroutine (like during
// server setup) because this method is not thread-safe; it is careful
// to create only one log writer per log file, even if the log file
// is shared by different sites or middlewares. This ensures that
// rolling is synchronized, since a process (or multiple processes)
// should not create more than one roller on the same file at the
// same time. See issue #1363.
func (l LogRoller) GetLogWriter() io.Writer {
return &lumberjack.Logger{
Filename: l.Filename,
MaxSize: l.MaxSize,
MaxAge: l.MaxAge,
MaxBackups: l.MaxBackups,
LocalTime: l.LocalTime,
absPath, err := filepath.Abs(l.Filename)
if err != nil {
absPath = l.Filename // oh well, hopefully they're consistent in how they specify the filename
}
lj, has := lumberjacks[absPath]
if !has {
lj = &lumberjack.Logger{
Filename: l.Filename,
MaxSize: l.MaxSize,
MaxAge: l.MaxAge,
MaxBackups: l.MaxBackups,
Compress: l.Compress,
LocalTime: l.LocalTime,
}
lumberjacks[absPath] = lj
}
return lj
}
// IsLogRollerSubdirective is true if the subdirective is for the log roller.
func IsLogRollerSubdirective(subdir string) bool {
return subdir == directiveRotateSize ||
subdir == directiveRotateAge ||
subdir == directiveRotateKeep ||
subdir == directiveRotateCompress ||
subdir == directiveRotateDisable
}
var errInvalidRollParameter = errors.New("invalid roller parameter")
// ParseRoller parses roller contents out of c.
func ParseRoller(l *LogRoller, what string, where ...string) error {
if l == nil {
l = DefaultLogRoller()
}
// rotate_compress doesn't accept any parameters.
// others only accept one parameter
if ((what == directiveRotateCompress || what == directiveRotateDisable) && len(where) != 0) ||
((what != directiveRotateCompress && what != directiveRotateDisable) && len(where) != 1) {
return errInvalidRollParameter
}
var (
value int
err error
)
if what != directiveRotateCompress && what != directiveRotateDisable {
value, err = strconv.Atoi(where[0])
if err != nil {
return err
}
}
switch what {
case directiveRotateDisable:
l.Disabled = true
case directiveRotateSize:
l.MaxSize = value
case directiveRotateAge:
l.MaxAge = value
case directiveRotateKeep:
l.MaxBackups = value
case directiveRotateCompress:
l.Compress = true
}
return nil
}
// DefaultLogRoller will roll logs by default.
func DefaultLogRoller() *LogRoller {
return &LogRoller{
MaxSize: defaultRotateSize,
MaxAge: defaultRotateAge,
MaxBackups: defaultRotateKeep,
Compress: false,
LocalTime: true,
}
}
// ParseRoller parses roller contents out of c.
func ParseRoller(c *caddy.Controller) (*LogRoller, error) {
var size, age, keep int
// This is kind of a hack to support nested blocks:
// As we are already in a block: either log or errors,
// c.nesting > 0 but, as soon as c meets a }, it thinks
// the block is over and return false for c.NextBlock.
for c.NextBlock() {
what := c.Val()
if !c.NextArg() {
return nil, c.ArgErr()
}
value := c.Val()
var err error
switch what {
case "size":
size, err = strconv.Atoi(value)
case "age":
age, err = strconv.Atoi(value)
case "keep":
keep, err = strconv.Atoi(value)
}
if err != nil {
return nil, err
}
}
return &LogRoller{
MaxSize: size,
MaxAge: age,
MaxBackups: keep,
LocalTime: true,
}, nil
}
const (
// defaultRotateSize is 100 MB.
defaultRotateSize = 100
// defaultRotateAge is 14 days.
defaultRotateAge = 14
// defaultRotateKeep is 10 files.
defaultRotateKeep = 10
directiveRotateDisable = "rotate_disable"
directiveRotateSize = "rotate_size"
directiveRotateAge = "rotate_age"
directiveRotateKeep = "rotate_keep"
directiveRotateCompress = "rotate_compress"
)
// lumberjacks maps log filenames to the logger
// that is being used to keep them rolled/maintained.
var lumberjacks = make(map[string]io.Writer)
File diff suppressed because it is too large Load Diff
+164 -9
View File
@@ -1,7 +1,22 @@
// 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/url"
"testing"
"time"
)
@@ -15,7 +30,7 @@ func TestAddress(t *testing.T) {
}
}
func TestMakeHTTPServer(t *testing.T) {
func TestMakeHTTPServerWithTimeouts(t *testing.T) {
for i, tc := range []struct {
group []*SiteConfig
expected Timeouts
@@ -92,7 +107,7 @@ func TestMakeHTTPServer(t *testing.T) {
},
},
} {
actual := makeHTTPServer("127.0.0.1:9005", tc.group)
actual := makeHTTPServerWithTimeouts("127.0.0.1:9005", tc.group)
if got, want := actual.Addr, "127.0.0.1:9005"; got != want {
t.Errorf("Test %d: Expected Addr=%s, but was %s", i, want, got)
@@ -100,15 +115,155 @@ func TestMakeHTTPServer(t *testing.T) {
if got, want := actual.ReadTimeout, tc.expected.ReadTimeout; got != want {
t.Errorf("Test %d: Expected ReadTimeout=%v, but was %v", i, want, got)
}
// TODO: ReadHeaderTimeout and IdleTimeout require Go 1.8
// if got, want := actual.ReadHeaderTimeout, tc.expected.ReadHeaderTimeout; got != want {
// t.Errorf("Test %d: Expected ReadHeaderTimeout=%v, but was %v", i, want, got)
// }
if got, want := actual.ReadHeaderTimeout, tc.expected.ReadHeaderTimeout; got != want {
t.Errorf("Test %d: Expected ReadHeaderTimeout=%v, but was %v", i, want, got)
}
if got, want := actual.WriteTimeout, tc.expected.WriteTimeout; got != want {
t.Errorf("Test %d: Expected WriteTimeout=%v, but was %v", i, want, got)
}
// if got, want := actual.IdleTimeout, tc.expected.IdleTimeout; got != want {
// t.Errorf("Test %d: Expected IdleTimeout=%v, but was %v", i, want, got)
// }
if got, want := actual.IdleTimeout, tc.expected.IdleTimeout; got != want {
t.Errorf("Test %d: Expected IdleTimeout=%v, but was %v", i, want, got)
}
}
}
func TestTrimPathPrefix(t *testing.T) {
for i, pt := range []struct {
url string
prefix string
expected string
shouldFail bool
}{
{
url: "/my/path",
prefix: "/my",
expected: "/path",
shouldFail: false,
},
{
url: "/my/%2f/path",
prefix: "/my",
expected: "/%2f/path",
shouldFail: false,
},
{
url: "/my/path",
prefix: "/my/",
expected: "/path",
shouldFail: false,
},
{
url: "/my///path",
prefix: "/my",
expected: "/path",
shouldFail: true,
},
{
url: "/my///path",
prefix: "/my",
expected: "///path",
shouldFail: false,
},
{
url: "/my/path///slash",
prefix: "/my",
expected: "/path///slash",
shouldFail: false,
},
{
url: "/my/%2f/path/%2f",
prefix: "/my",
expected: "/%2f/path/%2f",
shouldFail: false,
}, {
url: "/my/%20/path",
prefix: "/my",
expected: "/%20/path",
shouldFail: false,
}, {
url: "/path",
prefix: "",
expected: "/path",
shouldFail: false,
}, {
url: "/path/my/",
prefix: "/my",
expected: "/path/my/",
shouldFail: false,
}, {
url: "",
prefix: "/my",
expected: "/",
shouldFail: false,
}, {
url: "/apath",
prefix: "",
expected: "/apath",
shouldFail: false,
}, {
url: "/my/path/page.php?akey=value",
prefix: "/my",
expected: "/path/page.php?akey=value",
shouldFail: false,
}, {
url: "/my/path/page?key=value#fragment",
prefix: "/my",
expected: "/path/page?key=value#fragment",
shouldFail: false,
}, {
url: "/my/path/page#fragment",
prefix: "/my",
expected: "/path/page#fragment",
shouldFail: false,
}, {
url: "/my/apath?",
prefix: "/my",
expected: "/apath?",
shouldFail: false,
},
} {
u, _ := url.Parse(pt.url)
if got, want := trimPathPrefix(u, pt.prefix), pt.expected; got.String() != want {
if !pt.shouldFail {
t.Errorf("Test %d: Expected='%s', but was '%s' ", i, want, got.String())
}
} else if pt.shouldFail {
t.Errorf("SHOULDFAIL Test %d: Expected='%s', and was '%s' but should fail", i, want, got.String())
}
}
}
func TestMakeHTTPServerWithHeaderLimit(t *testing.T) {
for name, c := range map[string]struct {
group []*SiteConfig
expect int
}{
"disable": {
group: []*SiteConfig{{}},
expect: 0,
},
"oneSite": {
group: []*SiteConfig{{Limits: Limits{
MaxRequestHeaderSize: 100,
}}},
expect: 100,
},
"multiSites": {
group: []*SiteConfig{
{Limits: Limits{MaxRequestHeaderSize: 100}},
{Limits: Limits{MaxRequestHeaderSize: 50}},
},
expect: 50,
},
} {
c := c
t.Run(name, func(t *testing.T) {
actual := makeHTTPServerWithHeaderLimit(&http.Server{}, c.group)
if got := actual.MaxHeaderBytes; got != c.expect {
t.Errorf("Expect %d, but got %d", c.expect, got)
}
})
}
}
+44 -4
View File
@@ -1,9 +1,23 @@
// 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 (
"time"
"github.com/mholt/caddy/caddytls"
"github.com/caddyserver/caddy/caddytls"
)
// SiteConfig contains information about a site
@@ -12,6 +26,9 @@ type SiteConfig struct {
// The address of the site
Addr Address
// The list of viable index page names of the site
IndexPages []string
// The hostname to bind listener to;
// defaults to Addr.Host
ListenHost string
@@ -25,6 +42,9 @@ type SiteConfig struct {
// Compiled middleware stack
middlewareChain Handler
// listener middleware stack
listenerMiddleware []ListenerMiddleware
// Directory from which to serve files
Root string
@@ -35,8 +55,8 @@ type SiteConfig struct {
// for a request.
HiddenFiles []string
// Max amount of bytes a request can send on a given path
MaxRequestBodySizes []PathLimit
// Max request's header/body size
Limits Limits
// The path to the Caddyfile used to generate this site config
originCaddyfile string
@@ -49,10 +69,14 @@ type SiteConfig struct {
// preserving functionality needed for proxying,
// websockets, etc.
Timeouts Timeouts
// If true, any requests not matching other site definitions
// may be served by this site.
FallbackSite bool
}
// Timeouts specify various timeouts for a server to use.
// If the assocated bool field is true, then the duration
// If the associated bool field is true, then the duration
// value should be treated literally (i.e. a zero-value
// duration would mean "no timeout"). If false, the duration
// was left unset, so a zero-value duration would mean to
@@ -68,6 +92,12 @@ type Timeouts struct {
IdleTimeoutSet bool
}
// Limits specify size limit of request's header and body.
type Limits struct {
MaxRequestHeaderSize int64
MaxRequestBodySizes []PathLimit
}
// PathLimit is a mapping from a site's path to its corresponding
// maximum request body size (in bytes)
type PathLimit struct {
@@ -80,6 +110,11 @@ func (s *SiteConfig) AddMiddleware(m Middleware) {
s.middleware = append(s.middleware, m)
}
// AddListenerMiddleware adds a listener middleware to a site's listenerMiddleware stack.
func (s *SiteConfig) AddListenerMiddleware(l ListenerMiddleware) {
s.listenerMiddleware = append(s.listenerMiddleware, l)
}
// TLSConfig returns s.TLS.
func (s SiteConfig) TLSConfig() *caddytls.Config {
return s.TLS
@@ -99,3 +134,8 @@ func (s SiteConfig) Port() string {
func (s SiteConfig) Middleware() []Middleware {
return s.middleware
}
// ListenerMiddleware returns s.listenerMiddleware
func (s SiteConfig) ListenerMiddleware() []ListenerMiddleware {
return s.listenerMiddleware
}
@@ -1,19 +1,40 @@
// 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/rand"
"fmt"
"io/ioutil"
"log"
mathrand "math/rand"
"net"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"sync"
"text/template"
"time"
"os"
"github.com/caddyserver/caddy/caddytls"
"github.com/mholt/certmagic"
"github.com/russross/blackfriday"
)
@@ -26,6 +47,20 @@ type Context struct {
Req *http.Request
URL *url.URL
Args []interface{} // defined by arguments to .Include
// just used for adding preload links for server push
responseHeader http.Header
}
// NewContextWithHeader creates a context with given response header.
//
// To plugin developer:
// The returned context's exported fileds remain empty,
// you should then initialize them if you want.
func NewContextWithHeader(rh http.Header) Context {
return Context{
responseHeader: rh,
}
}
// Include returns the contents of filename relative to the site root.
@@ -61,6 +96,18 @@ func (c Context) Header(name string) string {
return c.Req.Header.Get(name)
}
// Hostname gets the (remote) hostname of the client making the request.
func (c Context) Hostname() string {
ip := c.IP()
hostnameList, err := net.LookupAddr(ip)
if err != nil || len(hostnameList) == 0 {
return c.Req.RemoteAddr
}
return hostnameList[0]
}
// Env gets a map of the environment variables.
func (c Context) Env() map[string]string {
osEnv := os.Environ()
@@ -83,6 +130,29 @@ func (c Context) IP() string {
return ip
}
// To mock the net.InterfaceAddrs from the test.
var networkInterfacesFn = net.InterfaceAddrs
// ServerIP gets the (local) IP address of the server.
// TODO: The bind directive should be honored in this method (see PR #1474).
func (c Context) ServerIP() string {
addrs, err := networkInterfacesFn()
if err != nil {
return ""
}
for _, address := range addrs {
// Validate the address and check if it's not a loopback
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil || ipnet.IP.To16() != nil {
return ipnet.IP.String()
}
}
}
return ""
}
// URI returns the raw, unprocessed request URI (including query
// string and hash) obtained directly from the Request-Line of
// the HTTP request.
@@ -110,7 +180,7 @@ func (c Context) Port() (string, error) {
if err != nil {
if !strings.Contains(c.Req.Host, ":") {
// common with sites served on the default port 80
return "80", nil
return strconv.Itoa(certmagic.HTTPPort), nil
}
return "", err
}
@@ -236,13 +306,15 @@ func ContextInclude(filename string, ctx interface{}, fs http.FileSystem) (strin
return "", err
}
tpl, err := template.New(filename).Parse(string(body))
tpl, err := template.New(filename).Funcs(TemplateFuncs).Parse(string(body))
if err != nil {
return "", err
}
var buf bytes.Buffer
err = tpl.Execute(&buf, ctx)
buf := includeBufs.Get().(*bytes.Buffer)
buf.Reset()
defer includeBufs.Put(buf)
err = tpl.Execute(buf, ctx)
if err != nil {
return "", err
}
@@ -321,3 +393,83 @@ func (c Context) Files(name string) ([]string, error) {
return names, nil
}
// IsMITM returns true if it seems likely that the TLS connection
// is being intercepted.
func (c Context) IsMITM() bool {
if val, ok := c.Req.Context().Value(MitmCtxKey).(bool); ok {
return val
}
return false
}
// RandomString generates a random string of random length given
// length bounds. Thanks to http://stackoverflow.com/a/35615565/1048862
// for the clever technique that is fairly fast, secure, and maintains
// proper distributions over the dictionary.
func (c Context) RandomString(minLen, maxLen int) string {
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
letterIdxBits = 6 // 6 bits to represent 64 possibilities (indexes)
letterIdxMask = 1<<letterIdxBits - 1 // all 1-bits, as many as letterIdxBits
)
if minLen < 0 || maxLen < 0 || maxLen < minLen {
return ""
}
n := mathrand.Intn(maxLen-minLen+1) + minLen // choose actual length
// secureRandomBytes returns a number of bytes using crypto/rand.
secureRandomBytes := func(numBytes int) []byte {
randomBytes := make([]byte, numBytes)
if _, err := rand.Read(randomBytes); err != nil {
log.Println("[ERROR] failed to read bytes: ", err)
}
return randomBytes
}
result := make([]byte, n)
bufferSize := int(float64(n) * 1.3)
for i, j, randomBytes := 0, 0, []byte{}; i < n; j++ {
if j%bufferSize == 0 {
randomBytes = secureRandomBytes(bufferSize)
}
if idx := int(randomBytes[j%n] & letterIdxMask); idx < len(letterBytes) {
result[i] = letterBytes[idx]
i++
}
}
return string(result)
}
// AddLink adds a link header in response
// see https://www.w3.org/wiki/LinkHeader
func (c Context) AddLink(link string) string {
if c.responseHeader == nil {
return ""
}
c.responseHeader.Add("Link", link)
return ""
}
// Returns either TLS protocol version if TLS used or empty string otherwise
func (c Context) TLSVersion() (ret string) {
if c.Req.TLS != nil {
// Safe to ignore an error
ret, _ = caddytls.GetSupportedProtocolName(c.Req.TLS.Version)
}
return
}
// buffer pool for .Include context actions
var includeBufs = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// TemplateFuncs contains user-defined functions
// for execution in templates.
var TemplateFuncs = template.FuncMap{}
@@ -1,10 +1,27 @@
// 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/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
@@ -71,12 +88,20 @@ func TestInclude(t *testing.T) {
shouldErr: true,
expectedErrorContent: `type httpserver.Context`,
},
// Test 4 - all good, with custom function
{
fileContent: `hello {{ caddy }}`,
expectedContent: "hello caddy",
shouldErr: false,
expectedErrorContent: "",
},
}
TemplateFuncs["caddy"] = func() string { return "caddy" }
for i, test := range tests {
testPrefix := getTestPrefix(i)
// WriteFile truncates the contentt
// WriteFile truncates the content
err := ioutil.WriteFile(absInFilePath, []byte(test.fileContent), os.ModePerm)
if err != nil {
t.Fatal(testPrefix+"Failed to create test file. Error was: %v", err)
@@ -137,7 +162,7 @@ func TestMarkdown(t *testing.T) {
for i, test := range tests {
testPrefix := getTestPrefix(i)
// WriteFile truncates the contentt
// WriteFile truncates the content
err := ioutil.WriteFile(absInFilePath, []byte(test.fileContent), os.ModePerm)
if err != nil {
t.Fatal(testPrefix+"Failed to create test file. Error was: %v", err)
@@ -235,6 +260,39 @@ func TestHeader(t *testing.T) {
}
}
func TestHostname(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
inputRemoteAddr string
expectedHostname string
}{
// TODO(mholt): Fix these tests, they're not portable. i.e. my resolver
// returns "fwdr-8.fwdr-8.fwdr-8.fwdr-8." instead of these google ones.
// Test 0 - ipv4 with port
// {"8.8.8.8:1111", "google-public-dns-a.google.com."},
// // Test 1 - ipv4 without port
// {"8.8.8.8", "google-public-dns-a.google.com."},
// // Test 2 - ipv6 with port
// {"[2001:4860:4860::8888]:11", "google-public-dns-a.google.com."},
// // Test 3 - ipv6 without port and brackets
// {"2001:4860:4860::8888", "google-public-dns-a.google.com."},
// Test 4 - no hostname available
{"0.0.0.0", "0.0.0.0"},
}
for i, test := range tests {
testPrefix := getTestPrefix(i)
context.Req.RemoteAddr = test.inputRemoteAddr
actualHostname := context.Hostname()
if actualHostname != test.expectedHostname {
t.Errorf(testPrefix+"Expected hostname %s, found %s", test.expectedHostname, actualHostname)
}
}
}
func TestEnv(t *testing.T) {
context := getContextOrFail(t)
@@ -302,6 +360,44 @@ func TestIP(t *testing.T) {
}
}
type myIP string
func (ip myIP) mockInterfaces() ([]net.Addr, error) {
a := net.ParseIP(string(ip))
return []net.Addr{
&net.IPNet{IP: a, Mask: nil},
}, nil
}
func TestServerIP(t *testing.T) {
context := getContextOrFail(t)
tests := []string{
// Test 0 - ipv4
"1.1.1.1",
// Test 1 - ipv6
"2001:db8:a0b:12f0::1",
}
for i, expectedIP := range tests {
testPrefix := getTestPrefix(i)
// Mock the network interface
ip := myIP(expectedIP)
networkInterfacesFn = ip.mockInterfaces
defer func() {
networkInterfacesFn = net.InterfaceAddrs
}()
actualIP := context.ServerIP()
if actualIP != expectedIP {
t.Errorf("%sExpected IP \"%s\", found \"%s\".", testPrefix, expectedIP, actualIP)
}
}
}
func TestURL(t *testing.T) {
context := getContextOrFail(t)
@@ -416,7 +512,7 @@ func TestMethod(t *testing.T) {
}
func TestPathMatches(t *testing.T) {
func TestContextPathMatches(t *testing.T) {
context := getContextOrFail(t)
tests := []struct {
@@ -651,8 +747,9 @@ func initTestContext() (Context, error) {
if err != nil {
return Context{}, err
}
res := httptest.NewRecorder()
return Context{Root: http.Dir(os.TempDir()), Req: request}, nil
return Context{Root: http.Dir(os.TempDir()), responseHeader: res.Header(), Req: request}, nil
}
func getContextOrFail(t *testing.T) Context {
@@ -794,3 +891,72 @@ func TestFiles(t *testing.T) {
}
}
}
func TestAddLink(t *testing.T) {
for name, c := range map[string]struct {
input string
expectLinks []string
}{
"oneLink": {
input: `{{.AddLink "</test.css>; rel=preload"}}`,
expectLinks: []string{"</test.css>; rel=preload"},
},
"multipleLinks": {
input: `{{.AddLink "</test1.css>; rel=preload"}} {{.AddLink "</test2.css>; rel=meta"}}`,
expectLinks: []string{"</test1.css>; rel=preload", "</test2.css>; rel=meta"},
},
} {
c := c
t.Run(name, func(t *testing.T) {
ctx := getContextOrFail(t)
tmpl, err := template.New("").Parse(c.input)
if err != nil {
t.Fatal(err)
}
err = tmpl.Execute(ioutil.Discard, ctx)
if err != nil {
t.Fatal(err)
}
if got := ctx.responseHeader["Link"]; !reflect.DeepEqual(got, c.expectLinks) {
t.Errorf("Result not match: expect %v, but got %v", c.expectLinks, got)
}
})
}
}
func TestTlsVersion(t *testing.T) {
for _, test := range []struct {
tlsState *tls.ConnectionState
expectedResult string
}{
{
&tls.ConnectionState{Version: tls.VersionTLS10},
"tls1.0",
},
{
&tls.ConnectionState{Version: tls.VersionTLS11},
"tls1.1",
},
{
&tls.ConnectionState{Version: tls.VersionTLS12},
"tls1.2",
},
// TLS not used
{
nil,
"",
},
// Unsupported version
{
&tls.ConnectionState{Version: 0x0399},
"",
},
} {
context := getContextOrFail(t)
context.Req.TLS = test.tlsState
result := context.TLSVersion()
if result != test.expectedResult {
t.Errorf("Expected %s got %s", test.expectedResult, result)
}
}
}

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