mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 319bbba402 |
-16
@@ -1,16 +0,0 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
_gitignore/
|
||||
Vagrantfile
|
||||
.vagrant/
|
||||
|
||||
dist/builds/
|
||||
dist/release/
|
||||
|
||||
error.log
|
||||
access.log
|
||||
|
||||
/*.conf
|
||||
Caddyfile
|
||||
|
||||
og_static/
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.4.2
|
||||
- 1.5.1
|
||||
- tip
|
||||
|
||||
install:
|
||||
- go get -d ./...
|
||||
- go get golang.org/x/tools/cmd/vet
|
||||
|
||||
script:
|
||||
- go vet ./...
|
||||
- go test ./...
|
||||
@@ -1,32 +0,0 @@
|
||||
## Contributing to Caddy
|
||||
|
||||
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup), then join the #caddy channel.)
|
||||
|
||||
This project gladly accepts contributions and we encourage interested users to get involved!
|
||||
|
||||
|
||||
#### For small tweaks, bug fixes, and tests
|
||||
|
||||
Submit [pull requests](https://github.com/mholt/caddy/pulls) at any time. Thank you for helping out in simple ways! Bug fixes should be under test to assert correct behavior.
|
||||
|
||||
|
||||
#### Ideas, questions, bug reports
|
||||
|
||||
You should totally [open an issue](https://github.com/mholt/caddy/issues) with your ideas, questions, and bug reports, if one does not already exist for it. Bug reports should state expected behavior and contain clear instructions for reproducing the problem.
|
||||
|
||||
|
||||
#### New features
|
||||
|
||||
Before submitting a pull request, please open an issue first to discuss it and claim it. This prevents overlapping efforts and keeps the project in-line with its goals. If you prefer to discuss the feature privately, you can reach other developers on Slack or you may email me directly. (My email address is below.)
|
||||
|
||||
And don't forget to write tests for new features!
|
||||
|
||||
|
||||
#### Vulnerabilities
|
||||
|
||||
If you've found a vulnerability that is serious, please email me: Matthew dot Holt at Gmail. If it's not a big deal, a pull request will probably be faster.
|
||||
|
||||
|
||||
## Thank you
|
||||
|
||||
Thanks for your help! Caddy would not be what it is today without your contributions.
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
@@ -1,132 +0,0 @@
|
||||
[](https://caddyserver.com)
|
||||
|
||||
[](https://godoc.org/github.com/mholt/caddy) [](https://travis-ci.org/mholt/caddy) [](https://ci.appveyor.com/project/mholt/caddy)
|
||||
|
||||
Caddy is a lightweight, general-purpose web server for Windows, Mac, Linux, BSD, and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android). It is a capable alternative to other popular and easy to use web servers.
|
||||
|
||||
The most notable features are HTTP/2, [Let's Encrypt](https://letsencrypt.org) support, Virtual Hosts, TLS + SNI, and easy configuration with a [Caddyfile](https://caddyserver.com/docs/caddyfile). In development, you usually put one Caddyfile with each site. In production, Caddy serves HTTPS by default and manages all cryptographic assets for you.
|
||||
|
||||
[Download](https://github.com/mholt/caddy/releases) · [User Guide](https://caddyserver.com/docs)
|
||||
|
||||
|
||||
|
||||
### Menu
|
||||
|
||||
- [Getting Caddy](#getting-caddy)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Running from Source](#running-from-source)
|
||||
- [Contributing](#contributing)
|
||||
- [About the Project](#about-the-project)
|
||||
|
||||
|
||||
|
||||
|
||||
## Getting Caddy
|
||||
|
||||
Caddy binaries have no dependencies and are available for nearly every platform.
|
||||
|
||||
[Latest release](https://github.com/mholt/caddy/releases/latest)
|
||||
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
The website has [full documentation](https://caddyserver.com/docs) but this will get you started in about 30 seconds:
|
||||
|
||||
Place a file named "Caddyfile" with your site. Paste this into it and save:
|
||||
|
||||
```
|
||||
localhost
|
||||
|
||||
gzip
|
||||
browse
|
||||
ext .html
|
||||
websocket /echo cat
|
||||
log ../access.log
|
||||
header /api Access-Control-Allow-Origin *
|
||||
```
|
||||
|
||||
Run `caddy` from that directory, and it will automatically use that Caddyfile to configure itself.
|
||||
|
||||
That simple file enables compression, allows directory browsing (for folders without an index file), serves clean URLs, hosts a WebSocket echo server at /echo, logs requests to access.log, and adds the coveted `Access-Control-Allow-Origin: *` header for all responses from some API.
|
||||
|
||||
Wow! Caddy can do a lot with just a few lines.
|
||||
|
||||
|
||||
#### Defining multiple sites
|
||||
|
||||
You can run multiple sites from the same Caddyfile, too:
|
||||
|
||||
```
|
||||
site1.com {
|
||||
# ...
|
||||
}
|
||||
|
||||
site2.com, sub.site2.com {
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
Note that all these sites will automatically be served over HTTPS using Let's Encrypt as the CA. Caddy will manage the certificates (including renewals) for you. You don't even have to think about it.
|
||||
|
||||
For more documentation, please view [the website](https://caddyserver.com/docs). You may also be interested in the [developer guide](https://github.com/mholt/caddy/wiki) on this project's GitHub wiki.
|
||||
|
||||
|
||||
|
||||
|
||||
## Running from Source
|
||||
|
||||
Note: You will need **[Go 1.4](https://golang.org/dl)** or newer
|
||||
|
||||
1. `$ go get github.com/mholt/caddy`
|
||||
2. `cd` into your website's directory
|
||||
3. Run `caddy` (assumes `$GOPATH/bin` is in your `$PATH`)
|
||||
|
||||
If you're tinkering, you can also use `go run main.go`.
|
||||
|
||||
By default, Caddy serves the current directory at [localhost:2015](http://localhost:2015). You can place a Caddyfile to configure Caddy for serving your site.
|
||||
|
||||
Caddy accepts some flags from the command line. Run `caddy -h` to view the help for flags. You can also pipe a Caddyfile into the caddy command.
|
||||
|
||||
**Running as root:** We advise against this; use setcap instead, like so: `setcap cap_net_bind_service=+ep ./caddy` This will allow you to listen on ports < 1024 like 80 and 443.
|
||||
|
||||
|
||||
|
||||
#### Docker Container
|
||||
|
||||
Caddy is available as a Docker container from any of these sources:
|
||||
|
||||
- [abiosoft/caddy](https://registry.hub.docker.com/u/abiosoft/caddy/)
|
||||
- [darron/caddy](https://registry.hub.docker.com/u/darron/caddy/)
|
||||
- [joshix/caddy](https://registry.hub.docker.com/u/joshix/caddy/)
|
||||
- [jumanjiman/caddy](https://registry.hub.docker.com/u/jumanjiman/caddy/)
|
||||
- [zenithar/nano-caddy](https://registry.hub.docker.com/u/zenithar/nano-caddy/)
|
||||
|
||||
|
||||
|
||||
#### 3rd-party dependencies
|
||||
|
||||
Although Caddy's binaries are completely static, Caddy relies on some excellent libraries. [Godoc.org](https://godoc.org/github.com/mholt/caddy) shows the packages that each Caddy package imports.
|
||||
|
||||
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
**[Join us on Slack](https://gophers.slack.com/messages/caddy/)** to chat with other Caddy developers! ([Request an invite](http://bit.ly/go-slack-signup), then join the #caddy channel.)
|
||||
|
||||
This project would not be what it is without your help. Please see the [contributing guidelines](https://github.com/mholt/caddy/blob/master/CONTRIBUTING.md) if you haven't already.
|
||||
|
||||
Thanks for making Caddy -- and the Web -- better!
|
||||
|
||||
Special thanks to [](https://www.digitalocean.com) for hosting the Caddy project.
|
||||
|
||||
|
||||
|
||||
|
||||
## About the project
|
||||
|
||||
Caddy was born out of the need for a "batteries-included" web server that runs anywhere and doesn't have to take its configuration with it. Caddy took inspiration from [spark](https://github.com/rif/spark), nginx, lighttpd, Websocketd, and Vagrant, and provides a pleasant mixture of features from each of them.
|
||||
|
||||
|
||||
*Twitter: [@mholt6](https://twitter.com/mholt6)*
|
||||
@@ -1,19 +0,0 @@
|
||||
version: "{build}"
|
||||
|
||||
os: Windows Server 2012 R2
|
||||
|
||||
clone_folder: c:\gopath\src\github.com\mholt\caddy
|
||||
|
||||
environment:
|
||||
GOPATH: c:\gopath
|
||||
|
||||
install:
|
||||
- go get golang.org/x/tools/cmd/vet
|
||||
- echo %GOPATH%
|
||||
- go version
|
||||
- go env
|
||||
- go get -d ./...
|
||||
|
||||
build_script:
|
||||
- go vet ./...
|
||||
- go test ./...
|
||||
@@ -1,29 +0,0 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Path returns the path to the folder
|
||||
// where the application may store data. This
|
||||
// currently resolves to ~/.caddy
|
||||
func Path() string {
|
||||
return filepath.Join(userHomeDir(), ".caddy")
|
||||
}
|
||||
|
||||
// userHomeDir returns the user's home directory according to
|
||||
// environment variables.
|
||||
//
|
||||
// Credit: http://stackoverflow.com/a/7922977/1048862
|
||||
func userHomeDir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
||||
if home == "" {
|
||||
home = os.Getenv("USERPROFILE")
|
||||
}
|
||||
return home
|
||||
}
|
||||
return os.Getenv("HOME")
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPath(t *testing.T) {
|
||||
if actual := Path(); !strings.HasSuffix(actual, ".caddy") {
|
||||
t.Errorf("Expected path to be a .caddy folder, got: %v", actual)
|
||||
}
|
||||
}
|
||||
-358
@@ -1,358 +0,0 @@
|
||||
// Package caddy implements the Caddy web server as a service.
|
||||
//
|
||||
// To use this package, follow a few simple steps:
|
||||
//
|
||||
// 1. Set the AppName and AppVersion variables.
|
||||
// 2. Call LoadCaddyfile() to get the Caddyfile (it
|
||||
// might have been piped in as part of a restart).
|
||||
// You should pass in your own Caddyfile loader.
|
||||
// 3. Call caddy.Start() to start Caddy, caddy.Stop()
|
||||
// to stop it, or caddy.Restart() to restart it.
|
||||
//
|
||||
// You should use caddy.Wait() to wait for all Caddy servers
|
||||
// to quit before your process exits.
|
||||
//
|
||||
// Importing this package has the side-effect of trapping
|
||||
// SIGINT on all platforms and SIGUSR1 on not-Windows systems.
|
||||
// It has to do this in order to perform shutdowns or reloads.
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/caddy/letsencrypt"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
var (
|
||||
// The name and version of the application.
|
||||
AppName, AppVersion string
|
||||
|
||||
// If true, initialization will not show any informative output.
|
||||
Quiet bool
|
||||
|
||||
// HTTP2 indicates whether HTTP2 is enabled or not
|
||||
HTTP2 bool // TODO: temporary flag until http2 is standard
|
||||
)
|
||||
|
||||
var (
|
||||
// caddyfile is the input configuration text used for this process
|
||||
caddyfile Input
|
||||
|
||||
// caddyfileMu protects caddyfile during changes
|
||||
caddyfileMu sync.Mutex
|
||||
|
||||
// incompleteRestartErr occurs if this process is a fork
|
||||
// of the parent but no Caddyfile was piped in
|
||||
incompleteRestartErr = errors.New("cannot finish restart successfully")
|
||||
|
||||
// servers is a list of all the currently-listening servers
|
||||
servers []*server.Server
|
||||
|
||||
// serversMu protects the servers slice during changes
|
||||
serversMu sync.Mutex
|
||||
|
||||
// wg is used to wait for all servers to shut down
|
||||
wg sync.WaitGroup
|
||||
|
||||
// loadedGob is used if this is a child process as part of
|
||||
// a graceful restart; it is used to map listeners to their
|
||||
// index in the list of inherited file descriptors. This
|
||||
// variable is not safe for concurrent access.
|
||||
loadedGob caddyfileGob
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultHost = "0.0.0.0"
|
||||
DefaultPort = "2015"
|
||||
DefaultRoot = "."
|
||||
)
|
||||
|
||||
// Start starts Caddy with the given Caddyfile. If cdyfile
|
||||
// is nil or the process is forked from a parent as part of
|
||||
// a graceful restart, Caddy will check to see if Caddyfile
|
||||
// was piped from stdin and use that. It blocks until all the
|
||||
// servers are listening.
|
||||
//
|
||||
// If this process is a fork and no Caddyfile was piped in,
|
||||
// an error will be returned (the Restart() function does this
|
||||
// for you automatically). If this process is NOT a fork and
|
||||
// cdyfile is nil, a default configuration will be assumed.
|
||||
// In any case, an error is returned if Caddy could not be
|
||||
// started.
|
||||
func Start(cdyfile Input) error {
|
||||
// TODO: What if already started -- is that an error?
|
||||
|
||||
var err error
|
||||
|
||||
// Input must never be nil; try to load something
|
||||
if cdyfile == nil {
|
||||
cdyfile, err = LoadCaddyfile(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
caddyfileMu.Lock()
|
||||
caddyfile = cdyfile
|
||||
caddyfileMu.Unlock()
|
||||
|
||||
// load the server configs (activates Let's Encrypt)
|
||||
configs, err := loadConfigs(path.Base(cdyfile.Path()), bytes.NewReader(cdyfile.Body()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// group virtualhosts by address
|
||||
groupings, err := arrangeBindings(configs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start each server with its one or more configurations
|
||||
err = startServers(groupings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Close remaining file descriptors we may have inherited that we don't need
|
||||
if IsRestart() {
|
||||
for _, fdIndex := range loadedGob.ListenerFds {
|
||||
file := os.NewFile(fdIndex, "")
|
||||
fln, err := net.FileListener(file)
|
||||
if err == nil {
|
||||
fln.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show initialization output
|
||||
if !Quiet && !IsRestart() {
|
||||
var checkedFdLimit bool
|
||||
for _, group := range groupings {
|
||||
for _, conf := range group.Configs {
|
||||
// Print address of site
|
||||
fmt.Println(conf.Address())
|
||||
|
||||
// Note if non-localhost site resolves to loopback interface
|
||||
if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
|
||||
fmt.Printf("Notice: %s is only accessible on this machine (%s)\n",
|
||||
conf.Host, group.BindAddr.IP.String())
|
||||
}
|
||||
if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) {
|
||||
checkFdlimit()
|
||||
checkedFdLimit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tell parent process that we got this
|
||||
if IsRestart() {
|
||||
ppipe := os.NewFile(3, "") // parent is listening on pipe at index 3
|
||||
ppipe.Write([]byte("success"))
|
||||
ppipe.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startServers starts all the servers in groupings,
|
||||
// taking into account whether or not this process is
|
||||
// a child from a graceful restart or not. It blocks
|
||||
// until the servers are listening.
|
||||
func startServers(groupings Group) error {
|
||||
var startupWg sync.WaitGroup
|
||||
errChan := make(chan error)
|
||||
|
||||
for _, group := range groupings {
|
||||
s, err := server.New(group.BindAddr.String(), group.Configs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s.HTTP2 = HTTP2 // TODO: This setting is temporary
|
||||
|
||||
var ln server.ListenerFile
|
||||
if IsRestart() {
|
||||
// Look up this server's listener in the map of inherited file descriptors;
|
||||
// if we don't have one, we must make a new one.
|
||||
if fdIndex, ok := loadedGob.ListenerFds[s.Addr]; ok {
|
||||
file := os.NewFile(fdIndex, "")
|
||||
|
||||
fln, err := net.FileListener(file)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ln, ok = fln.(server.ListenerFile)
|
||||
if !ok {
|
||||
log.Fatal("listener was not a ListenerFile")
|
||||
}
|
||||
|
||||
delete(loadedGob.ListenerFds, s.Addr) // mark it as used
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(s *server.Server, ln server.ListenerFile) {
|
||||
defer wg.Done()
|
||||
|
||||
if ln != nil {
|
||||
err = s.Serve(ln)
|
||||
} else {
|
||||
err = s.ListenAndServe()
|
||||
}
|
||||
|
||||
// "use of closed network connection" is normal if doing graceful shutdown...
|
||||
if err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
errChan <- err
|
||||
}
|
||||
}(s, ln)
|
||||
|
||||
startupWg.Add(1)
|
||||
go func(s *server.Server) {
|
||||
defer startupWg.Done()
|
||||
s.WaitUntilStarted()
|
||||
}(s)
|
||||
|
||||
serversMu.Lock()
|
||||
servers = append(servers, s)
|
||||
serversMu.Unlock()
|
||||
}
|
||||
|
||||
// Wait for all servers to finish starting
|
||||
startupWg.Wait()
|
||||
|
||||
// Return the first error, if any
|
||||
select {
|
||||
case err := <-errChan:
|
||||
return err
|
||||
default:
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops all servers. It blocks until they are all stopped.
|
||||
// It does NOT execute shutdown callbacks that may have been
|
||||
// configured by middleware (they are executed on SIGINT).
|
||||
func Stop() error {
|
||||
letsencrypt.Deactivate()
|
||||
|
||||
serversMu.Lock()
|
||||
for _, s := range servers {
|
||||
s.Stop() // TODO: error checking/reporting?
|
||||
}
|
||||
servers = []*server.Server{} // don't reuse servers
|
||||
serversMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait blocks until all servers are stopped.
|
||||
func Wait() {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// LoadCaddyfile loads a Caddyfile in a way that prioritizes
|
||||
// reading from stdin pipe; otherwise it calls loader to load
|
||||
// the Caddyfile. If loader does not return a Caddyfile, the
|
||||
// default one will be returned. Thus, if there are no other
|
||||
// errors, this function always returns at least the default
|
||||
// Caddyfile (not the previously-used Caddyfile).
|
||||
func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) {
|
||||
// If we are a fork, finishing the restart is highest priority;
|
||||
// piped input is required in this case.
|
||||
if IsRestart() {
|
||||
err := gob.NewDecoder(os.Stdin).Decode(&loadedGob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cdyfile = loadedGob.Caddyfile
|
||||
}
|
||||
|
||||
// Otherwise, we first try to get from stdin pipe
|
||||
if cdyfile == nil {
|
||||
cdyfile, err = CaddyfileFromPipe(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// No piped input, so try the user's loader instead
|
||||
if cdyfile == nil && loader != nil {
|
||||
cdyfile, err = loader()
|
||||
}
|
||||
|
||||
// Otherwise revert to default
|
||||
if cdyfile == nil {
|
||||
cdyfile = DefaultInput()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CaddyfileFromPipe loads the Caddyfile input from f if f is
|
||||
// not interactive input. f is assumed to be a pipe or stream,
|
||||
// such as os.Stdin. If f is not a pipe, no error is returned
|
||||
// but the Input value will be nil. An error is only returned
|
||||
// if there was an error reading the pipe, even if the length
|
||||
// of what was read is 0.
|
||||
func CaddyfileFromPipe(f *os.File) (Input, error) {
|
||||
fi, err := f.Stat()
|
||||
if err == nil && fi.Mode()&os.ModeCharDevice == 0 {
|
||||
// Note that a non-nil error is not a problem. Windows
|
||||
// will not create a stdin if there is no pipe, which
|
||||
// produces an error when calling Stat(). But Unix will
|
||||
// make one either way, which is why we also check that
|
||||
// bitmask.
|
||||
// BUG: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X)
|
||||
confBody, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CaddyfileInput{
|
||||
Contents: confBody,
|
||||
Filepath: f.Name(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// not having input from the pipe is not itself an error,
|
||||
// just means no input to return.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Caddyfile returns the current Caddyfile
|
||||
func Caddyfile() Input {
|
||||
caddyfileMu.Lock()
|
||||
defer caddyfileMu.Unlock()
|
||||
return caddyfile
|
||||
}
|
||||
|
||||
// Input represents a Caddyfile; its contents and file path
|
||||
// (which should include the file name at the end of the path).
|
||||
// If path does not apply (e.g. piped input) you may use
|
||||
// any understandable value. The path is mainly used for logging,
|
||||
// error messages, and debugging.
|
||||
type Input interface {
|
||||
// Gets the Caddyfile contents
|
||||
Body() []byte
|
||||
|
||||
// Gets the path to the origin file
|
||||
Path() string
|
||||
|
||||
// IsFile returns true if the original input was a file on the file system
|
||||
// that could be loaded again later if requested.
|
||||
IsFile() bool
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCaddyStartStop(t *testing.T) {
|
||||
caddyfile := "localhost:1984\ntls off"
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
err := Start(CaddyfileInput{Contents: []byte(caddyfile)})
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting, iteration %d: %v", i, err)
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Timeout: time.Duration(2 * time.Second),
|
||||
}
|
||||
resp, err := client.Get("http://localhost:1984")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected GET request to succeed (iteration %d), but it failed: %v", i, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
err = Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("Error stopping, iteration %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package caddyfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
)
|
||||
|
||||
const filename = "Caddyfile"
|
||||
|
||||
// ToJSON converts caddyfile to its JSON representation.
|
||||
func ToJSON(caddyfile []byte) ([]byte, error) {
|
||||
var j Caddyfile
|
||||
|
||||
serverBlocks, err := parse.ServerBlocks(filename, bytes.NewReader(caddyfile), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sb := range serverBlocks {
|
||||
block := ServerBlock{Body: make(map[string]interface{})}
|
||||
|
||||
for _, host := range sb.HostList() {
|
||||
block.Hosts = append(block.Hosts, strings.TrimSuffix(host, ":"))
|
||||
}
|
||||
|
||||
for dir, tokens := range sb.Tokens {
|
||||
disp := parse.NewDispenserTokens(filename, tokens)
|
||||
disp.Next() // the first token is the directive; skip it
|
||||
block.Body[dir] = constructLine(disp)
|
||||
}
|
||||
|
||||
j = append(j, block)
|
||||
}
|
||||
|
||||
result, err := json.Marshal(j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// constructLine transforms tokens into a JSON-encodable structure;
|
||||
// but only one line at a time, to be used at the top-level of
|
||||
// a server block only (where the first token on each line is a
|
||||
// directive) - not to be used at any other nesting level.
|
||||
func constructLine(d parse.Dispenser) interface{} {
|
||||
var args []interface{}
|
||||
|
||||
all := d.RemainingArgs()
|
||||
for _, arg := range all {
|
||||
args = append(args, arg)
|
||||
}
|
||||
|
||||
d.Next()
|
||||
if d.Val() == "{" {
|
||||
args = append(args, constructBlock(d))
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// constructBlock recursively processes tokens into a
|
||||
// JSON-encodable structure.
|
||||
func constructBlock(d parse.Dispenser) interface{} {
|
||||
block := make(map[string]interface{})
|
||||
|
||||
for d.Next() {
|
||||
if d.Val() == "}" {
|
||||
break
|
||||
}
|
||||
|
||||
dir := d.Val()
|
||||
all := d.RemainingArgs()
|
||||
|
||||
var args []interface{}
|
||||
for _, arg := range all {
|
||||
args = append(args, arg)
|
||||
}
|
||||
if d.Val() == "{" {
|
||||
args = append(args, constructBlock(d))
|
||||
}
|
||||
|
||||
block[dir] = args
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
// FromJSON converts JSON-encoded jsonBytes to Caddyfile text
|
||||
func FromJSON(jsonBytes []byte) ([]byte, error) {
|
||||
var j Caddyfile
|
||||
var result string
|
||||
|
||||
err := json.Unmarshal(jsonBytes, &j)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sb := range j {
|
||||
for i, host := range sb.Hosts {
|
||||
if hostname, port, err := net.SplitHostPort(host); err == nil {
|
||||
if port == "http" || port == "https" {
|
||||
host = port + "://" + hostname
|
||||
}
|
||||
}
|
||||
if i > 0 {
|
||||
result += ", "
|
||||
}
|
||||
result += strings.TrimSuffix(host, ":")
|
||||
}
|
||||
result += jsonToText(sb.Body, 1)
|
||||
}
|
||||
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
// jsonToText recursively transforms a scope of JSON into plain
|
||||
// Caddyfile text.
|
||||
func jsonToText(scope interface{}, depth int) string {
|
||||
var result string
|
||||
|
||||
switch val := scope.(type) {
|
||||
case string:
|
||||
if strings.ContainsAny(val, "\" \n\t\r") {
|
||||
result += ` "` + strings.Replace(val, "\"", "\\\"", -1) + `"`
|
||||
} else {
|
||||
result += " " + val
|
||||
}
|
||||
case int:
|
||||
result += " " + strconv.Itoa(val)
|
||||
case float64:
|
||||
result += " " + fmt.Sprintf("%v", val)
|
||||
case bool:
|
||||
result += " " + fmt.Sprintf("%t", val)
|
||||
case map[string]interface{}:
|
||||
result += " {\n"
|
||||
for param, args := range val {
|
||||
result += strings.Repeat("\t", depth) + param
|
||||
result += jsonToText(args, depth+1) + "\n"
|
||||
}
|
||||
result += strings.Repeat("\t", depth-1) + "}"
|
||||
case []interface{}:
|
||||
for _, v := range val {
|
||||
result += jsonToText(v, depth)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type Caddyfile []ServerBlock
|
||||
|
||||
type ServerBlock struct {
|
||||
Hosts []string `json:"hosts"`
|
||||
Body map[string]interface{} `json:"body"`
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package caddyfile
|
||||
|
||||
import "testing"
|
||||
|
||||
var tests = []struct {
|
||||
caddyfile, json string
|
||||
}{
|
||||
{ // 0
|
||||
caddyfile: `foo {
|
||||
root /bar
|
||||
}`,
|
||||
json: `[{"hosts":["foo"],"body":{"root":["/bar"]}}]`,
|
||||
},
|
||||
{ // 1
|
||||
caddyfile: `host1, host2 {
|
||||
dir {
|
||||
def
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1","host2"],"body":{"dir":[{"def":null}]}}]`,
|
||||
},
|
||||
{ // 2
|
||||
caddyfile: `host1, host2 {
|
||||
dir abc {
|
||||
def ghi
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1","host2"],"body":{"dir":["abc",{"def":["ghi"]}]}}]`,
|
||||
},
|
||||
{ // 3
|
||||
caddyfile: `host1:1234, host2:5678 {
|
||||
dir abc {
|
||||
}
|
||||
}`,
|
||||
json: `[{"hosts":["host1:1234","host2:5678"],"body":{"dir":["abc",{}]}}]`,
|
||||
},
|
||||
{ // 4
|
||||
caddyfile: `host {
|
||||
foo "bar baz"
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":{"foo":["bar baz"]}}]`,
|
||||
},
|
||||
{ // 5
|
||||
caddyfile: `host, host:80 {
|
||||
foo "bar \"baz\""
|
||||
}`,
|
||||
json: `[{"hosts":["host","host:80"],"body":{"foo":["bar \"baz\""]}}]`,
|
||||
},
|
||||
{ // 6
|
||||
caddyfile: `host {
|
||||
foo "bar
|
||||
baz"
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":{"foo":["bar\nbaz"]}}]`,
|
||||
},
|
||||
{ // 7
|
||||
caddyfile: `host {
|
||||
dir 123 4.56 true
|
||||
}`,
|
||||
json: `[{"hosts":["host"],"body":{"dir":["123","4.56","true"]}}]`, // NOTE: I guess we assume numbers and booleans should be encoded as strings...?
|
||||
},
|
||||
{ // 8
|
||||
caddyfile: `http://host, https://host {
|
||||
}`,
|
||||
json: `[{"hosts":["host:http","host:https"],"body":{}}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
|
||||
},
|
||||
}
|
||||
|
||||
func TestToJSON(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
output, err := ToJSON([]byte(test.caddyfile))
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: %v", i, err)
|
||||
}
|
||||
if string(output) != test.json {
|
||||
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.json, string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromJSON(t *testing.T) {
|
||||
for i, test := range tests {
|
||||
output, err := FromJSON([]byte(test.json))
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: %v", i, err)
|
||||
}
|
||||
if string(output) != test.caddyfile {
|
||||
t.Errorf("Test %d\nExpected:\n'%s'\nActual:\n'%s'", i, test.caddyfile, string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
-374
@@ -1,374 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/caddy/letsencrypt"
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultConfigFile is the name of the configuration file that is loaded
|
||||
// by default if no other file is specified.
|
||||
DefaultConfigFile = "Caddyfile"
|
||||
)
|
||||
|
||||
// loadConfigs reads input (named filename) and parses it, returning the
|
||||
// server configurations in the order they appeared in the input. As part
|
||||
// of this, it activates Let's Encrypt for the configs that are produced.
|
||||
// Thus, the returned configs are already optimally configured optimally
|
||||
// for HTTPS.
|
||||
func loadConfigs(filename string, input io.Reader) ([]server.Config, error) {
|
||||
var configs []server.Config
|
||||
|
||||
// turn off timestamp for parsing
|
||||
flags := log.Flags()
|
||||
log.SetFlags(0)
|
||||
|
||||
// Each server block represents similar hosts/addresses, since they
|
||||
// were grouped together in the Caddyfile.
|
||||
serverBlocks, err := parse.ServerBlocks(filename, input, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(serverBlocks) == 0 {
|
||||
newInput := DefaultInput()
|
||||
serverBlocks, err = parse.ServerBlocks(newInput.Path(), bytes.NewReader(newInput.Body()), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var lastDirectiveIndex int // we set up directives in two parts; this stores where we left off
|
||||
|
||||
// Iterate each server block and make a config for each one,
|
||||
// executing the directives that were parsed in order up to the tls
|
||||
// directive; this is because we must activate Let's Encrypt.
|
||||
for i, sb := range serverBlocks {
|
||||
onces := makeOnces()
|
||||
storages := makeStorages()
|
||||
|
||||
for j, addr := range sb.Addresses {
|
||||
config := server.Config{
|
||||
Host: addr.Host,
|
||||
Port: addr.Port,
|
||||
Root: Root,
|
||||
Middleware: make(map[string][]middleware.Middleware),
|
||||
ConfigFile: filename,
|
||||
AppName: AppName,
|
||||
AppVersion: AppVersion,
|
||||
}
|
||||
|
||||
// It is crucial that directives are executed in the proper order.
|
||||
for k, dir := range directiveOrder {
|
||||
// Execute directive if it is in the server block
|
||||
if tokens, ok := sb.Tokens[dir.name]; ok {
|
||||
// Each setup function gets a controller, from which setup functions
|
||||
// get access to the config, tokens, and other state information useful
|
||||
// to set up its own host only.
|
||||
controller := &setup.Controller{
|
||||
Config: &config,
|
||||
Dispenser: parse.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
onces[dir.name].Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockHostIndex: j,
|
||||
ServerBlockHosts: sb.HostList(),
|
||||
ServerBlockStorage: storages[dir.name],
|
||||
}
|
||||
// execute setup function and append middleware handler, if any
|
||||
midware, err := dir.setup(controller)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if midware != nil {
|
||||
// TODO: For now, we only support the default path scope /
|
||||
config.Middleware["/"] = append(config.Middleware["/"], midware)
|
||||
}
|
||||
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
|
||||
// Stop after TLS setup, since we need to activate Let's Encrypt before continuing;
|
||||
// it makes some changes to the configs that middlewares might want to know about.
|
||||
if dir.name == "tls" {
|
||||
lastDirectiveIndex = k
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
}
|
||||
}
|
||||
|
||||
// Now we have all the configs, but they have only been set up to the
|
||||
// point of tls. We need to activate Let's Encrypt before setting up
|
||||
// the rest of the middlewares so they have correct information regarding
|
||||
// TLS configuration, if necessary. (this call is append-only, so our
|
||||
// iterations below shouldn't be affected)
|
||||
configs, err = letsencrypt.Activate(configs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Finish setting up the rest of the directives, now that TLS is
|
||||
// optimally configured. These loops are similar to above except
|
||||
// we don't iterate all the directives from the beginning and we
|
||||
// don't create new configs.
|
||||
configIndex := -1
|
||||
for i, sb := range serverBlocks {
|
||||
onces := makeOnces()
|
||||
storages := makeStorages()
|
||||
|
||||
for j := range sb.Addresses {
|
||||
configIndex++
|
||||
|
||||
for k := lastDirectiveIndex + 1; k < len(directiveOrder); k++ {
|
||||
dir := directiveOrder[k]
|
||||
|
||||
if tokens, ok := sb.Tokens[dir.name]; ok {
|
||||
controller := &setup.Controller{
|
||||
Config: &configs[configIndex],
|
||||
Dispenser: parse.NewDispenserTokens(filename, tokens),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
var err error
|
||||
onces[dir.name].Do(func() {
|
||||
err = f()
|
||||
})
|
||||
return err
|
||||
},
|
||||
ServerBlockIndex: i,
|
||||
ServerBlockHostIndex: j,
|
||||
ServerBlockHosts: sb.HostList(),
|
||||
ServerBlockStorage: storages[dir.name],
|
||||
}
|
||||
midware, err := dir.setup(controller)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if midware != nil {
|
||||
// TODO: For now, we only support the default path scope /
|
||||
configs[configIndex].Middleware["/"] = append(configs[configIndex].Middleware["/"], midware)
|
||||
}
|
||||
storages[dir.name] = controller.ServerBlockStorage // persist for this server block
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// restore logging settings
|
||||
log.SetFlags(flags)
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// makeOnces makes a map of directive name to sync.Once
|
||||
// instance. This is intended to be called once per server
|
||||
// block when setting up configs so that Setup functions
|
||||
// for each directive can perform a task just once per
|
||||
// server block, even if there are multiple hosts on the block.
|
||||
//
|
||||
// We need one Once per directive, otherwise the first
|
||||
// directive to use it would exclude other directives from
|
||||
// using it at all, which would be a bug.
|
||||
func makeOnces() map[string]*sync.Once {
|
||||
onces := make(map[string]*sync.Once)
|
||||
for _, dir := range directiveOrder {
|
||||
onces[dir.name] = new(sync.Once)
|
||||
}
|
||||
return onces
|
||||
}
|
||||
|
||||
// makeStorages makes a map of directive name to interface{}
|
||||
// so that directives' setup functions can persist state
|
||||
// between different hosts on the same server block during the
|
||||
// setup phase.
|
||||
func makeStorages() map[string]interface{} {
|
||||
storages := make(map[string]interface{})
|
||||
for _, dir := range directiveOrder {
|
||||
storages[dir.name] = nil
|
||||
}
|
||||
return storages
|
||||
}
|
||||
|
||||
// arrangeBindings groups configurations by their bind address. For example,
|
||||
// a server that should listen on localhost and another on 127.0.0.1 will
|
||||
// be grouped into the same address: 127.0.0.1. It will return an error
|
||||
// if an address is malformed or a TLS listener is configured on the
|
||||
// same address as a plaintext HTTP listener. The return value is a map of
|
||||
// bind address to list of configs that would become VirtualHosts on that
|
||||
// server. Use the keys of the returned map to create listeners, and use
|
||||
// the associated values to set up the virtualhosts.
|
||||
func arrangeBindings(allConfigs []server.Config) (Group, error) {
|
||||
var groupings Group
|
||||
|
||||
// Group configs by bind address
|
||||
for _, conf := range allConfigs {
|
||||
// use default port if none is specified
|
||||
if conf.Port == "" {
|
||||
conf.Port = Port
|
||||
}
|
||||
|
||||
bindAddr, warnErr, fatalErr := resolveAddr(conf)
|
||||
if fatalErr != nil {
|
||||
return groupings, fatalErr
|
||||
}
|
||||
if warnErr != nil {
|
||||
log.Println("[Warning]", warnErr)
|
||||
}
|
||||
|
||||
// Make sure to compare the string representation of the address,
|
||||
// not the pointer, since a new *TCPAddr is created each time.
|
||||
var existing bool
|
||||
for i := 0; i < len(groupings); i++ {
|
||||
if groupings[i].BindAddr.String() == bindAddr.String() {
|
||||
groupings[i].Configs = append(groupings[i].Configs, conf)
|
||||
existing = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !existing {
|
||||
groupings = append(groupings, BindingMapping{
|
||||
BindAddr: bindAddr,
|
||||
Configs: []server.Config{conf},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow HTTP and HTTPS to be served on the same address
|
||||
for _, group := range groupings {
|
||||
isTLS := group.Configs[0].TLS.Enabled
|
||||
for _, config := range group.Configs {
|
||||
if config.TLS.Enabled != isTLS {
|
||||
thisConfigProto, otherConfigProto := "HTTP", "HTTP"
|
||||
if config.TLS.Enabled {
|
||||
thisConfigProto = "HTTPS"
|
||||
}
|
||||
if group.Configs[0].TLS.Enabled {
|
||||
otherConfigProto = "HTTPS"
|
||||
}
|
||||
return groupings, fmt.Errorf("configuration error: Cannot multiplex %s (%s) and %s (%s) on same address",
|
||||
group.Configs[0].Address(), otherConfigProto, config.Address(), thisConfigProto)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupings, nil
|
||||
}
|
||||
|
||||
// resolveAddr determines the address (host and port) that a config will
|
||||
// bind to. The returned address, resolvAddr, should be used to bind the
|
||||
// listener or group the config with other configs using the same address.
|
||||
// The first error, if not nil, is just a warning and should be reported
|
||||
// but execution may continue. The second error, if not nil, is a real
|
||||
// problem and the server should not be started.
|
||||
//
|
||||
// This function handles edge cases gracefully. If a port name like
|
||||
// "http" or "https" is unknown to the system, this function will
|
||||
// change them to 80 or 443 respectively. If a hostname fails to
|
||||
// resolve, that host can still be served but will be listening on
|
||||
// the wildcard host instead. This function takes care of this for you.
|
||||
func resolveAddr(conf server.Config) (resolvAddr *net.TCPAddr, warnErr error, fatalErr error) {
|
||||
bindHost := conf.BindHost
|
||||
|
||||
// TODO: Do we even need the port? Maybe we just need to look up the host.
|
||||
resolvAddr, warnErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(bindHost, conf.Port))
|
||||
if warnErr != nil {
|
||||
// Most likely the host lookup failed or the port is unknown
|
||||
tryPort := conf.Port
|
||||
|
||||
switch errVal := warnErr.(type) {
|
||||
case *net.AddrError:
|
||||
if errVal.Err == "unknown port" {
|
||||
// some odd Linux machines don't support these port names; see issue #136
|
||||
switch conf.Port {
|
||||
case "http":
|
||||
tryPort = "80"
|
||||
case "https":
|
||||
tryPort = "443"
|
||||
}
|
||||
}
|
||||
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort(bindHost, tryPort))
|
||||
if fatalErr != nil {
|
||||
return
|
||||
}
|
||||
default:
|
||||
// the hostname probably couldn't be resolved, just bind to wildcard then
|
||||
resolvAddr, fatalErr = net.ResolveTCPAddr("tcp", net.JoinHostPort("0.0.0.0", tryPort))
|
||||
if fatalErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// validDirective returns true if d is a valid
|
||||
// directive; false otherwise.
|
||||
func validDirective(d string) bool {
|
||||
for _, dir := range directiveOrder {
|
||||
if dir.name == d {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NewDefault makes a default configuration, which
|
||||
// is empty except for root, host, and port,
|
||||
// which are essentials for serving the cwd.
|
||||
func NewDefault() server.Config {
|
||||
return server.Config{
|
||||
Root: Root,
|
||||
Host: Host,
|
||||
Port: Port,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultInput returns the default Caddyfile input
|
||||
// to use when it is otherwise empty or missing.
|
||||
func DefaultInput() CaddyfileInput {
|
||||
return CaddyfileInput{
|
||||
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)),
|
||||
}
|
||||
}
|
||||
|
||||
// These defaults are configurable through the command line
|
||||
var (
|
||||
// Site root
|
||||
Root = DefaultRoot
|
||||
|
||||
// Site host
|
||||
Host = DefaultHost
|
||||
|
||||
// Site port
|
||||
Port = DefaultPort
|
||||
)
|
||||
|
||||
// BindingMapping maps a network address to configurations
|
||||
// that will bind to it. The order of the configs is important.
|
||||
type BindingMapping struct {
|
||||
BindAddr *net.TCPAddr
|
||||
Configs []server.Config
|
||||
}
|
||||
|
||||
// Group maps network addresses to their configurations.
|
||||
// Preserving the order of the groupings is important
|
||||
// (related to graceful shutdown and restart)
|
||||
// so this is a slice, not a literal map.
|
||||
type Group []BindingMapping
|
||||
@@ -1,138 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
func TestNewDefault(t *testing.T) {
|
||||
config := NewDefault()
|
||||
|
||||
if actual, expected := config.Root, DefaultRoot; actual != expected {
|
||||
t.Errorf("Root was %s but expected %s", actual, expected)
|
||||
}
|
||||
if actual, expected := config.Host, DefaultHost; actual != expected {
|
||||
t.Errorf("Host was %s but expected %s", actual, expected)
|
||||
}
|
||||
if actual, expected := config.Port, DefaultPort; actual != expected {
|
||||
t.Errorf("Port was %s but expected %s", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAddr(t *testing.T) {
|
||||
// NOTE: If tests fail due to comparing to string "127.0.0.1",
|
||||
// it's possible that system env resolves with IPv6, or ::1.
|
||||
// If that happens, maybe we should use actualAddr.IP.IsLoopback()
|
||||
// for the assertion, rather than a direct string comparison.
|
||||
|
||||
// NOTE: Tests with {Host: "", Port: ""} and {Host: "localhost", Port: ""}
|
||||
// will not behave the same cross-platform, so they have been omitted.
|
||||
|
||||
for i, test := range []struct {
|
||||
config server.Config
|
||||
shouldWarnErr bool
|
||||
shouldFatalErr bool
|
||||
expectedIP string
|
||||
expectedPort int
|
||||
}{
|
||||
{server.Config{Host: "127.0.0.1", Port: "1234"}, false, false, "<nil>", 1234},
|
||||
{server.Config{Host: "localhost", Port: "80"}, false, false, "<nil>", 80},
|
||||
{server.Config{BindHost: "localhost", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "127.0.0.1", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "should-not-resolve", Port: "1234"}, true, false, "0.0.0.0", 1234},
|
||||
{server.Config{BindHost: "localhost", Port: "http"}, false, false, "127.0.0.1", 80},
|
||||
{server.Config{BindHost: "localhost", Port: "https"}, false, false, "127.0.0.1", 443},
|
||||
{server.Config{BindHost: "", Port: "1234"}, false, false, "<nil>", 1234},
|
||||
{server.Config{BindHost: "localhost", Port: "abcd"}, false, true, "", 0},
|
||||
{server.Config{BindHost: "127.0.0.1", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "localhost", Host: "should-not-be-used", Port: "1234"}, false, false, "127.0.0.1", 1234},
|
||||
{server.Config{BindHost: "should-not-resolve", Host: "localhost", Port: "1234"}, true, false, "0.0.0.0", 1234},
|
||||
} {
|
||||
actualAddr, warnErr, fatalErr := resolveAddr(test.config)
|
||||
|
||||
if test.shouldFatalErr && fatalErr == nil {
|
||||
t.Errorf("Test %d: Expected error, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldFatalErr && fatalErr != nil {
|
||||
t.Errorf("Test %d: Expected no error, but there was one: %v", i, fatalErr)
|
||||
}
|
||||
if fatalErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if test.shouldWarnErr && warnErr == nil {
|
||||
t.Errorf("Test %d: Expected warning, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldWarnErr && warnErr != nil {
|
||||
t.Errorf("Test %d: Expected no warning, but there was one: %v", i, warnErr)
|
||||
}
|
||||
|
||||
if actual, expected := actualAddr.IP.String(), test.expectedIP; actual != expected {
|
||||
t.Errorf("Test %d: IP was %s but expected %s", i, actual, expected)
|
||||
}
|
||||
if actual, expected := actualAddr.Port, test.expectedPort; actual != expected {
|
||||
t.Errorf("Test %d: Port was %d but expected %d", i, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeOnces(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
onces := makeOnces()
|
||||
if len(onces) != len(directives) {
|
||||
t.Errorf("onces had len %d , expected %d", len(onces), len(directives))
|
||||
}
|
||||
expected := map[string]*sync.Once{
|
||||
"dummy": new(sync.Once),
|
||||
"dummy2": new(sync.Once),
|
||||
}
|
||||
if !reflect.DeepEqual(onces, expected) {
|
||||
t.Errorf("onces was %v, expected %v", onces, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeStorages(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
storages := makeStorages()
|
||||
if len(storages) != len(directives) {
|
||||
t.Errorf("storages had len %d , expected %d", len(storages), len(directives))
|
||||
}
|
||||
expected := map[string]interface{}{
|
||||
"dummy": nil,
|
||||
"dummy2": nil,
|
||||
}
|
||||
if !reflect.DeepEqual(storages, expected) {
|
||||
t.Errorf("storages was %v, expected %v", storages, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidDirective(t *testing.T) {
|
||||
directives := []directive{
|
||||
{"dummy", nil},
|
||||
{"dummy2", nil},
|
||||
}
|
||||
directiveOrder = directives
|
||||
for i, test := range []struct {
|
||||
directive string
|
||||
valid bool
|
||||
}{
|
||||
{"dummy", true},
|
||||
{"dummy2", true},
|
||||
{"dummy3", false},
|
||||
} {
|
||||
if actual, expected := validDirective(test.directive), test.valid; actual != expected {
|
||||
t.Errorf("Test %d: valid was %t, expected %t", i, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// The parse package must know which directives
|
||||
// are valid, but it must not import the setup
|
||||
// or config package. To solve this problem, we
|
||||
// fill up this map in our init function here.
|
||||
// The parse package does not need to know the
|
||||
// ordering of the directives.
|
||||
for _, dir := range directiveOrder {
|
||||
parse.ValidDirectives[dir.name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Directives are registered in the order they should be
|
||||
// executed. Middleware (directives that inject a handler)
|
||||
// are executed in the order A-B-C-*-C-B-A, assuming
|
||||
// they all call the Next handler in the chain.
|
||||
//
|
||||
// Ordering is VERY important. Every middleware will
|
||||
// feel the effects of all other middleware below
|
||||
// (after) them during a request, but they must not
|
||||
// care what middleware above them are doing.
|
||||
//
|
||||
// For example, log needs to know the status code and
|
||||
// exactly how many bytes were written to the client,
|
||||
// which every other middleware can affect, so it gets
|
||||
// registered first. The errors middleware does not
|
||||
// care if gzip or log modifies its response, so it
|
||||
// gets registered below them. Gzip, on the other hand,
|
||||
// DOES care what errors does to the response since it
|
||||
// must compress every output to the client, even error
|
||||
// pages, so it must be registered before the errors
|
||||
// middleware and any others that would write to the
|
||||
// response.
|
||||
var directiveOrder = []directive{
|
||||
// Essential directives that initialize vital configuration settings
|
||||
{"root", setup.Root},
|
||||
{"tls", setup.TLS}, // letsencrypt is set up just after tls
|
||||
{"bind", setup.BindHost},
|
||||
|
||||
// Other directives that don't create HTTP handlers
|
||||
{"startup", setup.Startup},
|
||||
{"shutdown", setup.Shutdown},
|
||||
|
||||
// Directives that inject handlers (middleware)
|
||||
{"log", setup.Log},
|
||||
{"gzip", setup.Gzip},
|
||||
{"errors", setup.Errors},
|
||||
{"header", setup.Headers},
|
||||
{"rewrite", setup.Rewrite},
|
||||
{"redir", setup.Redir},
|
||||
{"ext", setup.Ext},
|
||||
{"mime", setup.Mime},
|
||||
{"basicauth", setup.BasicAuth},
|
||||
{"internal", setup.Internal},
|
||||
{"proxy", setup.Proxy},
|
||||
{"fastcgi", setup.FastCGI},
|
||||
{"websocket", setup.WebSocket},
|
||||
{"markdown", setup.Markdown},
|
||||
{"templates", setup.Templates},
|
||||
{"browse", setup.Browse},
|
||||
}
|
||||
|
||||
// directive ties together a directive name with its setup function.
|
||||
type directive struct {
|
||||
name string
|
||||
setup SetupFunc
|
||||
}
|
||||
|
||||
// SetupFunc takes a controller and may optionally return a middleware.
|
||||
// If the resulting middleware is not nil, it will be chained into
|
||||
// the HTTP handlers in the order specified in this package.
|
||||
type SetupFunc func(c *setup.Controller) (middleware.Middleware, error)
|
||||
@@ -1,71 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/letsencrypt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
letsencrypt.OnChange = func() error { return Restart(nil) }
|
||||
}
|
||||
|
||||
// isLocalhost returns true if host looks explicitly like a localhost address.
|
||||
func isLocalhost(host string) bool {
|
||||
return host == "localhost" || host == "::1" || strings.HasPrefix(host, "127.")
|
||||
}
|
||||
|
||||
// checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum.
|
||||
func checkFdlimit() {
|
||||
const min = 4096
|
||||
|
||||
// Warn if ulimit is too low for production sites
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH
|
||||
if err == nil {
|
||||
// Note that an error here need not be reported
|
||||
lim, err := strconv.Atoi(string(bytes.TrimSpace(out)))
|
||||
if err == nil && lim < min {
|
||||
fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// caddyfileGob maps bind address to index of the file descriptor
|
||||
// in the Files array passed to the child process. It also contains
|
||||
// the caddyfile contents. Used only during graceful restarts.
|
||||
type caddyfileGob struct {
|
||||
ListenerFds map[string]uintptr
|
||||
Caddyfile Input
|
||||
}
|
||||
|
||||
// IsRestart returns whether this process is, according
|
||||
// to env variables, a fork as part of a graceful restart.
|
||||
func IsRestart() bool {
|
||||
return os.Getenv("CADDY_RESTART") == "true"
|
||||
}
|
||||
|
||||
// CaddyfileInput represents a Caddyfile as input
|
||||
// and is simply a convenient way to implement
|
||||
// the Input interface.
|
||||
type CaddyfileInput struct {
|
||||
Filepath string
|
||||
Contents []byte
|
||||
RealFile bool
|
||||
}
|
||||
|
||||
// Body returns c.Contents.
|
||||
func (c CaddyfileInput) Body() []byte { return c.Contents }
|
||||
|
||||
// Path returns c.Filepath.
|
||||
func (c CaddyfileInput) Path() string { return c.Filepath }
|
||||
|
||||
// Path returns true if the original input was a real file on the file system.
|
||||
func (c CaddyfileInput) IsFile() bool { return c.RealFile }
|
||||
@@ -1,30 +0,0 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// loadRSAPrivateKey loads a PEM-encoded RSA private key from file.
|
||||
func loadRSAPrivateKey(file string) (*rsa.PrivateKey, error) {
|
||||
keyBytes, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyBlock, _ := pem.Decode(keyBytes)
|
||||
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
|
||||
}
|
||||
|
||||
// saveRSAPrivateKey saves a PEM-encoded RSA private key to file.
|
||||
func saveRSAPrivateKey(key *rsa.PrivateKey, file string) error {
|
||||
pemKey := pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}
|
||||
keyOut, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer keyOut.Close()
|
||||
return pem.Encode(keyOut, &pemKey)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rsaKeySizeToUse = 128 // make tests faster; small key size OK for testing
|
||||
}
|
||||
|
||||
func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
|
||||
keyFile := "test.key"
|
||||
defer os.Remove(keyFile)
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test save
|
||||
err = saveRSAPrivateKey(privateKey, keyFile)
|
||||
if err != nil {
|
||||
t.Fatal("error saving private key:", err)
|
||||
}
|
||||
|
||||
// test load
|
||||
loadedKey, err := loadRSAPrivateKey(keyFile)
|
||||
if err != nil {
|
||||
t.Error("error loading private key:", err)
|
||||
}
|
||||
|
||||
// very loaded key is correct
|
||||
if !rsaPrivateKeysSame(privateKey, loadedKey) {
|
||||
t.Error("Expected key bytes to be the same, but they weren't")
|
||||
}
|
||||
}
|
||||
|
||||
// rsaPrivateKeyBytes returns the bytes of DER-encoded key.
|
||||
func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte {
|
||||
return x509.MarshalPKCS1PrivateKey(key)
|
||||
}
|
||||
|
||||
// rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same.
|
||||
func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool {
|
||||
return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b))
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
// Handler is a Caddy middleware that can proxy ACME requests
|
||||
// to the real ACME endpoint. This is necessary to renew certificates
|
||||
// while the server is running. Obviously, a site served on port
|
||||
// 443 (HTTPS) binds to that port, so another listener created by
|
||||
// our acme client can't bind successfully and solve the challenge.
|
||||
// Thus, we chain this handler in so that it can, when activated,
|
||||
// proxy ACME requests to an ACME client listening on an alternate
|
||||
// port.
|
||||
type Handler struct {
|
||||
sync.Mutex // protects the ChallengePath property
|
||||
Next middleware.Handler
|
||||
ChallengeActive int32 // use sync/atomic for speed to set/get this flag
|
||||
ChallengePath string // the exact request path to match before proxying
|
||||
}
|
||||
|
||||
// ServeHTTP is basically a no-op unless an ACME challenge is active on this host
|
||||
// and the request path matches the expected path exactly.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
// Only if challenge is active
|
||||
if atomic.LoadInt32(&h.ChallengeActive) == 1 {
|
||||
h.Lock()
|
||||
path := h.ChallengePath
|
||||
h.Unlock()
|
||||
|
||||
// Request path must be correct; if so, proxy to ACME client
|
||||
if r.URL.Path == path {
|
||||
upstream, err := url.Parse("https://" + r.Host + ":" + alternatePort)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(upstream)
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client uses self-signed cert
|
||||
}
|
||||
proxy.ServeHTTP(w, r)
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
return h.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// ChallengeOn enables h to proxy ACME requests.
|
||||
func (h *Handler) ChallengeOn(challengePath string) {
|
||||
h.Lock()
|
||||
h.ChallengePath = challengePath
|
||||
h.Unlock()
|
||||
atomic.StoreInt32(&h.ChallengeActive, 1)
|
||||
}
|
||||
|
||||
// ChallengeOff disables ACME proxying from this h.
|
||||
func (h *Handler) ChallengeOff(success bool) {
|
||||
atomic.StoreInt32(&h.ChallengeActive, 0)
|
||||
}
|
||||
@@ -1,510 +0,0 @@
|
||||
// Package letsencrypt integrates Let's Encrypt functionality into Caddy
|
||||
// with first-class support for creating and renewing certificates
|
||||
// automatically. It is designed to configure sites for HTTPS by default.
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// Activate sets up TLS for each server config in configs
|
||||
// as needed. It only skips the config if the cert and key
|
||||
// are already provided, if plaintext http is explicitly
|
||||
// specified as the port, TLS is explicitly disabled, or
|
||||
// the host looks like a loopback or wildcard address.
|
||||
//
|
||||
// This function may prompt the user to provide an email
|
||||
// address if none is available through other means. It
|
||||
// prefers the email address specified in the config, but
|
||||
// if that is not available it will check the command line
|
||||
// argument. If absent, it will use the most recent email
|
||||
// address from last time. If there isn't one, the user
|
||||
// will be prompted and shown SA link.
|
||||
//
|
||||
// Also note that calling this function activates asset
|
||||
// management automatically, which keeps certificates
|
||||
// renewed and OCSP stapling updated. This has the effect
|
||||
// of causing restarts when assets are updated.
|
||||
//
|
||||
// Activate returns the updated list of configs, since
|
||||
// some may have been appended, for example, to redirect
|
||||
// plaintext HTTP requests to their HTTPS counterpart.
|
||||
// This function only appends; it does not prepend or splice.
|
||||
func Activate(configs []server.Config) ([]server.Config, error) {
|
||||
// just in case previous caller forgot...
|
||||
Deactivate()
|
||||
|
||||
// TODO: All the output the end user should see when running caddy is something
|
||||
// simple like "Setting up HTTPS..." (and maybe 'done' at the end of the line when finished).
|
||||
// In other words, hide all the other logging except for on errors. Or maybe
|
||||
// have a place to put those logs.
|
||||
|
||||
// reset cached ocsp statuses from any previous activations
|
||||
ocspStatus = make(map[*[]byte]int)
|
||||
|
||||
// Identify and configure any eligible hosts for which
|
||||
// we already have certs and keys in storage from last time.
|
||||
configLen := len(configs) // avoid infinite loop since this loop appends plaintext to the slice
|
||||
for i := 0; i < configLen; i++ {
|
||||
if existingCertAndKey(configs[i].Host) && configQualifies(configs, i) {
|
||||
configs = autoConfigure(configs, i)
|
||||
}
|
||||
}
|
||||
|
||||
// Group configs by email address; only configs that are eligible
|
||||
// for TLS management are included. We group by email so that we
|
||||
// can request certificates in batches with the same client.
|
||||
// Note: The return value is a map, and iteration over a map is
|
||||
// not ordered. I don't think it will be a problem, but if an
|
||||
// ordering problem arises, look at this carefully.
|
||||
groupedConfigs, err := groupConfigsByEmail(configs)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// obtain certificates for configs that need one, and reconfigure each
|
||||
// config to use the certificates
|
||||
for leEmail, cfgIndexes := range groupedConfigs {
|
||||
// make client to service this email address with CA server
|
||||
client, err := newClient(leEmail)
|
||||
if err != nil {
|
||||
return configs, errors.New("error creating client: " + err.Error())
|
||||
}
|
||||
|
||||
// little bit of housekeeping; gather the hostnames into a slice
|
||||
hosts := make([]string, len(cfgIndexes))
|
||||
for i, idx := range cfgIndexes {
|
||||
hosts[i] = configs[idx].Host
|
||||
}
|
||||
|
||||
// client is ready, so let's get free, trusted SSL certificates!
|
||||
Obtain:
|
||||
certificates, failures := client.ObtainCertificates(hosts, true)
|
||||
if len(failures) > 0 {
|
||||
// Build an error string to return, using all the failures in the list.
|
||||
var errMsg string
|
||||
|
||||
// If an error is because of updated SA, only prompt user for agreement once
|
||||
var promptedForAgreement bool
|
||||
|
||||
for domain, obtainErr := range failures {
|
||||
// If the failure was simply because the terms have changed, re-prompt and re-try
|
||||
if tosErr, ok := obtainErr.(acme.TOSError); ok {
|
||||
if !Agreed && !promptedForAgreement {
|
||||
Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
|
||||
promptedForAgreement = true
|
||||
}
|
||||
if Agreed {
|
||||
err := client.AgreeToTOS()
|
||||
if err != nil {
|
||||
return configs, errors.New("error agreeing to updated terms: " + err.Error())
|
||||
}
|
||||
goto Obtain
|
||||
}
|
||||
}
|
||||
|
||||
// If user did not agree or it was any other kind of error, just append to the list of errors
|
||||
errMsg += "[" + domain + "] failed to get certificate: " + obtainErr.Error() + "\n"
|
||||
}
|
||||
|
||||
return configs, errors.New(errMsg)
|
||||
}
|
||||
|
||||
// ... that's it. save the certs, keys, and metadata files to disk
|
||||
err = saveCertsAndKeys(certificates)
|
||||
if err != nil {
|
||||
return configs, errors.New("error saving assets: " + err.Error())
|
||||
}
|
||||
|
||||
// it all comes down to this: turning on TLS with all the new certs
|
||||
for _, idx := range cfgIndexes {
|
||||
configs = autoConfigure(configs, idx)
|
||||
}
|
||||
}
|
||||
|
||||
// renew all certificates that need renewal
|
||||
renewCertificates(configs, false)
|
||||
|
||||
// keep certificates renewed and OCSP stapling updated
|
||||
go maintainAssets(configs, stopChan)
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// Deactivate cleans up long-term, in-memory resources
|
||||
// allocated by calling Activate(). Essentially, it stops
|
||||
// the asset maintainer from running, meaning that certificates
|
||||
// will not be renewed, OCSP staples will not be updated, etc.
|
||||
func Deactivate() (err error) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
err = errors.New("already deactivated")
|
||||
}
|
||||
}()
|
||||
close(stopChan)
|
||||
stopChan = make(chan struct{})
|
||||
return
|
||||
}
|
||||
|
||||
// configQualifies returns true if the config at cfgIndex (within allConfigs)
|
||||
// qualifes for automatic LE activation. It does NOT check to see if a cert
|
||||
// and key already exist for the config.
|
||||
func configQualifies(allConfigs []server.Config, cfgIndex int) bool {
|
||||
cfg := allConfigs[cfgIndex]
|
||||
return cfg.TLS.Certificate == "" && // user could provide their own cert and key
|
||||
cfg.TLS.Key == "" &&
|
||||
|
||||
// user can force-disable automatic HTTPS for this host
|
||||
cfg.Port != "http" &&
|
||||
cfg.TLS.LetsEncryptEmail != "off" &&
|
||||
|
||||
// obviously we get can't certs for loopback or internal hosts
|
||||
cfg.Host != "localhost" &&
|
||||
cfg.Host != "" &&
|
||||
cfg.Host != "0.0.0.0" &&
|
||||
cfg.Host != "::1" &&
|
||||
!strings.HasPrefix(cfg.Host, "127.") && // to use boulder on your own machine, add fake domain to hosts file
|
||||
// not excluding 10.* and 192.168.* hosts for possibility of running internal Boulder instance
|
||||
|
||||
// make sure another HTTPS version of this config doesn't exist in the list already
|
||||
!otherHostHasScheme(allConfigs, cfgIndex, "https")
|
||||
}
|
||||
|
||||
// groupConfigsByEmail groups configs by user email address. The returned map is
|
||||
// a map of email address to the configs that are serviced under that account.
|
||||
// If an email address is not available for an eligible config, the user will be
|
||||
// prompted to provide one. The returned map contains pointers to the original
|
||||
// server config values.
|
||||
func groupConfigsByEmail(configs []server.Config) (map[string][]int, error) {
|
||||
initMap := make(map[string][]int)
|
||||
for i := 0; i < len(configs); i++ {
|
||||
// filter out configs that we already have certs for and
|
||||
// that we won't be obtaining certs for - this way we won't
|
||||
// bother the user for an email address unnecessarily and
|
||||
// we don't obtain new certs for a host we already have certs for.
|
||||
if existingCertAndKey(configs[i].Host) || !configQualifies(configs, i) {
|
||||
continue
|
||||
}
|
||||
leEmail := getEmail(configs[i])
|
||||
initMap[leEmail] = append(initMap[leEmail], i)
|
||||
}
|
||||
return initMap, nil
|
||||
}
|
||||
|
||||
// existingCertAndKey returns true if the host has a certificate
|
||||
// and private key in storage already, false otherwise.
|
||||
func existingCertAndKey(host string) bool {
|
||||
_, err := os.Stat(storage.SiteCertFile(host))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = os.Stat(storage.SiteKeyFile(host))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// newClient creates a new ACME client to facilitate communication
|
||||
// with the Let's Encrypt CA server on behalf of the user specified
|
||||
// by leEmail. As part of this process, a user will be loaded from
|
||||
// disk (if already exists) or created new and registered via ACME
|
||||
// and saved to the file system for next time.
|
||||
func newClient(leEmail string) (*acme.Client, error) {
|
||||
return newClientPort(leEmail, exposePort)
|
||||
}
|
||||
|
||||
// newClientPort does the same thing as newClient, except it creates a
|
||||
// new client with a custom port used for ACME transactions instead of
|
||||
// the default port. This is important if the default port is already in
|
||||
// use or is not exposed to the public, etc.
|
||||
func newClientPort(leEmail, port string) (*acme.Client, error) {
|
||||
// Look up or create the LE user account
|
||||
leUser, err := getUser(leEmail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The client facilitates our communication with the CA server.
|
||||
client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse, port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If not registered, the user must register an account with the CA
|
||||
// and agree to terms
|
||||
if leUser.Registration == nil {
|
||||
reg, err := client.Register()
|
||||
if err != nil {
|
||||
return nil, errors.New("registration error: " + err.Error())
|
||||
}
|
||||
leUser.Registration = reg
|
||||
|
||||
if !Agreed && reg.TosURL == "" {
|
||||
Agreed = promptUserAgreement(saURL, false) // TODO - latest URL
|
||||
}
|
||||
if !Agreed && reg.TosURL == "" {
|
||||
return nil, errors.New("user must agree to terms")
|
||||
}
|
||||
|
||||
err = client.AgreeToTOS()
|
||||
if err != nil {
|
||||
saveUser(leUser) // TODO: Might as well try, right? Error check?
|
||||
return nil, errors.New("error agreeing to terms: " + err.Error())
|
||||
}
|
||||
|
||||
// save user to the file system
|
||||
err = saveUser(leUser)
|
||||
if err != nil {
|
||||
return nil, errors.New("could not save user: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// obtainCertificates obtains certificates from the CA server for
|
||||
// the configurations in serverConfigs using client.
|
||||
func obtainCertificates(client *acme.Client, serverConfigs []server.Config) ([]acme.CertificateResource, map[string]error) {
|
||||
// collect all the hostnames into one slice
|
||||
var hosts []string
|
||||
for _, cfg := range serverConfigs {
|
||||
hosts = append(hosts, cfg.Host)
|
||||
}
|
||||
|
||||
return client.ObtainCertificates(hosts, true)
|
||||
}
|
||||
|
||||
// saveCertificates saves each certificate resource to disk. This
|
||||
// includes the certificate file itself, the private key, and the
|
||||
// metadata file.
|
||||
func saveCertsAndKeys(certificates []acme.CertificateResource) error {
|
||||
for _, cert := range certificates {
|
||||
os.MkdirAll(storage.Site(cert.Domain), 0700)
|
||||
|
||||
// Save cert
|
||||
err := ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save private key
|
||||
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save cert metadata
|
||||
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// autoConfigure enables TLS on allConfigs[cfgIndex] and appends, if necessary,
|
||||
// a new config to allConfigs that redirects plaintext HTTP to its new HTTPS
|
||||
// counterpart. It expects the certificate and key to already be in storage. It
|
||||
// returns the new list of allConfigs, since it may append a new config. This
|
||||
// function assumes that allConfigs[cfgIndex] is already set up for HTTPS.
|
||||
func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config {
|
||||
cfg := &allConfigs[cfgIndex]
|
||||
|
||||
bundleBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
|
||||
// TODO: Handle these errors better
|
||||
if err == nil {
|
||||
ocsp, status, err := acme.GetOCSPForCert(bundleBytes)
|
||||
ocspStatus[&bundleBytes] = status
|
||||
if err == nil && status == acme.OCSPGood {
|
||||
cfg.TLS.OCSPStaple = ocsp
|
||||
}
|
||||
}
|
||||
cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host)
|
||||
cfg.TLS.Key = storage.SiteKeyFile(cfg.Host)
|
||||
cfg.TLS.Enabled = true
|
||||
if cfg.Port == "" {
|
||||
cfg.Port = "https"
|
||||
}
|
||||
|
||||
// Chain in ACME middleware proxy if we use up the SSL port
|
||||
if cfg.Port == "https" || cfg.Port == "443" {
|
||||
handler := new(Handler)
|
||||
mid := func(next middleware.Handler) middleware.Handler {
|
||||
handler.Next = next
|
||||
return handler
|
||||
}
|
||||
cfg.Middleware["/"] = append(cfg.Middleware["/"], mid)
|
||||
acmeHandlers[cfg.Host] = handler
|
||||
}
|
||||
|
||||
// Set up http->https redirect as long as there isn't already a http counterpart
|
||||
// in the configs and this isn't, for some reason, already on port 80
|
||||
if !otherHostHasScheme(allConfigs, cfgIndex, "http") &&
|
||||
cfg.Port != "80" && cfg.Port != "http" { // (would not be http port with current program flow, but just in case)
|
||||
allConfigs = append(allConfigs, redirPlaintextHost(*cfg))
|
||||
}
|
||||
|
||||
return allConfigs
|
||||
}
|
||||
|
||||
// otherHostHasScheme tells you whether there is ANOTHER config in allConfigs
|
||||
// for the same host but with the port equal to scheme as allConfigs[cfgIndex].
|
||||
// This function considers "443" and "https" to be the same scheme, as well as
|
||||
// "http" and "80". It does not tell you whether there is ANY config with scheme,
|
||||
// only if there's a different one with it.
|
||||
func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string) bool {
|
||||
if scheme == "80" {
|
||||
scheme = "http"
|
||||
} else if scheme == "443" {
|
||||
scheme = "https"
|
||||
}
|
||||
for i, otherCfg := range allConfigs {
|
||||
if i == cfgIndex {
|
||||
continue // has to be a config OTHER than the one we're comparing against
|
||||
}
|
||||
if otherCfg.Host == allConfigs[cfgIndex].Host {
|
||||
if (otherCfg.Port == scheme) ||
|
||||
(scheme == "https" && otherCfg.Port == "443") ||
|
||||
(scheme == "http" && otherCfg.Port == "80") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// redirPlaintextHost returns a new plaintext HTTP configuration for
|
||||
// a virtualHost that simply redirects to cfg, which is assumed to
|
||||
// be the HTTPS configuration. The returned configuration is set
|
||||
// to listen on the "http" port (port 80).
|
||||
func redirPlaintextHost(cfg server.Config) server.Config {
|
||||
toUrl := "https://" + cfg.Host
|
||||
if cfg.Port != "https" && cfg.Port != "http" {
|
||||
toUrl += ":" + cfg.Port
|
||||
}
|
||||
|
||||
redirMidware := func(next middleware.Handler) middleware.Handler {
|
||||
return redirect.Redirect{Next: next, Rules: []redirect.Rule{
|
||||
{
|
||||
FromScheme: "http",
|
||||
FromPath: "/",
|
||||
To: toUrl + "{uri}",
|
||||
Code: http.StatusMovedPermanently,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
return server.Config{
|
||||
Host: cfg.Host,
|
||||
Port: "http",
|
||||
Middleware: map[string][]middleware.Middleware{
|
||||
"/": []middleware.Middleware{redirMidware},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Revoke revokes the certificate for host via ACME protocol.
|
||||
func Revoke(host string) error {
|
||||
if !existingCertAndKey(host) {
|
||||
return errors.New("no certificate and key for " + host)
|
||||
}
|
||||
|
||||
email := getEmail(server.Config{Host: host})
|
||||
if email == "" {
|
||||
return errors.New("email is required to revoke")
|
||||
}
|
||||
|
||||
client, err := newClient(email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certFile := storage.SiteCertFile(host)
|
||||
certBytes, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.RevokeCertificate(certBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Remove(certFile)
|
||||
if err != nil {
|
||||
return errors.New("certificate revoked, but unable to delete certificate file: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
// Let's Encrypt account email to use if none provided
|
||||
DefaultEmail string
|
||||
|
||||
// Whether user has agreed to the Let's Encrypt SA
|
||||
Agreed bool
|
||||
|
||||
// The base URL to the CA's ACME endpoint
|
||||
CAUrl string
|
||||
)
|
||||
|
||||
// Some essential values related to the Let's Encrypt process
|
||||
const (
|
||||
// The port to expose to the CA server for Simple HTTP Challenge.
|
||||
// NOTE: Let's Encrypt requires port 443. If exposePort is not 443,
|
||||
// then port 443 must be forwarded to exposePort.
|
||||
exposePort = "443"
|
||||
|
||||
// If port 443 is in use by a Caddy server instance, then this is
|
||||
// port on which the acme client will solve challenges. (Whatever is
|
||||
// listening on port 443 must proxy ACME requests to this port.)
|
||||
alternatePort = "5033"
|
||||
|
||||
// How often to check certificates for renewal.
|
||||
renewInterval = 24 * time.Hour
|
||||
|
||||
// How often to update OCSP stapling.
|
||||
ocspInterval = 1 * time.Hour
|
||||
)
|
||||
|
||||
// KeySize represents the length of a key in bits.
|
||||
type KeySize int
|
||||
|
||||
// Key sizes are used to determine the strength of a key.
|
||||
const (
|
||||
ECC_224 KeySize = 224
|
||||
ECC_256 = 256
|
||||
RSA_2048 = 2048
|
||||
RSA_4096 = 4096
|
||||
)
|
||||
|
||||
// rsaKeySizeToUse is the size to use for new RSA keys.
|
||||
// This shouldn't need to change except for in tests;
|
||||
// the size can be drastically reduced for speed.
|
||||
var rsaKeySizeToUse = RSA_2048
|
||||
|
||||
// stopChan is used to signal the maintenance goroutine
|
||||
// to terminate.
|
||||
var stopChan chan struct{}
|
||||
|
||||
// ocspStatus maps certificate bundle to OCSP status at start.
|
||||
// It is used during regular OCSP checks to see if the OCSP
|
||||
// status has changed.
|
||||
var ocspStatus = make(map[*[]byte]int)
|
||||
@@ -1,51 +0,0 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
cfg := redirPlaintextHost(server.Config{
|
||||
Host: "example.com",
|
||||
Port: "http",
|
||||
})
|
||||
|
||||
// Check host and port
|
||||
if actual, expected := cfg.Host, "example.com"; actual != expected {
|
||||
t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
|
||||
}
|
||||
if actual, expected := cfg.Port, "http"; actual != expected {
|
||||
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Make sure redirect handler is set up properly
|
||||
if cfg.Middleware == nil || len(cfg.Middleware["/"]) != 1 {
|
||||
t.Fatalf("Redir config middleware not set up properly; got: %#v", cfg.Middleware)
|
||||
}
|
||||
|
||||
handler, ok := cfg.Middleware["/"][0](nil).(redirect.Redirect)
|
||||
if !ok {
|
||||
t.Fatalf("Expected a redirect.Redirect middleware, but got: %#v", handler)
|
||||
}
|
||||
if len(handler.Rules) != 1 {
|
||||
t.Fatalf("Expected one redirect rule, got: %#v", handler.Rules)
|
||||
}
|
||||
|
||||
// Check redirect rule for correctness
|
||||
if actual, expected := handler.Rules[0].FromScheme, "http"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be from scheme '%s' but is actually from '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].FromPath, "/"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be for path '%s' but is actually for '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].To, "https://example.com{uri}"; actual != expected {
|
||||
t.Errorf("Expected redirect rule to be to URL '%s' but is actually to '%s'", expected, actual)
|
||||
}
|
||||
if actual, expected := handler.Rules[0].Code, http.StatusMovedPermanently; actual != expected {
|
||||
t.Errorf("Expected redirect rule to have code %d but was %d", expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// OnChange is a callback function that will be used to restart
|
||||
// the application or the part of the application that uses
|
||||
// the certificates maintained by this package. When at least
|
||||
// one certificate is renewed or an OCSP status changes, this
|
||||
// function will be called.
|
||||
var OnChange func() error
|
||||
|
||||
// maintainAssets is a permanently-blocking function
|
||||
// that loops indefinitely and, on a regular schedule, checks
|
||||
// certificates for expiration and initiates a renewal of certs
|
||||
// that are expiring soon. It also updates OCSP stapling and
|
||||
// performs other maintenance of assets.
|
||||
//
|
||||
// You must pass in the server configs to maintain and the channel
|
||||
// which you'll close when maintenance should stop, to allow this
|
||||
// goroutine to clean up after itself and unblock.
|
||||
func maintainAssets(configs []server.Config, stopChan chan struct{}) {
|
||||
renewalTicker := time.NewTicker(renewInterval)
|
||||
ocspTicker := time.NewTicker(ocspInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-renewalTicker.C:
|
||||
n, errs := renewCertificates(configs, true)
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Printf("[ERROR] cert renewal: %v\n", err)
|
||||
}
|
||||
}
|
||||
// even if there was an error, some renewals may have succeeded
|
||||
if n > 0 && OnChange != nil {
|
||||
err := OnChange()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] onchange after cert renewal: %v\n", err)
|
||||
}
|
||||
}
|
||||
case <-ocspTicker.C:
|
||||
for bundle, oldStatus := range ocspStatus {
|
||||
_, newStatus, err := acme.GetOCSPForCert(*bundle)
|
||||
if err == nil && newStatus != oldStatus && OnChange != nil {
|
||||
log.Printf("[INFO] ocsp status changed from %v to %v\n", oldStatus, newStatus)
|
||||
err := OnChange()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] onchange after ocsp update: %v\n", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
case <-stopChan:
|
||||
renewalTicker.Stop()
|
||||
ocspTicker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renewCertificates loops through all configured site and
|
||||
// looks for certificates to renew. Nothing is mutated
|
||||
// through this function; all changes happen directly on disk.
|
||||
// It returns the number of certificates renewed and any errors
|
||||
// that occurred. It only performs a renewal if necessary.
|
||||
// If useCustomPort is true, a custom port will be used, and
|
||||
// whatever is listening at 443 better proxy ACME requests to it.
|
||||
// Otherwise, the acme package will create its own listener on 443.
|
||||
func renewCertificates(configs []server.Config, useCustomPort bool) (int, []error) {
|
||||
log.Print("[INFO] Processing certificate renewals...")
|
||||
var errs []error
|
||||
var n int
|
||||
|
||||
defer func() {
|
||||
// reset these so as to not interfere with other challenges
|
||||
acme.OnSimpleHTTPStart = nil
|
||||
acme.OnSimpleHTTPEnd = nil
|
||||
}()
|
||||
|
||||
for _, cfg := range configs {
|
||||
// Host must be TLS-enabled and have existing assets managed by LE
|
||||
if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read the certificate and get the NotAfter time.
|
||||
certBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue // still have to check other certificates
|
||||
}
|
||||
expTime, err := acme.GetPEMCertExpiration(certBytes)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// The time returned from the certificate is always in UTC.
|
||||
// So calculate the time left with local time as UTC.
|
||||
// Directly convert it to days for the following checks.
|
||||
daysLeft := int(expTime.Sub(time.Now().UTC()).Hours() / 24)
|
||||
|
||||
// Renew with two weeks or less remaining.
|
||||
if daysLeft <= 14 {
|
||||
log.Printf("[INFO] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host)
|
||||
var client *acme.Client
|
||||
if useCustomPort {
|
||||
client, err = newClientPort("", alternatePort) // email not used for renewal
|
||||
} else {
|
||||
client, err = newClient("")
|
||||
}
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Read metadata
|
||||
metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(cfg.Host))
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
privBytes, err := ioutil.ReadFile(storage.SiteKeyFile(cfg.Host))
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var certMeta acme.CertificateResource
|
||||
err = json.Unmarshal(metaBytes, &certMeta)
|
||||
certMeta.Certificate = certBytes
|
||||
certMeta.PrivateKey = privBytes
|
||||
|
||||
// Tell the handler to accept and proxy acme request in order to solve challenge
|
||||
acme.OnSimpleHTTPStart = acmeHandlers[cfg.Host].ChallengeOn
|
||||
acme.OnSimpleHTTPEnd = acmeHandlers[cfg.Host].ChallengeOff
|
||||
|
||||
// Renew certificate.
|
||||
// TODO: revokeOld should be an option in the caddyfile
|
||||
// TODO: bundle should be an option in the caddyfile as well :)
|
||||
Renew:
|
||||
newCertMeta, err := client.RenewCertificate(certMeta, true, true)
|
||||
if err != nil {
|
||||
if _, ok := err.(acme.TOSError); ok {
|
||||
err := client.AgreeToTOS()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
goto Renew
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
newCertMeta, err = client.RenewCertificate(certMeta, true, true)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
saveCertsAndKeys([]acme.CertificateResource{newCertMeta})
|
||||
n++
|
||||
} else if daysLeft <= 30 {
|
||||
// Warn on 30 days remaining. TODO: Just do this once...
|
||||
log.Printf("[WARN] There are %d days left on the certificate for %s. Will renew when 14 days remain.\n", daysLeft, cfg.Host)
|
||||
}
|
||||
}
|
||||
|
||||
return n, errs
|
||||
}
|
||||
|
||||
// acmeHandlers is a map of host to ACME handler. These
|
||||
// are used to proxy ACME requests to the ACME client
|
||||
// when port 443 is in use.
|
||||
var acmeHandlers = make(map[string]*Handler)
|
||||
@@ -1,94 +0,0 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/assets"
|
||||
)
|
||||
|
||||
// storage is used to get file paths in a consistent,
|
||||
// cross-platform way for persisting Let's Encrypt assets
|
||||
// on the file system.
|
||||
var storage = Storage(filepath.Join(assets.Path(), "letsencrypt"))
|
||||
|
||||
// Storage is a root directory and facilitates
|
||||
// forming file paths derived from it.
|
||||
type Storage string
|
||||
|
||||
// Sites gets the directory that stores site certificate and keys.
|
||||
func (s Storage) Sites() string {
|
||||
return filepath.Join(string(s), "sites")
|
||||
}
|
||||
|
||||
// Site returns the path to the folder containing assets for domain.
|
||||
func (s Storage) Site(domain string) string {
|
||||
return filepath.Join(s.Sites(), domain)
|
||||
}
|
||||
|
||||
// CertFile returns the path to the certificate file for domain.
|
||||
func (s Storage) SiteCertFile(domain string) string {
|
||||
return filepath.Join(s.Site(domain), domain+".crt")
|
||||
}
|
||||
|
||||
// SiteKeyFile returns the path to domain's private key file.
|
||||
func (s Storage) SiteKeyFile(domain string) string {
|
||||
return filepath.Join(s.Site(domain), domain+".key")
|
||||
}
|
||||
|
||||
// SiteMetaFile returns the path to the domain's asset metadata file.
|
||||
func (s Storage) SiteMetaFile(domain string) string {
|
||||
return filepath.Join(s.Site(domain), domain+".json")
|
||||
}
|
||||
|
||||
// Users gets the directory that stores account folders.
|
||||
func (s Storage) Users() string {
|
||||
return filepath.Join(string(s), "users")
|
||||
}
|
||||
|
||||
// User gets the account folder for the user with email.
|
||||
func (s Storage) User(email string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
return filepath.Join(s.Users(), email)
|
||||
}
|
||||
|
||||
// UserRegFile gets the path to the registration file for
|
||||
// the user with the given email address.
|
||||
func (s Storage) UserRegFile(email string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
fileName := emailUsername(email)
|
||||
if fileName == "" {
|
||||
fileName = "registration"
|
||||
}
|
||||
return filepath.Join(s.User(email), fileName+".json")
|
||||
}
|
||||
|
||||
// UserKeyFile gets the path to the private key file for
|
||||
// the user with the given email address.
|
||||
func (s Storage) UserKeyFile(email string) string {
|
||||
if email == "" {
|
||||
email = emptyEmail
|
||||
}
|
||||
fileName := emailUsername(email)
|
||||
if fileName == "" {
|
||||
fileName = "private"
|
||||
}
|
||||
return filepath.Join(s.User(email), fileName+".key")
|
||||
}
|
||||
|
||||
// emailUsername returns the username portion of an
|
||||
// email address (part before '@') or the original
|
||||
// input if it can't find the "@" symbol.
|
||||
func emailUsername(email string) string {
|
||||
at := strings.Index(email, "@")
|
||||
if at == -1 {
|
||||
return email
|
||||
} else if at == 0 {
|
||||
return email[1:]
|
||||
}
|
||||
return email[:at]
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStorage(t *testing.T) {
|
||||
storage = Storage("./letsencrypt")
|
||||
|
||||
if expected, actual := filepath.Join("letsencrypt", "sites"), storage.Sites(); actual != expected {
|
||||
t.Errorf("Expected Sites() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("letsencrypt", "sites", "test.com"), storage.Site("test.com"); actual != expected {
|
||||
t.Errorf("Expected Site() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("letsencrypt", "sites", "test.com", "test.com.crt"), storage.SiteCertFile("test.com"); actual != expected {
|
||||
t.Errorf("Expected SiteCertFile() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("letsencrypt", "sites", "test.com", "test.com.key"), storage.SiteKeyFile("test.com"); actual != expected {
|
||||
t.Errorf("Expected SiteKeyFile() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("letsencrypt", "sites", "test.com", "test.com.json"), storage.SiteMetaFile("test.com"); actual != expected {
|
||||
t.Errorf("Expected SiteMetaFile() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("letsencrypt", "users"), storage.Users(); actual != expected {
|
||||
t.Errorf("Expected Users() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("letsencrypt", "users", "me@example.com"), storage.User("me@example.com"); actual != expected {
|
||||
t.Errorf("Expected User() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("letsencrypt", "users", "me@example.com", "me.json"), storage.UserRegFile("me@example.com"); actual != expected {
|
||||
t.Errorf("Expected UserRegFile() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("letsencrypt", "users", "me@example.com", "me.key"), storage.UserKeyFile("me@example.com"); actual != expected {
|
||||
t.Errorf("Expected UserKeyFile() to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
|
||||
// Test with empty emails
|
||||
if expected, actual := filepath.Join("letsencrypt", "users", emptyEmail), storage.User(emptyEmail); actual != expected {
|
||||
t.Errorf("Expected User(\"\") to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("letsencrypt", "users", emptyEmail, emptyEmail+".json"), storage.UserRegFile(""); actual != expected {
|
||||
t.Errorf("Expected UserRegFile(\"\") to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if expected, actual := filepath.Join("letsencrypt", "users", emptyEmail, emptyEmail+".key"), storage.UserKeyFile(""); actual != expected {
|
||||
t.Errorf("Expected UserKeyFile(\"\") to return '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailUsername(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input, expect string
|
||||
}{
|
||||
{
|
||||
input: "username@example.com",
|
||||
expect: "username",
|
||||
},
|
||||
{
|
||||
input: "plus+addressing@example.com",
|
||||
expect: "plus+addressing",
|
||||
},
|
||||
{
|
||||
input: "me+plus-addressing@example.com",
|
||||
expect: "me+plus-addressing",
|
||||
},
|
||||
{
|
||||
input: "not-an-email",
|
||||
expect: "not-an-email",
|
||||
},
|
||||
{
|
||||
input: "@foobar.com",
|
||||
expect: "foobar.com",
|
||||
},
|
||||
{
|
||||
input: emptyEmail,
|
||||
expect: emptyEmail,
|
||||
},
|
||||
} {
|
||||
if actual := emailUsername(test.input); actual != test.expect {
|
||||
t.Errorf("Test %d: Expected username to be '%s' but was '%s'", i, test.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// User represents a Let's Encrypt user account.
|
||||
type User struct {
|
||||
Email string
|
||||
Registration *acme.RegistrationResource
|
||||
key *rsa.PrivateKey
|
||||
}
|
||||
|
||||
// GetEmail gets u's email.
|
||||
func (u User) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
// GetRegistration gets u's registration resource.
|
||||
func (u User) GetRegistration() *acme.RegistrationResource {
|
||||
return u.Registration
|
||||
}
|
||||
|
||||
// GetPrivateKey gets u's private key.
|
||||
func (u User) GetPrivateKey() *rsa.PrivateKey {
|
||||
return u.key
|
||||
}
|
||||
|
||||
// getUser loads the user with the given email from disk.
|
||||
// If the user does not exist, it will create a new one,
|
||||
// but it does NOT save new users to the disk or register
|
||||
// them via ACME.
|
||||
func getUser(email string) (User, error) {
|
||||
var user User
|
||||
|
||||
// open user file
|
||||
regFile, err := os.Open(storage.UserRegFile(email))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// create a new user
|
||||
return newUser(email)
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
defer regFile.Close()
|
||||
|
||||
// load user information
|
||||
err = json.NewDecoder(regFile).Decode(&user)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
// load their private key
|
||||
user.key, err = loadRSAPrivateKey(storage.UserKeyFile(email))
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// saveUser persists a user's key and account registration
|
||||
// to the file system. It does NOT register the user via ACME.
|
||||
func saveUser(user User) error {
|
||||
// make user account folder
|
||||
err := os.MkdirAll(storage.User(user.Email), 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// save private key file
|
||||
err = saveRSAPrivateKey(user.key, storage.UserKeyFile(user.Email))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// save registration file
|
||||
jsonBytes, err := json.MarshalIndent(&user, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(storage.UserRegFile(user.Email), jsonBytes, 0600)
|
||||
}
|
||||
|
||||
// newUser creates a new User for the given email address
|
||||
// with a new private key. This function does NOT save the
|
||||
// user to disk or register it via ACME. If you want to use
|
||||
// a user account that might already exist, call getUser
|
||||
// instead.
|
||||
func newUser(email string) (User, error) {
|
||||
user := User{Email: email}
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeySizeToUse)
|
||||
if err != nil {
|
||||
return user, errors.New("error generating private key: " + err.Error())
|
||||
}
|
||||
user.key = privateKey
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// getEmail does everything it can to obtain an email
|
||||
// address from the user to use for TLS for cfg. If it
|
||||
// cannot get an email address, it returns empty string.
|
||||
// (It will warn the user of the consequences of an
|
||||
// empty email.)
|
||||
func getEmail(cfg server.Config) string {
|
||||
// First try the tls directive from the Caddyfile
|
||||
leEmail := cfg.TLS.LetsEncryptEmail
|
||||
if leEmail == "" {
|
||||
// Then try memory (command line flag or typed by user previously)
|
||||
leEmail = DefaultEmail
|
||||
}
|
||||
if leEmail == "" {
|
||||
// Then try to get most recent user email ~/.caddy/users file
|
||||
userDirs, err := ioutil.ReadDir(storage.Users())
|
||||
if err == nil {
|
||||
var mostRecent os.FileInfo
|
||||
for _, dir := range userDirs {
|
||||
if !dir.IsDir() {
|
||||
continue
|
||||
}
|
||||
if mostRecent == nil || dir.ModTime().After(mostRecent.ModTime()) {
|
||||
mostRecent = dir
|
||||
}
|
||||
}
|
||||
if mostRecent != nil {
|
||||
leEmail = mostRecent.Name()
|
||||
}
|
||||
}
|
||||
}
|
||||
if leEmail == "" {
|
||||
// Alas, we must bother the user and ask for an email address;
|
||||
// if they proceed they also agree to the SA.
|
||||
reader := bufio.NewReader(stdin)
|
||||
fmt.Println("Your sites will be served over HTTPS automatically using Let's Encrypt.")
|
||||
fmt.Println("By continuing, you agree to the Let's Encrypt Subscriber Agreement at:")
|
||||
fmt.Println(" " + saURL) // TODO: Show current SA link
|
||||
fmt.Println("Please enter your email address so you can recover your account if needed.")
|
||||
fmt.Println("You can leave it blank, but you'll lose the ability to recover your account.")
|
||||
fmt.Print("Email address: ")
|
||||
var err error
|
||||
leEmail, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
DefaultEmail = leEmail
|
||||
Agreed = true
|
||||
}
|
||||
return strings.TrimSpace(leEmail)
|
||||
}
|
||||
|
||||
// promptUserAgreement prompts the user to agree to the agreement
|
||||
// at agreementURL via stdin. If the agreement has changed, then pass
|
||||
// true as the second argument. If this is the user's first time
|
||||
// agreeing, pass false. It returns whether the user agreed or not.
|
||||
func promptUserAgreement(agreementURL string, changed bool) bool {
|
||||
if changed {
|
||||
fmt.Printf("The Let's Encrypt Subscriber Agreement has changed:\n %s\n", agreementURL)
|
||||
fmt.Print("Do you agree to the new terms? (y/n): ")
|
||||
} else {
|
||||
fmt.Printf("To continue, you must agree to the Let's Encrypt Subscriber Agreement:\n %s\n", agreementURL)
|
||||
fmt.Print("Do you agree to the terms? (y/n): ")
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(stdin)
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
answer = strings.ToLower(strings.TrimSpace(answer))
|
||||
|
||||
return answer == "y" || answer == "yes"
|
||||
}
|
||||
|
||||
// stdin is used to read the user's input if prompted;
|
||||
// this is changed by tests during tests.
|
||||
var stdin = io.ReadWriter(os.Stdin)
|
||||
|
||||
// The name of the folder for accounts where the email
|
||||
// address was not provided; default 'username' if you will.
|
||||
const emptyEmail = "default"
|
||||
|
||||
// TODO: Use latest
|
||||
const saURL = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
|
||||
@@ -1,192 +0,0 @@
|
||||
package letsencrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 128)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not generate test private key: %v", err)
|
||||
}
|
||||
u := User{
|
||||
Email: "me@mine.com",
|
||||
Registration: new(acme.RegistrationResource),
|
||||
key: privateKey,
|
||||
}
|
||||
|
||||
if expected, actual := "me@mine.com", u.GetEmail(); actual != expected {
|
||||
t.Errorf("Expected email '%s' but got '%s'", expected, actual)
|
||||
}
|
||||
if u.GetRegistration() == nil {
|
||||
t.Error("Expected a registration resource, but got nil")
|
||||
}
|
||||
if expected, actual := privateKey, u.GetPrivateKey(); actual != expected {
|
||||
t.Errorf("Expected the private key at address %p but got one at %p instead ", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUser(t *testing.T) {
|
||||
email := "me@foobar.com"
|
||||
user, err := newUser(email)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating user: %v", err)
|
||||
}
|
||||
if user.key == nil {
|
||||
t.Error("Private key is nil")
|
||||
}
|
||||
if user.Email != email {
|
||||
t.Errorf("Expected email to be %s, but was %s", email, user.Email)
|
||||
}
|
||||
if user.Registration != nil {
|
||||
t.Error("New user already has a registration resource; it shouldn't")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveUser(t *testing.T) {
|
||||
storage = Storage("./testdata")
|
||||
defer os.RemoveAll(string(storage))
|
||||
|
||||
email := "me@foobar.com"
|
||||
user, err := newUser(email)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating user: %v", err)
|
||||
}
|
||||
|
||||
err = saveUser(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Error saving user: %v", err)
|
||||
}
|
||||
_, err = os.Stat(storage.UserRegFile(email))
|
||||
if err != nil {
|
||||
t.Errorf("Cannot access user registration file, error: %v", err)
|
||||
}
|
||||
_, err = os.Stat(storage.UserKeyFile(email))
|
||||
if err != nil {
|
||||
t.Errorf("Cannot access user private key file, error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserDoesNotAlreadyExist(t *testing.T) {
|
||||
storage = Storage("./testdata")
|
||||
defer os.RemoveAll(string(storage))
|
||||
|
||||
user, err := getUser("user_does_not_exist@foobar.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting user: %v", err)
|
||||
}
|
||||
|
||||
if user.key == nil {
|
||||
t.Error("Expected user to have a private key, but it was nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserAlreadyExists(t *testing.T) {
|
||||
storage = Storage("./testdata")
|
||||
defer os.RemoveAll(string(storage))
|
||||
|
||||
email := "me@foobar.com"
|
||||
|
||||
// Set up test
|
||||
user, err := newUser(email)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating user: %v", err)
|
||||
}
|
||||
err = saveUser(user)
|
||||
if err != nil {
|
||||
t.Fatalf("Error saving user: %v", err)
|
||||
}
|
||||
|
||||
// Expect to load user from disk
|
||||
user2, err := getUser(email)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting user: %v", err)
|
||||
}
|
||||
|
||||
// Assert keys are the same
|
||||
if !rsaPrivateKeysSame(user.key, user2.key) {
|
||||
t.Error("Expected private key to be the same after loading, but it wasn't")
|
||||
}
|
||||
|
||||
// Assert emails are the same
|
||||
if user.Email != user2.Email {
|
||||
t.Errorf("Expected emails to be equal, but was '%s' before and '%s' after loading", user.Email, user2.Email)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEmail(t *testing.T) {
|
||||
storage = Storage("./testdata")
|
||||
defer os.RemoveAll(string(storage))
|
||||
DefaultEmail = "test2@foo.com"
|
||||
|
||||
// Test1: Use email in config
|
||||
config := server.Config{
|
||||
TLS: server.TLSConfig{
|
||||
LetsEncryptEmail: "test1@foo.com",
|
||||
},
|
||||
}
|
||||
actual := getEmail(config)
|
||||
if actual != "test1@foo.com" {
|
||||
t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", "test1@foo.com", actual)
|
||||
}
|
||||
|
||||
// Test2: Use default email from flag (or user previously typing it)
|
||||
actual = getEmail(server.Config{})
|
||||
if actual != DefaultEmail {
|
||||
t.Errorf("Did not get correct email from config; expected '%s' but got '%s'", DefaultEmail, actual)
|
||||
}
|
||||
|
||||
// Test3: Get input from user
|
||||
DefaultEmail = ""
|
||||
stdin = new(bytes.Buffer)
|
||||
_, err := io.Copy(stdin, strings.NewReader("test3@foo.com\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not simulate user input, error: %v", err)
|
||||
}
|
||||
actual = getEmail(server.Config{})
|
||||
if actual != "test3@foo.com" {
|
||||
t.Errorf("Did not get correct email from user input prompt; expected '%s' but got '%s'", "test3@foo.com", actual)
|
||||
}
|
||||
|
||||
// Test4: Get most recent email from before
|
||||
DefaultEmail = ""
|
||||
for i, eml := range []string{
|
||||
"test4-3@foo.com",
|
||||
"test4-2@foo.com",
|
||||
"test4-1@foo.com",
|
||||
} {
|
||||
u, err := newUser(eml)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating user %d: %v", i, err)
|
||||
}
|
||||
err = saveUser(u)
|
||||
if err != nil {
|
||||
t.Fatalf("Error saving user %d: %v", i, err)
|
||||
}
|
||||
|
||||
// Change modified time so they're all different, so the test becomes deterministic
|
||||
f, err := os.Stat(storage.User(eml))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not access user folder for '%s': %v", eml, err)
|
||||
}
|
||||
chTime := f.ModTime().Add(-(time.Duration(i) * time.Second))
|
||||
if err := os.Chtimes(storage.User(eml), chTime, chTime); err != nil {
|
||||
t.Fatalf("Could not change user folder mod time for '%s': %v", eml, err)
|
||||
}
|
||||
}
|
||||
|
||||
actual = getEmail(server.Config{})
|
||||
if actual != "test4-3@foo.com" {
|
||||
t.Errorf("Did not get correct email from storage; expected '%s' but got '%s'", "test4-3@foo.com", actual)
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
||||
// except that it can do so with some notion of structure and has
|
||||
// some really convenient methods.
|
||||
type Dispenser struct {
|
||||
filename string
|
||||
tokens []token
|
||||
cursor int
|
||||
nesting int
|
||||
}
|
||||
|
||||
// NewDispenser returns a Dispenser, ready to use for parsing the given input.
|
||||
func NewDispenser(filename string, input io.Reader) Dispenser {
|
||||
return Dispenser{
|
||||
filename: filename,
|
||||
tokens: allTokens(input),
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDispenserTokens returns a Dispenser filled with the given tokens.
|
||||
func NewDispenserTokens(filename string, tokens []token) Dispenser {
|
||||
return Dispenser{
|
||||
filename: filename,
|
||||
tokens: tokens,
|
||||
cursor: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// Next loads the next token. Returns true if a token
|
||||
// was loaded; false otherwise. If false, all tokens
|
||||
// have been consumed.
|
||||
func (d *Dispenser) Next() bool {
|
||||
if d.cursor < len(d.tokens)-1 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextArg loads the next token if it is on the same
|
||||
// line. Returns true if a token was loaded; false
|
||||
// otherwise. If false, all tokens on the line have
|
||||
// been consumed. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextArg() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
d.tokens[d.cursor].file == d.tokens[d.cursor+1].file &&
|
||||
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) == d.tokens[d.cursor+1].line {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextLine loads the next token only if it is not on the same
|
||||
// line as the current token, and returns true if a token was
|
||||
// loaded; false otherwise. If false, there is not another token
|
||||
// or it is on the same line. It handles imported tokens correctly.
|
||||
func (d *Dispenser) NextLine() bool {
|
||||
if d.cursor < 0 {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
if d.cursor >= len(d.tokens) {
|
||||
return false
|
||||
}
|
||||
if d.cursor < len(d.tokens)-1 &&
|
||||
(d.tokens[d.cursor].file != d.tokens[d.cursor+1].file ||
|
||||
d.tokens[d.cursor].line+d.numLineBreaks(d.cursor) < d.tokens[d.cursor+1].line) {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextBlock can be used as the condition of a for loop
|
||||
// to load the next token as long as it opens a block or
|
||||
// is already in a block. It returns true if a token was
|
||||
// loaded, or false when the block's closing curly brace
|
||||
// was loaded and thus the block ended. Nested blocks are
|
||||
// not supported.
|
||||
func (d *Dispenser) NextBlock() bool {
|
||||
if d.nesting > 0 {
|
||||
d.Next()
|
||||
if d.Val() == "}" {
|
||||
d.nesting--
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if !d.NextArg() { // block must open on same line
|
||||
return false
|
||||
}
|
||||
if d.Val() != "{" {
|
||||
d.cursor-- // roll back if not opening brace
|
||||
return false
|
||||
}
|
||||
d.Next()
|
||||
if d.Val() == "}" {
|
||||
// Open and then closed right away
|
||||
return false
|
||||
}
|
||||
d.nesting++
|
||||
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 {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return ""
|
||||
}
|
||||
return d.tokens[d.cursor].text
|
||||
}
|
||||
|
||||
// Line gets the line number of the current token. If there is no token
|
||||
// loaded, it returns 0.
|
||||
func (d *Dispenser) Line() int {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return d.tokens[d.cursor].line
|
||||
}
|
||||
|
||||
// File gets the filename of the current token. If there is no token loaded,
|
||||
// it returns the filename originally given when parsing started.
|
||||
func (d *Dispenser) File() string {
|
||||
if d.cursor < 0 || d.cursor >= len(d.tokens) {
|
||||
return d.filename
|
||||
}
|
||||
if tokenFilename := d.tokens[d.cursor].file; tokenFilename != "" {
|
||||
return tokenFilename
|
||||
}
|
||||
return d.filename
|
||||
}
|
||||
|
||||
// Args is a convenience function that loads the next arguments
|
||||
// (tokens on the same line) into an arbitrary number of strings
|
||||
// pointed to in targets. If there are fewer tokens available
|
||||
// than string pointers, the remaining strings will not be changed
|
||||
// and false will be returned. If there were enough tokens available
|
||||
// to fill the arguments, then true will be returned.
|
||||
func (d *Dispenser) Args(targets ...*string) bool {
|
||||
enough := true
|
||||
for i := 0; i < len(targets); i++ {
|
||||
if !d.NextArg() {
|
||||
enough = false
|
||||
break
|
||||
}
|
||||
*targets[i] = d.Val()
|
||||
}
|
||||
return enough
|
||||
}
|
||||
|
||||
// RemainingArgs loads any more arguments (tokens on the same line)
|
||||
// into a slice and returns them. Open curly brace tokens also indicate
|
||||
// the end of arguments, and the curly brace is not included in
|
||||
// the return value nor is it loaded.
|
||||
func (d *Dispenser) RemainingArgs() []string {
|
||||
var args []string
|
||||
|
||||
for d.NextArg() {
|
||||
if d.Val() == "{" {
|
||||
d.cursor--
|
||||
break
|
||||
}
|
||||
args = append(args, d.Val())
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// ArgErr returns an argument error, meaning that another
|
||||
// argument was expected but not found. In other words,
|
||||
// a line break or open curly brace was encountered instead of
|
||||
// an argument.
|
||||
func (d *Dispenser) ArgErr() error {
|
||||
if d.Val() == "{" {
|
||||
return d.Err("Unexpected token '{', expecting argument")
|
||||
}
|
||||
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||
}
|
||||
|
||||
// SyntaxErr creates a generic syntax error which explains what was
|
||||
// found and what was expected.
|
||||
func (d *Dispenser) SyntaxErr(expected string) error {
|
||||
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// EOFErr returns an error indicating that the dispenser reached
|
||||
// the end of the input when searching for the next token.
|
||||
func (d *Dispenser) EOFErr() error {
|
||||
return d.Errf("Unexpected EOF")
|
||||
}
|
||||
|
||||
// Err generates a custom parse error with a message of msg.
|
||||
func (d *Dispenser) Err(msg string) error {
|
||||
msg = fmt.Sprintf("%s:%d - Parse error: %s", d.File(), d.Line(), msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// Errf is like Err, but for formatted error messages
|
||||
func (d *Dispenser) Errf(format string, args ...interface{}) error {
|
||||
return d.Err(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// numLineBreaks counts how many line breaks are in the token
|
||||
// value given by the token index tknIdx. It returns 0 if the
|
||||
// token does not exist or there are no line breaks.
|
||||
func (d *Dispenser) numLineBreaks(tknIdx int) int {
|
||||
if tknIdx < 0 || tknIdx >= len(d.tokens) {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(d.tokens[tknIdx].text, "\n")
|
||||
}
|
||||
|
||||
// isNewLine determines whether the current token is on a different
|
||||
// line (higher line number) than the previous token. It handles imported
|
||||
// tokens correctly. If there isn't a previous token, it returns true.
|
||||
func (d *Dispenser) isNewLine() bool {
|
||||
if d.cursor < 1 {
|
||||
return true
|
||||
}
|
||||
if d.cursor > len(d.tokens)-1 {
|
||||
return false
|
||||
}
|
||||
return d.tokens[d.cursor-1].file != d.tokens[d.cursor].file ||
|
||||
d.tokens[d.cursor-1].line+d.numLineBreaks(d.cursor-1) < d.tokens[d.cursor].line
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDispenser_Val_Next(t *testing.T) {
|
||||
input := `host:port
|
||||
dir1 arg1
|
||||
dir2 arg2 arg3
|
||||
dir3`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
if val := d.Val(); val != "" {
|
||||
t.Fatalf("Val(): Should return empty string when no token loaded; got '%s'", val)
|
||||
}
|
||||
|
||||
assertNext := func(shouldLoad bool, expectedCursor int, expectedVal string) {
|
||||
if loaded := d.Next(); loaded != shouldLoad {
|
||||
t.Errorf("Next(): Expected %v but got %v instead (val '%s')", shouldLoad, loaded, d.Val())
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
t.Errorf("Expected cursor to be %d, but was %d", expectedCursor, d.cursor)
|
||||
}
|
||||
if d.nesting != 0 {
|
||||
t.Errorf("Nesting should be 0, was %d instead", d.nesting)
|
||||
}
|
||||
if val := d.Val(); val != expectedVal {
|
||||
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
|
||||
}
|
||||
}
|
||||
|
||||
assertNext(true, 0, "host:port")
|
||||
assertNext(true, 1, "dir1")
|
||||
assertNext(true, 2, "arg1")
|
||||
assertNext(true, 3, "dir2")
|
||||
assertNext(true, 4, "arg2")
|
||||
assertNext(true, 5, "arg3")
|
||||
assertNext(true, 6, "dir3")
|
||||
// Note: This next test simply asserts existing behavior.
|
||||
// If desired, we may wish to empty the token value after
|
||||
// reading past the EOF. Open an issue if you want this change.
|
||||
assertNext(false, 6, "dir3")
|
||||
}
|
||||
|
||||
func TestDispenser_NextArg(t *testing.T) {
|
||||
input := `dir1 arg1
|
||||
dir2 arg2 arg3
|
||||
dir3`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
assertNext := func(shouldLoad bool, expectedVal string, expectedCursor int) {
|
||||
if d.Next() != shouldLoad {
|
||||
t.Errorf("Next(): Should load token but got false instead (val: '%s')", d.Val())
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
t.Errorf("Next(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor)
|
||||
}
|
||||
if val := d.Val(); val != expectedVal {
|
||||
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
|
||||
}
|
||||
}
|
||||
|
||||
assertNextArg := func(expectedVal string, loadAnother bool, expectedCursor int) {
|
||||
if d.NextArg() != true {
|
||||
t.Error("NextArg(): Should load next argument but got false instead")
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
t.Errorf("NextArg(): Expected cursor to be at %d, but it was %d", expectedCursor, d.cursor)
|
||||
}
|
||||
if val := d.Val(); val != expectedVal {
|
||||
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
|
||||
}
|
||||
if !loadAnother {
|
||||
if d.NextArg() != false {
|
||||
t.Fatalf("NextArg(): Should NOT load another argument, but got true instead (val: '%s')", d.Val())
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
t.Errorf("NextArg(): Expected cursor to remain at %d, but it was %d", expectedCursor, d.cursor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertNext(true, "dir1", 0)
|
||||
assertNextArg("arg1", false, 1)
|
||||
assertNext(true, "dir2", 2)
|
||||
assertNextArg("arg2", true, 3)
|
||||
assertNextArg("arg3", false, 4)
|
||||
assertNext(true, "dir3", 5)
|
||||
assertNext(false, "dir3", 5)
|
||||
}
|
||||
|
||||
func TestDispenser_NextLine(t *testing.T) {
|
||||
input := `host:port
|
||||
dir1 arg1
|
||||
dir2 arg2 arg3`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
assertNextLine := func(shouldLoad bool, expectedVal string, expectedCursor int) {
|
||||
if d.NextLine() != shouldLoad {
|
||||
t.Errorf("NextLine(): Should load token but got false instead (val: '%s')", d.Val())
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
t.Errorf("NextLine(): Expected cursor to be %d, instead was %d", expectedCursor, d.cursor)
|
||||
}
|
||||
if val := d.Val(); val != expectedVal {
|
||||
t.Errorf("Val(): Expected '%s' but got '%s'", expectedVal, val)
|
||||
}
|
||||
}
|
||||
|
||||
assertNextLine(true, "host:port", 0)
|
||||
assertNextLine(true, "dir1", 1)
|
||||
assertNextLine(false, "dir1", 1)
|
||||
d.Next() // arg1
|
||||
assertNextLine(true, "dir2", 3)
|
||||
assertNextLine(false, "dir2", 3)
|
||||
d.Next() // arg2
|
||||
assertNextLine(false, "arg2", 4)
|
||||
d.Next() // arg3
|
||||
assertNextLine(false, "arg3", 5)
|
||||
}
|
||||
|
||||
func TestDispenser_NextBlock(t *testing.T) {
|
||||
input := `foobar1 {
|
||||
sub1 arg1
|
||||
sub2
|
||||
}
|
||||
foobar2 {
|
||||
}`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
assertNextBlock := func(shouldLoad bool, expectedCursor, expectedNesting int) {
|
||||
if loaded := d.NextBlock(); loaded != shouldLoad {
|
||||
t.Errorf("NextBlock(): Should return %v but got %v", shouldLoad, loaded)
|
||||
}
|
||||
if d.cursor != expectedCursor {
|
||||
t.Errorf("NextBlock(): Expected cursor to be %d, was %d", expectedCursor, d.cursor)
|
||||
}
|
||||
if d.nesting != expectedNesting {
|
||||
t.Errorf("NextBlock(): Nesting should be %d, not %d", expectedNesting, d.nesting)
|
||||
}
|
||||
}
|
||||
|
||||
assertNextBlock(false, -1, 0)
|
||||
d.Next() // foobar1
|
||||
assertNextBlock(true, 2, 1)
|
||||
assertNextBlock(true, 3, 1)
|
||||
assertNextBlock(true, 4, 1)
|
||||
assertNextBlock(false, 5, 0)
|
||||
d.Next() // foobar2
|
||||
assertNextBlock(false, 8, 0) // empty block is as if it didn't exist
|
||||
}
|
||||
|
||||
func TestDispenser_Args(t *testing.T) {
|
||||
var s1, s2, s3 string
|
||||
input := `dir1 arg1 arg2 arg3
|
||||
dir2 arg4 arg5
|
||||
dir3 arg6 arg7
|
||||
dir4`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
d.Next() // dir1
|
||||
|
||||
// As many strings as arguments
|
||||
if all := d.Args(&s1, &s2, &s3); !all {
|
||||
t.Error("Args(): Expected true, got false")
|
||||
}
|
||||
if s1 != "arg1" {
|
||||
t.Errorf("Args(): Expected s1 to be 'arg1', got '%s'", s1)
|
||||
}
|
||||
if s2 != "arg2" {
|
||||
t.Errorf("Args(): Expected s2 to be 'arg2', got '%s'", s2)
|
||||
}
|
||||
if s3 != "arg3" {
|
||||
t.Errorf("Args(): Expected s3 to be 'arg3', got '%s'", s3)
|
||||
}
|
||||
|
||||
d.Next() // dir2
|
||||
|
||||
// More strings than arguments
|
||||
if all := d.Args(&s1, &s2, &s3); all {
|
||||
t.Error("Args(): Expected false, got true")
|
||||
}
|
||||
if s1 != "arg4" {
|
||||
t.Errorf("Args(): Expected s1 to be 'arg4', got '%s'", s1)
|
||||
}
|
||||
if s2 != "arg5" {
|
||||
t.Errorf("Args(): Expected s2 to be 'arg5', got '%s'", s2)
|
||||
}
|
||||
if s3 != "arg3" {
|
||||
t.Errorf("Args(): Expected s3 to be unchanged ('arg3'), instead got '%s'", s3)
|
||||
}
|
||||
|
||||
// (quick cursor check just for kicks and giggles)
|
||||
if d.cursor != 6 {
|
||||
t.Errorf("Cursor should be 6, but is %d", d.cursor)
|
||||
}
|
||||
|
||||
d.Next() // dir3
|
||||
|
||||
// More arguments than strings
|
||||
if all := d.Args(&s1); !all {
|
||||
t.Error("Args(): Expected true, got false")
|
||||
}
|
||||
if s1 != "arg6" {
|
||||
t.Errorf("Args(): Expected s1 to be 'arg6', got '%s'", s1)
|
||||
}
|
||||
|
||||
d.Next() // dir4
|
||||
|
||||
// No arguments or strings
|
||||
if all := d.Args(); !all {
|
||||
t.Error("Args(): Expected true, got false")
|
||||
}
|
||||
|
||||
// No arguments but at least one string
|
||||
if all := d.Args(&s1); all {
|
||||
t.Error("Args(): Expected false, got true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispenser_RemainingArgs(t *testing.T) {
|
||||
input := `dir1 arg1 arg2 arg3
|
||||
dir2 arg4 arg5
|
||||
dir3 arg6 { arg7
|
||||
dir4`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
d.Next() // dir1
|
||||
|
||||
args := d.RemainingArgs()
|
||||
if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(args, expected) {
|
||||
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
|
||||
}
|
||||
|
||||
d.Next() // dir2
|
||||
|
||||
args = d.RemainingArgs()
|
||||
if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(args, expected) {
|
||||
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
|
||||
}
|
||||
|
||||
d.Next() // dir3
|
||||
|
||||
args = d.RemainingArgs()
|
||||
if expected := []string{"arg6"}; !reflect.DeepEqual(args, expected) {
|
||||
t.Errorf("RemainingArgs(): Expected %v, got %v", expected, args)
|
||||
}
|
||||
|
||||
d.Next() // {
|
||||
d.Next() // arg7
|
||||
d.Next() // dir4
|
||||
|
||||
args = d.RemainingArgs()
|
||||
if len(args) != 0 {
|
||||
t.Errorf("RemainingArgs(): Expected %v, got %v", []string{}, args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispenser_ArgErr_Err(t *testing.T) {
|
||||
input := `dir1 {
|
||||
}
|
||||
dir2 arg1 arg2`
|
||||
d := NewDispenser("Testfile", strings.NewReader(input))
|
||||
|
||||
d.cursor = 1 // {
|
||||
|
||||
if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "{") {
|
||||
t.Errorf("ArgErr(): Expected an error message with { in it, but got '%v'", err)
|
||||
}
|
||||
|
||||
d.cursor = 5 // arg2
|
||||
|
||||
if err := d.ArgErr(); err == nil || !strings.Contains(err.Error(), "arg2") {
|
||||
t.Errorf("ArgErr(): Expected an error message with 'arg2' in it; got '%v'", err)
|
||||
}
|
||||
|
||||
err := d.Err("foobar")
|
||||
if err == nil {
|
||||
t.Fatalf("Err(): Expected an error, got nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "Testfile:3") {
|
||||
t.Errorf("Expected error message with filename:line in it; got '%v'", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "foobar") {
|
||||
t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
dir2 arg1 arg2
|
||||
dir3
|
||||
@@ -1,4 +0,0 @@
|
||||
host1 {
|
||||
dir1
|
||||
dir2 arg1
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type (
|
||||
// lexer is a utility which can get values, token by
|
||||
// token, from a Reader. A token is a word, and tokens
|
||||
// are separated by whitespace. A word can be enclosed
|
||||
// in quotes if it contains whitespace.
|
||||
lexer struct {
|
||||
reader *bufio.Reader
|
||||
token token
|
||||
line int
|
||||
}
|
||||
|
||||
// token represents a single parsable unit.
|
||||
token struct {
|
||||
file string
|
||||
line int
|
||||
text string
|
||||
}
|
||||
)
|
||||
|
||||
// load prepares the lexer to scan an input for tokens.
|
||||
func (l *lexer) load(input io.Reader) error {
|
||||
l.reader = bufio.NewReader(input)
|
||||
l.line = 1
|
||||
return nil
|
||||
}
|
||||
|
||||
// next loads the next token into the lexer.
|
||||
// A token is delimited by whitespace, unless
|
||||
// the token starts with a quotes character (")
|
||||
// in which case the token goes until the closing
|
||||
// quotes (the enclosing quotes are not included).
|
||||
// Inside quoted strings, quotes may be escaped
|
||||
// with a preceding \ character. No other chars
|
||||
// may be escaped. The rest of the line is skipped
|
||||
// if a "#" character is read in. Returns true if
|
||||
// a token was loaded; false otherwise.
|
||||
func (l *lexer) next() bool {
|
||||
var val []rune
|
||||
var comment, quoted, escaped bool
|
||||
|
||||
makeToken := func() bool {
|
||||
l.token.text = string(val)
|
||||
return true
|
||||
}
|
||||
|
||||
for {
|
||||
ch, _, err := l.reader.ReadRune()
|
||||
if err != nil {
|
||||
if len(val) > 0 {
|
||||
return makeToken()
|
||||
}
|
||||
if err == io.EOF {
|
||||
return false
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if quoted {
|
||||
if !escaped {
|
||||
if ch == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
} else if ch == '"' {
|
||||
quoted = false
|
||||
return makeToken()
|
||||
}
|
||||
}
|
||||
if ch == '\n' {
|
||||
l.line++
|
||||
}
|
||||
if escaped {
|
||||
// only escape quotes
|
||||
if ch != '"' {
|
||||
val = append(val, '\\')
|
||||
}
|
||||
}
|
||||
val = append(val, ch)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if unicode.IsSpace(ch) {
|
||||
if ch == '\r' {
|
||||
continue
|
||||
}
|
||||
if ch == '\n' {
|
||||
l.line++
|
||||
comment = false
|
||||
}
|
||||
if len(val) > 0 {
|
||||
return makeToken()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '#' {
|
||||
comment = true
|
||||
}
|
||||
|
||||
if comment {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(val) == 0 {
|
||||
l.token = token{line: l.line}
|
||||
if ch == '"' {
|
||||
quoted = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
val = append(val, ch)
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type lexerTestCase struct {
|
||||
input string
|
||||
expected []token
|
||||
}
|
||||
|
||||
func TestLexer(t *testing.T) {
|
||||
testCases := []lexerTestCase{
|
||||
{
|
||||
input: `host:123`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123
|
||||
|
||||
directive`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 3, text: "directive"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
directive
|
||||
}`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 1, text: "{"},
|
||||
{line: 2, text: "directive"},
|
||||
{line: 3, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 { directive }`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 1, text: "{"},
|
||||
{line: 1, text: "directive"},
|
||||
{line: 1, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `host:123 {
|
||||
#comment
|
||||
directive
|
||||
# comment
|
||||
foobar # another comment
|
||||
}`,
|
||||
expected: []token{
|
||||
{line: 1, text: "host:123"},
|
||||
{line: 1, text: "{"},
|
||||
{line: 3, text: "directive"},
|
||||
{line: 5, text: "foobar"},
|
||||
{line: 6, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `a "quoted value" b
|
||||
foobar`,
|
||||
expected: []token{
|
||||
{line: 1, text: "a"},
|
||||
{line: 1, text: "quoted value"},
|
||||
{line: 1, text: "b"},
|
||||
{line: 2, text: "foobar"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted \"value\" inside" B`,
|
||||
expected: []token{
|
||||
{line: 1, text: "A"},
|
||||
{line: 1, text: `quoted "value" inside`},
|
||||
{line: 1, text: "B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\escape"`,
|
||||
expected: []token{
|
||||
{line: 1, text: `don't\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"don't\\escape"`,
|
||||
expected: []token{
|
||||
{line: 1, text: `don't\\escape`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `A "quoted value with line
|
||||
break inside" {
|
||||
foobar
|
||||
}`,
|
||||
expected: []token{
|
||||
{line: 1, text: "A"},
|
||||
{line: 1, text: "quoted value with line\n\t\t\t\t\tbreak inside"},
|
||||
{line: 2, text: "{"},
|
||||
{line: 3, text: "foobar"},
|
||||
{line: 4, text: "}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `"C:\php\php-cgi.exe"`,
|
||||
expected: []token{
|
||||
{line: 1, text: `C:\php\php-cgi.exe`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `empty "" string`,
|
||||
expected: []token{
|
||||
{line: 1, text: `empty`},
|
||||
{line: 1, text: ``},
|
||||
{line: 1, text: `string`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "skip those\r\nCR characters",
|
||||
expected: []token{
|
||||
{line: 1, text: "skip"},
|
||||
{line: 1, text: "those"},
|
||||
{line: 2, text: "CR"},
|
||||
{line: 2, text: "characters"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actual := tokenize(testCase.input)
|
||||
lexerCompare(t, i, testCase.expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func tokenize(input string) (tokens []token) {
|
||||
l := lexer{}
|
||||
l.load(strings.NewReader(input))
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func lexerCompare(t *testing.T, n int, expected, actual []token) {
|
||||
if len(expected) != len(actual) {
|
||||
t.Errorf("Test case %d: expected %d token(s) but got %d", n, len(expected), len(actual))
|
||||
}
|
||||
|
||||
for i := 0; i < len(actual) && i < len(expected); i++ {
|
||||
if actual[i].line != expected[i].line {
|
||||
t.Errorf("Test case %d token %d ('%s'): expected line %d but was line %d",
|
||||
n, i, expected[i].text, expected[i].line, actual[i].line)
|
||||
break
|
||||
}
|
||||
if actual[i].text != expected[i].text {
|
||||
t.Errorf("Test case %d token %d: expected text '%s' but was '%s'",
|
||||
n, i, expected[i].text, actual[i].text)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Package parse provides facilities for parsing configuration files.
|
||||
package parse
|
||||
|
||||
import "io"
|
||||
|
||||
// ServerBlocks parses the input just enough to organize tokens,
|
||||
// in order, by server block. No further parsing is performed.
|
||||
// If checkDirectives is true, only valid directives will be allowed
|
||||
// otherwise we consider it a parse error. Server blocks are returned
|
||||
// in the order in which they appear.
|
||||
func ServerBlocks(filename string, input io.Reader, checkDirectives bool) ([]serverBlock, error) {
|
||||
p := parser{Dispenser: NewDispenser(filename, input), checkDirectives: checkDirectives}
|
||||
blocks, err := p.parseAll()
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
// allTokens lexes the entire input, but does not parse it.
|
||||
// It returns all the tokens from the input, unstructured
|
||||
// and in order.
|
||||
func allTokens(input io.Reader) (tokens []token) {
|
||||
l := new(lexer)
|
||||
l.load(input)
|
||||
for l.next() {
|
||||
tokens = append(tokens, l.token)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Set of directives that are valid (unordered). Populated
|
||||
// by config package's init function.
|
||||
var ValidDirectives = make(map[string]struct{})
|
||||
@@ -1,22 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAllTokens(t *testing.T) {
|
||||
input := strings.NewReader("a b c\nd e")
|
||||
expected := []string{"a", "b", "c", "d", "e"}
|
||||
tokens := allTokens(input)
|
||||
|
||||
if len(tokens) != len(expected) {
|
||||
t.Fatalf("Expected %d tokens, got %d", len(expected), len(tokens))
|
||||
}
|
||||
|
||||
for i, val := range expected {
|
||||
if tokens[i].text != val {
|
||||
t.Errorf("Token %d should be '%s' but was '%s'", i, val, tokens[i].text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,329 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type parser struct {
|
||||
Dispenser
|
||||
block serverBlock // current server block being parsed
|
||||
eof bool // if we encounter a valid EOF in a hard place
|
||||
checkDirectives bool // if true, directives must be known
|
||||
}
|
||||
|
||||
func (p *parser) parseAll() ([]serverBlock, error) {
|
||||
var blocks []serverBlock
|
||||
|
||||
for p.Next() {
|
||||
err := p.parseOne()
|
||||
if err != nil {
|
||||
return blocks, err
|
||||
}
|
||||
if len(p.block.Addresses) > 0 {
|
||||
blocks = append(blocks, p.block)
|
||||
}
|
||||
}
|
||||
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseOne() error {
|
||||
p.block = serverBlock{Tokens: make(map[string][]token)}
|
||||
|
||||
err := p.begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) begin() error {
|
||||
if len(p.tokens) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := p.addresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.eof {
|
||||
// this happens if the Caddyfile consists of only
|
||||
// a line of addresses and nothing else
|
||||
return nil
|
||||
}
|
||||
|
||||
err = p.blockContents()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) addresses() error {
|
||||
var expectingAnother bool
|
||||
|
||||
for {
|
||||
tkn := p.Val()
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if tkn == "import" && p.isNewLine() {
|
||||
err := p.doImport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Open brace definitely indicates end of addresses
|
||||
if tkn == "{" {
|
||||
if expectingAnother {
|
||||
return p.Errf("Expected another address but had '%s' - check for extra comma", tkn)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if tkn != "" { // empty token possible if user typed "" in Caddyfile
|
||||
// Trailing comma indicates another address will follow, which
|
||||
// may possibly be on the next line
|
||||
if tkn[len(tkn)-1] == ',' {
|
||||
tkn = tkn[:len(tkn)-1]
|
||||
expectingAnother = true
|
||||
} else {
|
||||
expectingAnother = false // but we may still see another one on this line
|
||||
}
|
||||
|
||||
// Parse and save this address
|
||||
host, port, err := standardAddress(tkn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.block.Addresses = append(p.block.Addresses, address{host, port})
|
||||
}
|
||||
|
||||
// Advance token and possibly break out of loop or return error
|
||||
hasNext := p.Next()
|
||||
if expectingAnother && !hasNext {
|
||||
return p.EOFErr()
|
||||
}
|
||||
if !hasNext {
|
||||
p.eof = true
|
||||
break // EOF
|
||||
}
|
||||
if !expectingAnother && p.isNewLine() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) blockContents() error {
|
||||
errOpenCurlyBrace := p.openCurlyBrace()
|
||||
if errOpenCurlyBrace != nil {
|
||||
// single-server configs don't need curly braces
|
||||
p.cursor--
|
||||
}
|
||||
|
||||
err := p.directives()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Only look for close curly brace if there was an opening
|
||||
if errOpenCurlyBrace == nil {
|
||||
err = p.closeCurlyBrace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// directives parses through all the lines for directives
|
||||
// and it expects the next token to be the first
|
||||
// directive. It goes until EOF or closing curly brace
|
||||
// which ends the server block.
|
||||
func (p *parser) directives() error {
|
||||
for p.Next() {
|
||||
// end of server block
|
||||
if p.Val() == "}" {
|
||||
break
|
||||
}
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if p.Val() == "import" {
|
||||
err := p.doImport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
continue
|
||||
}
|
||||
|
||||
// normal case: parse a directive on this line
|
||||
if err := p.directive(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// doImport swaps out the import directive and its argument
|
||||
// (a total of 2 tokens) with the tokens in the file specified.
|
||||
// When the function returns, the cursor is on the token before
|
||||
// where the import directive was. In other words, call Next()
|
||||
// to access the first token that was imported.
|
||||
func (p *parser) doImport() error {
|
||||
if !p.NextArg() {
|
||||
return p.ArgErr()
|
||||
}
|
||||
importFile := p.Val()
|
||||
if p.NextArg() {
|
||||
return p.Err("Import allows only one file to import")
|
||||
}
|
||||
|
||||
file, err := os.Open(importFile)
|
||||
if err != nil {
|
||||
return p.Errf("Could not import %s - %v", importFile, err)
|
||||
}
|
||||
defer file.Close()
|
||||
importedTokens := allTokens(file)
|
||||
|
||||
// Tack the filename onto these tokens so any errors show the imported file's name
|
||||
for i := 0; i < len(importedTokens); i++ {
|
||||
importedTokens[i].file = filepath.Base(importFile)
|
||||
}
|
||||
|
||||
// Splice out the import directive and its argument (2 tokens total)
|
||||
// and insert the imported tokens in their place.
|
||||
tokensBefore := p.tokens[:p.cursor-1]
|
||||
tokensAfter := p.tokens[p.cursor+1:]
|
||||
p.tokens = append(tokensBefore, append(importedTokens, tokensAfter...)...)
|
||||
p.cursor-- // cursor was advanced one position to read the filename; rewind it
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// directive collects tokens until the directive's scope
|
||||
// closes (either end of line or end of curly brace block).
|
||||
// It expects the currently-loaded token to be a directive
|
||||
// (or } that ends a server block). The collected tokens
|
||||
// are loaded into the current server block for later use
|
||||
// by directive setup functions.
|
||||
func (p *parser) directive() error {
|
||||
dir := p.Val()
|
||||
nesting := 0
|
||||
|
||||
if p.checkDirectives {
|
||||
if _, ok := ValidDirectives[dir]; !ok {
|
||||
return p.Errf("Unknown directive '%s'", dir)
|
||||
}
|
||||
}
|
||||
|
||||
// The directive itself is appended as a relevant token
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
|
||||
for p.Next() {
|
||||
if p.Val() == "{" {
|
||||
nesting++
|
||||
} else if p.isNewLine() && nesting == 0 {
|
||||
p.cursor-- // read too far
|
||||
break
|
||||
} else if p.Val() == "}" && nesting > 0 {
|
||||
nesting--
|
||||
} else if p.Val() == "}" && nesting == 0 {
|
||||
return p.Err("Unexpected '}' because no matching opening brace")
|
||||
}
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
}
|
||||
|
||||
if nesting > 0 {
|
||||
return p.EOFErr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// openCurlyBrace expects the current token to be an
|
||||
// opening curly brace. This acts like an assertion
|
||||
// because it returns an error if the token is not
|
||||
// a opening curly brace. It does NOT advance the token.
|
||||
func (p *parser) openCurlyBrace() error {
|
||||
if p.Val() != "{" {
|
||||
return p.SyntaxErr("{")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// closeCurlyBrace expects the current token to be
|
||||
// a closing curly brace. This acts like an assertion
|
||||
// because it returns an error if the token is not
|
||||
// a closing curly brace. It does NOT advance the token.
|
||||
func (p *parser) closeCurlyBrace() error {
|
||||
if p.Val() != "}" {
|
||||
return p.SyntaxErr("}")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// standardAddress turns the accepted host and port patterns
|
||||
// into a format accepted by net.Dial.
|
||||
func standardAddress(str string) (host, port string, err error) {
|
||||
var schemePort, splitPort string
|
||||
|
||||
if strings.HasPrefix(str, "https://") {
|
||||
schemePort = "https"
|
||||
str = str[8:]
|
||||
} else if strings.HasPrefix(str, "http://") {
|
||||
schemePort = "http"
|
||||
str = str[7:]
|
||||
}
|
||||
|
||||
host, splitPort, err = net.SplitHostPort(str)
|
||||
if err != nil {
|
||||
host, splitPort, err = net.SplitHostPort(str + ":") // tack on empty port
|
||||
}
|
||||
if err != nil {
|
||||
// ¯\_(ツ)_/¯
|
||||
host = str
|
||||
}
|
||||
|
||||
if splitPort != "" {
|
||||
port = splitPort
|
||||
} else {
|
||||
port = schemePort
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type (
|
||||
// serverBlock associates tokens with a list of addresses
|
||||
// and groups tokens by directive name.
|
||||
serverBlock struct {
|
||||
Addresses []address
|
||||
Tokens map[string][]token
|
||||
}
|
||||
|
||||
address struct {
|
||||
Host, Port string
|
||||
}
|
||||
)
|
||||
|
||||
// HostList converts the list of addresses (hosts)
|
||||
// that are associated with this server block into
|
||||
// a slice of strings. Each string is a host:port
|
||||
// combination.
|
||||
func (sb serverBlock) HostList() []string {
|
||||
sbHosts := make([]string, len(sb.Addresses))
|
||||
for j, addr := range sb.Addresses {
|
||||
sbHosts[j] = net.JoinHostPort(addr.Host, addr.Port)
|
||||
}
|
||||
return sbHosts
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStandardAddress(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
host, port string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`localhost`, "localhost", "", false},
|
||||
{`localhost:1234`, "localhost", "1234", false},
|
||||
{`localhost:`, "localhost", "", false},
|
||||
{`0.0.0.0`, "0.0.0.0", "", false},
|
||||
{`127.0.0.1:1234`, "127.0.0.1", "1234", false},
|
||||
{`:1234`, "", "1234", false},
|
||||
{`[::1]`, "::1", "", false},
|
||||
{`[::1]:1234`, "::1", "1234", false},
|
||||
{`:`, "", "", false},
|
||||
{`localhost:http`, "localhost", "http", false},
|
||||
{`localhost:https`, "localhost", "https", false},
|
||||
{`:http`, "", "http", false},
|
||||
{`:https`, "", "https", false},
|
||||
{`http://localhost`, "localhost", "http", false},
|
||||
{`https://localhost`, "localhost", "https", false},
|
||||
{`http://127.0.0.1`, "127.0.0.1", "http", false},
|
||||
{`https://127.0.0.1`, "127.0.0.1", "https", false},
|
||||
{`http://[::1]`, "::1", "http", false},
|
||||
{`http://localhost:1234`, "localhost", "1234", false},
|
||||
{`https://127.0.0.1:1234`, "127.0.0.1", "1234", false},
|
||||
{`http://[::1]:1234`, "::1", "1234", false},
|
||||
{``, "", "", false},
|
||||
{`::1`, "::1", "", true},
|
||||
{`localhost::`, "localhost::", "", true},
|
||||
{`#$%@`, "#$%@", "", true},
|
||||
} {
|
||||
host, port, err := standardAddress(test.input)
|
||||
|
||||
if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d: Expected no error, but had error: %v", i, err)
|
||||
}
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d: Expected error, but had none", i)
|
||||
}
|
||||
|
||||
if host != test.host {
|
||||
t.Errorf("Test %d: Expected host '%s', got '%s'", i, test.host, host)
|
||||
}
|
||||
|
||||
if port != test.port {
|
||||
t.Errorf("Test %d: Expected port '%s', got '%s'", i, test.port, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOneAndImport(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
testParseOne := func(input string) (serverBlock, error) {
|
||||
p := testParser(input)
|
||||
p.Next() // parseOne doesn't call Next() to start, so we must
|
||||
err := p.parseOne()
|
||||
return p.block, err
|
||||
}
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
addresses []address
|
||||
tokens map[string]int // map of directive name to number of tokens expected
|
||||
}{
|
||||
{`localhost`, false, []address{
|
||||
{"localhost", ""},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1`, false, []address{
|
||||
{"localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234
|
||||
dir1 foo bar`, false, []address{
|
||||
{"localhost", "1234"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost {
|
||||
dir1
|
||||
}`, false, []address{
|
||||
{"localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
dir1 foo bar
|
||||
dir2
|
||||
}`, false, []address{
|
||||
{"localhost", "1234"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost
|
||||
dir1 foo bar`, false, []address{
|
||||
{"localhost", "http"},
|
||||
{"localhost", "https"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []address{
|
||||
{"localhost", "http"},
|
||||
{"localhost", "https"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, https://localhost {
|
||||
dir1 foo bar
|
||||
}`, false, []address{
|
||||
{"localhost", "http"},
|
||||
{"localhost", "https"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`http://localhost, {
|
||||
}`, true, []address{
|
||||
{"localhost", "http"},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`host1:80, http://host2.com
|
||||
dir1 foo bar
|
||||
dir2 baz`, false, []address{
|
||||
{"host1", "80"},
|
||||
{"host2.com", "http"},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`http://host1.com,
|
||||
http://host2.com,
|
||||
https://host3.com`, false, []address{
|
||||
{"host1.com", "http"},
|
||||
{"host2.com", "http"},
|
||||
{"host3.com", "https"},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`http://host1.com:1234, https://host2.com
|
||||
dir1 foo {
|
||||
bar baz
|
||||
}
|
||||
dir2`, false, []address{
|
||||
{"host1.com", "1234"},
|
||||
{"host2.com", "https"},
|
||||
}, map[string]int{
|
||||
"dir1": 6,
|
||||
"dir2": 1,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
dir1 {
|
||||
bar baz
|
||||
}
|
||||
dir2 {
|
||||
foo bar
|
||||
}`, false, []address{
|
||||
{"127.0.0.1", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 5,
|
||||
"dir2": 5,
|
||||
}},
|
||||
|
||||
{`127.0.0.1
|
||||
unknown_directive`, true, []address{
|
||||
{"127.0.0.1", ""},
|
||||
}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
foo`, true, []address{
|
||||
{"localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
}`, false, []address{
|
||||
{"localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
} }`, true, []address{
|
||||
{"localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 3,
|
||||
}},
|
||||
|
||||
{`localhost
|
||||
dir1 {
|
||||
nested {
|
||||
foo
|
||||
}
|
||||
}
|
||||
dir2 foo bar`, false, []address{
|
||||
{"localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 7,
|
||||
"dir2": 3,
|
||||
}},
|
||||
|
||||
{``, false, []address{}, map[string]int{}},
|
||||
|
||||
{`localhost
|
||||
dir1 arg1
|
||||
import import_test1.txt`, false, []address{
|
||||
{"localhost", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 2,
|
||||
"dir2": 3,
|
||||
"dir3": 1,
|
||||
}},
|
||||
|
||||
{`import import_test2.txt`, false, []address{
|
||||
{"host1", ""},
|
||||
}, map[string]int{
|
||||
"dir1": 1,
|
||||
"dir2": 2,
|
||||
}},
|
||||
|
||||
{`import import_test1.txt import_test2.txt`, true, []address{}, map[string]int{}},
|
||||
|
||||
{`import not_found.txt`, true, []address{}, map[string]int{}},
|
||||
|
||||
{`""`, false, []address{}, map[string]int{}},
|
||||
|
||||
{``, false, []address{}, map[string]int{}},
|
||||
} {
|
||||
result, err := testParseOne(test.input)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(result.Addresses) != len(test.addresses) {
|
||||
t.Errorf("Test %d: Expected %d addresses, got %d",
|
||||
i, len(test.addresses), len(result.Addresses))
|
||||
continue
|
||||
}
|
||||
for j, addr := range result.Addresses {
|
||||
if addr.Host != test.addresses[j].Host {
|
||||
t.Errorf("Test %d, address %d: Expected host to be '%s', but was '%s'",
|
||||
i, j, test.addresses[j].Host, addr.Host)
|
||||
}
|
||||
if addr.Port != test.addresses[j].Port {
|
||||
t.Errorf("Test %d, address %d: Expected port to be '%s', but was '%s'",
|
||||
i, j, test.addresses[j].Port, addr.Port)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Tokens) != len(test.tokens) {
|
||||
t.Errorf("Test %d: Expected %d directives, had %d",
|
||||
i, len(test.tokens), len(result.Tokens))
|
||||
continue
|
||||
}
|
||||
for directive, tokens := range result.Tokens {
|
||||
if len(tokens) != test.tokens[directive] {
|
||||
t.Errorf("Test %d, directive '%s': Expected %d tokens, counted %d",
|
||||
i, directive, test.tokens[directive], len(tokens))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAll(t *testing.T) {
|
||||
setupParseTests()
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
addresses [][]address // addresses per server block, in order
|
||||
}{
|
||||
{`localhost`, false, [][]address{
|
||||
{{"localhost", ""}},
|
||||
}},
|
||||
|
||||
{`localhost:1234`, false, [][]address{
|
||||
[]address{{"localhost", "1234"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234 {
|
||||
}
|
||||
localhost:2015 {
|
||||
}`, false, [][]address{
|
||||
[]address{{"localhost", "1234"}},
|
||||
[]address{{"localhost", "2015"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2`, false, [][]address{
|
||||
[]address{{"localhost", "1234"}, {"host2", "http"}},
|
||||
}},
|
||||
|
||||
{`localhost:1234, http://host2,`, true, [][]address{}},
|
||||
|
||||
{`http://host1.com, http://host2.com {
|
||||
}
|
||||
https://host3.com, https://host4.com {
|
||||
}`, false, [][]address{
|
||||
[]address{{"host1.com", "http"}, {"host2.com", "http"}},
|
||||
[]address{{"host3.com", "https"}, {"host4.com", "https"}},
|
||||
}},
|
||||
} {
|
||||
p := testParser(test.input)
|
||||
blocks, err := p.parseAll()
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected an error, but didn't get one", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but got: %v", i, err)
|
||||
}
|
||||
|
||||
if len(blocks) != len(test.addresses) {
|
||||
t.Errorf("Test %d: Expected %d server blocks, got %d",
|
||||
i, len(test.addresses), len(blocks))
|
||||
continue
|
||||
}
|
||||
for j, block := range blocks {
|
||||
if len(block.Addresses) != len(test.addresses[j]) {
|
||||
t.Errorf("Test %d: Expected %d addresses in block %d, got %d",
|
||||
i, len(test.addresses[j]), j, len(block.Addresses))
|
||||
continue
|
||||
}
|
||||
for k, addr := range block.Addresses {
|
||||
if addr.Host != test.addresses[j][k].Host {
|
||||
t.Errorf("Test %d, block %d, address %d: Expected host to be '%s', but was '%s'",
|
||||
i, j, k, test.addresses[j][k].Host, addr.Host)
|
||||
}
|
||||
if addr.Port != test.addresses[j][k].Port {
|
||||
t.Errorf("Test %d, block %d, address %d: Expected port to be '%s', but was '%s'",
|
||||
i, j, k, test.addresses[j][k].Port, addr.Port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupParseTests() {
|
||||
// Set up some bogus directives for testing
|
||||
ValidDirectives = map[string]struct{}{
|
||||
"dir1": {},
|
||||
"dir2": {},
|
||||
"dir3": {},
|
||||
}
|
||||
}
|
||||
|
||||
func testParser(input string) parser {
|
||||
buf := strings.NewReader(input)
|
||||
p := parser{Dispenser: NewDispenser("Test", buf), checkDirectives: true}
|
||||
return p
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(CaddyfileInput{})
|
||||
}
|
||||
|
||||
// Restart restarts the entire application; gracefully with zero
|
||||
// downtime if on a POSIX-compatible system, or forcefully if on
|
||||
// Windows but with imperceptibly-short downtime.
|
||||
//
|
||||
// The restarted application will use newCaddyfile as its input
|
||||
// configuration. If newCaddyfile is nil, the current (existing)
|
||||
// Caddyfile configuration will be used.
|
||||
//
|
||||
// Note: The process must exist in the same place on the disk in
|
||||
// order for this to work. Thus, multiple graceful restarts don't
|
||||
// work if executing with `go run`, since the binary is cleaned up
|
||||
// when `go run` sees the initial parent process exit.
|
||||
func Restart(newCaddyfile Input) error {
|
||||
if newCaddyfile == nil {
|
||||
caddyfileMu.Lock()
|
||||
newCaddyfile = caddyfile
|
||||
caddyfileMu.Unlock()
|
||||
}
|
||||
|
||||
if len(os.Args) == 0 { // this should never happen...
|
||||
os.Args = []string{""}
|
||||
}
|
||||
|
||||
// Tell the child that it's a restart
|
||||
os.Setenv("CADDY_RESTART", "true")
|
||||
|
||||
// Prepare our payload to the child process
|
||||
cdyfileGob := caddyfileGob{
|
||||
ListenerFds: make(map[string]uintptr),
|
||||
Caddyfile: newCaddyfile,
|
||||
}
|
||||
|
||||
// Prepare a pipe to the fork's stdin so it can get the Caddyfile
|
||||
rpipe, wpipe, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare a pipe that the child process will use to communicate
|
||||
// its success or failure with us, the parent
|
||||
sigrpipe, sigwpipe, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Pass along current environment and file descriptors to child.
|
||||
// Ordering here is very important: stdin, stdout, stderr, sigpipe,
|
||||
// and then the listener file descriptors (in order).
|
||||
fds := []uintptr{rpipe.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), sigwpipe.Fd()}
|
||||
|
||||
// Now add file descriptors of the sockets
|
||||
serversMu.Lock()
|
||||
for i, s := range servers {
|
||||
fds = append(fds, s.ListenerFd())
|
||||
cdyfileGob.ListenerFds[s.Addr] = uintptr(4 + i) // 4 fds come before any of the listeners
|
||||
}
|
||||
serversMu.Unlock()
|
||||
|
||||
// Fork the process with the current environment and file descriptors
|
||||
execSpec := &syscall.ProcAttr{
|
||||
Env: os.Environ(),
|
||||
Files: fds,
|
||||
}
|
||||
_, err = syscall.ForkExec(os.Args[0], os.Args, execSpec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Feed it the Caddyfile
|
||||
err = gob.NewEncoder(wpipe).Encode(cdyfileGob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wpipe.Close()
|
||||
|
||||
// Wait for child process to signal success or fail
|
||||
sigwpipe.Close() // close our copy of the write end of the pipe or we might be stuck
|
||||
answer, err := ioutil.ReadAll(sigrpipe)
|
||||
if err != nil || len(answer) == 0 {
|
||||
log.Println("restart: child failed to initialize; changes not applied")
|
||||
return incompleteRestartErr
|
||||
}
|
||||
|
||||
// Child process is listening now; we can stop all our servers here.
|
||||
return Stop()
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package caddy
|
||||
|
||||
func Restart(newCaddyfile Input) error {
|
||||
if newCaddyfile == nil {
|
||||
caddyfileMu.Lock()
|
||||
newCaddyfile = caddyfile
|
||||
caddyfileMu.Unlock()
|
||||
}
|
||||
|
||||
wg.Add(1) // barrier so Wait() doesn't unblock
|
||||
|
||||
err := Stop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = Start(newCaddyfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wg.Done() // take down our barrier
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/basicauth"
|
||||
)
|
||||
|
||||
// BasicAuth configures a new BasicAuth middleware instance.
|
||||
func BasicAuth(c *Controller) (middleware.Middleware, error) {
|
||||
root := c.Root
|
||||
|
||||
rules, err := basicAuthParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
basic := basicauth.BasicAuth{Rules: rules}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
basic.Next = next
|
||||
basic.SiteRoot = root
|
||||
return basic
|
||||
}, nil
|
||||
}
|
||||
|
||||
func basicAuthParse(c *Controller) ([]basicauth.Rule, error) {
|
||||
var rules []basicauth.Rule
|
||||
|
||||
var err error
|
||||
for c.Next() {
|
||||
var rule basicauth.Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 2:
|
||||
rule.Username = args[0]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[1], c.Root); err != nil {
|
||||
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]
|
||||
if rule.Password, err = passwordMatcher(rule.Username, args[2], c.Root); err != nil {
|
||||
return rules, c.Errf("Get password matcher from %s: %v", c.Val(), err)
|
||||
}
|
||||
default:
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func passwordMatcher(username, passw, siteRoot string) (basicauth.PasswordMatcher, error) {
|
||||
if !strings.HasPrefix(passw, "htpasswd=") {
|
||||
return basicauth.PlainMatcher(passw), nil
|
||||
}
|
||||
|
||||
return basicauth.GetHtpasswdMatcher(passw[9:], username, siteRoot)
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/basicauth"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
c := NewTestController(`basicauth user pwd`)
|
||||
|
||||
mid, err := BasicAuth(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(basicauth.BasicAuth)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type BasicAuth, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthParse(t *testing.T) {
|
||||
htpasswdPasswd := "IedFOuGmTpT8"
|
||||
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
|
||||
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
|
||||
var skipHtpassword bool
|
||||
htfh, err := ioutil.TempFile(".", "basicauth-")
|
||||
if err != nil {
|
||||
t.Logf("Error creating temp file (%v), will skip htpassword test", err)
|
||||
skipHtpassword = true
|
||||
} else {
|
||||
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
|
||||
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
|
||||
}
|
||||
htfh.Close()
|
||||
defer os.Remove(htfh.Name())
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
password string
|
||||
expected []basicauth.Rule
|
||||
}{
|
||||
{`basicauth user pwd`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
}`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user"},
|
||||
}},
|
||||
{`basicauth user pwd {
|
||||
/resource1
|
||||
/resource2
|
||||
}`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user", Resources: []string{"/resource1", "/resource2"}},
|
||||
}},
|
||||
{`basicauth /resource user pwd`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user", Resources: []string{"/resource"}},
|
||||
}},
|
||||
{`basicauth /res1 user1 pwd1
|
||||
basicauth /res2 user2 pwd2`, false, "pwd", []basicauth.Rule{
|
||||
{Username: "user1", Resources: []string{"/res1"}},
|
||||
{Username: "user2", Resources: []string{"/res2"}},
|
||||
}},
|
||||
{`basicauth user`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth`, true, "", []basicauth.Rule{}},
|
||||
{`basicauth /resource user pwd asdf`, true, "", []basicauth.Rule{}},
|
||||
|
||||
{`basicauth sha1 htpasswd=` + htfh.Name(), false, htpasswdPasswd, []basicauth.Rule{
|
||||
{Username: "sha1"},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := basicAuthParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, expectedRule := range test.expected {
|
||||
actualRule := actual[j]
|
||||
|
||||
if actualRule.Username != expectedRule.Username {
|
||||
t.Errorf("Test %d, rule %d: Expected username '%s', got '%s'",
|
||||
i, j, expectedRule.Username, actualRule.Username)
|
||||
}
|
||||
|
||||
if strings.Contains(test.input, "htpasswd=") && skipHtpassword {
|
||||
continue
|
||||
}
|
||||
pwd := test.password
|
||||
if len(actual) > 1 {
|
||||
pwd = fmt.Sprintf("%s%d", pwd, j+1)
|
||||
}
|
||||
if !actualRule.Password(pwd) || actualRule.Password(test.password+"!") {
|
||||
t.Errorf("Test %d, rule %d: Expected password '%v', got '%v'",
|
||||
i, j, test.password, actualRule.Password)
|
||||
}
|
||||
|
||||
expectedRes := fmt.Sprintf("%v", expectedRule.Resources)
|
||||
actualRes := fmt.Sprintf("%v", actualRule.Resources)
|
||||
if actualRes != expectedRes {
|
||||
t.Errorf("Test %d, rule %d: Expected resource list %s, but got %s",
|
||||
i, j, expectedRes, actualRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package setup
|
||||
|
||||
import "github.com/mholt/caddy/middleware"
|
||||
|
||||
func BindHost(c *Controller) (middleware.Middleware, error) {
|
||||
for c.Next() {
|
||||
if !c.Args(&c.BindHost) {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/browse"
|
||||
)
|
||||
|
||||
// Browse configures a new Browse middleware instance.
|
||||
func Browse(c *Controller) (middleware.Middleware, error) {
|
||||
configs, err := browseParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
browse := browse.Browse{
|
||||
Root: c.Root,
|
||||
Configs: configs,
|
||||
IgnoreIndexes: false,
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
browse.Next = next
|
||||
return browse
|
||||
}, nil
|
||||
}
|
||||
|
||||
func browseParse(c *Controller) ([]browse.Config, error) {
|
||||
var configs []browse.Config
|
||||
|
||||
appendCfg := func(bc browse.Config) error {
|
||||
for _, c := range configs {
|
||||
if c.PathScope == bc.PathScope {
|
||||
return fmt.Errorf("duplicate browsing config for %s", c.PathScope)
|
||||
}
|
||||
}
|
||||
configs = append(configs, bc)
|
||||
return nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
var bc browse.Config
|
||||
|
||||
// First argument is directory to allow browsing; default is site root
|
||||
if c.NextArg() {
|
||||
bc.PathScope = c.Val()
|
||||
} else {
|
||||
bc.PathScope = "/"
|
||||
}
|
||||
|
||||
// Second argument would be the template file to use
|
||||
var tplText string
|
||||
if c.NextArg() {
|
||||
tplBytes, err := ioutil.ReadFile(c.Val())
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
tplText = string(tplBytes)
|
||||
} else {
|
||||
tplText = defaultTemplate
|
||||
}
|
||||
|
||||
// Build the template
|
||||
tpl, err := template.New("listing").Parse(tplText)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
bc.Template = tpl
|
||||
|
||||
// Save configuration
|
||||
err = appendCfg(bc)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// The default template to use when serving up directory listings
|
||||
const defaultTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Name}}</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
* { padding: 0; margin: 0; }
|
||||
|
||||
body {
|
||||
padding: 1% 2%;
|
||||
font: 16px Arial;
|
||||
}
|
||||
|
||||
header {
|
||||
font-size: 45px;
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
header a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
header .up {
|
||||
display: inline-block;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
header a.up:hover {
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 0;
|
||||
border-collapse: collapse;
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 4px 20px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.5em; /* emoji are kind of odd heights */
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.hideable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header,
|
||||
header h1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background: #333;
|
||||
color: #FFF;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header .up {
|
||||
height: auto;
|
||||
width: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
header a.up {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 40px;
|
||||
height: 48px;
|
||||
font-size: 35px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-top: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{{if .CanGoUp}}
|
||||
<a href=".." class="up" title="Up one level">⬑</a>
|
||||
{{else}}
|
||||
<div class="up"> </div>
|
||||
{{end}}
|
||||
|
||||
<h1>{{.Path}}</h1>
|
||||
</header>
|
||||
<main>
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
{{if and (eq .Sort "name") (ne .Order "desc")}}
|
||||
<a href="?sort=name&order=desc">Name ▲</a>
|
||||
{{else if and (eq .Sort "name") (ne .Order "asc")}}
|
||||
<a href="?sort=name&order=asc">Name ▼</a>
|
||||
{{else}}
|
||||
<a href="?sort=name&order=asc">Name</a>
|
||||
{{end}}
|
||||
</th>
|
||||
<th>
|
||||
{{if and (eq .Sort "size") (ne .Order "desc")}}
|
||||
<a href="?sort=size&order=desc">Size ▲</a>
|
||||
{{else if and (eq .Sort "size") (ne .Order "asc")}}
|
||||
<a href="?sort=size&order=asc">Size ▼</a>
|
||||
{{else}}
|
||||
<a href="?sort=size&order=asc">Size</a>
|
||||
{{end}}
|
||||
</th>
|
||||
<th class="hideable">
|
||||
{{if and (eq .Sort "time") (ne .Order "desc")}}
|
||||
<a href="?sort=time&order=desc">Modified ▲</a>
|
||||
{{else if and (eq .Sort "time") (ne .Order "asc")}}
|
||||
<a href="?sort=time&order=asc">Modified ▼</a>
|
||||
{{else}}
|
||||
<a href="?sort=time&order=asc">Modified</a>
|
||||
{{end}}
|
||||
</th>
|
||||
</tr>
|
||||
{{range .Items}}
|
||||
<tr>
|
||||
<td>
|
||||
{{if .IsDir}}📂{{else}}📄{{end}}
|
||||
<a href="{{.URL}}" class="name">{{.Name}}</a>
|
||||
</td>
|
||||
<td>{{.HumanSize}}</td>
|
||||
<td class="hideable">{{.HumanModTime "01/02/2006 3:04:05 PM -0700"}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</main>
|
||||
</body>
|
||||
</html>`
|
||||
@@ -1,83 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy/parse"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Controller is given to the setup function of middlewares which
|
||||
// gives them access to be able to read tokens and set config. Each
|
||||
// virtualhost gets their own server config and dispenser.
|
||||
type Controller struct {
|
||||
*server.Config
|
||||
parse.Dispenser
|
||||
|
||||
// OncePerServerBlock is a function that executes f
|
||||
// exactly once per server block, no matter how many
|
||||
// hosts are associated with it. If it is the first
|
||||
// time, the function f is executed immediately
|
||||
// (not deferred) and may return an error which is
|
||||
// returned by OncePerServerBlock.
|
||||
OncePerServerBlock func(f func() error) error
|
||||
|
||||
// ServerBlockIndex is the 0-based index of the
|
||||
// server block as it appeared in the input.
|
||||
ServerBlockIndex int
|
||||
|
||||
// ServerBlockHostIndex is the 0-based index of this
|
||||
// host as it appeared in the input at the head of the
|
||||
// server block.
|
||||
ServerBlockHostIndex int
|
||||
|
||||
// ServerBlockHosts is a list of hosts that are
|
||||
// associated with this server block. All these
|
||||
// hosts, consequently, share the same tokens.
|
||||
ServerBlockHosts []string
|
||||
|
||||
// ServerBlockStorage is used by a directive's
|
||||
// setup function to persist state between all
|
||||
// the hosts on a server block.
|
||||
ServerBlockStorage interface{}
|
||||
}
|
||||
|
||||
// NewTestController creates a new *Controller for
|
||||
// the input specified, with a filename of "Testfile".
|
||||
// The Config is bare, consisting only of a Root of cwd.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience. Does not initialize
|
||||
// the server-block-related fields.
|
||||
func NewTestController(input string) *Controller {
|
||||
return &Controller{
|
||||
Config: &server.Config{
|
||||
Root: ".",
|
||||
},
|
||||
Dispenser: parse.NewDispenser("Testfile", strings.NewReader(input)),
|
||||
OncePerServerBlock: func(f func() error) error {
|
||||
return f()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EmptyNext is a no-op function that can be passed into
|
||||
// middleware.Middleware functions so that the assignment
|
||||
// to the Next field of the Handler can be tested.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience.
|
||||
var EmptyNext = middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
return 0, nil
|
||||
})
|
||||
|
||||
// SameNext does a pointer comparison between next1 and next2.
|
||||
//
|
||||
// Used primarily for testing but needs to be exported so
|
||||
// add-ons can use this as a convenience.
|
||||
func SameNext(next1, next2 middleware.Handler) bool {
|
||||
return fmt.Sprintf("%v", next1) == fmt.Sprintf("%v", next2)
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/errors"
|
||||
)
|
||||
|
||||
// Errors configures a new gzip middleware instance.
|
||||
func Errors(c *Controller) (middleware.Middleware, error) {
|
||||
handler, err := errorsParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open the log file for writing when the server starts
|
||||
c.Startup = append(c.Startup, 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 {
|
||||
writer = file
|
||||
}
|
||||
}
|
||||
|
||||
handler.Log = log.New(writer, "", 0)
|
||||
return nil
|
||||
})
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
handler.Next = next
|
||||
return handler
|
||||
}, nil
|
||||
}
|
||||
|
||||
func errorsParse(c *Controller) (*errors.ErrorHandler, error) {
|
||||
// Very important that we make a pointer because the Startup
|
||||
// function that opens the log file must have access to the
|
||||
// same instance of the handler, not a copy.
|
||||
handler := &errors.ErrorHandler{ErrorPages: make(map[int]string)}
|
||||
|
||||
optionalBlock := func() (bool, error) {
|
||||
var hadBlock bool
|
||||
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
|
||||
what := c.Val()
|
||||
if !c.NextArg() {
|
||||
return hadBlock, c.ArgErr()
|
||||
}
|
||||
where := c.Val()
|
||||
|
||||
if what == "log" {
|
||||
if where == "visible" {
|
||||
handler.Debug = true
|
||||
} else {
|
||||
handler.LogFile = where
|
||||
if c.NextArg() {
|
||||
if c.Val() == "{" {
|
||||
c.IncrNest()
|
||||
logRoller, err := parseRoller(c)
|
||||
if err != nil {
|
||||
return hadBlock, err
|
||||
}
|
||||
handler.LogRoller = logRoller
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Error page; ensure it exists
|
||||
where = filepath.Join(c.Root, where)
|
||||
f, err := os.Open(where)
|
||||
if err != nil {
|
||||
fmt.Println("Warning: Unable to open error page '" + where + "': " + err.Error())
|
||||
}
|
||||
f.Close()
|
||||
|
||||
whatInt, err := strconv.Atoi(what)
|
||||
if err != nil {
|
||||
return hadBlock, c.Err("Expecting a numeric status code, got '" + what + "'")
|
||||
}
|
||||
handler.ErrorPages[whatInt] = where
|
||||
}
|
||||
}
|
||||
return hadBlock, nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
// weird hack to avoid having the handler values overwritten.
|
||||
if c.Val() == "}" {
|
||||
continue
|
||||
}
|
||||
// Configuration may be in a block
|
||||
hadBlock, err := optionalBlock()
|
||||
if err != nil {
|
||||
return handler, err
|
||||
}
|
||||
|
||||
// Otherwise, the only argument would be an error log file name or 'visible'
|
||||
if !hadBlock {
|
||||
if c.NextArg() {
|
||||
if c.Val() == "visible" {
|
||||
handler.Debug = true
|
||||
} else {
|
||||
handler.LogFile = c.Val()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handler, nil
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/errors"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
c := NewTestController(`errors`)
|
||||
mid, err := Errors(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(*errors.ErrorHandler)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type ErrorHandler, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.LogFile != "" {
|
||||
t.Errorf("Expected '%s' as the default LogFile", "")
|
||||
}
|
||||
if myHandler.LogRoller != nil {
|
||||
t.Errorf("Expected LogRoller to be nil, got: %v", *myHandler.LogRoller)
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
// Test Startup function
|
||||
if len(c.Startup) == 0 {
|
||||
t.Fatal("Expected 1 startup function, had 0")
|
||||
}
|
||||
err = c.Startup[0]()
|
||||
if myHandler.Log == nil {
|
||||
t.Error("Expected Log to be non-nil after startup because Debug is not enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorsParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputErrorsRules string
|
||||
shouldErr bool
|
||||
expectedErrorHandler errors.ErrorHandler
|
||||
}{
|
||||
{`errors`, false, errors.ErrorHandler{
|
||||
LogFile: "",
|
||||
}},
|
||||
{`errors errors.txt`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
}},
|
||||
{`errors visible`, false, errors.ErrorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log visible }`, false, errors.ErrorHandler{
|
||||
LogFile: "",
|
||||
Debug: true,
|
||||
}},
|
||||
{`errors { log errors.txt
|
||||
404 404.html
|
||||
500 500.html
|
||||
}`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
500: "500.html",
|
||||
},
|
||||
}},
|
||||
{`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
LogRoller: &middleware.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
LocalTime: true,
|
||||
},
|
||||
}},
|
||||
{`errors { log errors.txt {
|
||||
size 3
|
||||
age 11
|
||||
keep 5
|
||||
}
|
||||
404 404.html
|
||||
503 503.html
|
||||
}`, false, errors.ErrorHandler{
|
||||
LogFile: "errors.txt",
|
||||
ErrorPages: map[int]string{
|
||||
404: "404.html",
|
||||
503: "503.html",
|
||||
},
|
||||
LogRoller: &middleware.LogRoller{
|
||||
MaxSize: 3,
|
||||
MaxAge: 11,
|
||||
MaxBackups: 5,
|
||||
LocalTime: true,
|
||||
},
|
||||
}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputErrorsRules)
|
||||
actualErrorsRule, err := errorsParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if actualErrorsRule.LogFile != test.expectedErrorHandler.LogFile {
|
||||
t.Errorf("Test %d expected LogFile to be %s, but got %s",
|
||||
i, test.expectedErrorHandler.LogFile, actualErrorsRule.LogFile)
|
||||
}
|
||||
if actualErrorsRule.Debug != test.expectedErrorHandler.Debug {
|
||||
t.Errorf("Test %d expected Debug to be %v, but got %v",
|
||||
i, test.expectedErrorHandler.Debug, actualErrorsRule.Debug)
|
||||
}
|
||||
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller == nil || actualErrorsRule.LogRoller == nil && test.expectedErrorHandler.LogRoller != nil {
|
||||
t.Fatalf("Test %d expected LogRoller to be %v, but got %v",
|
||||
i, test.expectedErrorHandler.LogRoller, actualErrorsRule.LogRoller)
|
||||
}
|
||||
if len(actualErrorsRule.ErrorPages) != len(test.expectedErrorHandler.ErrorPages) {
|
||||
t.Fatalf("Test %d expected %d no of Error pages, but got %d ",
|
||||
i, len(test.expectedErrorHandler.ErrorPages), len(actualErrorsRule.ErrorPages))
|
||||
}
|
||||
if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller != nil {
|
||||
if actualErrorsRule.LogRoller.Filename != test.expectedErrorHandler.LogRoller.Filename {
|
||||
t.Fatalf("Test %d expected LogRoller Filename to be %s, but got %s",
|
||||
i, test.expectedErrorHandler.LogRoller.Filename, actualErrorsRule.LogRoller.Filename)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.MaxAge != test.expectedErrorHandler.LogRoller.MaxAge {
|
||||
t.Fatalf("Test %d expected LogRoller MaxAge to be %d, but got %d",
|
||||
i, test.expectedErrorHandler.LogRoller.MaxAge, actualErrorsRule.LogRoller.MaxAge)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.MaxBackups != test.expectedErrorHandler.LogRoller.MaxBackups {
|
||||
t.Fatalf("Test %d expected LogRoller MaxBackups to be %d, but got %d",
|
||||
i, test.expectedErrorHandler.LogRoller.MaxBackups, actualErrorsRule.LogRoller.MaxBackups)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.MaxSize != test.expectedErrorHandler.LogRoller.MaxSize {
|
||||
t.Fatalf("Test %d expected LogRoller MaxSize to be %d, but got %d",
|
||||
i, test.expectedErrorHandler.LogRoller.MaxSize, actualErrorsRule.LogRoller.MaxSize)
|
||||
}
|
||||
if actualErrorsRule.LogRoller.LocalTime != test.expectedErrorHandler.LogRoller.LocalTime {
|
||||
t.Fatalf("Test %d expected LogRoller LocalTime to be %t, but got %t",
|
||||
i, test.expectedErrorHandler.LogRoller.LocalTime, actualErrorsRule.LogRoller.LocalTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/extensions"
|
||||
)
|
||||
|
||||
// Ext configures a new instance of 'extensions' middleware for clean URLs.
|
||||
func Ext(c *Controller) (middleware.Middleware, error) {
|
||||
root := c.Root
|
||||
|
||||
exts, err := extParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return extensions.Ext{
|
||||
Next: next,
|
||||
Extensions: exts,
|
||||
Root: root,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extParse sets up an instance of extension middleware
|
||||
// from a middleware controller and returns a list of extensions.
|
||||
func extParse(c *Controller) ([]string, error) {
|
||||
var exts []string
|
||||
|
||||
for c.Next() {
|
||||
// At least one extension is required
|
||||
if !c.NextArg() {
|
||||
return exts, c.ArgErr()
|
||||
}
|
||||
exts = append(exts, c.Val())
|
||||
|
||||
// Tack on any other extensions that may have been listed
|
||||
exts = append(exts, c.RemainingArgs()...)
|
||||
}
|
||||
|
||||
return exts, nil
|
||||
}
|
||||
|
||||
// resourceExists returns true if the file specified at
|
||||
// root + path exists; false otherwise.
|
||||
func resourceExists(root, path string) bool {
|
||||
_, err := os.Stat(root + path)
|
||||
// technically we should use os.IsNotExist(err)
|
||||
// but we don't handle any other kinds of errors anyway
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/extensions"
|
||||
)
|
||||
|
||||
func TestExt(t *testing.T) {
|
||||
c := NewTestController(`ext .html .htm .php`)
|
||||
|
||||
mid, err := Ext(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(extensions.Ext)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Ext, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Extensions[0] != ".html" {
|
||||
t.Errorf("Expected .html in the list of Extensions")
|
||||
}
|
||||
if myHandler.Extensions[1] != ".htm" {
|
||||
t.Errorf("Expected .htm in the list of Extensions")
|
||||
}
|
||||
if myHandler.Extensions[2] != ".php" {
|
||||
t.Errorf("Expected .php in the list of Extensions")
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestExtParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputExts string
|
||||
shouldErr bool
|
||||
expectedExts []string
|
||||
}{
|
||||
{`ext .html .htm .php`, false, []string{".html", ".htm", ".php"}},
|
||||
{`ext .php .html .xml`, false, []string{".php", ".html", ".xml"}},
|
||||
{`ext .txt .php .xml`, false, []string{".txt", ".php", ".xml"}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputExts)
|
||||
actualExts, err := extParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actualExts) != len(test.expectedExts) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expectedExts), len(actualExts))
|
||||
}
|
||||
for j, actualExt := range actualExts {
|
||||
if actualExt != test.expectedExts[j] {
|
||||
t.Fatalf("Test %d expected %dth extension to be %s , but got %s",
|
||||
i, j, test.expectedExts[j], actualExt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/fastcgi"
|
||||
)
|
||||
|
||||
// FastCGI configures a new FastCGI middleware instance.
|
||||
func FastCGI(c *Controller) (middleware.Middleware, error) {
|
||||
absRoot, err := filepath.Abs(c.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rules, err := fastcgiParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return fastcgi.Handler{
|
||||
Next: next,
|
||||
Rules: rules,
|
||||
Root: c.Root,
|
||||
AbsRoot: absRoot,
|
||||
FileSys: http.Dir(c.Root),
|
||||
SoftwareName: c.AppName,
|
||||
SoftwareVersion: c.AppVersion,
|
||||
ServerName: c.Host,
|
||||
ServerPort: c.Port,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fastcgiParse(c *Controller) ([]fastcgi.Rule, error) {
|
||||
var rules []fastcgi.Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule fastcgi.Rule
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return rules, c.ArgErr()
|
||||
case 1:
|
||||
rule.Path = "/"
|
||||
rule.Address = args[0]
|
||||
case 2:
|
||||
rule.Path = args[0]
|
||||
rule.Address = args[1]
|
||||
case 3:
|
||||
rule.Path = args[0]
|
||||
rule.Address = args[1]
|
||||
err := fastcgiPreset(args[2], &rule)
|
||||
if err != nil {
|
||||
return rules, c.Err("Invalid fastcgi rule preset '" + args[2] + "'")
|
||||
}
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "ext":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.Ext = c.Val()
|
||||
case "split":
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.SplitPath = c.Val()
|
||||
case "index":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.IndexFiles = args
|
||||
case "env":
|
||||
envArgs := c.RemainingArgs()
|
||||
if len(envArgs) < 2 {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
rule.EnvVars = append(rule.EnvVars, [2]string{envArgs[0], envArgs[1]})
|
||||
}
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
// fastcgiPreset configures rule according to name. It returns an error if
|
||||
// name is not a recognized preset name.
|
||||
func fastcgiPreset(name string, rule *fastcgi.Rule) error {
|
||||
switch name {
|
||||
case "php":
|
||||
rule.Ext = ".php"
|
||||
rule.SplitPath = ".php"
|
||||
rule.IndexFiles = []string{"index.php"}
|
||||
default:
|
||||
return errors.New(name + " is not a valid preset name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mholt/caddy/middleware/fastcgi"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFastCGI(t *testing.T) {
|
||||
|
||||
c := NewTestController(`fastcgi / 127.0.0.1:9000`)
|
||||
|
||||
mid, err := FastCGI(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(fastcgi.Handler)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type , got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].Path != "/" {
|
||||
t.Errorf("Expected / as the Path")
|
||||
}
|
||||
if myHandler.Rules[0].Address != "127.0.0.1:9000" {
|
||||
t.Errorf("Expected 127.0.0.1:9000 as the Address")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFastcgiParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputFastcgiConfig string
|
||||
shouldErr bool
|
||||
expectedFastcgiConfig []fastcgi.Rule
|
||||
}{
|
||||
|
||||
{`fastcgi /blog 127.0.0.1:9000 php`,
|
||||
false, []fastcgi.Rule{{
|
||||
Path: "/blog",
|
||||
Address: "127.0.0.1:9000",
|
||||
Ext: ".php",
|
||||
SplitPath: ".php",
|
||||
IndexFiles: []string{"index.php"},
|
||||
}}},
|
||||
{`fastcgi / 127.0.0.1:9001 {
|
||||
split .html
|
||||
}`,
|
||||
false, []fastcgi.Rule{{
|
||||
Path: "/",
|
||||
Address: "127.0.0.1:9001",
|
||||
Ext: "",
|
||||
SplitPath: ".html",
|
||||
IndexFiles: []string{},
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputFastcgiConfig)
|
||||
actualFastcgiConfigs, err := fastcgiParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualFastcgiConfigs) != len(test.expectedFastcgiConfig) {
|
||||
t.Fatalf("Test %d expected %d no of FastCGI configs, but got %d ",
|
||||
i, len(test.expectedFastcgiConfig), len(actualFastcgiConfigs))
|
||||
}
|
||||
for j, actualFastcgiConfig := range actualFastcgiConfigs {
|
||||
|
||||
if actualFastcgiConfig.Path != test.expectedFastcgiConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth FastCGI Path to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Path, actualFastcgiConfig.Path)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Address != test.expectedFastcgiConfig[j].Address {
|
||||
t.Errorf("Test %d expected %dth FastCGI Address to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Address, actualFastcgiConfig.Address)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.Ext != test.expectedFastcgiConfig[j].Ext {
|
||||
t.Errorf("Test %d expected %dth FastCGI Ext to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].Ext, actualFastcgiConfig.Ext)
|
||||
}
|
||||
|
||||
if actualFastcgiConfig.SplitPath != test.expectedFastcgiConfig[j].SplitPath {
|
||||
t.Errorf("Test %d expected %dth FastCGI SplitPath to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].SplitPath, actualFastcgiConfig.SplitPath)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualFastcgiConfig.IndexFiles) != fmt.Sprint(test.expectedFastcgiConfig[j].IndexFiles) {
|
||||
t.Errorf("Test %d expected %dth FastCGI IndexFiles to be %s , but got %s",
|
||||
i, j, test.expectedFastcgiConfig[j].IndexFiles, actualFastcgiConfig.IndexFiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/gzip"
|
||||
)
|
||||
|
||||
// Gzip configures a new gzip middleware instance.
|
||||
func Gzip(c *Controller) (middleware.Middleware, error) {
|
||||
configs, err := gzipParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return gzip.Gzip{Next: next, Configs: configs}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func gzipParse(c *Controller) ([]gzip.Config, error) {
|
||||
var configs []gzip.Config
|
||||
|
||||
for c.Next() {
|
||||
config := gzip.Config{}
|
||||
|
||||
pathFilter := gzip.PathFilter{IgnoredPaths: make(gzip.Set)}
|
||||
extFilter := gzip.ExtFilter{Exts: make(gzip.Set)}
|
||||
|
||||
// No extra args expected
|
||||
if len(c.RemainingArgs()) > 0 {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "ext":
|
||||
exts := c.RemainingArgs()
|
||||
if len(exts) == 0 {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
for _, e := range exts {
|
||||
if !strings.HasPrefix(e, ".") && e != gzip.ExtWildCard {
|
||||
return configs, fmt.Errorf(`gzip: invalid extension "%v" (must start with dot)`, e)
|
||||
}
|
||||
extFilter.Exts.Add(e)
|
||||
}
|
||||
case "not":
|
||||
paths := c.RemainingArgs()
|
||||
if len(paths) == 0 {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
for _, p := range paths {
|
||||
if p == "/" {
|
||||
return configs, fmt.Errorf(`gzip: cannot exclude path "/" - remove directive entirely instead`)
|
||||
}
|
||||
if !strings.HasPrefix(p, "/") {
|
||||
return configs, fmt.Errorf(`gzip: invalid path "%v" (must start with /)`, p)
|
||||
}
|
||||
pathFilter.IgnoredPaths.Add(p)
|
||||
}
|
||||
case "level":
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
level, _ := strconv.Atoi(c.Val())
|
||||
config.Level = level
|
||||
default:
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
}
|
||||
|
||||
config.Filters = []gzip.Filter{}
|
||||
|
||||
// If ignored paths are specified, put in front to filter with path first
|
||||
if len(pathFilter.IgnoredPaths) > 0 {
|
||||
config.Filters = []gzip.Filter{pathFilter}
|
||||
}
|
||||
|
||||
// Then, if extensions are specified, use those to filter.
|
||||
// Otherwise, use default extensions filter.
|
||||
if len(extFilter.Exts) > 0 {
|
||||
config.Filters = append(config.Filters, extFilter)
|
||||
} else {
|
||||
config.Filters = append(config.Filters, gzip.DefaultExtFilter())
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/gzip"
|
||||
)
|
||||
|
||||
func TestGzip(t *testing.T) {
|
||||
c := NewTestController(`gzip`)
|
||||
|
||||
mid, err := Gzip(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(gzip.Gzip)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Gzip, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`gzip {`, true},
|
||||
{`gzip {}`, true},
|
||||
{`gzip a b`, true},
|
||||
{`gzip a {`, true},
|
||||
{`gzip { not f } `, true},
|
||||
{`gzip { not } `, true},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
} `, false},
|
||||
{`gzip { level 9 } `, false},
|
||||
{`gzip { ext } `, true},
|
||||
{`gzip { ext /f
|
||||
} `, true},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
}
|
||||
gzip`, false},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
}
|
||||
gzip { not /file1
|
||||
ext .htm
|
||||
level 3
|
||||
}
|
||||
`, false},
|
||||
{`gzip { not /file
|
||||
ext .html
|
||||
level 1
|
||||
}
|
||||
gzip { not /file1
|
||||
ext .htm
|
||||
level 3
|
||||
}
|
||||
`, false},
|
||||
{`gzip { not /file
|
||||
ext *
|
||||
level 1
|
||||
}
|
||||
`, false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
_, err := gzipParse(c)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil", i)
|
||||
} else if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/headers"
|
||||
)
|
||||
|
||||
// Headers configures a new Headers middleware instance.
|
||||
func Headers(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := headersParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return headers.Headers{Next: next, Rules: rules}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func headersParse(c *Controller) ([]headers.Rule, error) {
|
||||
var rules []headers.Rule
|
||||
|
||||
for c.NextLine() {
|
||||
var head headers.Rule
|
||||
var isNewPattern bool
|
||||
|
||||
if !c.NextArg() {
|
||||
return rules, c.ArgErr()
|
||||
}
|
||||
pattern := c.Val()
|
||||
|
||||
// See if we already have a definition for this Path pattern...
|
||||
for _, h := range rules {
|
||||
if h.Path == pattern {
|
||||
head = h
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ...otherwise, this is a new pattern
|
||||
if head.Path == "" {
|
||||
head.Path = pattern
|
||||
isNewPattern = true
|
||||
}
|
||||
|
||||
for c.NextBlock() {
|
||||
// A block of headers was opened...
|
||||
|
||||
h := headers.Header{Name: c.Val()}
|
||||
|
||||
if c.NextArg() {
|
||||
h.Value = c.Val()
|
||||
}
|
||||
|
||||
head.Headers = append(head.Headers, h)
|
||||
}
|
||||
if c.NextArg() {
|
||||
// ... or single header was defined as an argument instead.
|
||||
|
||||
h := headers.Header{Name: c.Val()}
|
||||
|
||||
h.Value = c.Val()
|
||||
|
||||
if c.NextArg() {
|
||||
h.Value = c.Val()
|
||||
}
|
||||
|
||||
head.Headers = append(head.Headers, h)
|
||||
}
|
||||
|
||||
if isNewPattern {
|
||||
rules = append(rules, head)
|
||||
} else {
|
||||
for i := 0; i < len(rules); i++ {
|
||||
if rules[i].Path == pattern {
|
||||
rules[i] = head
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/headers"
|
||||
)
|
||||
|
||||
func TestHeaders(t *testing.T) {
|
||||
c := NewTestController(`header / Foo Bar`)
|
||||
|
||||
mid, err := Headers(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(headers.Headers)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Headers, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadersParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []headers.Rule
|
||||
}{
|
||||
{`header /foo Foo "Bar Baz"`,
|
||||
false, []headers.Rule{
|
||||
{Path: "/foo", Headers: []headers.Header{
|
||||
{"Foo", "Bar Baz"},
|
||||
}},
|
||||
}},
|
||||
{`header /bar { Foo "Bar Baz" Baz Qux }`,
|
||||
false, []headers.Rule{
|
||||
{Path: "/bar", Headers: []headers.Header{
|
||||
{"Foo", "Bar Baz"},
|
||||
{"Baz", "Qux"},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := headersParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, expectedRule := range test.expected {
|
||||
actualRule := actual[j]
|
||||
|
||||
if actualRule.Path != expectedRule.Path {
|
||||
t.Errorf("Test %d, rule %d: Expected path %s, but got %s",
|
||||
i, j, expectedRule.Path, actualRule.Path)
|
||||
}
|
||||
|
||||
expectedHeaders := fmt.Sprintf("%v", expectedRule.Headers)
|
||||
actualHeaders := fmt.Sprintf("%v", actualRule.Headers)
|
||||
|
||||
if actualHeaders != expectedHeaders {
|
||||
t.Errorf("Test %d, rule %d: Expected headers %s, but got %s",
|
||||
i, j, expectedHeaders, actualHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/inner"
|
||||
)
|
||||
|
||||
// Internal configures a new Internal middleware instance.
|
||||
func Internal(c *Controller) (middleware.Middleware, error) {
|
||||
paths, err := internalParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return inner.Internal{Next: next, Paths: paths}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func internalParse(c *Controller) ([]string, error) {
|
||||
var paths []string
|
||||
|
||||
for c.Next() {
|
||||
if !c.NextArg() {
|
||||
return paths, c.ArgErr()
|
||||
}
|
||||
paths = append(paths, c.Val())
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/inner"
|
||||
)
|
||||
|
||||
func TestInternal(t *testing.T) {
|
||||
c := NewTestController(`internal /internal`)
|
||||
|
||||
mid, err := Internal(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(inner.Internal)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Internal, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Paths[0] != "/internal" {
|
||||
t.Errorf("Expected internal in the list of internal Paths")
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInternalParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputInternalPaths string
|
||||
shouldErr bool
|
||||
expectedInternalPaths []string
|
||||
}{
|
||||
{`internal /internal`, false, []string{"/internal"}},
|
||||
|
||||
{`internal /internal1
|
||||
internal /internal2`, false, []string{"/internal1", "/internal2"}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputInternalPaths)
|
||||
actualInternalPaths, err := internalParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
if len(actualInternalPaths) != len(test.expectedInternalPaths) {
|
||||
t.Fatalf("Test %d expected %d InternalPaths, but got %d",
|
||||
i, len(test.expectedInternalPaths), len(actualInternalPaths))
|
||||
}
|
||||
for j, actualInternalPath := range actualInternalPaths {
|
||||
if actualInternalPath != test.expectedInternalPaths[j] {
|
||||
t.Fatalf("Test %d expected %dth Internal Path to be %s , but got %s",
|
||||
i, j, test.expectedInternalPaths[j], actualInternalPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/go-syslog"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
caddylog "github.com/mholt/caddy/middleware/log"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
// Log sets up the logging middleware.
|
||||
func Log(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := logParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Open the log files for writing when the server starts
|
||||
c.Startup = append(c.Startup, func() error {
|
||||
for i := 0; i < len(rules); i++ {
|
||||
var err error
|
||||
var writer io.Writer
|
||||
|
||||
if rules[i].OutputFile == "stdout" {
|
||||
writer = os.Stdout
|
||||
} else if rules[i].OutputFile == "stderr" {
|
||||
writer = os.Stderr
|
||||
} else if rules[i].OutputFile == "syslog" {
|
||||
writer, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "caddy")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var file *os.File
|
||||
file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rules[i].Roller != nil {
|
||||
file.Close()
|
||||
rules[i].Roller.Filename = rules[i].OutputFile
|
||||
writer = rules[i].Roller.GetLogWriter()
|
||||
} else {
|
||||
writer = file
|
||||
}
|
||||
}
|
||||
|
||||
rules[i].Log = log.New(writer, "", 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return caddylog.Logger{Next: next, Rules: rules, ErrorFunc: server.DefaultErrorFunc}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func logParse(c *Controller) ([]caddylog.Rule, error) {
|
||||
var rules []caddylog.Rule
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
|
||||
var logRoller *middleware.LogRoller
|
||||
if c.NextBlock() {
|
||||
if c.Val() == "rotate" {
|
||||
if c.NextArg() {
|
||||
if c.Val() == "{" {
|
||||
var err error
|
||||
logRoller, err = parseRoller(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// This part doesn't allow having something after the rotate block
|
||||
if c.Next() {
|
||||
if c.Val() != "}" {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(args) == 0 {
|
||||
// Nothing specified; use defaults
|
||||
rules = append(rules, caddylog.Rule{
|
||||
PathScope: "/",
|
||||
OutputFile: caddylog.DefaultLogFilename,
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
Roller: logRoller,
|
||||
})
|
||||
} else if len(args) == 1 {
|
||||
// Only an output file specified
|
||||
rules = append(rules, caddylog.Rule{
|
||||
PathScope: "/",
|
||||
OutputFile: args[0],
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
Roller: logRoller,
|
||||
})
|
||||
} else {
|
||||
// Path scope, output file, and maybe a format specified
|
||||
|
||||
format := caddylog.DefaultLogFormat
|
||||
|
||||
if len(args) > 2 {
|
||||
switch args[2] {
|
||||
case "{common}":
|
||||
format = caddylog.CommonLogFormat
|
||||
case "{combined}":
|
||||
format = caddylog.CombinedLogFormat
|
||||
default:
|
||||
format = args[2]
|
||||
}
|
||||
}
|
||||
|
||||
rules = append(rules, caddylog.Rule{
|
||||
PathScope: args[0],
|
||||
OutputFile: args[1],
|
||||
Format: format,
|
||||
Roller: logRoller,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
caddylog "github.com/mholt/caddy/middleware/log"
|
||||
)
|
||||
|
||||
func TestLog(t *testing.T) {
|
||||
|
||||
c := NewTestController(`log`)
|
||||
|
||||
mid, err := Log(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(caddylog.Logger)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Logger, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].PathScope != "/" {
|
||||
t.Errorf("Expected / as the default PathScope")
|
||||
}
|
||||
if myHandler.Rules[0].OutputFile != caddylog.DefaultLogFilename {
|
||||
t.Errorf("Expected %s as the default OutputFile", caddylog.DefaultLogFilename)
|
||||
}
|
||||
if myHandler.Rules[0].Format != caddylog.DefaultLogFormat {
|
||||
t.Errorf("Expected %s as the default Log Format", caddylog.DefaultLogFormat)
|
||||
}
|
||||
if myHandler.Rules[0].Roller != nil {
|
||||
t.Errorf("Expected Roller to be nil, got: %v", *myHandler.Rules[0].Roller)
|
||||
}
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLogParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputLogRules string
|
||||
shouldErr bool
|
||||
expectedLogRules []caddylog.Rule
|
||||
}{
|
||||
{`log`, false, []caddylog.Rule{{
|
||||
PathScope: "/",
|
||||
OutputFile: caddylog.DefaultLogFilename,
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
}}},
|
||||
{`log log.txt`, false, []caddylog.Rule{{
|
||||
PathScope: "/",
|
||||
OutputFile: "log.txt",
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
}}},
|
||||
{`log /api log.txt`, false, []caddylog.Rule{{
|
||||
PathScope: "/api",
|
||||
OutputFile: "log.txt",
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
}}},
|
||||
{`log /serve stdout`, false, []caddylog.Rule{{
|
||||
PathScope: "/serve",
|
||||
OutputFile: "stdout",
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
}}},
|
||||
{`log /myapi log.txt {common}`, false, []caddylog.Rule{{
|
||||
PathScope: "/myapi",
|
||||
OutputFile: "log.txt",
|
||||
Format: caddylog.CommonLogFormat,
|
||||
}}},
|
||||
{`log /test accesslog.txt {combined}`, false, []caddylog.Rule{{
|
||||
PathScope: "/test",
|
||||
OutputFile: "accesslog.txt",
|
||||
Format: caddylog.CombinedLogFormat,
|
||||
}}},
|
||||
{`log /api1 log.txt
|
||||
log /api2 accesslog.txt {combined}`, false, []caddylog.Rule{{
|
||||
PathScope: "/api1",
|
||||
OutputFile: "log.txt",
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
}, {
|
||||
PathScope: "/api2",
|
||||
OutputFile: "accesslog.txt",
|
||||
Format: caddylog.CombinedLogFormat,
|
||||
}}},
|
||||
{`log /api3 stdout {host}
|
||||
log /api4 log.txt {when}`, false, []caddylog.Rule{{
|
||||
PathScope: "/api3",
|
||||
OutputFile: "stdout",
|
||||
Format: "{host}",
|
||||
}, {
|
||||
PathScope: "/api4",
|
||||
OutputFile: "log.txt",
|
||||
Format: "{when}",
|
||||
}}},
|
||||
{`log access.log { rotate { size 2 age 10 keep 3 } }`, false, []caddylog.Rule{{
|
||||
PathScope: "/",
|
||||
OutputFile: "access.log",
|
||||
Format: caddylog.DefaultLogFormat,
|
||||
Roller: &middleware.LogRoller{
|
||||
MaxSize: 2,
|
||||
MaxAge: 10,
|
||||
MaxBackups: 3,
|
||||
LocalTime: true,
|
||||
},
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputLogRules)
|
||||
actualLogRules, err := logParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualLogRules) != len(test.expectedLogRules) {
|
||||
t.Fatalf("Test %d expected %d no of Log rules, but got %d ",
|
||||
i, len(test.expectedLogRules), len(actualLogRules))
|
||||
}
|
||||
for j, actualLogRule := range actualLogRules {
|
||||
|
||||
if actualLogRule.PathScope != test.expectedLogRules[j].PathScope {
|
||||
t.Errorf("Test %d expected %dth LogRule PathScope to be %s , but got %s",
|
||||
i, j, test.expectedLogRules[j].PathScope, actualLogRule.PathScope)
|
||||
}
|
||||
|
||||
if actualLogRule.OutputFile != test.expectedLogRules[j].OutputFile {
|
||||
t.Errorf("Test %d expected %dth LogRule OutputFile to be %s , but got %s",
|
||||
i, j, test.expectedLogRules[j].OutputFile, actualLogRule.OutputFile)
|
||||
}
|
||||
|
||||
if actualLogRule.Format != test.expectedLogRules[j].Format {
|
||||
t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s",
|
||||
i, j, test.expectedLogRules[j].Format, actualLogRule.Format)
|
||||
}
|
||||
if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller == nil || actualLogRule.Roller == nil && test.expectedLogRules[j].Roller != nil {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller to be %v, but got %v",
|
||||
i, j, test.expectedLogRules[j].Roller, actualLogRule.Roller)
|
||||
}
|
||||
if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller != nil {
|
||||
if actualLogRule.Roller.Filename != test.expectedLogRules[j].Roller.Filename {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller Filename to be %s, but got %s",
|
||||
i, j, test.expectedLogRules[j].Roller.Filename, actualLogRule.Roller.Filename)
|
||||
}
|
||||
if actualLogRule.Roller.MaxAge != test.expectedLogRules[j].Roller.MaxAge {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller MaxAge to be %d, but got %d",
|
||||
i, j, test.expectedLogRules[j].Roller.MaxAge, actualLogRule.Roller.MaxAge)
|
||||
}
|
||||
if actualLogRule.Roller.MaxBackups != test.expectedLogRules[j].Roller.MaxBackups {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller MaxBackups to be %d, but got %d",
|
||||
i, j, test.expectedLogRules[j].Roller.MaxBackups, actualLogRule.Roller.MaxBackups)
|
||||
}
|
||||
if actualLogRule.Roller.MaxSize != test.expectedLogRules[j].Roller.MaxSize {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller MaxSize to be %d, but got %d",
|
||||
i, j, test.expectedLogRules[j].Roller.MaxSize, actualLogRule.Roller.MaxSize)
|
||||
}
|
||||
if actualLogRule.Roller.LocalTime != test.expectedLogRules[j].Roller.LocalTime {
|
||||
t.Fatalf("Test %d expected %dth LogRule Roller LocalTime to be %t, but got %t",
|
||||
i, j, test.expectedLogRules[j].Roller.LocalTime, actualLogRule.Roller.LocalTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/markdown"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
// Markdown configures a new Markdown middleware instance.
|
||||
func Markdown(c *Controller) (middleware.Middleware, error) {
|
||||
mdconfigs, err := markdownParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
md := markdown.Markdown{
|
||||
Root: c.Root,
|
||||
FileSys: http.Dir(c.Root),
|
||||
Configs: mdconfigs,
|
||||
IndexFiles: []string{"index.md"},
|
||||
}
|
||||
|
||||
// Sweep the whole path at startup to at least generate link index, maybe generate static site
|
||||
c.Startup = append(c.Startup, func() error {
|
||||
for i := range mdconfigs {
|
||||
cfg := mdconfigs[i]
|
||||
|
||||
// Generate link index and static files (if enabled)
|
||||
if err := markdown.GenerateStatic(md, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch file changes for static site generation if not in development mode.
|
||||
if !cfg.Development {
|
||||
markdown.Watch(md, cfg, markdown.DefaultInterval)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
md.Next = next
|
||||
return md
|
||||
}, nil
|
||||
}
|
||||
|
||||
func markdownParse(c *Controller) ([]*markdown.Config, error) {
|
||||
var mdconfigs []*markdown.Config
|
||||
|
||||
for c.Next() {
|
||||
md := &markdown.Config{
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
Templates: make(map[string]string),
|
||||
StaticFiles: make(map[string]string),
|
||||
}
|
||||
|
||||
// Get the path scope
|
||||
if !c.NextArg() || c.Val() == "{" {
|
||||
return mdconfigs, c.ArgErr()
|
||||
}
|
||||
md.PathScope = c.Val()
|
||||
|
||||
// Load any other configuration parameters
|
||||
for c.NextBlock() {
|
||||
if err := loadParams(c, md); err != nil {
|
||||
return mdconfigs, err
|
||||
}
|
||||
}
|
||||
|
||||
// If no extensions were specified, assume .md
|
||||
if len(md.Extensions) == 0 {
|
||||
md.Extensions = []string{".md"}
|
||||
}
|
||||
|
||||
mdconfigs = append(mdconfigs, md)
|
||||
}
|
||||
|
||||
return mdconfigs, nil
|
||||
}
|
||||
|
||||
func loadParams(c *Controller, mdc *markdown.Config) error {
|
||||
switch c.Val() {
|
||||
case "ext":
|
||||
exts := c.RemainingArgs()
|
||||
if len(exts) == 0 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
mdc.Extensions = append(mdc.Extensions, exts...)
|
||||
return nil
|
||||
case "css":
|
||||
if !c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
mdc.Styles = append(mdc.Styles, c.Val())
|
||||
return nil
|
||||
case "js":
|
||||
if !c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
mdc.Scripts = append(mdc.Scripts, c.Val())
|
||||
return nil
|
||||
case "template":
|
||||
tArgs := c.RemainingArgs()
|
||||
switch len(tArgs) {
|
||||
case 0:
|
||||
return c.ArgErr()
|
||||
case 1:
|
||||
if _, ok := mdc.Templates[markdown.DefaultTemplate]; ok {
|
||||
return c.Err("only one default template is allowed, use alias.")
|
||||
}
|
||||
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0]))
|
||||
mdc.Templates[markdown.DefaultTemplate] = fpath
|
||||
return nil
|
||||
case 2:
|
||||
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1]))
|
||||
mdc.Templates[tArgs[0]] = fpath
|
||||
return nil
|
||||
default:
|
||||
return c.ArgErr()
|
||||
}
|
||||
case "sitegen":
|
||||
if c.NextArg() {
|
||||
mdc.StaticDir = path.Join(c.Root, c.Val())
|
||||
} else {
|
||||
mdc.StaticDir = path.Join(c.Root, markdown.DefaultStaticDir)
|
||||
}
|
||||
if c.NextArg() {
|
||||
// only 1 argument allowed
|
||||
return c.ArgErr()
|
||||
}
|
||||
return nil
|
||||
case "dev":
|
||||
if c.NextArg() {
|
||||
mdc.Development = strings.ToLower(c.Val()) == "true"
|
||||
} else {
|
||||
mdc.Development = true
|
||||
}
|
||||
if c.NextArg() {
|
||||
// only 1 argument allowed
|
||||
return c.ArgErr()
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return c.Err("Expected valid markdown configuration property")
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/markdown"
|
||||
)
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
|
||||
c := NewTestController(`markdown /blog`)
|
||||
|
||||
mid, err := Markdown(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(markdown.Markdown)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Markdown, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Configs[0].PathScope != "/blog" {
|
||||
t.Errorf("Expected /blog as the Path Scope")
|
||||
}
|
||||
if fmt.Sprint(myHandler.Configs[0].Extensions) != fmt.Sprint([]string{".md"}) {
|
||||
t.Errorf("Expected .md as the Default Extension")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownStaticGen(t *testing.T) {
|
||||
c := NewTestController(`markdown /blog {
|
||||
ext .md
|
||||
template tpl_with_include.html
|
||||
sitegen
|
||||
}`)
|
||||
|
||||
c.Root = "./testdata"
|
||||
mid, err := Markdown(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
for _, start := range c.Startup {
|
||||
err := start()
|
||||
if err != nil {
|
||||
t.Errorf("Startup error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
next := middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Next shouldn't be called")
|
||||
return 0, nil
|
||||
})
|
||||
hndlr := mid(next)
|
||||
mkdwn, ok := hndlr.(markdown.Markdown)
|
||||
if !ok {
|
||||
t.Fatalf("Was expecting a markdown.Markdown but got %T", hndlr)
|
||||
}
|
||||
|
||||
expectedStaticFiles := map[string]string{"/blog/first_post.md": "testdata/generated_site/blog/first_post.md/index.html"}
|
||||
if fmt.Sprint(expectedStaticFiles) != fmt.Sprint(mkdwn.Configs[0].StaticFiles) {
|
||||
t.Fatalf("Test expected StaticFiles to be %s, but got %s",
|
||||
fmt.Sprint(expectedStaticFiles), fmt.Sprint(mkdwn.Configs[0].StaticFiles))
|
||||
}
|
||||
|
||||
filePath := "testdata/generated_site/blog/first_post.md/index.html"
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
t.Fatalf("An error occured when getting the file information: %v", err)
|
||||
}
|
||||
|
||||
html, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("An error occured when getting the file content: %v", err)
|
||||
}
|
||||
|
||||
expectedBody := []byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>first_post</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Header title</h1>
|
||||
|
||||
<h1>Test h1</h1>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
if !bytes.Equal(html, expectedBody) {
|
||||
t.Fatalf("Expected file content: %s got: %s", string(expectedBody), string(html))
|
||||
}
|
||||
|
||||
fp := filepath.Join(c.Root, markdown.DefaultStaticDir)
|
||||
if err = os.RemoveAll(fp); err != nil {
|
||||
t.Errorf("Error while removing the generated static files: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputMarkdownConfig string
|
||||
shouldErr bool
|
||||
expectedMarkdownConfig []markdown.Config
|
||||
}{
|
||||
|
||||
{`markdown /blog {
|
||||
ext .md .txt
|
||||
css /resources/css/blog.css
|
||||
js /resources/js/blog.js
|
||||
}`, false, []markdown.Config{{
|
||||
PathScope: "/blog",
|
||||
Extensions: []string{".md", ".txt"},
|
||||
Styles: []string{"/resources/css/blog.css"},
|
||||
Scripts: []string{"/resources/js/blog.js"},
|
||||
}}},
|
||||
{`markdown /blog {
|
||||
ext .md
|
||||
template tpl_with_include.html
|
||||
sitegen
|
||||
}`, false, []markdown.Config{{
|
||||
PathScope: "/blog",
|
||||
Extensions: []string{".md"},
|
||||
Templates: map[string]string{markdown.DefaultTemplate: "testdata/tpl_with_include.html"},
|
||||
StaticDir: markdown.DefaultStaticDir,
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputMarkdownConfig)
|
||||
c.Root = "./testdata"
|
||||
actualMarkdownConfigs, err := markdownParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualMarkdownConfigs) != len(test.expectedMarkdownConfig) {
|
||||
t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
|
||||
i, len(test.expectedMarkdownConfig), len(actualMarkdownConfigs))
|
||||
}
|
||||
for j, actualMarkdownConfig := range actualMarkdownConfigs {
|
||||
|
||||
if actualMarkdownConfig.PathScope != test.expectedMarkdownConfig[j].PathScope {
|
||||
t.Errorf("Test %d expected %dth Markdown PathScope to be %s , but got %s",
|
||||
i, j, test.expectedMarkdownConfig[j].PathScope, actualMarkdownConfig.PathScope)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualMarkdownConfig.Styles) != fmt.Sprint(test.expectedMarkdownConfig[j].Styles) {
|
||||
t.Errorf("Test %d expected %dth Markdown Config Styles to be %s , but got %s",
|
||||
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Styles), fmt.Sprint(actualMarkdownConfig.Styles))
|
||||
}
|
||||
if fmt.Sprint(actualMarkdownConfig.Scripts) != fmt.Sprint(test.expectedMarkdownConfig[j].Scripts) {
|
||||
t.Errorf("Test %d expected %dth Markdown Config Scripts to be %s , but got %s",
|
||||
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Scripts), fmt.Sprint(actualMarkdownConfig.Scripts))
|
||||
}
|
||||
if fmt.Sprint(actualMarkdownConfig.Templates) != fmt.Sprint(test.expectedMarkdownConfig[j].Templates) {
|
||||
t.Errorf("Test %d expected %dth Markdown Config Templates to be %s , but got %s",
|
||||
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Templates), fmt.Sprint(actualMarkdownConfig.Templates))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/mime"
|
||||
)
|
||||
|
||||
// Mime configures a new mime middleware instance.
|
||||
func Mime(c *Controller) (middleware.Middleware, error) {
|
||||
configs, err := mimeParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return mime.Mime{Next: next, Configs: configs}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mimeParse(c *Controller) ([]mime.Config, error) {
|
||||
var configs []mime.Config
|
||||
|
||||
for c.Next() {
|
||||
// At least one extension is required
|
||||
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 2:
|
||||
if err := validateExt(args[0]); err != nil {
|
||||
return configs, err
|
||||
}
|
||||
configs = append(configs, mime.Config{Ext: args[0], ContentType: args[1]})
|
||||
case 1:
|
||||
return configs, c.ArgErr()
|
||||
case 0:
|
||||
for c.NextBlock() {
|
||||
ext := c.Val()
|
||||
if err := validateExt(ext); err != nil {
|
||||
return configs, err
|
||||
}
|
||||
if !c.NextArg() {
|
||||
return configs, c.ArgErr()
|
||||
}
|
||||
configs = append(configs, mime.Config{Ext: ext, ContentType: c.Val()})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// validateExt checks for valid file name extension.
|
||||
func validateExt(ext string) error {
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
return fmt.Errorf(`mime: invalid extension "%v" (must start with dot)`, ext)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/mime"
|
||||
)
|
||||
|
||||
func TestMime(t *testing.T) {
|
||||
|
||||
c := NewTestController(`mime .txt text/plain`)
|
||||
|
||||
mid, err := Mime(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(mime.Mime)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Mime, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
}{
|
||||
{`mime {`, true},
|
||||
{`mime {}`, true},
|
||||
{`mime a b`, true},
|
||||
{`mime a {`, true},
|
||||
{`mime { txt f } `, true},
|
||||
{`mime { html } `, true},
|
||||
{`mime {
|
||||
.html text/html
|
||||
.txt text/plain
|
||||
} `, false},
|
||||
{`mime { .html text/html } `, false},
|
||||
{`mime { .html
|
||||
} `, true},
|
||||
{`mime .txt text/plain`, false},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
m, err := mimeParse(c)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %v: Expected error but found nil %v", i, m)
|
||||
} else if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/proxy"
|
||||
)
|
||||
|
||||
// Proxy configures a new Proxy middleware instance.
|
||||
func Proxy(c *Controller) (middleware.Middleware, error) {
|
||||
upstreams, err := proxy.NewStaticUpstreams(c.Dispenser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return proxy.Proxy{Next: next, Upstreams: upstreams}
|
||||
}, nil
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
)
|
||||
|
||||
// Redir configures a new Redirect middleware instance.
|
||||
func Redir(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := redirParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return redirect.Redirect{Next: next, Rules: rules}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func redirParse(c *Controller) ([]redirect.Rule, error) {
|
||||
var redirects []redirect.Rule
|
||||
|
||||
// setRedirCode sets the redirect code for rule if it can, or returns an error
|
||||
setRedirCode := func(code string, rule *redirect.Rule) error {
|
||||
if code == "meta" {
|
||||
rule.Meta = true
|
||||
} else if codeNumber, ok := httpRedirs[code]; ok {
|
||||
rule.Code = codeNumber
|
||||
} else {
|
||||
return c.Errf("Invalid redirect code '%v'", code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkAndSaveRule checks the rule for validity (except the redir code)
|
||||
// and saves it if it's valid, or returns an error.
|
||||
checkAndSaveRule := func(rule redirect.Rule) error {
|
||||
if rule.FromPath == rule.To {
|
||||
return c.Err("'from' and 'to' values of redirect rule cannot be the same")
|
||||
}
|
||||
|
||||
for _, otherRule := range redirects {
|
||||
if otherRule.FromPath == rule.FromPath {
|
||||
return c.Errf("rule with duplicate 'from' value: %s -> %s", otherRule.FromPath, otherRule.To)
|
||||
}
|
||||
}
|
||||
|
||||
redirects = append(redirects, rule)
|
||||
return nil
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
|
||||
var hadOptionalBlock bool
|
||||
for c.NextBlock() {
|
||||
hadOptionalBlock = true
|
||||
|
||||
var rule redirect.Rule
|
||||
|
||||
if c.Config.TLS.Enabled {
|
||||
rule.FromScheme = "https"
|
||||
} else {
|
||||
rule.FromScheme = "http"
|
||||
}
|
||||
|
||||
// Set initial redirect code
|
||||
// BUG: If the code is specified for a whole block and that code is invalid,
|
||||
// the line number will appear on the first line inside the block, even if that
|
||||
// line overwrites the block-level code with a valid redirect code. The program
|
||||
// still functions correctly, but the line number in the error reporting is
|
||||
// misleading to the user.
|
||||
if len(args) == 1 {
|
||||
err := setRedirCode(args[0], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
} else {
|
||||
rule.Code = http.StatusMovedPermanently // default code
|
||||
}
|
||||
|
||||
// RemainingArgs only gets the values after the current token, but in our
|
||||
// case we want to include the current token to get an accurate count.
|
||||
insideArgs := append([]string{c.Val()}, c.RemainingArgs()...)
|
||||
|
||||
switch len(insideArgs) {
|
||||
case 1:
|
||||
// To specified (catch-all redirect)
|
||||
// Not sure why user is doing this in a table, as it causes all other redirects to be ignored.
|
||||
// As such, this feature remains undocumented.
|
||||
rule.FromPath = "/"
|
||||
rule.To = insideArgs[0]
|
||||
case 2:
|
||||
// From and To specified
|
||||
rule.FromPath = insideArgs[0]
|
||||
rule.To = insideArgs[1]
|
||||
case 3:
|
||||
// From, To, and Code specified
|
||||
rule.FromPath = insideArgs[0]
|
||||
rule.To = insideArgs[1]
|
||||
err := setRedirCode(insideArgs[2], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
default:
|
||||
return redirects, c.ArgErr()
|
||||
}
|
||||
|
||||
err := checkAndSaveRule(rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
}
|
||||
|
||||
if !hadOptionalBlock {
|
||||
var rule redirect.Rule
|
||||
|
||||
if c.Config.TLS.Enabled {
|
||||
rule.FromScheme = "https"
|
||||
} else {
|
||||
rule.FromScheme = "http"
|
||||
}
|
||||
|
||||
rule.Code = http.StatusMovedPermanently // default
|
||||
|
||||
switch len(args) {
|
||||
case 1:
|
||||
// To specified (catch-all redirect)
|
||||
rule.FromPath = "/"
|
||||
rule.To = args[0]
|
||||
case 2:
|
||||
// To and Code specified (catch-all redirect)
|
||||
rule.FromPath = "/"
|
||||
rule.To = args[0]
|
||||
err := setRedirCode(args[1], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
case 3:
|
||||
// From, To, and Code specified
|
||||
rule.FromPath = args[0]
|
||||
rule.To = args[1]
|
||||
err := setRedirCode(args[2], &rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
default:
|
||||
return redirects, c.ArgErr()
|
||||
}
|
||||
|
||||
err := checkAndSaveRule(rule)
|
||||
if err != nil {
|
||||
return redirects, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return redirects, nil
|
||||
}
|
||||
|
||||
// httpRedirs is a list of supported HTTP redirect codes.
|
||||
var httpRedirs = map[string]int{
|
||||
"300": http.StatusMultipleChoices,
|
||||
"301": http.StatusMovedPermanently,
|
||||
"302": http.StatusFound, // (NOT CORRECT for "Temporary Redirect", see 307)
|
||||
"303": http.StatusSeeOther,
|
||||
"304": http.StatusNotModified,
|
||||
"305": http.StatusUseProxy,
|
||||
"307": http.StatusTemporaryRedirect,
|
||||
"308": 308, // Permanent Redirect
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/rewrite"
|
||||
)
|
||||
|
||||
// Rewrite configures a new Rewrite middleware instance.
|
||||
func Rewrite(c *Controller) (middleware.Middleware, error) {
|
||||
rewrites, err := rewriteParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return rewrite.Rewrite{Next: next, Rules: rewrites}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
|
||||
var simpleRules []rewrite.Rule
|
||||
var regexpRules []rewrite.Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule rewrite.Rule
|
||||
var err error
|
||||
var base = "/"
|
||||
var pattern, to string
|
||||
var ext []string
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 2:
|
||||
rule = rewrite.NewSimpleRule(args[0], args[1])
|
||||
simpleRules = append(simpleRules, rule)
|
||||
case 1:
|
||||
base = args[0]
|
||||
fallthrough
|
||||
case 0:
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "r", "regexp":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
pattern = c.Val()
|
||||
case "to":
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
to = c.Val()
|
||||
case "ext":
|
||||
args1 := c.RemainingArgs()
|
||||
if len(args1) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
ext = args1
|
||||
default:
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
}
|
||||
// ensure pattern and to are specified
|
||||
if pattern == "" || to == "" {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
if rule, err = rewrite.NewRegexpRule(base, pattern, to, ext); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
regexpRules = append(regexpRules, rule)
|
||||
default:
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// put simple rules in front to avoid regexp computation for them
|
||||
return append(simpleRules, regexpRules...), nil
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/mholt/caddy/middleware/rewrite"
|
||||
)
|
||||
|
||||
func TestRewrite(t *testing.T) {
|
||||
c := NewTestController(`rewrite /from /to`)
|
||||
|
||||
mid, err := Rewrite(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, but got: %v", err)
|
||||
}
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(rewrite.Rewrite)
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Rewrite, got: %#v", handler)
|
||||
}
|
||||
|
||||
if !SameNext(myHandler.Next, EmptyNext) {
|
||||
t.Error("'Next' field of handler was not set properly")
|
||||
}
|
||||
|
||||
if len(myHandler.Rules) != 1 {
|
||||
t.Errorf("Expected handler to have %d rule, has %d instead", 1, len(myHandler.Rules))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteParse(t *testing.T) {
|
||||
simpleTests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []rewrite.Rule
|
||||
}{
|
||||
{`rewrite /from /to`, false, []rewrite.Rule{
|
||||
rewrite.SimpleRule{From: "/from", To: "/to"},
|
||||
}},
|
||||
{`rewrite /from /to
|
||||
rewrite a b`, false, []rewrite.Rule{
|
||||
rewrite.SimpleRule{From: "/from", To: "/to"},
|
||||
rewrite.SimpleRule{From: "a", To: "b"},
|
||||
}},
|
||||
{`rewrite a`, true, []rewrite.Rule{}},
|
||||
{`rewrite`, true, []rewrite.Rule{}},
|
||||
{`rewrite a b c`, true, []rewrite.Rule{
|
||||
rewrite.SimpleRule{From: "a", To: "b"},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range simpleTests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := rewriteParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
} else if err != nil && test.shouldErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, e := range test.expected {
|
||||
actualRule := actual[j].(rewrite.SimpleRule)
|
||||
expectedRule := e.(rewrite.SimpleRule)
|
||||
|
||||
if actualRule.From != expectedRule.From {
|
||||
t.Errorf("Test %d, rule %d: Expected From=%s, got %s",
|
||||
i, j, expectedRule.From, actualRule.From)
|
||||
}
|
||||
|
||||
if actualRule.To != expectedRule.To {
|
||||
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
|
||||
i, j, expectedRule.To, actualRule.To)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
regexpTests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expected []rewrite.Rule
|
||||
}{
|
||||
{`rewrite {
|
||||
r .*
|
||||
to /to
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.RegexpRule{Base: "/", To: "/to", Regexp: regexp.MustCompile(".*")},
|
||||
}},
|
||||
{`rewrite {
|
||||
regexp .*
|
||||
to /to
|
||||
ext / html txt
|
||||
}`, false, []rewrite.Rule{
|
||||
&rewrite.RegexpRule{Base: "/", To: "/to", Exts: []string{"/", "html", "txt"}, Regexp: regexp.MustCompile(".*")},
|
||||
}},
|
||||
{`rewrite /path {
|
||||
r rr
|
||||
to /dest
|
||||
}
|
||||
rewrite / {
|
||||
regexp [a-z]+
|
||||
to /to
|
||||
}
|
||||
`, false, []rewrite.Rule{
|
||||
&rewrite.RegexpRule{Base: "/path", To: "/dest", Regexp: regexp.MustCompile("rr")},
|
||||
&rewrite.RegexpRule{Base: "/", To: "/to", Regexp: regexp.MustCompile("[a-z]+")},
|
||||
}},
|
||||
{`rewrite {
|
||||
to /to
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.RegexpRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
r .*
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.RegexpRule{},
|
||||
}},
|
||||
{`rewrite {
|
||||
|
||||
}`, true, []rewrite.Rule{
|
||||
&rewrite.RegexpRule{},
|
||||
}},
|
||||
{`rewrite /`, true, []rewrite.Rule{
|
||||
&rewrite.RegexpRule{},
|
||||
}},
|
||||
}
|
||||
|
||||
for i, test := range regexpTests {
|
||||
c := NewTestController(test.input)
|
||||
actual, err := rewriteParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
} else if err != nil && test.shouldErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
t.Fatalf("Test %d expected %d rules, but got %d",
|
||||
i, len(test.expected), len(actual))
|
||||
}
|
||||
|
||||
for j, e := range test.expected {
|
||||
actualRule := actual[j].(*rewrite.RegexpRule)
|
||||
expectedRule := e.(*rewrite.RegexpRule)
|
||||
|
||||
if actualRule.Base != expectedRule.Base {
|
||||
t.Errorf("Test %d, rule %d: Expected Base=%s, got %s",
|
||||
i, j, expectedRule.Base, actualRule.Base)
|
||||
}
|
||||
|
||||
if actualRule.To != expectedRule.To {
|
||||
t.Errorf("Test %d, rule %d: Expected To=%s, got %s",
|
||||
i, j, expectedRule.To, actualRule.To)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualRule.Exts) != fmt.Sprint(expectedRule.Exts) {
|
||||
t.Errorf("Test %d, rule %d: Expected Ext=%v, got %v",
|
||||
i, j, expectedRule.To, actualRule.To)
|
||||
}
|
||||
|
||||
if actualRule.String() != expectedRule.String() {
|
||||
t.Errorf("Test %d, rule %d: Expected Pattern=%s, got %s",
|
||||
i, j, expectedRule.String(), actualRule.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
func parseRoller(c *Controller) (*middleware.LogRoller, error) {
|
||||
var size, age, keep int
|
||||
// This is kind of a hack to support nested blocks:
|
||||
// As we are already in a block: either log or errors,
|
||||
// c.nesting > 0 but, as soon as c meets a }, it thinks
|
||||
// the block is over and return false for c.NextBlock.
|
||||
for c.NextBlock() {
|
||||
what := c.Val()
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
value := c.Val()
|
||||
var err error
|
||||
switch what {
|
||||
case "size":
|
||||
size, err = strconv.Atoi(value)
|
||||
case "age":
|
||||
age, err = strconv.Atoi(value)
|
||||
case "keep":
|
||||
keep, err = strconv.Atoi(value)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &middleware.LogRoller{
|
||||
MaxSize: size,
|
||||
MaxAge: age,
|
||||
MaxBackups: keep,
|
||||
LocalTime: true,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
func Root(c *Controller) (middleware.Middleware, error) {
|
||||
for c.Next() {
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
c.Root = c.Val()
|
||||
}
|
||||
|
||||
// Check if root path exists
|
||||
_, err := os.Stat(c.Root)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Allow this, because the folder might appear later.
|
||||
// But make sure the user knows!
|
||||
log.Printf("Warning: Root path does not exist: %s", c.Root)
|
||||
} else {
|
||||
return nil, c.Errf("Unable to access root path '%s': %v", c.Root, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRoot(t *testing.T) {
|
||||
|
||||
// Predefined error substrings
|
||||
parseErrContent := "Parse error:"
|
||||
unableToAccessErrContent := "Unable to access root path"
|
||||
|
||||
existingDirPath, err := getTempDirPath()
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
|
||||
}
|
||||
|
||||
nonExistingDir := filepath.Join(existingDirPath, "highly_unlikely_to_exist_dir")
|
||||
|
||||
existingFile, err := ioutil.TempFile("", "root_test")
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to create temp file for testing! Error was: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
existingFile.Close()
|
||||
os.Remove(existingFile.Name())
|
||||
}()
|
||||
|
||||
inaccessiblePath := getInaccessiblePath(existingFile.Name())
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldErr bool
|
||||
expectedRoot string // expected root, set to the controller. Empty for negative cases.
|
||||
expectedErrContent string // substring from the expected error. Empty for positive cases.
|
||||
}{
|
||||
// positive
|
||||
{
|
||||
fmt.Sprintf(`root %s`, nonExistingDir), false, nonExistingDir, "",
|
||||
},
|
||||
{
|
||||
fmt.Sprintf(`root %s`, existingDirPath), false, existingDirPath, "",
|
||||
},
|
||||
// negative
|
||||
{
|
||||
`root `, true, "", parseErrContent,
|
||||
},
|
||||
{
|
||||
fmt.Sprintf(`root %s`, inaccessiblePath), true, "", unableToAccessErrContent,
|
||||
},
|
||||
{
|
||||
fmt.Sprintf(`root {
|
||||
%s
|
||||
}`, existingDirPath), true, "", parseErrContent,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
mid, err := Root(c)
|
||||
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if !test.shouldErr {
|
||||
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), test.expectedErrContent) {
|
||||
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input)
|
||||
}
|
||||
}
|
||||
|
||||
// the Root method always returns a nil middleware
|
||||
if mid != nil {
|
||||
t.Errorf("Middware, returned from Root() was not nil: %v", mid)
|
||||
}
|
||||
|
||||
// check c.Root only if we are in a positive test.
|
||||
if !test.shouldErr && test.expectedRoot != c.Root {
|
||||
t.Errorf("Root not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedRoot, c.Root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getTempDirPath returnes the path to the system temp directory. If it does not exists - an error is returned.
|
||||
func getTempDirPath() (string, error) {
|
||||
tempDir := os.TempDir()
|
||||
|
||||
_, err := os.Stat(tempDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tempDir, nil
|
||||
}
|
||||
|
||||
func getInaccessiblePath(file string) string {
|
||||
// null byte in filename is not allowed on Windows AND unix
|
||||
return filepath.Join("C:", "file\x00name")
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
func Startup(c *Controller) (middleware.Middleware, error) {
|
||||
return nil, registerCallback(c, &c.Startup)
|
||||
}
|
||||
|
||||
func Shutdown(c *Controller) (middleware.Middleware, error) {
|
||||
return nil, registerCallback(c, &c.Shutdown)
|
||||
}
|
||||
|
||||
// registerCallback registers a callback function to execute by
|
||||
// using c to parse the line. It appends the callback function
|
||||
// to the list of callback functions passed in by reference.
|
||||
func registerCallback(c *Controller, list *[]func() error) error {
|
||||
var funcs []func() error
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return c.ArgErr()
|
||||
}
|
||||
|
||||
nonblock := false
|
||||
if len(args) > 1 && args[len(args)-1] == "&" {
|
||||
// Run command in background; non-blocking
|
||||
nonblock = true
|
||||
args = args[:len(args)-1]
|
||||
}
|
||||
|
||||
command, args, err := middleware.SplitCommandAndArgs(strings.Join(args, " "))
|
||||
if err != nil {
|
||||
return c.Err(err.Error())
|
||||
}
|
||||
|
||||
fn := func() error {
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if nonblock {
|
||||
return cmd.Start()
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
funcs = append(funcs, fn)
|
||||
}
|
||||
|
||||
return c.OncePerServerBlock(func() error {
|
||||
*list = append(*list, funcs...)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The Startup function's tests are symmetrical to Shutdown tests,
|
||||
// because the Startup and Shutdown functions share virtually the
|
||||
// same functionality
|
||||
func TestStartup(t *testing.T) {
|
||||
|
||||
tempDirPath, err := getTempDirPath()
|
||||
if err != nil {
|
||||
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
|
||||
}
|
||||
|
||||
testDir := filepath.Join(tempDirPath, "temp_dir_for_testing_startupshutdown.go")
|
||||
osSenitiveTestDir := filepath.FromSlash(testDir)
|
||||
|
||||
exec.Command("rm", "-r", osSenitiveTestDir).Run() // removes osSenitiveTestDir from the OS's temp directory, if the osSenitiveTestDir already exists
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
shouldExecutionErr bool
|
||||
shouldRemoveErr bool
|
||||
}{
|
||||
// test case #0 tests proper functionality blocking commands
|
||||
{"startup mkdir " + osSenitiveTestDir, false, false},
|
||||
|
||||
// test case #1 tests proper functionality of non-blocking commands
|
||||
{"startup mkdir " + osSenitiveTestDir + " &", false, true},
|
||||
|
||||
// test case #2 tests handling of non-existant commands
|
||||
{"startup " + strconv.Itoa(int(time.Now().UnixNano())), true, true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.input)
|
||||
_, err = Startup(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
err = c.Startup[0]()
|
||||
if err != nil && !test.shouldExecutionErr {
|
||||
t.Errorf("Test %d recieved an error of:\n%v", i, err)
|
||||
}
|
||||
err = os.Remove(osSenitiveTestDir)
|
||||
if err != nil && !test.shouldRemoveErr {
|
||||
t.Errorf("Test %d recieved an error of:\n%v", i, err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/templates"
|
||||
)
|
||||
|
||||
// Templates configures a new Templates middleware instance.
|
||||
func Templates(c *Controller) (middleware.Middleware, error) {
|
||||
rules, err := templatesParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tmpls := templates.Templates{
|
||||
Rules: rules,
|
||||
Root: c.Root,
|
||||
FileSys: http.Dir(c.Root),
|
||||
}
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
tmpls.Next = next
|
||||
return tmpls
|
||||
}, nil
|
||||
}
|
||||
|
||||
func templatesParse(c *Controller) ([]templates.Rule, error) {
|
||||
var rules []templates.Rule
|
||||
|
||||
for c.Next() {
|
||||
var rule templates.Rule
|
||||
|
||||
rule.Path = defaultTemplatePath
|
||||
rule.Extensions = defaultTemplateExtensions
|
||||
|
||||
args := c.RemainingArgs()
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
// Optional block
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "path":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
rule.Path = args[0]
|
||||
|
||||
case "ext":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
rule.Extensions = args
|
||||
|
||||
case "between":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
rule.Delims[0] = args[0]
|
||||
rule.Delims[1] = args[1]
|
||||
}
|
||||
}
|
||||
default:
|
||||
// First argument would be the path
|
||||
rule.Path = args[0]
|
||||
|
||||
// Any remaining arguments are extensions
|
||||
rule.Extensions = args[1:]
|
||||
if len(rule.Extensions) == 0 {
|
||||
rule.Extensions = defaultTemplateExtensions
|
||||
}
|
||||
}
|
||||
|
||||
for _, ext := range rule.Extensions {
|
||||
rule.IndexFiles = append(rule.IndexFiles, "index"+ext)
|
||||
}
|
||||
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
const defaultTemplatePath = "/"
|
||||
|
||||
var defaultTemplateExtensions = []string{".html", ".htm", ".tmpl", ".tpl", ".txt"}
|
||||
@@ -1,112 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/templates"
|
||||
)
|
||||
|
||||
func TestTemplates(t *testing.T) {
|
||||
|
||||
c := NewTestController(`templates`)
|
||||
|
||||
mid, err := Templates(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(templates.Templates)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type Templates, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Rules[0].Path != defaultTemplatePath {
|
||||
t.Errorf("Expected / as the default Path")
|
||||
}
|
||||
if fmt.Sprint(myHandler.Rules[0].Extensions) != fmt.Sprint(defaultTemplateExtensions) {
|
||||
t.Errorf("Expected %v to be the Default Extensions", defaultTemplateExtensions)
|
||||
}
|
||||
var indexFiles []string
|
||||
for _, extension := range defaultTemplateExtensions {
|
||||
indexFiles = append(indexFiles, "index"+extension)
|
||||
}
|
||||
if fmt.Sprint(myHandler.Rules[0].IndexFiles) != fmt.Sprint(indexFiles) {
|
||||
t.Errorf("Expected %v to be the Default Index files", indexFiles)
|
||||
}
|
||||
if myHandler.Rules[0].Delims != [2]string{} {
|
||||
t.Errorf("Expected %v to be the Default Delims", [2]string{})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplatesParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputTemplateConfig string
|
||||
shouldErr bool
|
||||
expectedTemplateConfig []templates.Rule
|
||||
}{
|
||||
{`templates /api1`, false, []templates.Rule{{
|
||||
Path: "/api1",
|
||||
Extensions: defaultTemplateExtensions,
|
||||
Delims: [2]string{},
|
||||
}}},
|
||||
{`templates /api2 .txt .htm`, false, []templates.Rule{{
|
||||
Path: "/api2",
|
||||
Extensions: []string{".txt", ".htm"},
|
||||
Delims: [2]string{},
|
||||
}}},
|
||||
|
||||
{`templates /api3 .htm .html
|
||||
templates /api4 .txt .tpl `, false, []templates.Rule{{
|
||||
Path: "/api3",
|
||||
Extensions: []string{".htm", ".html"},
|
||||
Delims: [2]string{},
|
||||
}, {
|
||||
Path: "/api4",
|
||||
Extensions: []string{".txt", ".tpl"},
|
||||
Delims: [2]string{},
|
||||
}}},
|
||||
{`templates {
|
||||
path /api5
|
||||
ext .html
|
||||
between {% %}
|
||||
}`, false, []templates.Rule{{
|
||||
Path: "/api5",
|
||||
Extensions: []string{".html"},
|
||||
Delims: [2]string{"{%", "%}"},
|
||||
}}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputTemplateConfig)
|
||||
actualTemplateConfigs, err := templatesParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualTemplateConfigs) != len(test.expectedTemplateConfig) {
|
||||
t.Fatalf("Test %d expected %d no of Template configs, but got %d ",
|
||||
i, len(test.expectedTemplateConfig), len(actualTemplateConfigs))
|
||||
}
|
||||
for j, actualTemplateConfig := range actualTemplateConfigs {
|
||||
|
||||
if actualTemplateConfig.Path != test.expectedTemplateConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth Template Config Path to be %s , but got %s",
|
||||
i, j, test.expectedTemplateConfig[j].Path, actualTemplateConfig.Path)
|
||||
}
|
||||
|
||||
if fmt.Sprint(actualTemplateConfig.Extensions) != fmt.Sprint(test.expectedTemplateConfig[j].Extensions) {
|
||||
t.Errorf("Expected %v to be the Extensions , but got %v instead", test.expectedTemplateConfig[j].Extensions, actualTemplateConfig.Extensions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
# Test h1
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
<h1>Header title</h1>
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Doc.title}}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{.Include "header.html"}}
|
||||
{{.Doc.body}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,153 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
func TLS(c *Controller) (middleware.Middleware, error) {
|
||||
if c.Port == "http" {
|
||||
c.TLS.Enabled = false
|
||||
log.Printf("Warning: TLS disabled for %s://%s. To force TLS over the plaintext HTTP port, "+
|
||||
"specify port 80 explicitly (https://%s:80).", c.Port, c.Host, c.Host)
|
||||
} else {
|
||||
c.TLS.Enabled = true // they had a tls directive, so assume it's on unless we confirm otherwise later
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
args := c.RemainingArgs()
|
||||
switch len(args) {
|
||||
case 1:
|
||||
c.TLS.LetsEncryptEmail = args[0]
|
||||
|
||||
// user can force-disable LE activation this way
|
||||
if c.TLS.LetsEncryptEmail == "off" {
|
||||
c.TLS.Enabled = false
|
||||
}
|
||||
case 2:
|
||||
c.TLS.Certificate = args[0]
|
||||
c.TLS.Key = args[1]
|
||||
|
||||
// manual HTTPS configuration without port specified should be
|
||||
// served on the HTTPS port; that is what user would expect, and
|
||||
// makes it consistent with how the letsencrypt package works.
|
||||
if c.Port == "" {
|
||||
c.Port = "https"
|
||||
}
|
||||
default:
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
|
||||
// Optional block
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "protocols":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 2 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
value, ok := supportedProtocols[strings.ToLower(args[0])]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.ProtocolMinVersion = value
|
||||
value, ok = supportedProtocols[strings.ToLower(args[1])]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong protocol name or protocol not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.ProtocolMaxVersion = value
|
||||
case "ciphers":
|
||||
for c.NextArg() {
|
||||
value, ok := supportedCiphersMap[strings.ToUpper(c.Val())]
|
||||
if !ok {
|
||||
return nil, c.Errf("Wrong cipher name or cipher not supported '%s'", c.Val())
|
||||
}
|
||||
c.TLS.Ciphers = append(c.TLS.Ciphers, value)
|
||||
}
|
||||
case "clients":
|
||||
c.TLS.ClientCerts = c.RemainingArgs()
|
||||
if len(c.TLS.ClientCerts) == 0 {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
default:
|
||||
return nil, c.Errf("Unknown keyword '%s'", c.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no ciphers provided, use all that Caddy supports for the protocol
|
||||
if len(c.TLS.Ciphers) == 0 {
|
||||
c.TLS.Ciphers = supportedCiphers
|
||||
}
|
||||
|
||||
// Not a cipher suite, but still important for mitigating protocol downgrade attacks
|
||||
c.TLS.Ciphers = append(c.TLS.Ciphers, tls.TLS_FALLBACK_SCSV)
|
||||
|
||||
// Set default protocol min and max versions - must balance compatibility and security
|
||||
if c.TLS.ProtocolMinVersion == 0 {
|
||||
c.TLS.ProtocolMinVersion = tls.VersionTLS10
|
||||
}
|
||||
if c.TLS.ProtocolMaxVersion == 0 {
|
||||
c.TLS.ProtocolMaxVersion = tls.VersionTLS12
|
||||
}
|
||||
|
||||
// Prefer server cipher suites
|
||||
c.TLS.PreferServerCipherSuites = true
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Map of supported protocols
|
||||
// SSLv3 will be not supported in future release
|
||||
// HTTP/2 only supports TLS 1.2 and higher
|
||||
var supportedProtocols = map[string]uint16{
|
||||
"ssl3.0": tls.VersionSSL30,
|
||||
"tls1.0": tls.VersionTLS10,
|
||||
"tls1.1": tls.VersionTLS11,
|
||||
"tls1.2": tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// Map of supported ciphers, used only for parsing config.
|
||||
//
|
||||
// Note that, at time of writing, HTTP/2 blacklists 276 cipher suites,
|
||||
// including all but two of the suites below (the two GCM suites).
|
||||
// See https://http2.github.io/http2-spec/#BadCipherSuites
|
||||
//
|
||||
// TLS_FALLBACK_SCSV is not in this list because we manually ensure
|
||||
// it is always added (even though it is not technically a cipher suite).
|
||||
//
|
||||
// This map, like any map, is NOT ORDERED. Do not range over this map.
|
||||
var supportedCiphersMap = map[string]uint16{
|
||||
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
"ECDHE-RSA-AES128-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
"ECDHE-RSA-AES256-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-ECDSA-AES256-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-ECDSA-AES128-CBC-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
"RSA-AES128-CBC-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
"RSA-AES256-CBC-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
"ECDHE-RSA-3DES-EDE-CBC-SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
"RSA-3DES-EDE-CBC-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
}
|
||||
|
||||
// List of supported cipher suites in descending order of preference.
|
||||
// Ordering is very important! Getting the wrong order will break
|
||||
// mainstream clients, especially with HTTP/2.
|
||||
//
|
||||
// Note that TLS_FALLBACK_SCSV is not in this list since it is always
|
||||
// added manually.
|
||||
var supportedCiphers = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTLSParseBasic(t *testing.T) {
|
||||
c := NewTestController(`tls cert.pem key.pem`)
|
||||
|
||||
_, err := TLS(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
// Basic checks
|
||||
if c.TLS.Certificate != "cert.pem" {
|
||||
t.Errorf("Expected certificate arg to be 'cert.pem', was '%s'", c.TLS.Certificate)
|
||||
}
|
||||
if c.TLS.Key != "key.pem" {
|
||||
t.Errorf("Expected key arg to be 'key.pem', was '%s'", c.TLS.Key)
|
||||
}
|
||||
if !c.TLS.Enabled {
|
||||
t.Error("Expected TLS Enabled=true, but was false")
|
||||
}
|
||||
|
||||
// Security defaults
|
||||
if c.TLS.ProtocolMinVersion != tls.VersionTLS10 {
|
||||
t.Errorf("Expected 'tls1.0 (0x0301)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion)
|
||||
}
|
||||
if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 {
|
||||
t.Errorf("Expected 'tls1.2 (0x0303)' as ProtocolMaxVersion, got %v", c.TLS.ProtocolMaxVersion)
|
||||
}
|
||||
|
||||
// Cipher checks
|
||||
expectedCiphers := []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_FALLBACK_SCSV,
|
||||
}
|
||||
|
||||
// Ensure count is correct (plus one for TLS_FALLBACK_SCSV)
|
||||
if len(c.TLS.Ciphers) != len(supportedCiphers)+1 {
|
||||
t.Errorf("Expected %v Ciphers (including TLS_FALLBACK_SCSV), got %v",
|
||||
len(supportedCiphers)+1, len(c.TLS.Ciphers))
|
||||
}
|
||||
|
||||
// Ensure ordering is correct
|
||||
for i, actual := range c.TLS.Ciphers {
|
||||
if actual != expectedCiphers[i] {
|
||||
t.Errorf("Expected cipher in position %d to be %0x, got %0x", i, expectedCiphers[i], actual)
|
||||
}
|
||||
}
|
||||
|
||||
if !c.TLS.PreferServerCipherSuites {
|
||||
t.Error("Expected PreferServerCipherSuites = true, but was false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSParseIncompleteParams(t *testing.T) {
|
||||
c := NewTestController(`tls`)
|
||||
|
||||
_, err := TLS(c)
|
||||
if err == nil {
|
||||
t.Errorf("Expected errors (first check), but no error returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSParseWithOptionalParams(t *testing.T) {
|
||||
params := `tls cert.crt cert.key {
|
||||
protocols ssl3.0 tls1.2
|
||||
ciphers RSA-3DES-EDE-CBC-SHA RSA-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256
|
||||
}`
|
||||
c := NewTestController(params)
|
||||
|
||||
_, err := TLS(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if c.TLS.ProtocolMinVersion != tls.VersionSSL30 {
|
||||
t.Errorf("Expected 'ssl3.0 (0x0300)' as ProtocolMinVersion, got %#v", c.TLS.ProtocolMinVersion)
|
||||
}
|
||||
|
||||
if c.TLS.ProtocolMaxVersion != tls.VersionTLS12 {
|
||||
t.Errorf("Expected 'tls1.2 (0x0302)' as ProtocolMaxVersion, got %#v", c.TLS.ProtocolMaxVersion)
|
||||
}
|
||||
|
||||
if len(c.TLS.Ciphers)-1 != 3 {
|
||||
t.Errorf("Expected 3 Ciphers (not including TLS_FALLBACK_SCSV), got %v", len(c.TLS.Ciphers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSParseWithWrongOptionalParams(t *testing.T) {
|
||||
// Test protocols wrong params
|
||||
params := `tls cert.crt cert.key {
|
||||
protocols ssl tls
|
||||
}`
|
||||
c := NewTestController(params)
|
||||
_, err := TLS(c)
|
||||
if err == nil {
|
||||
t.Errorf("Expected errors, but no error returned")
|
||||
}
|
||||
|
||||
// Test ciphers wrong params
|
||||
params = `tls cert.crt cert.key {
|
||||
ciphers not-valid-cipher
|
||||
}`
|
||||
c = NewTestController(params)
|
||||
_, err = TLS(c)
|
||||
if err == nil {
|
||||
t.Errorf("Expected errors, but no error returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSParseWithClientAuth(t *testing.T) {
|
||||
params := `tls cert.crt cert.key {
|
||||
clients client_ca.crt client2_ca.crt
|
||||
}`
|
||||
c := NewTestController(params)
|
||||
_, err := TLS(c)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if count := len(c.TLS.ClientCerts); count != 2 {
|
||||
t.Fatalf("Expected two client certs, had %d", count)
|
||||
}
|
||||
if actual := c.TLS.ClientCerts[0]; actual != "client_ca.crt" {
|
||||
t.Errorf("Expected first client cert file to be '%s', but was '%s'", "client_ca.crt", actual)
|
||||
}
|
||||
if actual := c.TLS.ClientCerts[1]; actual != "client2_ca.crt" {
|
||||
t.Errorf("Expected second client cert file to be '%s', but was '%s'", "client2_ca.crt", actual)
|
||||
}
|
||||
|
||||
// Test missing client cert file
|
||||
params = `tls cert.crt cert.key {
|
||||
clients
|
||||
}`
|
||||
c = NewTestController(params)
|
||||
_, err = TLS(c)
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error, but no error returned")
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/websocket"
|
||||
)
|
||||
|
||||
// WebSocket configures a new WebSocket middleware instance.
|
||||
func WebSocket(c *Controller) (middleware.Middleware, error) {
|
||||
|
||||
websocks, err := webSocketParse(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websocket.GatewayInterface = c.AppName + "-CGI/1.1"
|
||||
websocket.ServerSoftware = c.AppName + "/" + c.AppVersion
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
return websocket.WebSocket{Next: next, Sockets: websocks}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func webSocketParse(c *Controller) ([]websocket.Config, error) {
|
||||
var websocks []websocket.Config
|
||||
var respawn bool
|
||||
|
||||
optionalBlock := func() (hadBlock bool, err error) {
|
||||
for c.NextBlock() {
|
||||
hadBlock = true
|
||||
if c.Val() == "respawn" {
|
||||
respawn = true
|
||||
} else {
|
||||
return true, c.Err("Expected websocket configuration parameter in block")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for c.Next() {
|
||||
var val, path, command string
|
||||
|
||||
// Path or command; not sure which yet
|
||||
if !c.NextArg() {
|
||||
return nil, c.ArgErr()
|
||||
}
|
||||
val = c.Val()
|
||||
|
||||
// Extra configuration may be in a block
|
||||
hadBlock, err := optionalBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !hadBlock {
|
||||
// The next argument on this line will be the command or an open curly brace
|
||||
if c.NextArg() {
|
||||
path = val
|
||||
command = c.Val()
|
||||
} else {
|
||||
path = "/"
|
||||
command = val
|
||||
}
|
||||
|
||||
// Okay, check again for optional block
|
||||
hadBlock, err = optionalBlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Split command into the actual command and its arguments
|
||||
cmd, args, err := middleware.SplitCommandAndArgs(command)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
websocks = append(websocks, websocket.Config{
|
||||
Path: path,
|
||||
Command: cmd,
|
||||
Arguments: args,
|
||||
Respawn: respawn, // TODO: This isn't used currently
|
||||
})
|
||||
}
|
||||
|
||||
return websocks, nil
|
||||
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware/websocket"
|
||||
)
|
||||
|
||||
func TestWebSocket(t *testing.T) {
|
||||
|
||||
c := NewTestController(`websocket cat`)
|
||||
|
||||
mid, err := WebSocket(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
handler := mid(EmptyNext)
|
||||
myHandler, ok := handler.(websocket.WebSocket)
|
||||
|
||||
if !ok {
|
||||
t.Fatalf("Expected handler to be type WebSocket, got: %#v", handler)
|
||||
}
|
||||
|
||||
if myHandler.Sockets[0].Path != "/" {
|
||||
t.Errorf("Expected / as the default Path")
|
||||
}
|
||||
if myHandler.Sockets[0].Command != "cat" {
|
||||
t.Errorf("Expected %s as the command", "cat")
|
||||
}
|
||||
|
||||
}
|
||||
func TestWebSocketParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
inputWebSocketConfig string
|
||||
shouldErr bool
|
||||
expectedWebSocketConfig []websocket.Config
|
||||
}{
|
||||
{`websocket /api1 cat`, false, []websocket.Config{{
|
||||
Path: "/api1",
|
||||
Command: "cat",
|
||||
}}},
|
||||
|
||||
{`websocket /api3 cat
|
||||
websocket /api4 cat `, false, []websocket.Config{{
|
||||
Path: "/api3",
|
||||
Command: "cat",
|
||||
}, {
|
||||
Path: "/api4",
|
||||
Command: "cat",
|
||||
}}},
|
||||
|
||||
{`websocket /api5 "cmd arg1 arg2 arg3"`, false, []websocket.Config{{
|
||||
Path: "/api5",
|
||||
Command: "cmd",
|
||||
Arguments: []string{"arg1", "arg2", "arg3"},
|
||||
}}},
|
||||
|
||||
// accept respawn
|
||||
{`websocket /api6 cat {
|
||||
respawn
|
||||
}`, false, []websocket.Config{{
|
||||
Path: "/api6",
|
||||
Command: "cat",
|
||||
}}},
|
||||
|
||||
// invalid configuration
|
||||
{`websocket /api7 cat {
|
||||
invalid
|
||||
}`, true, []websocket.Config{}},
|
||||
}
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputWebSocketConfig)
|
||||
actualWebSocketConfigs, err := webSocketParse(c)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
if len(actualWebSocketConfigs) != len(test.expectedWebSocketConfig) {
|
||||
t.Fatalf("Test %d expected %d no of WebSocket configs, but got %d ",
|
||||
i, len(test.expectedWebSocketConfig), len(actualWebSocketConfigs))
|
||||
}
|
||||
for j, actualWebSocketConfig := range actualWebSocketConfigs {
|
||||
|
||||
if actualWebSocketConfig.Path != test.expectedWebSocketConfig[j].Path {
|
||||
t.Errorf("Test %d expected %dth WebSocket Config Path to be %s , but got %s",
|
||||
i, j, test.expectedWebSocketConfig[j].Path, actualWebSocketConfig.Path)
|
||||
}
|
||||
|
||||
if actualWebSocketConfig.Command != test.expectedWebSocketConfig[j].Command {
|
||||
t.Errorf("Test %d expected %dth WebSocket Config Command to be %s , but got %s",
|
||||
i, j, test.expectedWebSocketConfig[j].Command, actualWebSocketConfig.Command)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Trap quit signals (cross-platform)
|
||||
go func() {
|
||||
shutdown := make(chan os.Signal, 1)
|
||||
signal.Notify(shutdown, os.Interrupt, os.Kill)
|
||||
<-shutdown
|
||||
|
||||
var exitCode int
|
||||
|
||||
serversMu.Lock()
|
||||
errs := server.ShutdownCallbacks(servers)
|
||||
serversMu.Unlock()
|
||||
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Println(err)
|
||||
}
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
os.Exit(exitCode)
|
||||
}()
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Trap POSIX-only signals
|
||||
go func() {
|
||||
reload := make(chan os.Signal, 1)
|
||||
signal.Notify(reload, syscall.SIGUSR1) // reload configuration
|
||||
|
||||
for {
|
||||
<-reload
|
||||
|
||||
var updatedCaddyfile Input
|
||||
|
||||
caddyfileMu.Lock()
|
||||
if caddyfile.IsFile() {
|
||||
body, err := ioutil.ReadFile(caddyfile.Path())
|
||||
if err == nil {
|
||||
caddyfile = CaddyfileInput{
|
||||
Filepath: caddyfile.Path(),
|
||||
Contents: body,
|
||||
RealFile: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
caddyfileMu.Unlock()
|
||||
|
||||
err := Restart(updatedCaddyfile)
|
||||
if err != nil {
|
||||
log.Println("error at restart:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
Vendored
-150
@@ -1,150 +0,0 @@
|
||||
CHANGES
|
||||
|
||||
0.8 beta
|
||||
- Let's Encrypt (free, automatic, fully-managed HTTPS for your sites)
|
||||
- Graceful restarts (for POSIX-compatible systems)
|
||||
- Major internal refactoring to allow use of Caddy as library
|
||||
- New directive 'mime' to customize Content-Type based on file extension
|
||||
- New -accept flag to accept Let's Encrypt SA without prompt
|
||||
- New -email flag to customize default email used for ACME transactions
|
||||
- New -ca flag to customize ACME CA server URL
|
||||
- New -revoke flag to revoke a certificate
|
||||
- browse: Render filenames with multiple whitespace properly
|
||||
- markdown: Include Last-Modified header in response
|
||||
- startup, shutdown: Better Windows support
|
||||
- templates: Bug fix for .Host when port is absent
|
||||
- templates: Include Last-Modified header in response
|
||||
- templates: Support for custom delimiters
|
||||
- tls: For non-local hosts, default port is now 443 unless specified
|
||||
- tls: Force-disable HTTPS
|
||||
- tls: Specify Let's Encrypt email address
|
||||
- Many, many more tests and numerous bug fixes and improvements
|
||||
|
||||
|
||||
0.7.6 (September 28, 2015)
|
||||
- Pass in simple Caddyfile as command line arguments
|
||||
- basicauth: Support for legacy htpasswd files
|
||||
- browse: JSON response with file listing
|
||||
- core: Caddyfile as command line argument
|
||||
- errors: Can write full stack trace to HTTP response for debugging
|
||||
- errors, log: Roll log files after certain size or age
|
||||
- proxy: Fix for 32-bit architectures
|
||||
- rewrite: Better compatibility with fastcgi and PHP apps
|
||||
- templates: Added .StripExt and .StripHTML methods
|
||||
- Internal improvements and minor bug fixes
|
||||
|
||||
|
||||
0.7.5 (August 5, 2015)
|
||||
- core: All listeners bind to 0.0.0.0 unless 'bind' directive is used
|
||||
- fastcgi: Set HTTPS env variable if connection is secure
|
||||
- log: Output to system log (except Windows)
|
||||
- markdown: Added dev command to disable caching during development
|
||||
- markdown: Fixed error reporting during initial site generation
|
||||
- markdown: Fixed crash if path does not exist when server starts
|
||||
- markdown: Fixed site generation and link indexing when files change
|
||||
- templates: Added .NowDate for use in date-related functions
|
||||
- Several bug fixes related to startup and shutdown functions
|
||||
|
||||
|
||||
0.7.4 (July 30, 2015)
|
||||
- browse: Sorting preference persisted in cookie
|
||||
- browse: Added index.txt and default.txt to list of default files
|
||||
- browse: Template files may now use Caddy template actions
|
||||
- markdown: Template files may now use Caddy template actions
|
||||
- markdown: Several bug fixes, especially for large and empty Markdown files
|
||||
- markdown: Generate index pages to link to markdown pages (sitegen only)
|
||||
- markdown: Flatten structure of front matter, changed template variables
|
||||
- redir: Can use variables (placeholders) like log formats can
|
||||
- redir: Catch-all redirects no longer preserve path; use {uri} instead
|
||||
- redir: Syntax supports redirect tables by opening a block
|
||||
- templates: Renamed .Date to .Now and added .Truncate, .Replace actions
|
||||
- Other minor internal improvements and more tests
|
||||
|
||||
|
||||
0.7.3 (July 15, 2015)
|
||||
- errors: Error log now shows timestamp with each entry
|
||||
- gzip: Fixed; Default filtering is by extension; removed MIME type filter
|
||||
- import: Fixed; works inside and outside server blocks
|
||||
- redir: Query string preserved on catch-all redirects
|
||||
- templates: Proper 403 or 404 errors for restricted or missing files
|
||||
|
||||
|
||||
0.7.2 (July 1, 2015)
|
||||
- Custom builds through caddyserver.com - extend Caddy by writing addons
|
||||
- browse: Sort by clicking column heading or using query string
|
||||
- core: Serving hostname that doesn't resolve issues warning then listens on 0.0.0.0
|
||||
- errors: Missing error page during parse time is warning, not error
|
||||
- ext: Extension only appended if request path does not end in /
|
||||
- fastcgi: Fix for backend responding without status text
|
||||
- fastcgi: Fix PATH_TRANSLATED when PATH_INFO is empty (RFC 3875)
|
||||
- git: Removed from core (available as add-on)
|
||||
- gzip: Enable by file path and/or extension
|
||||
- gzip: Customize compression level
|
||||
- log: Fix for missing status in log entry when error unhandled
|
||||
- proxy: Strip prefix from path for proxy to path
|
||||
- redir: Meta tag redirects
|
||||
- templates: Support for nested includes
|
||||
- Internal improvements and more tests
|
||||
|
||||
|
||||
0.7.1 (June 2, 2015)
|
||||
- basicauth: Patched timing vulnerability
|
||||
- proxy: Support for WebSocket backends
|
||||
- tls: Client authentication
|
||||
|
||||
|
||||
0.7.0 (May 25, 2015)
|
||||
- New directive 'internal' to protect resources with X-Accel-Redirect
|
||||
- New -version flag to show program name and version
|
||||
- core: Fixed escaped backslash characters inside quoted strings
|
||||
- core: Fixed parsing Caddyfile for IPv6 addresses missing ports
|
||||
- core: A notice is shown when non-local address resolves to loopback interface
|
||||
- core: Warns if file descriptor limit is too low for production site (Mac/Linux)
|
||||
- fastcgi: Support for Unix sockets
|
||||
- git: Fixed issue that prevented pulling at designated interval
|
||||
- header: Remove a header field by prefixing field name with "-"
|
||||
- markdown: Simple static site generation
|
||||
- markdown: Support for metadata ("front matter") at beginning of files
|
||||
- rewrite: Experimental support for regular expressions
|
||||
- tls: Customize cipher suites and protocols
|
||||
- tls: Removed RC4 ciphers
|
||||
- Other internal improvements that are not user-facing (more tests, etc.)
|
||||
|
||||
|
||||
0.6.0 (May 7, 2015)
|
||||
- New directive 'git' to automatically pull changes
|
||||
- New directive 'bind' to override host server binds to
|
||||
- New -root flag to specify root path to default site
|
||||
- Ability to receive config data piped through stdin
|
||||
- core: Warning if root directory doesn't exist at startup
|
||||
- core: Entire process dies if any server fails to start
|
||||
- gzip: Fixed Content-Length value when proxying requests
|
||||
- errors: Error log now includes file and line number of panics
|
||||
- fastcgi: Pass custom environment variables
|
||||
- fastcgi: Support for HEAD, OPTIONS, PUT, PATCH, and DELETE methods
|
||||
- fastcgi: Fixed SERVER_SOFTWARE variables
|
||||
- markdown: Support for index files when URL points to a directory
|
||||
- proxy: Load balancing with multiple backends, health checks, failovers, and multiple policies
|
||||
- proxy: Add custom headers
|
||||
- startup/shutdown: Run command in background with '&' at end
|
||||
- templates: Added .tpl and .tmpl as default extensions
|
||||
- templates: Support for index files when URL points to a directory
|
||||
- templates: Changed .RemoteAddr to .IP and stripped out remote port
|
||||
- tls: TLS disabled (with warning) for servers that are explicitly http://
|
||||
- websocket: Fixed SERVER_SOFTWARE and GATEWAY_INTERFACE variables
|
||||
- Many internal improvements
|
||||
|
||||
|
||||
0.5.1 (April 30, 2015)
|
||||
- Default host is now 0.0.0.0 (wildcard)
|
||||
- New -host and -port flags to override default host and port
|
||||
- core: Support for binding to 0.0.0.0
|
||||
- core: Graceful error handling during heavy load; proper error responses
|
||||
- errors: Fixed file path handling
|
||||
- errors: Fixed panic due to nil log file
|
||||
- fastcgi: Support for index files
|
||||
- fastcgi: Fix for handling errors that come from responder
|
||||
|
||||
|
||||
0.5.0 (April 28, 2015)
|
||||
- Initial release
|
||||
Vendored
-539
@@ -1,539 +0,0 @@
|
||||
The enclosed software makes use of third-party libraries either in full
|
||||
or in part, original or modified. This file is part of your download so
|
||||
as to be in full compliance with the licenses of all bundled property.
|
||||
|
||||
|
||||
|
||||
###
|
||||
### github.com/mholt/caddy
|
||||
###
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###
|
||||
### Go standard library and http2
|
||||
###
|
||||
|
||||
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###
|
||||
### github.com/russross/blackfriday
|
||||
###
|
||||
|
||||
|
||||
Blackfriday is distributed under the Simplified BSD License:
|
||||
|
||||
> Copyright © 2011 Russ Ross
|
||||
> All rights reserved.
|
||||
>
|
||||
> Redistribution and use in source and binary forms, with or without
|
||||
> modification, are permitted provided that the following conditions
|
||||
> are met:
|
||||
>
|
||||
> 1. Redistributions of source code must retain the above copyright
|
||||
> notice, this list of conditions and the following disclaimer.
|
||||
>
|
||||
> 2. Redistributions in binary form must reproduce the above
|
||||
> copyright notice, this list of conditions and the following
|
||||
> disclaimer in the documentation and/or other materials provided with
|
||||
> the distribution.
|
||||
>
|
||||
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
> POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###
|
||||
### github.com/dustin/go-humanize
|
||||
###
|
||||
|
||||
|
||||
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
<http://www.opensource.org/licenses/mit-license.php>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###
|
||||
### github.com/flynn/go-shlex
|
||||
###
|
||||
|
||||
Apache 2.0 license as found in this file
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###
|
||||
### github.com/go-yaml/yaml
|
||||
###
|
||||
|
||||
|
||||
Copyright (c) 2011-2014 - Canonical Inc.
|
||||
|
||||
This software is licensed under the LGPLv3, included below.
|
||||
|
||||
As a special exception to the GNU Lesser General Public License version 3
|
||||
("LGPL3"), the copyright holders of this Library give you permission to
|
||||
convey to a third party a Combined Work that links statically or dynamically
|
||||
to this Library without providing any Minimal Corresponding Source or
|
||||
Minimal Application Code as set out in 4d or providing the installation
|
||||
information set out in section 4e, provided that you comply with the other
|
||||
provisions of LGPL3 and provided that you meet, for the Application the
|
||||
terms and conditions of the license(s) which apply to the Application.
|
||||
|
||||
Except as stated in this special exception, the provisions of LGPL3 will
|
||||
continue to comply in full to this Library. If you modify this Library, you
|
||||
may apply this exception to your version of this Library, but you are not
|
||||
obliged to do so. If you do not wish to do so, delete this exception
|
||||
statement from your version. This exception does not (and cannot) modify any
|
||||
license terms which apply to the Application, with which you must still
|
||||
comply.
|
||||
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
Vendored
-20
@@ -1,20 +0,0 @@
|
||||
CADDY 0.8 beta 2
|
||||
|
||||
Website
|
||||
https://caddyserver.com
|
||||
|
||||
Source Code
|
||||
https://github.com/mholt/caddy
|
||||
|
||||
|
||||
For instructions on using Caddy, please see the user guide on the website. For a list of what's new in this version, see CHANGES.txt.
|
||||
|
||||
If you have a question, bug report, or would like to contribute, please open an issue or submit a pull request on GitHub. Your contributions do not go unnoticed!
|
||||
|
||||
For a good time, follow @mholt6 on Twitter.
|
||||
|
||||
And thanks - you're awesome!
|
||||
|
||||
|
||||
---
|
||||
(c) 2015 Matthew Holt
|
||||
Vendored
-56
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
shopt -s nullglob # if no files match glob, assume empty list instead of string literal
|
||||
|
||||
|
||||
## PACKAGE TO BUILD
|
||||
Package=github.com/mholt/caddy
|
||||
|
||||
|
||||
## PATHS TO USE
|
||||
DistDir=$GOPATH/src/$Package/dist
|
||||
BuildDir=$DistDir/builds
|
||||
ReleaseDir=$DistDir/release
|
||||
|
||||
|
||||
## BEGIN
|
||||
|
||||
# Compile binaries
|
||||
mkdir -p $BuildDir
|
||||
cd $BuildDir
|
||||
rm -f caddy*
|
||||
gox $Package
|
||||
|
||||
# Zip them up with release notes and stuff
|
||||
mkdir -p $ReleaseDir
|
||||
cd $ReleaseDir
|
||||
rm -f caddy*
|
||||
for f in $BuildDir/*
|
||||
do
|
||||
# Name .zip file same as binary, but strip .exe from end
|
||||
zipname=$(basename ${f%".exe"})
|
||||
if [[ $f == *"linux"* ]] || [[ $f == *"bsd"* ]]; then
|
||||
zipname=${zipname}.tar.gz
|
||||
else
|
||||
zipname=${zipname}.zip
|
||||
fi
|
||||
|
||||
# Binary inside the zip file is simply the project name
|
||||
binbase=$(basename $Package)
|
||||
if [[ $f == *.exe ]]; then
|
||||
binbase=$binbase.exe
|
||||
fi
|
||||
bin=$BuildDir/$binbase
|
||||
mv $f $bin
|
||||
|
||||
# Compress distributable
|
||||
if [[ $zipname == *.zip ]]; then
|
||||
zip -j $zipname $bin $DistDir/CHANGES.txt $DistDir/LICENSES.txt $DistDir/README.txt
|
||||
else
|
||||
tar -cvzf $zipname -C $BuildDir $binbase -C $DistDir CHANGES.txt LICENSES.txt README.txt
|
||||
fi
|
||||
|
||||
# Put binary filename back to original
|
||||
mv $bin $f
|
||||
done
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,164 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/caddy"
|
||||
"github.com/mholt/caddy/caddy/letsencrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
conf string
|
||||
cpu string
|
||||
version bool
|
||||
revoke string
|
||||
)
|
||||
|
||||
const (
|
||||
appName = "Caddy"
|
||||
appVersion = "0.8 beta 2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")")
|
||||
flag.BoolVar(&caddy.HTTP2, "http2", true, "HTTP/2 support") // TODO: temporary flag until http2 merged into std lib
|
||||
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
|
||||
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
|
||||
flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site")
|
||||
flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host")
|
||||
flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port")
|
||||
flag.BoolVar(&version, "version", false, "Show version")
|
||||
// TODO: Boulder dev URL is: http://192.168.99.100:4000
|
||||
// TODO: Staging API URL is: https://acme-staging.api.letsencrypt.org
|
||||
// TODO: Production endpoint is: https://acme-v01.api.letsencrypt.org
|
||||
flag.StringVar(&letsencrypt.CAUrl, "ca", "https://acme-staging.api.letsencrypt.org", "Certificate authority ACME server")
|
||||
flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement")
|
||||
flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default Let's Encrypt account email address")
|
||||
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
caddy.AppName = appName
|
||||
caddy.AppVersion = appVersion
|
||||
|
||||
if version {
|
||||
fmt.Printf("%s %s\n", caddy.AppName, caddy.AppVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
if revoke != "" {
|
||||
err := letsencrypt.Revoke(revoke)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("Revoked certificate for %s\n", revoke)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Set CPU cap
|
||||
err := setCPU(cpu)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Get Caddyfile input
|
||||
caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Start your engines
|
||||
err = caddy.Start(caddyfile)
|
||||
if err != nil {
|
||||
if caddy.IsRestart() {
|
||||
log.Println("error starting servers:", err)
|
||||
} else {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Twiddle your thumbs
|
||||
caddy.Wait()
|
||||
}
|
||||
|
||||
func loadCaddyfile() (caddy.Input, error) {
|
||||
// -conf flag
|
||||
if conf != "" {
|
||||
contents, err := ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
RealFile: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// command line args
|
||||
if flag.NArg() > 0 {
|
||||
confBody := ":" + caddy.DefaultPort + "\n" + strings.Join(flag.Args(), "\n")
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: []byte(confBody),
|
||||
Filepath: "args",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Caddyfile in cwd
|
||||
contents, err := ioutil.ReadFile(caddy.DefaultConfigFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return caddy.DefaultInput(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: caddy.DefaultConfigFile,
|
||||
RealFile: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// setCPU parses string cpu and sets GOMAXPROCS
|
||||
// according to its value. It accepts either
|
||||
// a number (e.g. 3) or a percent (e.g. 50%).
|
||||
func setCPU(cpu string) error {
|
||||
var numCPU int
|
||||
|
||||
availCPU := runtime.NumCPU()
|
||||
|
||||
if strings.HasSuffix(cpu, "%") {
|
||||
// Percent
|
||||
var percent float32
|
||||
pctStr := cpu[:len(cpu)-1]
|
||||
pctInt, err := strconv.Atoi(pctStr)
|
||||
if err != nil || pctInt < 1 || pctInt > 100 {
|
||||
return errors.New("invalid CPU value: percentage must be between 1-100")
|
||||
}
|
||||
percent = float32(pctInt) / 100
|
||||
numCPU = int(float32(availCPU) * percent)
|
||||
} else {
|
||||
// Number
|
||||
num, err := strconv.Atoi(cpu)
|
||||
if err != nil || num < 1 {
|
||||
return errors.New("invalid CPU value: provide a number or percent greater than 0")
|
||||
}
|
||||
numCPU = num
|
||||
}
|
||||
|
||||
if numCPU > availCPU {
|
||||
numCPU = availCPU
|
||||
}
|
||||
|
||||
runtime.GOMAXPROCS(numCPU)
|
||||
return nil
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetCPU(t *testing.T) {
|
||||
currentCPU := runtime.GOMAXPROCS(-1)
|
||||
maxCPU := runtime.NumCPU()
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
output int
|
||||
shouldErr bool
|
||||
}{
|
||||
{"1", 1, false},
|
||||
{"-1", currentCPU, true},
|
||||
{"0", currentCPU, true},
|
||||
{"100%", maxCPU, false},
|
||||
{"50%", int(0.5 * float32(maxCPU)), false},
|
||||
{"110%", currentCPU, true},
|
||||
{"-10%", currentCPU, true},
|
||||
{"invalid input", currentCPU, true},
|
||||
{"invalid input%", currentCPU, true},
|
||||
{"9999", maxCPU, false}, // over available CPU
|
||||
} {
|
||||
err := setCPU(test.input)
|
||||
if test.shouldErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error, but there wasn't any", i)
|
||||
}
|
||||
if !test.shouldErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error, but there was one: %v", i, err)
|
||||
}
|
||||
if actual, expected := runtime.GOMAXPROCS(-1), test.output; actual != expected {
|
||||
t.Errorf("Test %d: GOMAXPROCS was %d but expected %d", i, actual, expected)
|
||||
}
|
||||
// teardown
|
||||
runtime.GOMAXPROCS(currentCPU)
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
// Package basicauth implements HTTP Basic Authentication.
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/jimstudt/http-authentication/basic"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
// BasicAuth is middleware to protect resources with a username and password.
|
||||
// Note that HTTP Basic Authentication is not secure by itself and should
|
||||
// not be used to protect important assets without HTTPS. Even then, the
|
||||
// security of HTTP Basic Auth is disputed. Use discretion when deciding
|
||||
// what to protect with BasicAuth.
|
||||
type BasicAuth struct {
|
||||
Next middleware.Handler
|
||||
SiteRoot string
|
||||
Rules []Rule
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middleware.Handler interface.
|
||||
func (a BasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
|
||||
var hasAuth bool
|
||||
var isAuthenticated bool
|
||||
|
||||
for _, rule := range a.Rules {
|
||||
for _, res := range rule.Resources {
|
||||
if !middleware.Path(r.URL.Path).Matches(res) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Path matches; parse auth header
|
||||
username, password, ok := r.BasicAuth()
|
||||
hasAuth = true
|
||||
|
||||
// Check credentials
|
||||
if !ok ||
|
||||
username != rule.Username ||
|
||||
!rule.Password(password) {
|
||||
//subtle.ConstantTimeCompare([]byte(password), []byte(rule.Password)) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Flag set only on successful authentication
|
||||
isAuthenticated = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasAuth {
|
||||
if !isAuthenticated {
|
||||
w.Header().Set("WWW-Authenticate", "Basic")
|
||||
return http.StatusUnauthorized, nil
|
||||
}
|
||||
// "It's an older code, sir, but it checks out. I was about to clear them."
|
||||
return a.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Pass-thru when no paths match
|
||||
return a.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Rule represents a BasicAuth rule. A username and password
|
||||
// combination protect the associated resources, which are
|
||||
// file or directory paths.
|
||||
type Rule struct {
|
||||
Username string
|
||||
Password func(string) bool
|
||||
Resources []string
|
||||
}
|
||||
|
||||
// PasswordMatcher determines whether a password mathes a rule.
|
||||
type PasswordMatcher func(pw string) bool
|
||||
|
||||
var (
|
||||
htpasswords map[string]map[string]PasswordMatcher
|
||||
htpasswordsMu sync.Mutex
|
||||
)
|
||||
|
||||
func GetHtpasswdMatcher(filename, username, siteRoot string) (PasswordMatcher, error) {
|
||||
filename = filepath.Join(siteRoot, filename)
|
||||
htpasswordsMu.Lock()
|
||||
if htpasswords == nil {
|
||||
htpasswords = make(map[string]map[string]PasswordMatcher)
|
||||
}
|
||||
pm := htpasswords[filename]
|
||||
if pm == nil {
|
||||
fh, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open %q: %v", filename, err)
|
||||
}
|
||||
defer fh.Close()
|
||||
pm = make(map[string]PasswordMatcher)
|
||||
if err = parseHtpasswd(pm, fh); err != nil {
|
||||
return nil, fmt.Errorf("parsing htpasswd %q: %v", fh.Name(), err)
|
||||
}
|
||||
htpasswords[filename] = pm
|
||||
}
|
||||
htpasswordsMu.Unlock()
|
||||
if pm[username] == nil {
|
||||
return nil, fmt.Errorf("username %q not found in %q", username, filename)
|
||||
}
|
||||
return pm[username], nil
|
||||
}
|
||||
|
||||
func parseHtpasswd(pm map[string]PasswordMatcher, r io.Reader) error {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.IndexByte(line, '#') == 0 {
|
||||
continue
|
||||
}
|
||||
i := strings.IndexByte(line, ':')
|
||||
if i <= 0 {
|
||||
return fmt.Errorf("malformed line, no color: %q", line)
|
||||
}
|
||||
user, encoded := line[:i], line[i+1:]
|
||||
for _, p := range basic.DefaultSystems {
|
||||
matcher, err := p(encoded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if matcher != nil {
|
||||
pm[user] = matcher.MatchesPassword
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// PlainMatcher returns a PasswordMatcher that does a constant-time
|
||||
// byte-wise comparison.
|
||||
func PlainMatcher(passw string) PasswordMatcher {
|
||||
return func(pw string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(pw), []byte(passw)) == 1
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package basicauth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
|
||||
rw := BasicAuth{
|
||||
Next: middleware.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "test", Password: PlainMatcher("ttest"), Resources: []string{"/testing"}},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
from string
|
||||
result int
|
||||
cred string
|
||||
}{
|
||||
{"/testing", http.StatusUnauthorized, "ttest:test"},
|
||||
{"/testing", http.StatusOK, "test:ttest"},
|
||||
{"/testing", http.StatusUnauthorized, ""},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
||||
req, err := http.NewRequest("GET", test.from, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not create HTTP request %v", i, err)
|
||||
}
|
||||
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(test.cred))
|
||||
req.Header.Set("Authorization", auth)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("Test %d: Expected Header '%d' but was '%d'",
|
||||
i, test.result, result)
|
||||
}
|
||||
if result == http.StatusUnauthorized {
|
||||
headers := rec.Header()
|
||||
if val, ok := headers["Www-Authenticate"]; ok {
|
||||
if val[0] != "Basic" {
|
||||
t.Errorf("Test %d, Www-Authenticate should be %s provided %s", i, "Basic", val[0])
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Test %d, should provide a header Www-Authenticate", i)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMultipleOverlappingRules(t *testing.T) {
|
||||
rw := BasicAuth{
|
||||
Next: middleware.HandlerFunc(contentHandler),
|
||||
Rules: []Rule{
|
||||
{Username: "t", Password: PlainMatcher("p1"), Resources: []string{"/t"}},
|
||||
{Username: "t1", Password: PlainMatcher("p2"), Resources: []string{"/t/t"}},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
from string
|
||||
result int
|
||||
cred string
|
||||
}{
|
||||
{"/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"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
||||
req, err := http.NewRequest("GET", test.from, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not create HTTP request %v", i, err)
|
||||
}
|
||||
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(test.cred))
|
||||
req.Header.Set("Authorization", auth)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
result, err := rw.ServeHTTP(rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Could not ServeHTTP %v", i, err)
|
||||
}
|
||||
if result != test.result {
|
||||
t.Errorf("Test %d: Expected Header '%d' but was '%d'",
|
||||
i, test.result, result)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func contentHandler(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
fmt.Fprintf(w, r.URL.String())
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func TestHtpasswd(t *testing.T) {
|
||||
htpasswdPasswd := "IedFOuGmTpT8"
|
||||
htpasswdFile := `sha1:{SHA}dcAUljwz99qFjYR0YLTXx0RqLww=
|
||||
md5:$apr1$l42y8rex$pOA2VJ0x/0TwaFeAF9nX61`
|
||||
|
||||
htfh, err := ioutil.TempFile("", "basicauth-")
|
||||
if err != nil {
|
||||
t.Skipf("Error creating temp file (%v), will skip htpassword test")
|
||||
return
|
||||
}
|
||||
defer os.Remove(htfh.Name())
|
||||
if _, err = htfh.Write([]byte(htpasswdFile)); err != nil {
|
||||
t.Fatalf("write htpasswd file %q: %v", htfh.Name(), err)
|
||||
}
|
||||
htfh.Close()
|
||||
|
||||
for i, username := range []string{"sha1", "md5"} {
|
||||
rule := Rule{Username: username, Resources: []string{"/testing"}}
|
||||
|
||||
siteRoot := filepath.Dir(htfh.Name())
|
||||
filename := filepath.Base(htfh.Name())
|
||||
if rule.Password, err = GetHtpasswdMatcher(filename, rule.Username, siteRoot); err != nil {
|
||||
t.Fatalf("GetHtpasswdMatcher(%q, %q): %v", htfh.Name(), rule.Username, err)
|
||||
}
|
||||
t.Logf("%d. username=%q password=%v", i, rule.Username, rule.Password)
|
||||
if !rule.Password(htpasswdPasswd) || rule.Password(htpasswdPasswd+"!") {
|
||||
t.Errorf("%d (%s) password does not match.", i, rule.Username)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
// Package browse provides middleware for listing files in a directory
|
||||
// when directory path is requested instead of a specific file.
|
||||
package browse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
// Browse is an http.Handler that can show a file listing when
|
||||
// directories in the given paths are specified.
|
||||
type Browse struct {
|
||||
Next middleware.Handler
|
||||
Root string
|
||||
Configs []Config
|
||||
IgnoreIndexes bool
|
||||
}
|
||||
|
||||
// Config is a configuration for browsing in a particular path.
|
||||
type Config struct {
|
||||
PathScope string
|
||||
Variables interface{}
|
||||
Template *template.Template
|
||||
}
|
||||
|
||||
// A Listing is used to fill out a template.
|
||||
type Listing struct {
|
||||
// The name of the directory (the last element of the path)
|
||||
Name string
|
||||
|
||||
// The full path of the request
|
||||
Path string
|
||||
|
||||
// Whether the parent directory is browsable
|
||||
CanGoUp bool
|
||||
|
||||
// The items (files and folders) in the path
|
||||
Items []FileInfo
|
||||
|
||||
// Which sorting order is used
|
||||
Sort string
|
||||
|
||||
// And which order
|
||||
Order string
|
||||
|
||||
// Optional custom variables for use in browse templates
|
||||
User interface{}
|
||||
|
||||
middleware.Context
|
||||
}
|
||||
|
||||
// FileInfo is the info about a particular file or directory
|
||||
type FileInfo struct {
|
||||
IsDir bool
|
||||
Name string
|
||||
Size int64
|
||||
URL string
|
||||
ModTime time.Time
|
||||
Mode os.FileMode
|
||||
}
|
||||
|
||||
// Implement sorting for Listing
|
||||
type byName Listing
|
||||
type bySize Listing
|
||||
type byTime Listing
|
||||
|
||||
// By Name
|
||||
func (l byName) Len() int { return len(l.Items) }
|
||||
func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
||||
|
||||
// Treat upper and lower case equally
|
||||
func (l byName) Less(i, j int) bool {
|
||||
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
|
||||
}
|
||||
|
||||
// By Size
|
||||
func (l bySize) Len() int { return len(l.Items) }
|
||||
func (l bySize) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
||||
func (l bySize) Less(i, j int) bool { return l.Items[i].Size < l.Items[j].Size }
|
||||
|
||||
// By Time
|
||||
func (l byTime) Len() int { return len(l.Items) }
|
||||
func (l byTime) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }
|
||||
func (l byTime) Less(i, j int) bool { return l.Items[i].ModTime.Before(l.Items[j].ModTime) }
|
||||
|
||||
// Add sorting method to "Listing"
|
||||
// it will apply what's in ".Sort" and ".Order"
|
||||
func (l Listing) applySort() {
|
||||
// Check '.Order' to know how to sort
|
||||
if l.Order == "desc" {
|
||||
switch l.Sort {
|
||||
case "name":
|
||||
sort.Sort(sort.Reverse(byName(l)))
|
||||
case "size":
|
||||
sort.Sort(sort.Reverse(bySize(l)))
|
||||
case "time":
|
||||
sort.Sort(sort.Reverse(byTime(l)))
|
||||
default:
|
||||
// If not one of the above, do nothing
|
||||
return
|
||||
}
|
||||
} else { // If we had more Orderings we could add them here
|
||||
switch l.Sort {
|
||||
case "name":
|
||||
sort.Sort(byName(l))
|
||||
case "size":
|
||||
sort.Sort(bySize(l))
|
||||
case "time":
|
||||
sort.Sort(byTime(l))
|
||||
default:
|
||||
// If not one of the above, do nothing
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HumanSize returns the size of the file as a human-readable string
|
||||
// in IEC format (i.e. power of 2 or base 1024).
|
||||
func (fi FileInfo) HumanSize() string {
|
||||
return humanize.IBytes(uint64(fi.Size))
|
||||
}
|
||||
|
||||
// HumanModTime returns the modified time of the file as a human-readable string.
|
||||
func (fi FileInfo) HumanModTime(format string) string {
|
||||
return fi.ModTime.Format(format)
|
||||
}
|
||||
|
||||
func directoryListing(files []os.FileInfo, r *http.Request, canGoUp bool, root string, ignoreIndexes bool, vars interface{}) (Listing, error) {
|
||||
var fileinfos []FileInfo
|
||||
var urlPath = r.URL.Path
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
|
||||
// Directory is not browsable if it contains index file
|
||||
if !ignoreIndexes {
|
||||
for _, indexName := range middleware.IndexPages {
|
||||
if name == indexName {
|
||||
return Listing{}, errors.New("Directory contains index file, not browsable!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
|
||||
url := url.URL{Path: name}
|
||||
|
||||
fileinfos = append(fileinfos, FileInfo{
|
||||
IsDir: f.IsDir(),
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
URL: url.String(),
|
||||
ModTime: f.ModTime(),
|
||||
Mode: f.Mode(),
|
||||
})
|
||||
}
|
||||
|
||||
return Listing{
|
||||
Name: path.Base(urlPath),
|
||||
Path: urlPath,
|
||||
CanGoUp: canGoUp,
|
||||
Items: fileinfos,
|
||||
Context: middleware.Context{
|
||||
Root: http.Dir(root),
|
||||
Req: r,
|
||||
URL: r.URL,
|
||||
},
|
||||
User: vars,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ServeHTTP implements the middleware.Handler interface.
|
||||
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
filename := b.Root + r.URL.Path
|
||||
info, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// See if there's a browse configuration to match the path
|
||||
for _, bc := range b.Configs {
|
||||
if !middleware.Path(r.URL.Path).Matches(bc.PathScope) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Browsing navigation gets messed up if browsing a directory
|
||||
// that doesn't end in "/" (which it should, anyway)
|
||||
if r.URL.Path[len(r.URL.Path)-1] != '/' {
|
||||
http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Load directory contents
|
||||
file, err := os.Open(b.Root + r.URL.Path)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
files, err := file.Readdir(-1)
|
||||
if err != nil {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
|
||||
// Determine if user can browse up another folder
|
||||
var canGoUp bool
|
||||
curPath := strings.TrimSuffix(r.URL.Path, "/")
|
||||
for _, other := range b.Configs {
|
||||
if strings.HasPrefix(path.Dir(curPath), other.PathScope) {
|
||||
canGoUp = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Assemble listing of directory contents
|
||||
listing, err := directoryListing(files, r, canGoUp, b.Root, b.IgnoreIndexes, bc.Variables)
|
||||
if err != nil { // directory isn't browsable
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the query vales and store them in the Listing struct
|
||||
listing.Sort, listing.Order = r.URL.Query().Get("sort"), r.URL.Query().Get("order")
|
||||
|
||||
// If the query 'sort' or 'order' is empty, check the cookies
|
||||
if listing.Sort == "" {
|
||||
sortCookie, sortErr := r.Cookie("sort")
|
||||
// if there's no sorting values in the cookies, default to "name" and "asc"
|
||||
if sortErr != nil {
|
||||
listing.Sort = "name"
|
||||
} else { // if we have values in the cookies, use them
|
||||
listing.Sort = sortCookie.Value
|
||||
}
|
||||
} else { // save the query value of 'sort' and 'order' as cookies
|
||||
http.SetCookie(w, &http.Cookie{Name: "sort", Value: listing.Sort, Path: "/"})
|
||||
http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, Path: "/"})
|
||||
}
|
||||
|
||||
if listing.Order == "" {
|
||||
orderCookie, orderErr := r.Cookie("order")
|
||||
// if there's no sorting values in the cookies, default to "name" and "asc"
|
||||
if orderErr != nil {
|
||||
listing.Order = "asc"
|
||||
} else { // if we have values in the cookies, use them
|
||||
listing.Order = orderCookie.Value
|
||||
}
|
||||
} else { // save the query value of 'sort' and 'order' as cookies
|
||||
http.SetCookie(w, &http.Cookie{Name: "order", Value: listing.Order, Path: "/"})
|
||||
}
|
||||
|
||||
// Apply the sorting
|
||||
listing.applySort()
|
||||
|
||||
var buf bytes.Buffer
|
||||
// check if we should provide json
|
||||
acceptHeader := strings.Join(r.Header["Accept"], ",")
|
||||
if strings.Contains(strings.ToLower(acceptHeader), "application/json") {
|
||||
var marsh []byte
|
||||
// check if we are limited
|
||||
if limitQuery := r.URL.Query().Get("limit"); limitQuery != "" {
|
||||
limit, err := strconv.Atoi(limitQuery)
|
||||
if err != nil { // if the 'limit' query can't be interpreted as a number, return err
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
// if `limit` is equal or less than len(listing.Items) and bigger than 0, list them
|
||||
if limit <= len(listing.Items) && limit > 0 {
|
||||
marsh, err = json.Marshal(listing.Items[:limit])
|
||||
} else { // if the 'limit' query is empty, or has the wrong value, list everything
|
||||
marsh, err = json.Marshal(listing.Items)
|
||||
}
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
} else { // there's no 'limit' query, list them all
|
||||
marsh, err = json.Marshal(listing.Items)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
// write the marshaled json to buf
|
||||
if _, err = buf.Write(marsh); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
} else { // there's no 'application/json' in the 'Accept' header, browse normally
|
||||
err = bc.Template.Execute(&buf, listing)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
}
|
||||
|
||||
buf.WriteTo(w)
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
// Didn't qualify; pass-thru
|
||||
return b.Next.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
package browse
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
// "sort" package has "IsSorted" function, but no "IsReversed";
|
||||
func isReversed(data sort.Interface) bool {
|
||||
n := data.Len()
|
||||
for i := n - 1; i > 0; i-- {
|
||||
if !data.Less(i, i-1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
// making up []fileInfo with bogus values;
|
||||
// to be used to make up our "listing"
|
||||
fileInfos := []FileInfo{
|
||||
{
|
||||
Name: "fizz",
|
||||
Size: 4,
|
||||
ModTime: time.Now().AddDate(-1, 1, 0),
|
||||
},
|
||||
{
|
||||
Name: "buzz",
|
||||
Size: 2,
|
||||
ModTime: time.Now().AddDate(0, -3, 3),
|
||||
},
|
||||
{
|
||||
Name: "bazz",
|
||||
Size: 1,
|
||||
ModTime: time.Now().AddDate(0, -2, -23),
|
||||
},
|
||||
{
|
||||
Name: "jazz",
|
||||
Size: 3,
|
||||
ModTime: time.Now(),
|
||||
},
|
||||
}
|
||||
listing := Listing{
|
||||
Name: "foobar",
|
||||
Path: "/fizz/buzz",
|
||||
CanGoUp: false,
|
||||
Items: fileInfos,
|
||||
}
|
||||
|
||||
// sort by name
|
||||
listing.Sort = "name"
|
||||
listing.applySort()
|
||||
if !sort.IsSorted(byName(listing)) {
|
||||
t.Errorf("The listing isn't name sorted: %v", listing.Items)
|
||||
}
|
||||
|
||||
// sort by size
|
||||
listing.Sort = "size"
|
||||
listing.applySort()
|
||||
if !sort.IsSorted(bySize(listing)) {
|
||||
t.Errorf("The listing isn't size sorted: %v", listing.Items)
|
||||
}
|
||||
|
||||
// sort by Time
|
||||
listing.Sort = "time"
|
||||
listing.applySort()
|
||||
if !sort.IsSorted(byTime(listing)) {
|
||||
t.Errorf("The listing isn't time sorted: %v", listing.Items)
|
||||
}
|
||||
|
||||
// reverse by name
|
||||
listing.Sort = "name"
|
||||
listing.Order = "desc"
|
||||
listing.applySort()
|
||||
if !isReversed(byName(listing)) {
|
||||
t.Errorf("The listing isn't reversed by name: %v", listing.Items)
|
||||
}
|
||||
|
||||
// reverse by size
|
||||
listing.Sort = "size"
|
||||
listing.Order = "desc"
|
||||
listing.applySort()
|
||||
if !isReversed(bySize(listing)) {
|
||||
t.Errorf("The listing isn't reversed by size: %v", listing.Items)
|
||||
}
|
||||
|
||||
// reverse by time
|
||||
listing.Sort = "time"
|
||||
listing.Order = "desc"
|
||||
listing.applySort()
|
||||
if !isReversed(byTime(listing)) {
|
||||
t.Errorf("The listing isn't reversed by time: %v", listing.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrowseTemplate(t *testing.T) {
|
||||
tmpl, err := template.ParseFiles("testdata/photos.tpl")
|
||||
if err != nil {
|
||||
t.Fatalf("An error occured while parsing the template: %v", err)
|
||||
}
|
||||
|
||||
b := Browse{
|
||||
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Next shouldn't be called")
|
||||
return 0, nil
|
||||
}),
|
||||
Root: "./testdata",
|
||||
Configs: []Config{
|
||||
{
|
||||
PathScope: "/photos",
|
||||
Template: tmpl,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/photos/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test: Could not create HTTP request: %v", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
code, err := b.ServeHTTP(rec, req)
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
|
||||
}
|
||||
|
||||
respBody := rec.Body.String()
|
||||
expectedBody := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Template</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Header</h1>
|
||||
|
||||
<h1>/photos/</h1>
|
||||
|
||||
<a href="test.html">test.html</a><br>
|
||||
|
||||
<a href="test2.html">test2.html</a><br>
|
||||
|
||||
<a href="test3.html">test3.html</a><br>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
if respBody != expectedBody {
|
||||
t.Fatalf("Expected body: %v got: %v", expectedBody, respBody)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestBrowseJson(t *testing.T) {
|
||||
|
||||
b := Browse{
|
||||
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Next shouldn't be called")
|
||||
return 0, nil
|
||||
}),
|
||||
Root: "./testdata",
|
||||
Configs: []Config{
|
||||
{
|
||||
PathScope: "/photos/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//Getting the listing from the ./testdata/photos, the listing returned will be used to validate test results
|
||||
testDataPath := b.Root + "/photos/"
|
||||
file, err := os.Open(testDataPath)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
t.Fatalf("Os Permission Error")
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
files, err := file.Readdir(-1)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to Read Contents of the directory")
|
||||
}
|
||||
var fileinfos []FileInfo
|
||||
|
||||
for i, f := range files {
|
||||
name := f.Name()
|
||||
|
||||
// Tests fail in CI environment because all file mod times are the same for
|
||||
// some reason, making the sorting unpredictable. To hack around this,
|
||||
// we ensure here that each file has a different mod time.
|
||||
chTime := f.ModTime().Add(-(time.Duration(i) * time.Second))
|
||||
if err := os.Chtimes(filepath.Join(testDataPath, name), chTime, chTime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
|
||||
url := url.URL{Path: name}
|
||||
|
||||
fileinfos = append(fileinfos, FileInfo{
|
||||
IsDir: f.IsDir(),
|
||||
Name: f.Name(),
|
||||
Size: f.Size(),
|
||||
URL: url.String(),
|
||||
ModTime: chTime,
|
||||
Mode: f.Mode(),
|
||||
})
|
||||
}
|
||||
listing := Listing{Items: fileinfos} // this listing will be used for validation inside the tests
|
||||
|
||||
tests := []struct {
|
||||
QueryUrl string
|
||||
SortBy string
|
||||
OrderBy string
|
||||
Limit int
|
||||
shouldErr bool
|
||||
expectedResult []FileInfo
|
||||
}{
|
||||
//test case 1: testing for default sort and order and without the limit parameter, default sort is by name and the default order is ascending
|
||||
//without the limit query entire listing will be produced
|
||||
{"/", "", "", -1, false, listing.Items},
|
||||
//test case 2: limit is set to 1, orderBy and sortBy is default
|
||||
{"/?limit=1", "", "", 1, false, listing.Items[:1]},
|
||||
//test case 3 : if the listing request is bigger than total size of listing then it should return everything
|
||||
{"/?limit=100000000", "", "", 100000000, false, listing.Items},
|
||||
//test case 4 : testing for negative limit
|
||||
{"/?limit=-1", "", "", -1, false, listing.Items},
|
||||
//test case 5 : testing with limit set to -1 and order set to descending
|
||||
{"/?limit=-1&order=desc", "", "desc", -1, false, listing.Items},
|
||||
//test case 6 : testing with limit set to 2 and order set to descending
|
||||
{"/?limit=2&order=desc", "", "desc", 2, false, listing.Items},
|
||||
//test case 7 : testing with limit set to 3 and order set to descending
|
||||
{"/?limit=3&order=desc", "", "desc", 3, false, listing.Items},
|
||||
//test case 8 : testing with limit set to 3 and order set to ascending
|
||||
{"/?limit=3&order=asc", "", "asc", 3, false, listing.Items},
|
||||
//test case 9 : testing with limit set to 1111111 and order set to ascending
|
||||
{"/?limit=1111111&order=asc", "", "asc", 1111111, false, listing.Items},
|
||||
//test case 10 : testing with limit set to default and order set to ascending and sorting by size
|
||||
{"/?order=asc&sort=size", "size", "asc", -1, false, listing.Items},
|
||||
//test case 11 : testing with limit set to default and order set to ascending and sorting by last modified
|
||||
{"/?order=asc&sort=time", "time", "asc", -1, false, listing.Items},
|
||||
//test case 12 : testing with limit set to 1 and order set to ascending and sorting by last modified
|
||||
{"/?order=asc&sort=time&limit=1", "time", "asc", 1, false, listing.Items},
|
||||
//test case 13 : testing with limit set to -100 and order set to ascending and sorting by last modified
|
||||
{"/?order=asc&sort=time&limit=-100", "time", "asc", -100, false, listing.Items},
|
||||
//test case 14 : testing with limit set to -100 and order set to ascending and sorting by size
|
||||
{"/?order=asc&sort=size&limit=-100", "size", "asc", -100, false, listing.Items},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
var marsh []byte
|
||||
req, err := http.NewRequest("GET", "/photos"+test.QueryUrl, nil)
|
||||
|
||||
if err == nil && test.shouldErr {
|
||||
t.Errorf("Test %d didn't error, but it should have", i)
|
||||
} else if err != nil && !test.shouldErr {
|
||||
t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
code, err := b.ServeHTTP(rec, req)
|
||||
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("Wrong status, expected %d, got %d", http.StatusOK, code)
|
||||
}
|
||||
if rec.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
|
||||
t.Fatalf("Expected Content type to be application/json; charset=utf-8, but got %s ", rec.HeaderMap.Get("Content-Type"))
|
||||
}
|
||||
|
||||
actualJSONResponse := rec.Body.String()
|
||||
copyOflisting := listing
|
||||
if test.SortBy == "" {
|
||||
copyOflisting.Sort = "name"
|
||||
} else {
|
||||
copyOflisting.Sort = test.SortBy
|
||||
}
|
||||
if test.OrderBy == "" {
|
||||
copyOflisting.Order = "asc"
|
||||
} else {
|
||||
copyOflisting.Order = test.OrderBy
|
||||
}
|
||||
|
||||
copyOflisting.applySort()
|
||||
|
||||
limit := test.Limit
|
||||
if limit <= len(copyOflisting.Items) && limit > 0 {
|
||||
marsh, err = json.Marshal(copyOflisting.Items[:limit])
|
||||
} else { // if the 'limit' query is empty, or has the wrong value, list everything
|
||||
marsh, err = json.Marshal(copyOflisting.Items)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to Marshal the listing ")
|
||||
}
|
||||
expectedJSON := string(marsh)
|
||||
|
||||
if actualJSONResponse != expectedJSON {
|
||||
t.Errorf("JSON response doesn't match the expected for test number %d with sort=%s, order=%s\nExpected response %s\nActual response = %s\n",
|
||||
i+1, test.SortBy, test.OrderBy, expectedJSON, actualJSONResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
<h1>Header</h1>
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Template</title>
|
||||
</head>
|
||||
<body>
|
||||
{{.Include "header.html"}}
|
||||
<h1>{{.Path}}</h1>
|
||||
{{range .Items}}
|
||||
<a href="{{.URL}}">{{.Name}}</a><br>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test 2</title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user