Compare commits

..

53 Commits

Author SHA1 Message Date
Matthew Holt 4d907d57fa Whoops, emergency bug fix
Made a faulty assumption that virualhosts could share acme proxy handlers; turns out they can't without fumbling up the middleware configuration (middleware chains overlap and cross over into other virtualhosts)!
2015-11-18 18:41:01 -07:00
Matthew Holt 24352e799a Remove SimpleHTTP and bump version to 0.8 beta 4! 2015-11-18 17:40:35 -07:00
Matthew Holt e17d43b58a Default host now empty string; default port now depends on host
Hosts which are eligible for automatic HTTPS have default port "https" but other hosts (wildcards, loopback, etc.) have the default port 2015. The default host of empty string should be more IPv6-compatible.
2015-11-18 10:05:13 -07:00
Matthew Holt 580b50ea20 letsencrypt: Support for http-01, awkwardly straddling that and SimpleHTTP for now 2015-11-17 18:11:19 -07:00
Matthew Holt 659df6967e letsencrypt: Don't assume default port of 443 2015-11-17 16:17:43 -07:00
Matthew Holt b9244cdf2e templates: Another context fix when host header is missing port 2015-11-17 14:35:18 -07:00
Matthew Holt a2ba00bdc8 Update docs n things 2015-11-17 10:19:03 -07:00
Matthew Holt 1d47e590e5 proxy: Make headers when upstream is created; avoid potential nil ptr deref 2015-11-17 10:18:13 -07:00
Matt Holt 280ba9db85 Merge pull request #345 from tw4452852/my_proxy
proxy: make http header block scoped
2015-11-17 08:20:29 -07:00
Matt Holt 7f98a6cccf Merge pull request #347 from tw4452852/my_health
proxy: make tests workable when offline
2015-11-17 08:14:50 -07:00
Tw a5b117fcdf proxy: make tests workable when offline
Instead of accessing the google website, we setup a local server
for test, then tests will work fine even we are offline.

Fix issue #346

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

Fix issue #341

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

This commit adds FirstStartup to the virtualhost config
2015-11-10 19:46:18 -07:00
Matt Holt a39ed2823e Merge pull request #332 from coolaj86/coolaj86-alphabetize
Alphabetize command line options, add -pidfile
2015-11-10 19:02:39 -07:00
AJ ONeal 4bed399ca4 Alphabetize command line options, vars, and checks
As per https://github.com/mholt/caddy/issues/331
2015-11-10 17:52:29 -08:00
AJ ONeal 93c330c4ce add --pidfile string option
As per https://github.com/mholt/caddy/issues/317
2015-11-10 17:44:00 -08:00
Matthew Holt 76ec785e87 ext: Fix panic when URL path is empty 2015-11-10 16:04:02 -07:00
Matthew Holt e9b9432da5 "-conf stdin" required to pipe in Caddyfile
Some programs (Node.js, supervisor, etc.) open a stdin pipe by default and don't use it, causing Caddy to block. It is their error, but we have to try to accommodate unfortunately. To fix this more universally, parent must explicitly set -conf to "stdin" to read from pipe.
2015-11-10 15:06:47 -07:00
Matthew Holt c31e86db02 caddyfile: Change JSON format to use arrays, not objects
Since a directive can appear on multiple lines, the object syntax wasn't working well. This also fixes several other serialization bugs.
2015-11-10 11:49:01 -07:00
Matthew Holt 13557eb5ef core: Fix bug that caused parent process to block indefinitely
The error channel used when starting all the servers must be buffered so that, even if there are no errors at startup, the returns that insert into the error channel will not be blocked, since after startup, nobody is reading that channel anymore.
2015-11-09 11:52:43 -07:00
Matthew Holt 02213402e8 Unexport internal types; improved markdown summaries 2015-11-09 07:45:37 -07:00
Matt Holt e1f23a1eb7 Merge pull request #326 from PatelNDipen/master
Wrote tests for browse.go and redir.go
2015-11-08 09:37:04 -07:00
Dipen Patel 485af2c6ba removed comment from browse test 2015-11-08 08:40:18 -05:00
Matthew Holt 171fd34b3c markdown: Make base path optional, always generate links
The base path being optional in the Caddyfile is convenient when you just want the whole site to be markdown-enabled. The other change is to always generate links... this is because an index page for markdown files may not be statically generated, but it should still show links. Commit 09341fc was a regression, and this fixes it.
2015-11-07 20:24:17 -07:00
Dipen Patel 1017142d9b Made style adjustments to browse and redir tests 2015-11-07 22:17:26 -05:00
Matthew Holt be9f644425 -host and -port flags affect shorthand caddyfile 2015-11-07 20:03:02 -07:00
Dipen Patel 2b1cc77f4b Wrote tests for browse.go and redir.go 2015-11-07 18:03:21 -05:00
Michael Banzon 8774c90709 Removed the Windows part. It wasn't working properly.
For issue #304.
2015-11-05 18:12:06 +01:00
Michael Banzon 01465932e7 Environment variables Windows style
Added the Windows style ({%%}) for environment variables in Caddyfiles
(for issue #304).
2015-11-05 18:01:44 +01:00
Michael Banzon 72c0527b7d Added support for env vars in Caddyfile
This is work in progress for #304
2015-11-05 08:26:10 +01:00
48 changed files with 1039 additions and 412 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
[![Documentation](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/mholt/caddy) [![Linux Build Status](https://img.shields.io/travis/mholt/caddy.svg?style=flat-square&label=linux+build)](https://travis-ci.org/mholt/caddy) [![Windows Build Status](https://img.shields.io/appveyor/ci/mholt/caddy.svg?style=flat-square&label=windows+build)](https://ci.appveyor.com/project/mholt/caddy)
Caddy is a lightweight, general-purpose web server for Windows, Mac, Linux, BSD, and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android). It is a capable alternative to other popular and easy to use web servers.
Caddy is a lightweight, general-purpose web server for Windows, Mac, Linux, BSD, and [Android](https://github.com/mholt/caddy/wiki/Running-Caddy-on-Android). It is a capable alternative to other popular and easy to use web servers. ([@caddyserver](https://twitter.com/caddyserver) on Twitter)
The most notable features are HTTP/2, [Let's Encrypt](https://letsencrypt.org) support, Virtual Hosts, TLS + SNI, and easy configuration with a [Caddyfile](https://caddyserver.com/docs/caddyfile). In development, you usually put one Caddyfile with each site. In production, Caddy serves HTTPS by default and manages all cryptographic assets for you.
+79 -51
View File
@@ -11,10 +11,6 @@
//
// 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 (
@@ -23,11 +19,13 @@ import (
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"path"
"strings"
"sync"
"time"
"github.com/mholt/caddy/caddy/letsencrypt"
"github.com/mholt/caddy/server"
@@ -42,8 +40,14 @@ var (
// If true, initialization will not show any informative output.
Quiet bool
// HTTP2 indicates whether HTTP2 is enabled or not
// HTTP2 indicates whether HTTP2 is enabled or not.
HTTP2 bool // TODO: temporary flag until http2 is standard
// PidFile is the path to the pidfile to create.
PidFile string
// GracefulTimeout is the maximum duration of a graceful shutdown.
GracefulTimeout time.Duration
)
var (
@@ -55,7 +59,7 @@ var (
// errIncompleteRestart occurs if this process is a fork
// of the parent but no Caddyfile was piped in
errIncompleteRestart = errors.New("cannot finish restart successfully")
errIncompleteRestart = errors.New("incomplete restart")
// servers is a list of all the currently-listening servers
servers []*server.Server
@@ -71,11 +75,15 @@ var (
// index in the list of inherited file descriptors. This
// variable is not safe for concurrent access.
loadedGob caddyfileGob
// startedBefore should be set to true if caddy has been started
// at least once (does not indicate whether currently running).
startedBefore bool
)
const (
// DefaultHost is the default host.
DefaultHost = "0.0.0.0"
DefaultHost = ""
// DefaultPort is the default port.
DefaultPort = "2015"
// DefaultRoot is the default root folder.
@@ -83,21 +91,34 @@ const (
)
// 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.
// is nil, the LoadCaddyfile function will be called to get
// one.
//
// 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
// This function blocks until all the servers are listening.
//
// Note (POSIX): If Start is called in the child process of a
// restart more than once within the duration of the graceful
// cutoff (i.e. the child process called Start a first time,
// then called Stop, then Start again within the first 5 seconds
// or however long GracefulTimeout is) and the Caddyfiles have
// at least one listener address in common, the second Start
// may fail with "address already in use" as there's no
// guarantee that the parent process has relinquished the
// address before the grace period ends.
func Start(cdyfile Input) (err error) {
// If we return with no errors, we must do two things: tell the
// parent that we succeeded and write to the pidfile.
defer func() {
if err == nil {
signalSuccessToParent() // TODO: Is doing this more than once per process a bad idea? Start could get called more than once in other apps.
if PidFile != "" {
err := writePidFile()
if err != nil {
log.Printf("[ERROR] Could not write pidfile: %v", err)
}
}
}
}()
// Input must never be nil; try to load something
if cdyfile == nil {
@@ -128,17 +149,7 @@ func Start(cdyfile Input) error {
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()
}
}
}
startedBefore = true
// Show initialization output
if !Quiet && !IsRestart() {
@@ -161,13 +172,6 @@ func Start(cdyfile Input) error {
}
}
// 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
}
@@ -175,12 +179,12 @@ func Start(cdyfile Input) error {
// 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 {
func startServers(groupings bindingGroup) error {
var startupWg sync.WaitGroup
errChan := make(chan error)
errChan := make(chan error, len(groupings)) // must be buffered to allow Serve functions below to return if stopped later
for _, group := range groupings {
s, err := server.New(group.BindAddr.String(), group.Configs)
s, err := server.New(group.BindAddr.String(), group.Configs, GracefulTimeout)
if err != nil {
return err
}
@@ -203,13 +207,26 @@ func startServers(groupings Group) error {
return errors.New("listener for " + s.Addr + " was not a ListenerFile")
}
delete(loadedGob.ListenerFds, s.Addr) // mark it as used
file.Close()
delete(loadedGob.ListenerFds, s.Addr)
}
}
wg.Add(1)
go func(s *server.Server, ln server.ListenerFile) {
defer wg.Done()
// run startup functions that should only execute when
// the original parent process is starting.
if !IsRestart() && !startedBefore {
err := s.RunFirstStartupFuncs()
if err != nil {
errChan <- err
return
}
}
// start the server
if ln != nil {
errChan <- s.Serve(ln)
} else {
@@ -228,6 +245,14 @@ func startServers(groupings Group) error {
serversMu.Unlock()
}
// Close the remaining (unused) file descriptors to free up resources
if IsRestart() {
for key, fdIndex := range loadedGob.ListenerFds {
os.NewFile(fdIndex, "").Close()
delete(loadedGob.ListenerFds, key)
}
}
// Wait for all servers to finish starting
startupWg.Wait()
@@ -246,13 +271,15 @@ func startServers(groupings Group) error {
// 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).
// configured by middleware (they must be executed separately).
func Stop() error {
letsencrypt.Deactivate()
serversMu.Lock()
for _, s := range servers {
s.Stop() // TODO: error checking/reporting?
if err := s.Stop(); err != nil {
log.Printf("[ERROR] Stopping %s: %v", s.Addr, err)
}
}
servers = []*server.Server{} // don't reuse servers
serversMu.Unlock()
@@ -265,12 +292,13 @@ 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).
// LoadCaddyfile loads a Caddyfile, prioritizing a Caddyfile
// piped from stdin as part of a restart (only happens on first call
// to LoadCaddyfile). If it is not a restart, this function tries
// calling the user's loader function, and if that returns nil, then
// this function resorts to the default configuration. Thus, if there
// are no other errors, this function always returns at least the
// default Caddyfile.
func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) {
// If we are a fork, finishing the restart is highest priority;
// piped input is required in this case.
+56 -41
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net"
"sort"
"strconv"
"strings"
@@ -23,18 +24,29 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
}
for _, sb := range serverBlocks {
block := ServerBlock{Body: make(map[string]interface{})}
block := ServerBlock{Body: [][]interface{}{}}
// Fill up host list
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)
// Extract directives deterministically by sorting them
var directives = make([]string, len(sb.Tokens))
for dir := range sb.Tokens {
directives = append(directives, dir)
}
sort.Strings(directives)
// Convert each directive's tokens into our JSON structure
for _, dir := range directives {
disp := parse.NewDispenserTokens(filename, sb.Tokens[dir])
for disp.Next() {
block.Body = append(block.Body, constructLine(&disp))
}
}
// tack this block onto the end of the list
j = append(j, block)
}
@@ -50,17 +62,18 @@ func ToJSON(caddyfile []byte) ([]byte, error) {
// but only one line at a time, to be used at the top-level of
// a server block only (where the first token on each line is a
// directive) - not to be used at any other nesting level.
func constructLine(d parse.Dispenser) interface{} {
// goes to end of line
func constructLine(d *parse.Dispenser) []interface{} {
var args []interface{}
all := d.RemainingArgs()
for _, arg := range all {
args = append(args, arg)
}
args = append(args, d.Val())
d.Next()
if d.Val() == "{" {
args = append(args, constructBlock(d))
for d.NextArg() {
if d.Val() == "{" {
args = append(args, constructBlock(d))
continue
}
args = append(args, d.Val())
}
return args
@@ -68,26 +81,15 @@ func constructLine(d parse.Dispenser) interface{} {
// constructBlock recursively processes tokens into a
// JSON-encodable structure.
func constructBlock(d parse.Dispenser) interface{} {
block := make(map[string]interface{})
// goes to end of block
func constructBlock(d *parse.Dispenser) [][]interface{} {
block := [][]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
block = append(block, constructLine(d))
}
return block
@@ -103,7 +105,10 @@ func FromJSON(jsonBytes []byte) ([]byte, error) {
return nil, err
}
for _, sb := range j {
for sbPos, sb := range j {
if sbPos > 0 {
result += "\n\n"
}
for i, host := range sb.Hosts {
if hostname, port, err := net.SplitHostPort(host); err == nil {
if port == "http" || port == "https" {
@@ -129,26 +134,36 @@ func jsonToText(scope interface{}, depth int) string {
switch val := scope.(type) {
case string:
if strings.ContainsAny(val, "\" \n\t\r") {
result += ` "` + strings.Replace(val, "\"", "\\\"", -1) + `"`
result += `"` + strings.Replace(val, "\"", "\\\"", -1) + `"`
} else {
result += " " + val
result += val
}
case int:
result += " " + strconv.Itoa(val)
result += strconv.Itoa(val)
case float64:
result += " " + fmt.Sprintf("%v", val)
result += fmt.Sprintf("%v", val)
case bool:
result += " " + fmt.Sprintf("%t", val)
case map[string]interface{}:
result += fmt.Sprintf("%t", val)
case [][]interface{}:
result += " {\n"
for param, args := range val {
result += strings.Repeat("\t", depth) + param
result += jsonToText(args, depth+1) + "\n"
for _, arg := range val {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
case []interface{}:
for _, v := range val {
for i, v := range val {
if block, ok := v.([]interface{}); ok {
result += "{\n"
for _, arg := range block {
result += strings.Repeat("\t", depth) + jsonToText(arg, depth+1) + "\n"
}
result += strings.Repeat("\t", depth-1) + "}"
continue
}
result += jsonToText(v, depth)
if i < len(val)-1 {
result += " "
}
}
}
@@ -160,6 +175,6 @@ type Caddyfile []ServerBlock
// ServerBlock represents a server block.
type ServerBlock struct {
Hosts []string `json:"hosts"`
Body map[string]interface{} `json:"body"`
Hosts []string `json:"hosts"`
Body [][]interface{} `json:"body"`
}
+44 -9
View File
@@ -9,7 +9,7 @@ var tests = []struct {
caddyfile: `foo {
root /bar
}`,
json: `[{"hosts":["foo"],"body":{"root":["/bar"]}}]`,
json: `[{"hosts":["foo"],"body":[["root","/bar"]]}]`,
},
{ // 1
caddyfile: `host1, host2 {
@@ -17,52 +17,87 @@ var tests = []struct {
def
}
}`,
json: `[{"hosts":["host1","host2"],"body":{"dir":[{"def":null}]}}]`,
json: `[{"hosts":["host1","host2"],"body":[["dir",[["def"]]]]}]`,
},
{ // 2
caddyfile: `host1, host2 {
dir abc {
def ghi
jkl
}
}`,
json: `[{"hosts":["host1","host2"],"body":{"dir":["abc",{"def":["ghi"]}]}}]`,
json: `[{"hosts":["host1","host2"],"body":[["dir","abc",[["def","ghi"],["jkl"]]]]}]`,
},
{ // 3
caddyfile: `host1:1234, host2:5678 {
dir abc {
}
}`,
json: `[{"hosts":["host1:1234","host2:5678"],"body":{"dir":["abc",{}]}}]`,
json: `[{"hosts":["host1:1234","host2:5678"],"body":[["dir","abc",[]]]}]`,
},
{ // 4
caddyfile: `host {
foo "bar baz"
}`,
json: `[{"hosts":["host"],"body":{"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\""]}}]`,
json: `[{"hosts":["host","host:80"],"body":[["foo","bar \"baz\""]]}]`,
},
{ // 6
caddyfile: `host {
foo "bar
baz"
}`,
json: `[{"hosts":["host"],"body":{"foo":["bar\nbaz"]}}]`,
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...?
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
json: `[{"hosts":["host:http","host:https"],"body":[]}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
},
{ // 9
caddyfile: `host {
dir1 a b
dir2 c d
}`,
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2","c","d"]]}]`,
},
{ // 10
caddyfile: `host {
dir a b
dir c d
}`,
json: `[{"hosts":["host"],"body":[["dir","a","b"],["dir","c","d"]]}]`,
},
{ // 11
caddyfile: `host {
dir1 a b
dir2 {
c
d
}
}`,
json: `[{"hosts":["host"],"body":[["dir1","a","b"],["dir2",[["c"],["d"]]]]}]`,
},
{ // 12
caddyfile: `host1 {
dir1
}
host2 {
dir2
}`,
json: `[{"hosts":["host1"],"body":[["dir1"]]},{"hosts":["host2"],"body":[["dir2"]]}]`,
},
}
+15 -19
View File
@@ -213,8 +213,8 @@ func makeStorages() map[string]interface{} {
// 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
func arrangeBindings(allConfigs []server.Config) (bindingGroup, error) {
var groupings bindingGroup
// Group configs by bind address
for _, conf := range allConfigs {
@@ -242,7 +242,7 @@ func arrangeBindings(allConfigs []server.Config) (Group, error) {
}
}
if !existing {
groupings = append(groupings, BindingMapping{
groupings = append(groupings, bindingMapping{
BindAddr: bindAddr,
Configs: []server.Config{conf},
})
@@ -331,22 +331,18 @@ func validDirective(d string) bool {
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.
// It uses the default host and port (depends on
// host, e.g. localhost is 2015, otherwise https) and
// root.
func DefaultInput() CaddyfileInput {
port := Port
if letsencrypt.HostQualifies(Host) {
port = "https"
}
return CaddyfileInput{
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)),
Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", Host, port, Root)),
}
}
@@ -362,15 +358,15 @@ var (
Port = DefaultPort
)
// BindingMapping maps a network address to configurations
// bindingMapping maps a network address to configurations
// that will bind to it. The order of the configs is important.
type BindingMapping struct {
type bindingMapping struct {
BindAddr *net.TCPAddr
Configs []server.Config
}
// Group maps network addresses to their configurations.
// bindingGroup maps network addresses to their configurations.
// Preserving the order of the groupings is important
// (related to graceful shutdown and restart)
// so this is a slice, not a literal map.
type Group []BindingMapping
type bindingGroup []bindingMapping
+17 -8
View File
@@ -8,17 +8,26 @@ import (
"github.com/mholt/caddy/server"
)
func TestNewDefault(t *testing.T) {
config := NewDefault()
func TestDefaultInput(t *testing.T) {
if actual, expected := string(DefaultInput().Body()), ":2015\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
if actual, expected := config.Root, DefaultRoot; actual != expected {
t.Errorf("Root was %s but expected %s", actual, expected)
// next few tests simulate user providing -host flag
Host = "not-localhost.com"
if actual, expected := string(DefaultInput().Body()), "not-localhost.com:https\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
if actual, expected := config.Host, DefaultHost; actual != expected {
t.Errorf("Host was %s but expected %s", actual, expected)
Host = "[::1]"
if actual, expected := string(DefaultInput().Body()), "[::1]:2015\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
if actual, expected := config.Port, DefaultPort; actual != expected {
t.Errorf("Port was %s but expected %s", actual, expected)
Host = "127.0.1.1"
if actual, expected := string(DefaultInput().Body()), "127.0.1.1:2015\nroot ."; actual != expected {
t.Errorf("Host=%s; Port=%s; Root=%s;\nEXPECTED: '%s'\n ACTUAL: '%s'", Host, Port, Root, expected, actual)
}
}
+35
View File
@@ -3,11 +3,14 @@ package caddy
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"github.com/mholt/caddy/caddy/letsencrypt"
)
@@ -38,6 +41,32 @@ func checkFdlimit() {
}
}
// signalSuccessToParent tells the parent our status using pipe at index 3.
// If this process is not a restart, this function does nothing.
// Calling this function once this process has successfully initialized
// is vital so that the parent process can unblock and kill itself.
// This function is idempotent; it executes at most once per process.
func signalSuccessToParent() {
signalParentOnce.Do(func() {
if IsRestart() {
ppipe := os.NewFile(3, "") // parent is reading from pipe at index 3
_, err := ppipe.Write([]byte("success")) // we must send some bytes to the parent
if err != nil {
log.Printf("[ERROR] Communicating successful init to parent: %v", err)
}
ppipe.Close()
}
})
}
// signalParentOnce is used to make sure that the parent is only
// signaled once; doing so more than once breaks whatever socket is
// at fd 4 (the reason for this is still unclear - to reproduce,
// call Stop() and Start() in succession at least once after a
// restart, then try loading first host of Caddyfile in the browser).
// Do not use this directly - call signalSuccessToParent instead.
var signalParentOnce sync.Once
// caddyfileGob maps bind address to index of the file descriptor
// in the Files array passed to the child process. It also contains
// the caddyfile contents. Used only during graceful restarts.
@@ -52,6 +81,12 @@ func IsRestart() bool {
return os.Getenv("CADDY_RESTART") == "true"
}
// writePidFile writes the process ID to the file at PidFile, if specified.
func writePidFile() error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
return ioutil.WriteFile(PidFile, pid, 0644)
}
// CaddyfileInput represents a Caddyfile as input
// and is simply a convenient way to implement
// the Input interface.
+33 -45
View File
@@ -2,66 +2,54 @@ package letsencrypt
import (
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"sync/atomic"
"strings"
"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.
const challengeBasePath = "/.well-known/acme-challenge"
// Handler is a Caddy middleware that can proxy ACME challenge
// requests to the real ACME client endpoint. This is necessary
// to renew certificates while the server is running.
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
Next middleware.Handler
//ChallengeActive int32 // (TODO) use sync/atomic to set/get this flag safely and efficiently
}
// 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
// Proxy challenge requests to ACME client
// TODO: Only do this if a challenge is active?
if strings.HasPrefix(r.URL.Path, challengeBasePath) {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
hostname, _, err := net.SplitHostPort(r.URL.Host)
if err != nil {
hostname = r.URL.Host
}
upstream, err := url.Parse(scheme + "://" + hostname + ":" + alternatePort)
if err != nil {
return http.StatusInternalServerError, err
}
proxy := httputil.NewSingleHostReverseProxy(upstream)
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client would use 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)
}
+52 -31
View File
@@ -166,17 +166,29 @@ func configQualifies(allConfigs []server.Config, cfgIndex int) bool {
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
HostQualifies(cfg.Host) &&
// make sure another HTTPS version of this config doesn't exist in the list already
!otherHostHasScheme(allConfigs, cfgIndex, "https")
}
// HostQualifies returns true if the hostname alone
// appears eligible for automatic HTTPS. For example,
// localhost, empty hostname, and wildcard hosts are
// not eligible because we cannot obtain certificates
// for those names.
func HostQualifies(hostname string) bool {
return hostname != "localhost" &&
strings.TrimSpace(hostname) != "" &&
hostname != "0.0.0.0" &&
hostname != "[::]" && // before parsing
hostname != "::" && // after parsing
hostname != "[::1]" && // before parsing
hostname != "::1" && // after parsing
!strings.HasPrefix(hostname, "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
}
// 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
@@ -218,7 +230,7 @@ func existingCertAndKey(host string) bool {
// 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)
return newClientPort(leEmail, "")
}
// newClientPort does the same thing as newClient, except it creates a
@@ -273,12 +285,10 @@ func newClientPort(leEmail, port string) (*acme.Client, error) {
// 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)
}
@@ -287,10 +297,13 @@ func obtainCertificates(client *acme.Client, serverConfigs []server.Config) ([]a
// metadata file.
func saveCertsAndKeys(certificates []acme.CertificateResource) error {
for _, cert := range certificates {
os.MkdirAll(storage.Site(cert.Domain), 0700)
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
if err != nil {
return err
}
// Save cert
err := ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
if err != nil {
return err
}
@@ -341,24 +354,36 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config {
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
// in the configs and this isn't, for some reason, already on port 80.
// Also, the port 80 variant of this config is necessary for proxying challenge requests.
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))
}
// To support renewals, we need handlers at ports 80 and 443,
// depending on the challenge type that is used to complete renewal.
for i, c := range allConfigs {
if c.Address() == cfg.Host+":80" ||
c.Address() == cfg.Host+":443" ||
c.Address() == cfg.Host+":http" ||
c.Address() == cfg.Host+":https" {
// Each virtualhost must have their own handlers, or the chaining gets messed up when middlewares are compiled!
handler := new(Handler)
mid := func(next middleware.Handler) middleware.Handler {
handler.Next = next
return handler
}
// TODO: Currently, acmeHandlers are not referenced, but we need to add a way to toggle
// their proxy functionality -- or maybe not. Gotta figure this out for sure.
acmeHandlers[c.Address()] = handler
allConfigs[i].Middleware["/"] = append(allConfigs[i].Middleware["/"], mid)
}
}
return allConfigs
}
@@ -466,14 +491,10 @@ var (
// 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 is the port on which the acme client will open a
// listener and solve the CA's challenges. If this alternate port
// is used instead of the default port (80 or 443), then the
// default port for the challenge must be forwarded to this one.
alternatePort = "5033"
// How often to check certificates for renewal.
+28
View File
@@ -8,6 +8,34 @@ import (
"github.com/mholt/caddy/server"
)
func TestHostQualifies(t *testing.T) {
for i, test := range []struct {
host string
expect bool
}{
{"localhost", false},
{"127.0.0.1", false},
{"127.0.1.5", false},
{"::1", false},
{"[::1]", false},
{"[::]", false},
{"::", false},
{"", false},
{" ", false},
{"0.0.0.0", false},
{"192.168.1.3", true},
{"10.0.2.1", true},
{"foobar.com", true},
} {
if HostQualifies(test.host) && !test.expect {
t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
}
if !HostQualifies(test.host) && test.expect {
t.Errorf("Test %d: Expected '%s' to qualify, but it did NOT", i, test.host)
}
}
}
func TestRedirPlaintextHost(t *testing.T) {
cfg := redirPlaintextHost(server.Config{
Host: "example.com",
+3 -18
View File
@@ -79,12 +79,6 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
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) {
@@ -122,31 +116,23 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
continue
}
// Read metadata
// Read and set up cert meta, required for renewal
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 certificate
Renew:
newCertMeta, err := client.RenewCertificate(certMeta, true, true)
if err != nil {
@@ -178,6 +164,5 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
}
// 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.
// are used to proxy ACME requests to the ACME client.
var acmeHandlers = make(map[string]*Handler)
+4
View File
@@ -76,6 +76,10 @@ func TestEmailUsername(t *testing.T) {
input: emptyEmail,
expect: emptyEmail,
},
{
input: "",
expect: "",
},
} {
if actual := emailUsername(test.input); actual != test.expect {
t.Errorf("Test %d: Expected username to be '%s' but was '%s'", i, test.expect, actual)
+27 -1
View File
@@ -69,7 +69,7 @@ func (p *parser) addresses() error {
var expectingAnother bool
for {
tkn := p.Val()
tkn := replaceEnvVars(p.Val())
// special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() {
@@ -241,6 +241,7 @@ func (p *parser) directive() error {
} else if p.Val() == "}" && nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace")
}
p.tokens[p.cursor].text = replaceEnvVars(p.tokens[p.cursor].text)
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
}
@@ -303,6 +304,31 @@ func standardAddress(str string) (host, port string, err error) {
return
}
// replaceEnvVars replaces environment variables that appear in the token
// and understands both the Unix $SYNTAX and Windows %SYNTAX%.
func replaceEnvVars(s string) string {
s = replaceEnvReferences(s, "{%", "%}")
s = replaceEnvReferences(s, "{$", "}")
return s
}
// replaceEnvReferences performs the actual replacement of env variables
// in s, given the placeholder start and placeholder end strings.
func replaceEnvReferences(s, refStart, refEnd string) string {
index := strings.Index(s, refStart)
for index != -1 {
endIndex := strings.Index(s, refEnd)
if endIndex != -1 {
ref := s[index : endIndex+len(refEnd)]
s = strings.Replace(s, ref, os.Getenv(ref[len(refStart):len(ref)-len(refEnd)]), -1)
} else {
return s
}
index = strings.Index(s, refStart)
}
return s
}
type (
// serverBlock associates tokens with a list of addresses
// and groups tokens by directive name.
+77
View File
@@ -1,6 +1,7 @@
package parse
import (
"os"
"strings"
"testing"
)
@@ -364,6 +365,82 @@ func TestParseAll(t *testing.T) {
}
}
func TestEnvironmentReplacement(t *testing.T) {
setupParseTests()
os.Setenv("PORT", "8080")
os.Setenv("ADDRESS", "servername.com")
os.Setenv("FOOBAR", "foobar")
// basic test; unix-style env vars
p := testParser(`{$ADDRESS}`)
blocks, _ := p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
// multiple vars per token
p = testParser(`{$ADDRESS}:{$PORT}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// windows-style var and unix style in same token
p = testParser(`{%ADDRESS%}:{$PORT}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// reverse order
p = testParser(`{$ADDRESS}:{%PORT%}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Host, "servername.com"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
// env var in server block body as argument
p = testParser(":{%PORT%}\ndir1 {$FOOBAR}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Port, "8080"; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
if actual, expected := blocks[0].Tokens["dir1"][1].text, "foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// combined windows env vars in argument
p = testParser(":{%PORT%}\ndir1 {%ADDRESS%}/{%FOOBAR%}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].text, "servername.com/foobar"; expected != actual {
t.Errorf("Expected argument to be '%s' but was '%s'", expected, actual)
}
// malformed env var (windows)
p = testParser(":1234\ndir1 {%ADDRESS}")
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Tokens["dir1"][1].text, "{%ADDRESS}"; expected != actual {
t.Errorf("Expected host to be '%s' but was '%s'", expected, actual)
}
// malformed (non-existent) env var (unix)
p = testParser(`:{$PORT$}`)
blocks, _ = p.parseAll()
if actual, expected := blocks[0].Addresses[0].Port, ""; expected != actual {
t.Errorf("Expected port to be '%s' but was '%s'", expected, actual)
}
}
func setupParseTests() {
// Set up some bogus directives for testing
ValidDirectives = map[string]struct{}{
+32 -22
View File
@@ -7,7 +7,7 @@ import (
"io/ioutil"
"log"
"os"
"syscall"
"os/exec"
)
func init() {
@@ -33,7 +33,7 @@ func Restart(newCaddyfile Input) error {
caddyfileMu.Unlock()
}
if len(os.Args) == 0 { // this should never happen...
if len(os.Args) == 0 { // this should never happen, but...
os.Args = []string{""}
}
@@ -53,50 +53,60 @@ func Restart(newCaddyfile Input) error {
}
// Prepare a pipe that the child process will use to communicate
// its success or failure with us, the parent
// its success with us by sending > 0 bytes
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()}
// Pass along relevant file descriptors to child process; ordering
// is very important since we rely on these being in certain positions.
extraFiles := []*os.File{sigwpipe}
// Now add file descriptors of the sockets
// Add file descriptors of all the sockets
serversMu.Lock()
for i, s := range servers {
fds = append(fds, s.ListenerFd())
extraFiles = append(extraFiles, 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)
// Set up the command
cmd := exec.Command(os.Args[0], os.Args[1:]...)
cmd.Stdin = rpipe // fd 0
cmd.Stdout = os.Stdout // fd 1
cmd.Stderr = os.Stderr // fd 2
cmd.ExtraFiles = extraFiles
// Spawn the child process
err = cmd.Start()
if err != nil {
return err
}
// Feed it the Caddyfile
// Immediately close our dup'ed fds and the write end of our signal pipe
for _, f := range extraFiles {
f.Close()
}
// Feed Caddyfile to the child
err = gob.NewEncoder(wpipe).Encode(cdyfileGob)
if err != nil {
return err
}
wpipe.Close()
// 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("[ERROR] Restart: child failed to initialize; changes not applied")
// Determine whether child startup succeeded
answer, readErr := ioutil.ReadAll(sigrpipe)
if answer == nil || len(answer) == 0 {
cmdErr := cmd.Wait() // get exit status
log.Printf("[ERROR] Restart: child failed to initialize (%v) - changes not applied", cmdErr)
if readErr != nil {
log.Printf("[ERROR] Restart: additionally, error communicating with child process: %v", readErr)
}
return errIncompleteRestart
}
// Child process is listening now; we can stop all our servers here.
// Looks like child is successful; we can exit gracefully.
return Stop()
}
+2
View File
@@ -1,5 +1,7 @@
package caddy
// Restart restarts Caddy forcefully using newCaddyfile,
// or, if nil, the current/existing Caddyfile is reused.
func Restart(newCaddyfile Input) error {
if newCaddyfile == nil {
caddyfileMu.Lock()
+65
View File
@@ -0,0 +1,65 @@
package setup
import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/mholt/caddy/middleware/browse"
)
func TestBrowse(t *testing.T) {
tempDirPath, err := getTempDirPath()
if err != nil {
t.Fatalf("BeforeTest: Failed to find an existing directory for testing! Error was: %v", err)
}
nonExistantDirPath := filepath.Join(tempDirPath, strconv.Itoa(int(time.Now().UnixNano())))
tempTemplate, err := ioutil.TempFile(".", "tempTemplate")
if err != nil {
t.Fatalf("BeforeTest: Failed to create a temporary file in the working directory! Error was: %v", err)
}
defer os.Remove(tempTemplate.Name())
tempTemplatePath := filepath.Join(".", tempTemplate.Name())
for i, test := range []struct {
input string
expectedPathScope []string
shouldErr bool
}{
// test case #0 tests handling of multiple pathscopes
{"browse " + tempDirPath + "\n browse .", []string{tempDirPath, "."}, false},
// test case #1 tests instantiation of browse.Config with default values
{"browse /", []string{"/"}, false},
// test case #2 tests detectaction of custom template
{"browse . " + tempTemplatePath, []string{"."}, false},
// test case #3 tests detection of non-existant template
{"browse . " + nonExistantDirPath, nil, true},
// test case #4 tests detection of duplicate pathscopes
{"browse " + tempDirPath + "\n browse " + tempDirPath, nil, true},
} {
recievedFunc, err := Browse(NewTestController(test.input))
if err != nil && !test.shouldErr {
t.Errorf("Test case #%d recieved an error of %v", i, err)
}
if test.expectedPathScope == nil {
continue
}
recievedConfigs := recievedFunc(nil).(browse.Browse).Configs
for j, config := range recievedConfigs {
if config.PathScope != test.expectedPathScope[j] {
t.Errorf("Test case #%d expected a pathscope of %v, but got %v", i, test.expectedPathScope, config.PathScope)
}
}
}
}
+9 -4
View File
@@ -61,10 +61,15 @@ func markdownParse(c *Controller) ([]*markdown.Config, error) {
}
// Get the path scope
if !c.NextArg() || c.Val() == "{" {
args := c.RemainingArgs()
switch len(args) {
case 0:
md.PathScope = "/"
case 1:
md.PathScope = args[0]
default:
return mdconfigs, c.ArgErr()
}
md.PathScope = c.Val()
// Load any other configuration parameters
for c.NextBlock() {
@@ -73,9 +78,9 @@ func markdownParse(c *Controller) ([]*markdown.Config, error) {
}
}
// If no extensions were specified, assume .md
// If no extensions were specified, assume some defaults
if len(md.Extensions) == 0 {
md.Extensions = []string{".md"}
md.Extensions = []string{".md", ".markdown", ".mdown"}
}
mdconfigs = append(mdconfigs, md)
+2 -2
View File
@@ -37,8 +37,8 @@ func TestMarkdown(t *testing.T) {
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")
if fmt.Sprint(myHandler.Configs[0].Extensions) != fmt.Sprint([]string{".md", ".markdown", ".mdown"}) {
t.Errorf("Expected .md, .markdown, and .mdown as default extensions")
}
}
+67
View File
@@ -0,0 +1,67 @@
package setup
import (
"testing"
"github.com/mholt/caddy/middleware/redirect"
)
func TestRedir(t *testing.T) {
for j, test := range []struct {
input string
shouldErr bool
expectedRules []redirect.Rule
}{
// test case #0 tests the recognition of a valid HTTP status code defined outside of block statement
{"redir 300 {\n/ /foo\n}", false, []redirect.Rule{redirect.Rule{FromPath: "/", To: "/foo", Code: 300}}},
// test case #1 tests the recognition of an invalid HTTP status code defined outside of block statement
{"redir 9000 {\n/ /foo\n}", true, []redirect.Rule{redirect.Rule{}}},
// test case #2 tests the detection of a valid HTTP status code outside of a block statement being overriden by an invalid HTTP status code inside statement of a block statement
{"redir 300 {\n/ /foo 9000\n}", true, []redirect.Rule{redirect.Rule{}}},
// test case #3 tests the detection of an invalid HTTP status code outside of a block statement being overriden by a valid HTTP status code inside statement of a block statement
{"redir 9000 {\n/ /foo 300\n}", true, []redirect.Rule{redirect.Rule{}}},
// test case #4 tests the recognition of a TO redirection in a block statement.The HTTP status code is set to the default of 301 - MovedPermanently
{"redir 302 {\n/foo\n}", false, []redirect.Rule{redirect.Rule{FromPath: "/", To: "/foo", Code: 302}}},
// test case #5 tests the recognition of a TO and From redirection in a block statement
{"redir {\n/bar /foo 303\n}", false, []redirect.Rule{redirect.Rule{FromPath: "/bar", To: "/foo", Code: 303}}},
// test case #6 tests the recognition of a TO redirection in a non-block statement. The HTTP status code is set to the default of 301 - MovedPermanently
{"redir /foo", false, []redirect.Rule{redirect.Rule{FromPath: "/", To: "/foo", Code: 301}}},
// test case #7 tests the recognition of a TO and From redirection in a non-block statement
{"redir /bar /foo 303", false, []redirect.Rule{redirect.Rule{FromPath: "/bar", To: "/foo", Code: 303}}},
// test case #8 tests the recognition of multiple redirections
{"redir {\n / /foo 304 \n} \n redir {\n /bar /foobar 305 \n}", false, []redirect.Rule{redirect.Rule{FromPath: "/", To: "/foo", Code: 304}, redirect.Rule{FromPath: "/bar", To: "/foobar", Code: 305}}},
// test case #9 tests the detection of duplicate redirections
{"redir {\n /bar /foo 304 \n} redir {\n /bar /foo 304 \n}", true, []redirect.Rule{redirect.Rule{}}},
} {
recievedFunc, err := Redir(NewTestController(test.input))
if err != nil && !test.shouldErr {
t.Errorf("Test case #%d recieved an error of %v", j, err)
} else if test.shouldErr {
continue
}
recievedRules := recievedFunc(nil).(redirect.Redirect).Rules
for i, recievedRule := range recievedRules {
if recievedRule.FromPath != test.expectedRules[i].FromPath {
t.Errorf("Test case #%d.%d expected a from path of %s, but recieved a from path of %s", j, i, test.expectedRules[i].FromPath, recievedRule.FromPath)
}
if recievedRule.To != test.expectedRules[i].To {
t.Errorf("Test case #%d.%d expected a TO path of %s, but recieved a TO path of %s", j, i, test.expectedRules[i].To, recievedRule.To)
}
if recievedRule.Code != test.expectedRules[i].Code {
t.Errorf("Test case #%d.%d expected a HTTP status code of %d, but recieved a code of %d", j, i, test.expectedRules[i].Code, recievedRule.Code)
}
}
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
// Startup registers a startup callback to execute during server start.
func Startup(c *Controller) (middleware.Middleware, error) {
return nil, registerCallback(c, &c.Startup)
return nil, registerCallback(c, &c.FirstStartup)
}
// Shutdown registers a shutdown callback to execute during process exit.
+1 -1
View File
@@ -45,7 +45,7 @@ func TestStartup(t *testing.T) {
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
err = c.Startup[0]()
err = c.FirstStartup[0]()
if err != nil && !test.shouldExecutionErr {
t.Errorf("Test %d recieved an error of:\n%v", i, err)
}
+39 -9
View File
@@ -4,30 +4,60 @@ import (
"log"
"os"
"os/signal"
"sync"
"github.com/mholt/caddy/server"
)
func init() {
// Trap quit signals (cross-platform)
// TrapSignals create signal handlers for all applicable signals for this
// system. If your Go program uses signals, this is a rather invasive
// function; best to implement them yourself in that case. Signals are not
// required for the caddy package to function properly, but this is a
// convenient way to allow the user to control this package of your program.
func TrapSignals() {
trapSignalsCrossPlatform()
trapSignalsPosix()
}
// trapSignalsCrossPlatform captures SIGINT, which triggers forceful
// shutdown that executes shutdown callbacks first. A second interrupt
// signal will exit the process immediately.
func trapSignalsCrossPlatform() {
go func() {
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt, os.Kill)
<-shutdown
signal.Notify(shutdown, os.Interrupt)
var exitCode int
for i := 0; true; i++ {
<-shutdown
if i > 0 {
log.Println("[INFO] SIGINT: Force quit")
os.Exit(1)
}
log.Println("[INFO] SIGINT: Shutting down")
go os.Exit(executeShutdownCallbacks("SIGINT"))
}
}()
}
// executeShutdownCallbacks executes the shutdown callbacks as initiated
// by signame. It logs any errors and returns the recommended exit status.
// This function is idempotent; subsequent invocations always return 0.
func executeShutdownCallbacks(signame string) (exitCode int) {
shutdownCallbacksOnce.Do(func() {
serversMu.Lock()
errs := server.ShutdownCallbacks(servers)
serversMu.Unlock()
if len(errs) > 0 {
for _, err := range errs {
log.Printf("[ERROR] Shutting down: %v", err)
log.Printf("[ERROR] %s shutdown: %v", signame, err)
}
exitCode = 1
}
os.Exit(exitCode)
}()
})
return
}
var shutdownCallbacksOnce sync.Once
+50 -26
View File
@@ -10,39 +10,63 @@ import (
"syscall"
)
func init() {
// Trap POSIX-only signals
// trapSignalsPosix captures POSIX-only signals.
func trapSignalsPosix() {
go func() {
reload := make(chan os.Signal, 1)
signal.Notify(reload, syscall.SIGUSR1) // reload configuration
sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1)
for {
<-reload
for sig := range sigchan {
switch sig {
case syscall.SIGTERM:
log.Println("[INFO] SIGTERM: Terminating process")
os.Exit(0)
var updatedCaddyfile Input
case syscall.SIGQUIT:
log.Println("[INFO] SIGQUIT: Shutting down")
exitCode := executeShutdownCallbacks("SIGQUIT")
err := Stop()
if err != nil {
log.Printf("[ERROR] SIGQUIT stop: %v", err)
exitCode = 1
}
os.Exit(exitCode)
caddyfileMu.Lock()
if caddyfile == nil {
// Hmm, did spawing process forget to close stdin? Anyhow, this is unusual.
log.Println("[ERROR] SIGUSR1: no caddyfile to reload (was stdin left open?)")
caddyfileMu.Unlock()
continue
}
if caddyfile.IsFile() {
body, err := ioutil.ReadFile(caddyfile.Path())
if err == nil {
caddyfile = CaddyfileInput{
Filepath: caddyfile.Path(),
Contents: body,
RealFile: true,
case syscall.SIGHUP:
log.Println("[INFO] SIGHUP: Hanging up")
err := Stop()
if err != nil {
log.Printf("[ERROR] SIGHUP stop: %v", err)
}
case syscall.SIGUSR1:
log.Println("[INFO] SIGUSR1: Reloading")
var updatedCaddyfile Input
caddyfileMu.Lock()
if caddyfile == nil {
// Hmm, did spawing process forget to close stdin? Anyhow, this is unusual.
log.Println("[ERROR] SIGUSR1: no Caddyfile to reload (was stdin left open?)")
caddyfileMu.Unlock()
continue
}
if caddyfile.IsFile() {
body, err := ioutil.ReadFile(caddyfile.Path())
if err == nil {
updatedCaddyfile = CaddyfileInput{
Filepath: caddyfile.Path(),
Contents: body,
RealFile: true,
}
}
}
}
caddyfileMu.Unlock()
caddyfileMu.Unlock()
err := Restart(updatedCaddyfile)
if err != nil {
log.Printf("[ERROR] SIGUSR1: Restart returned: %v", err)
err := Restart(updatedCaddyfile)
if err != nil {
log.Printf("[ERROR] SIGUSR1: %v", err)
}
}
}
}()
+3
View File
@@ -0,0 +1,3 @@
package caddy
func trapSignalsPosix() {}
+5
View File
@@ -10,8 +10,13 @@ CHANGES
- New -ca flag to customize ACME CA server URL
- New -revoke flag to revoke a certificate
- New -log flag to enable process log
- New -pidfile flag to enable writing pidfile
- New -grace flag to customize the graceful shutdown timeout
- New support for SIGHUP, SIGTERM, and SIGQUIT signals
- browse: Render filenames with multiple whitespace properly
- markdown: Include Last-Modified header in response
- markdown: Render tables, strikethrough, and fenced code blocks
- proxy: Ability to exclude/ignore paths from proxying
- startup, shutdown: Better Windows support
- templates: Bug fix for .Host when port is absent
- templates: Include Last-Modified header in response
+2 -1
View File
@@ -1,7 +1,8 @@
CADDY 0.8 beta 3
CADDY 0.8 beta 4
Website
https://caddyserver.com
@caddyserver
Source Code
https://github.com/mholt/caddy
+38 -44
View File
@@ -10,6 +10,7 @@ import (
"runtime"
"strconv"
"strings"
"time"
"github.com/mholt/caddy/caddy"
"github.com/mholt/caddy/caddy/letsencrypt"
@@ -18,33 +19,33 @@ import (
var (
conf string
cpu string
version bool
revoke string
logfile string
revoke string
version bool
)
const (
appName = "Caddy"
appVersion = "0.8 beta 3"
appVersion = "0.8 beta 4"
)
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")
caddy.TrapSignals()
flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement")
flag.StringVar(&letsencrypt.CAUrl, "ca", "https://acme-staging.api.letsencrypt.org/directory", "Certificate authority ACME server")
flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")")
flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default Let's Encrypt account email address")
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
flag.DurationVar(&caddy.GracefulTimeout, "grace", 5*time.Second, "Maximum duration of graceful shutdown")
flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host")
flag.BoolVar(&caddy.HTTP2, "http2", true, "HTTP/2 support") // TODO: temporary flag until http2 merged into std lib
flag.StringVar(&logfile, "log", "", "Process log file")
flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port")
flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site")
flag.BoolVar(&version, "version", false, "Show version")
}
func main() {
@@ -64,15 +65,11 @@ func main() {
default:
file, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("Error opening log file: %v", err)
log.Fatalf("Error opening process log file: %v", err)
}
log.SetOutput(file)
}
if version {
fmt.Printf("%s %s\n", caddy.AppName, caddy.AppVersion)
os.Exit(0)
}
if revoke != "" {
err := letsencrypt.Revoke(revoke)
if err != nil {
@@ -81,6 +78,10 @@ func main() {
fmt.Printf("Revoked certificate for %s\n", revoke)
os.Exit(0)
}
if version {
fmt.Printf("%s %s\n", caddy.AppName, caddy.AppVersion)
os.Exit(0)
}
// Set CPU cap
err := setCPU(cpu)
@@ -97,11 +98,7 @@ func main() {
// Start your engines
err = caddy.Start(caddyfile)
if err != nil {
if caddy.IsRestart() {
log.Printf("[ERROR] Upon starting %s: %v", appName, err)
} else {
mustLogFatal(err)
}
mustLogFatal(err)
}
// Twiddle your thumbs
@@ -109,33 +106,30 @@ func main() {
}
// mustLogFatal just wraps log.Fatal() in a way that ensures the
// output is always printed to stderr so the user can see it,
// even if the process log was not enabled.
// output is always printed to stderr so the user can see it
// if the user is still there, even if the process log was not
// enabled. If this process is a restart, however, and the user
// might not be there anymore, this just logs to the process log
// and exits.
func mustLogFatal(args ...interface{}) {
log.SetOutput(os.Stderr)
if !caddy.IsRestart() {
log.SetOutput(os.Stderr)
}
log.Fatal(args...)
}
func loadCaddyfile() (caddy.Input, error) {
// First try stdin pipe
cdyfile, err := caddy.CaddyfileFromPipe(os.Stdin)
if err != nil {
return nil, err
}
if cdyfile != nil {
// it is an error if -conf is also specified because, which to use?
if conf != "" {
return nil, errors.New("load: can't choose between stdin pipe and -conf flag")
}
return cdyfile, err
}
// -conf flag
// Try -conf flag
if conf != "" {
if conf == "stdin" {
return caddy.CaddyfileFromPipe(os.Stdin)
}
contents, err := ioutil.ReadFile(conf)
if err != nil {
return nil, err
}
return caddy.CaddyfileInput{
Contents: contents,
Filepath: conf,
@@ -145,7 +139,7 @@ func loadCaddyfile() (caddy.Input, error) {
// command line args
if flag.NArg() > 0 {
confBody := ":" + caddy.DefaultPort + "\n" + strings.Join(flag.Args(), "\n")
confBody := caddy.Host + ":" + caddy.Port + "\n" + strings.Join(flag.Args(), "\n")
return caddy.CaddyfileInput{
Contents: []byte(confBody),
Filepath: "args",
+5 -1
View File
@@ -8,6 +8,10 @@ import (
func TestSetCPU(t *testing.T) {
currentCPU := runtime.GOMAXPROCS(-1)
maxCPU := runtime.NumCPU()
halfCPU := int(0.5 * float32(maxCPU))
if halfCPU < 1 {
halfCPU = 1
}
for i, test := range []struct {
input string
output int
@@ -17,7 +21,7 @@ func TestSetCPU(t *testing.T) {
{"-1", currentCPU, true},
{"0", currentCPU, true},
{"100%", maxCPU, false},
{"50%", int(0.5 * float32(maxCPU)), false},
{"50%", halfCPU, false},
{"110%", currentCPU, true},
{"-10%", currentCPU, true},
{"invalid input", currentCPU, true},
+4
View File
@@ -110,6 +110,10 @@ func (c Context) Host() (string, error) {
func (c Context) Port() (string, error) {
_, port, err := net.SplitHostPort(c.Req.Host)
if err != nil {
if !strings.Contains(c.Req.Host, ":") {
// common with sites served on the default port 80
return "80", nil
}
return "", err
}
return port, nil
+7 -2
View File
@@ -260,14 +260,19 @@ func TestPort(t *testing.T) {
},
{
input: "localhost",
expectedPort: "",
shouldErr: true, // missing port in address
expectedPort: "80", // assuming 80 is the default port
shouldErr: false,
},
{
input: ":8080",
expectedPort: "8080",
shouldErr: false,
},
{
input: "[::]",
expectedPort: "",
shouldErr: true,
},
}
for _, test := range tests {
+1 -1
View File
@@ -31,7 +31,7 @@ type Ext struct {
// ServeHTTP implements the middleware.Handler interface.
func (e Ext) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
urlpath := strings.TrimSuffix(r.URL.Path, "/")
if path.Ext(urlpath) == "" && r.URL.Path[len(r.URL.Path)-1] != '/' {
if path.Ext(urlpath) == "" && len(r.URL.Path) > 0 && r.URL.Path[len(r.URL.Path)-1] != '/' {
for _, ext := range e.Extensions {
if resourceExists(e.Root, urlpath+ext) {
r.URL.Path = urlpath + ext
+1
View File
@@ -30,6 +30,7 @@ func TestExtensions(t *testing.T) {
{"/extensions_test/", []string{".html"}, "/extensions_test/"},
{"/extensions_test", []string{".json"}, "/extensions_test"},
{"/another_test", []string{".html"}, "/another_test"},
{"", []string{".html"}, ""},
} {
ex := Ext{
Next: middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
+1
View File
@@ -47,6 +47,7 @@ outer:
r.Header.Del("Accept-Encoding")
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Vary", "Accept-Encoding")
gzipWriter, err := newWriter(c, w)
if err != nil {
// should not happen
+3
View File
@@ -87,6 +87,9 @@ func nextFunc(shouldGzip bool) middleware.Handler {
if w.Header().Get("Content-Encoding") != "gzip" {
return 0, fmt.Errorf("Content-Encoding must be gzip, found %v", r.Header.Get("Content-Encoding"))
}
if w.Header().Get("Vary") != "Accept-Encoding" {
return 0, fmt.Errorf("Vary must be Accept-Encoding, found %v", r.Header.Get("Vary"))
}
if _, ok := w.(gzipResponseWriter); !ok {
return 0, fmt.Errorf("ResponseWriter should be gzipResponseWriter, found %T", w)
}
+13 -11
View File
@@ -17,22 +17,24 @@ import (
// It only generates static files if it is enabled (cfg.StaticDir
// must be set).
func GenerateStatic(md Markdown, cfg *Config) error {
// If static site generation is enabled.
// Generate links since they may be needed, even without sitegen.
generated, err := generateLinks(md, cfg)
if err != nil {
return err
}
// No new file changes, return.
if !generated {
return nil
}
// If static site generation is enabled, generate the site.
if cfg.StaticDir != "" {
generated, err := generateLinks(md, cfg)
if err != nil {
return err
}
// No new file changes, return.
if !generated {
return nil
}
if err := generateStaticHTML(md, cfg); err != nil {
return err
}
}
return nil
}
+4 -2
View File
@@ -118,15 +118,17 @@ func (l *linkGen) generateLinks(md Markdown, cfg *Config) bool {
}
reqPath = "/" + filepath.ToSlash(reqPath)
// Make the summary
parser := findParser(body)
if parser == nil {
// no metadata, ignore.
continue
}
summary, err := parser.Parse(body)
summaryRaw, err := parser.Parse(body)
if err != nil {
return err
}
summary := blackfriday.Markdown(summaryRaw, SummaryRenderer{}, 0)
// truncate summary to maximum length
if len(summary) > summaryLen {
@@ -145,7 +147,7 @@ func (l *linkGen) generateLinks(md Markdown, cfg *Config) bool {
Title: metadata.Title,
URL: reqPath,
Date: metadata.Date,
Summary: string(blackfriday.Markdown(summary, SummaryRenderer{}, 0)),
Summary: string(summary),
})
break // don't try other file extensions
+16 -1
View File
@@ -1,9 +1,24 @@
package proxy
import (
"net/http"
"net/http/httptest"
"os"
"testing"
)
var workableServer *httptest.Server
func TestMain(m *testing.M) {
workableServer = httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// do nothing
}))
r := m.Run()
workableServer.Close()
os.Exit(r)
}
type customPolicy struct{}
func (r *customPolicy) Select(pool HostPool) *UpstreamHost {
@@ -13,7 +28,7 @@ func (r *customPolicy) Select(pool HostPool) *UpstreamHost {
func testPool() HostPool {
pool := []*UpstreamHost{
{
Name: "http://google.com", // this should resolve (healthcheck test)
Name: workableServer.URL, // this should resolve (healthcheck test)
},
{
Name: "http://shouldnot.resolve", // this shouldn't
+3 -1
View File
@@ -26,6 +26,8 @@ type Upstream interface {
From() string
// Selects an upstream host to be routed to.
Select() *UpstreamHost
// Checks if subpath is not an ignored path
IsAllowedPath(string) bool
}
// UpstreamHostDownFunc can be used to customize how Down behaves.
@@ -59,7 +61,7 @@ func (uh *UpstreamHost) Down() bool {
func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
for _, upstream := range p.Upstreams {
if middleware.Path(r.URL.Path).Matches(upstream.From()) {
if middleware.Path(r.URL.Path).Matches(upstream.From()) && upstream.IsAllowedPath(r.URL.Path) {
var replacer middleware.Replacer
start := time.Now()
requestHost := r.Host
+7 -6
View File
@@ -98,11 +98,6 @@ func TestWebSocketReverseProxyFromWSClient(t *testing.T) {
// also sets up the rules/environment for testing WebSocket
// proxy.
func newWebSocketTestProxy(backendAddr string) *Proxy {
proxyHeaders = http.Header{
"Connection": {"{>Connection}"},
"Upgrade": {"{>Upgrade}"},
}
return &Proxy{
Upstreams: []Upstream{&fakeUpstream{name: backendAddr}},
}
@@ -121,10 +116,16 @@ func (u *fakeUpstream) Select() *UpstreamHost {
return &UpstreamHost{
Name: u.name,
ReverseProxy: NewSingleHostReverseProxy(uri, ""),
ExtraHeaders: proxyHeaders,
ExtraHeaders: http.Header{
"Connection": {"{>Connection}"},
"Upgrade": {"{>Upgrade}"}},
}
}
func (u *fakeUpstream) IsAllowedPath(requestPath string) bool {
return true
}
// recorderHijacker is a ResponseRecorder that can
// be hijacked.
type recorderHijacker struct {
+32 -13
View File
@@ -5,22 +5,24 @@ import (
"io/ioutil"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/mholt/caddy/caddy/parse"
"github.com/mholt/caddy/middleware"
)
var (
supportedPolicies = make(map[string]func() Policy)
proxyHeaders = make(http.Header)
)
type staticUpstream struct {
from string
Hosts HostPool
Policy Policy
from string
proxyHeaders http.Header
Hosts HostPool
Policy Policy
FailTimeout time.Duration
MaxFails int32
@@ -29,6 +31,7 @@ type staticUpstream struct {
Interval time.Duration
}
WithoutPathPrefix string
IgnoredSubPaths []string
}
// NewStaticUpstreams parses the configuration input and sets up
@@ -37,11 +40,12 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) {
var upstreams []Upstream
for c.Next() {
upstream := &staticUpstream{
from: "",
Hosts: nil,
Policy: &Random{},
FailTimeout: 10 * time.Second,
MaxFails: 1,
from: "",
proxyHeaders: make(http.Header),
Hosts: nil,
Policy: &Random{},
FailTimeout: 10 * time.Second,
MaxFails: 1,
}
if !c.Args(&upstream.from) {
@@ -69,7 +73,7 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) {
Fails: 0,
FailTimeout: upstream.FailTimeout,
Unhealthy: false,
ExtraHeaders: proxyHeaders,
ExtraHeaders: upstream.proxyHeaders,
CheckDown: func(upstream *staticUpstream) UpstreamHostDownFunc {
return func(uh *UpstreamHost) bool {
if uh.Unhealthy {
@@ -156,15 +160,21 @@ func parseBlock(c *parse.Dispenser, u *staticUpstream) error {
if !c.Args(&header, &value) {
return c.ArgErr()
}
proxyHeaders.Add(header, value)
u.proxyHeaders.Add(header, value)
case "websocket":
proxyHeaders.Add("Connection", "{>Connection}")
proxyHeaders.Add("Upgrade", "{>Upgrade}")
u.proxyHeaders.Add("Connection", "{>Connection}")
u.proxyHeaders.Add("Upgrade", "{>Upgrade}")
case "without":
if !c.NextArg() {
return c.ArgErr()
}
u.WithoutPathPrefix = c.Val()
case "except":
ignoredPaths := c.RemainingArgs()
if len(ignoredPaths) == 0 {
return c.ArgErr()
}
u.IgnoredSubPaths = ignoredPaths
default:
return c.Errf("unknown property '%s'", c.Val())
}
@@ -223,3 +233,12 @@ func (u *staticUpstream) Select() *UpstreamHost {
}
return u.Policy.Select(pool)
}
func (u *staticUpstream) IsAllowedPath(requestPath string) bool {
for _, ignoredSubPath := range u.IgnoredSubPaths {
if middleware.Path(path.Clean(requestPath)).Matches(path.Join(u.From(), ignoredSubPath)) {
return false
}
}
return true
}
+30
View File
@@ -51,3 +51,33 @@ func TestRegisterPolicy(t *testing.T) {
}
}
func TestAllowedPaths(t *testing.T) {
upstream := &staticUpstream{
from: "/proxy",
IgnoredSubPaths: []string{"/download", "/static"},
}
tests := []struct {
url string
expected bool
}{
{"/proxy", true},
{"/proxy/dl", true},
{"/proxy/download", false},
{"/proxy/download/static", false},
{"/proxy/static", false},
{"/proxy/static/download", false},
{"/proxy/something/download", true},
{"/proxy/something/static", true},
{"/proxy//static", false},
{"/proxy//static//download", false},
{"/proxy//download", false},
}
for i, test := range tests {
isAllowed := upstream.IsAllowedPath(test.url)
if test.expected != isAllowed {
t.Errorf("Test %d: expected %v found %v", i+1, test.expected, isAllowed)
}
}
}
+6
View File
@@ -149,11 +149,17 @@ func serveWS(w http.ResponseWriter, r *http.Request, config *Config) (int, error
// cmdPath should be the path of the command being run.
// The returned string slice can be set to the command's Env property.
func buildEnv(cmdPath string, r *http.Request) (metavars []string, err error) {
if !strings.Contains(r.RemoteAddr, ":") {
r.RemoteAddr += ":"
}
remoteHost, remotePort, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return
}
if !strings.Contains(r.Host, ":") {
r.Host += ":"
}
serverHost, serverPort, err := net.SplitHostPort(r.Host)
if err != nil {
return
+22
View File
@@ -0,0 +1,22 @@
package websocket
import (
"net/http"
"testing"
)
func TestBuildEnv(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost", nil)
if err != nil {
t.Fatal("Error setting up request:", err)
}
req.RemoteAddr = "localhost:50302"
env, err := buildEnv("/bin/command", req)
if err != nil {
t.Fatal("Didn't expect an error:", err)
}
if len(env) == 0 {
t.Fatalf("Expected non-empty environment; got %#v", env)
}
}
+13 -3
View File
@@ -26,11 +26,21 @@ type Config struct {
// Middleware stack; map of path scope to middleware -- TODO: Support path scope?
Middleware map[string][]middleware.Middleware
// Functions (or methods) to execute at server start; these
// are executed before any parts of the server are configured,
// and the functions are blocking
// Startup is a list of functions (or methods) to execute at
// server startup and restart; these are executed before any
// parts of the server are configured, and the functions are
// blocking. These are good for setting up middlewares and
// starting goroutines.
Startup []func() error
// FirstStartup is like Startup but these functions only execute
// during the initial startup, not on subsequent restarts.
//
// (Note: The server does not ever run these on its own; it is up
// to the calling application to do so, and do so only once, as the
// server itself has no notion whether it's a restart or not.)
FirstStartup []func() error
// Functions (or methods) to execute when the server quits;
// these are executed in response to SIGINT and are blocking
Shutdown []func() error
+25
View File
@@ -0,0 +1,25 @@
package server
import "testing"
func TestConfigAddress(t *testing.T) {
cfg := Config{Host: "foobar", Port: "1234"}
if actual, expected := cfg.Address(), "foobar:1234"; expected != actual {
t.Errorf("Expected '%s' but got '%s'", expected, actual)
}
cfg = Config{Host: "", Port: "1234"}
if actual, expected := cfg.Address(), ":1234"; expected != actual {
t.Errorf("Expected '%s' but got '%s'", expected, actual)
}
cfg = Config{Host: "foobar", Port: ""}
if actual, expected := cfg.Address(), "foobar:"; expected != actual {
t.Errorf("Expected '%s' but got '%s'", expected, actual)
}
cfg = Config{Host: "::1", Port: "https"}
if actual, expected := cfg.Address(), "[::1]:https"; expected != actual {
t.Errorf("Expected '%s' but got '%s'", expected, actual)
}
}
+10 -10
View File
@@ -2,7 +2,6 @@ package server
import (
"net"
"os"
"sync"
"syscall"
)
@@ -13,7 +12,9 @@ func newGracefulListener(l ListenerFile, wg *sync.WaitGroup) *gracefulListener {
gl := &gracefulListener{ListenerFile: l, stop: make(chan error), httpWg: wg}
go func() {
<-gl.stop
gl.Lock()
gl.stopped = true
gl.Unlock()
gl.stop <- gl.ListenerFile.Close()
}()
return gl
@@ -24,12 +25,13 @@ func newGracefulListener(l ListenerFile, wg *sync.WaitGroup) *gracefulListener {
// methods mainly wrap net.Listener to be graceful.
type gracefulListener struct {
ListenerFile
stop chan error
stopped bool
httpWg *sync.WaitGroup // pointer to the host's wg used for counting connections
stop chan error
stopped bool
sync.Mutex // protects the stopped flag
httpWg *sync.WaitGroup // pointer to the host's wg used for counting connections
}
// Accept accepts a connection. This type wraps
// Accept accepts a connection.
func (gl *gracefulListener) Accept() (c net.Conn, err error) {
c, err = gl.ListenerFile.Accept()
if err != nil {
@@ -42,18 +44,16 @@ func (gl *gracefulListener) Accept() (c net.Conn, err error) {
// Close immediately closes the listener.
func (gl *gracefulListener) Close() error {
gl.Lock()
if gl.stopped {
gl.Unlock()
return syscall.EINVAL
}
gl.Unlock()
gl.stop <- nil
return <-gl.stop
}
// File implements ListenerFile; it gets the file of the listening socket.
func (gl *gracefulListener) File() (*os.File, error) {
return gl.ListenerFile.File()
}
// gracefulConn represents a connection on a
// gracefulListener so that we can keep track
// of the number of connections, thus facilitating
+49 -27
View File
@@ -8,7 +8,6 @@ import (
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
@@ -26,13 +25,14 @@ import (
// graceful termination (POSIX only).
type Server struct {
*http.Server
HTTP2 bool // temporary while http2 is not in std lib (TODO: remove flag when part of std lib)
tls bool // whether this server is serving all HTTPS hosts or not
vhosts map[string]virtualHost // virtual hosts keyed by their address
listener ListenerFile // the listener which is bound to the socket
listenerMu sync.Mutex // protects listener
httpWg sync.WaitGroup // used to wait on outstanding connections
startChan chan struct{} // used to block until server is finished starting
HTTP2 bool // temporary while http2 is not in std lib (TODO: remove flag when part of std lib)
tls bool // whether this server is serving all HTTPS hosts or not
vhosts map[string]virtualHost // virtual hosts keyed by their address
listener ListenerFile // the listener which is bound to the socket
listenerMu sync.Mutex // protects listener
httpWg sync.WaitGroup // used to wait on outstanding connections
startChan chan struct{} // used to block until server is finished starting
connTimeout time.Duration // the maximum duration of a graceful shutdown
}
// ListenerFile represents a listener.
@@ -42,14 +42,17 @@ type ListenerFile interface {
}
// New creates a new Server which will bind to addr and serve
// the sites/hosts configured in configs. This function does
// not start serving.
// the sites/hosts configured in configs. Its listener will
// gracefully close when the server is stopped which will take
// no longer than gracefulTimeout.
//
// This function does not start serving.
//
// Do not re-use a server (start, stop, then start again). We
// could probably add more locking to make this possible, but
// as it stands, you should dispose of a server after stopping it.
// The behavior of serving with a spent server is undefined.
func New(addr string, configs []Config) (*Server, error) {
func New(addr string, configs []Config, gracefulTimeout time.Duration) (*Server, error) {
var tls bool
if len(configs) > 0 {
tls = configs[0].TLS.Enabled
@@ -63,9 +66,10 @@ func New(addr string, configs []Config) (*Server, error) {
// WriteTimeout: 2 * time.Minute,
// MaxHeaderBytes: 1 << 16,
},
tls: tls,
vhosts: make(map[string]virtualHost),
startChan: make(chan struct{}),
tls: tls,
vhosts: make(map[string]virtualHost),
startChan: make(chan struct{}),
connTimeout: gracefulTimeout,
}
s.Handler = s // this is weird, but whatever
@@ -241,7 +245,7 @@ func serveTLSWithSNI(s *Server, ln net.Listener, tlsConfigs []TLSConfig) error {
// connections to close (up to a max timeout of a few
// seconds); on Windows it will close the listener
// immediately.
func (s *Server) Stop() error {
func (s *Server) Stop() (err error) {
s.Server.SetKeepAlivesEnabled(false)
if runtime.GOOS != "windows" {
@@ -256,20 +260,19 @@ func (s *Server) Stop() error {
// Wait for remaining connections to finish or
// force them all to close after timeout
select {
case <-time.After(5 * time.Second): // TODO: make configurable
case <-time.After(s.connTimeout):
case <-done:
}
}
// Close the listener now; this stops the server without delay
s.listenerMu.Lock()
err := s.listener.Close()
s.listenerMu.Unlock()
if err != nil {
log.Printf("[ERROR] Closing listener for %s: %v", s.Addr, err)
if s.listener != nil {
err = s.listener.Close()
}
s.listenerMu.Unlock()
return err
return
}
// WaitUntilStarted blocks until the server s is started, meaning
@@ -279,15 +282,17 @@ func (s *Server) WaitUntilStarted() {
<-s.startChan
}
// ListenerFd gets the file descriptor of the listener.
func (s *Server) ListenerFd() uintptr {
// ListenerFd gets a dup'ed file of the listener. If there
// is no underlying file, the return value will be nil. It
// is the caller's responsibility to close the file.
func (s *Server) ListenerFd() *os.File {
s.listenerMu.Lock()
defer s.listenerMu.Unlock()
file, err := s.listener.File()
if err != nil {
return 0
if s.listener != nil {
file, _ := s.listener.File()
return file
}
return file.Fd()
return nil
}
// ServeHTTP is the entry point for every request to the address that s
@@ -371,6 +376,23 @@ func setupClientAuth(tlsConfigs []TLSConfig, config *tls.Config) error {
return nil
}
// RunFirstStartupFuncs runs all of the server's FirstStartup
// callback functions unless one of them returns an error first.
// It is the caller's responsibility to call this only once and
// at the correct time. The functions here should not be executed
// at restarts or where the user does not explicitly start a new
// instance of the server.
func (s *Server) RunFirstStartupFuncs() error {
for _, vh := range s.vhosts {
for _, f := range vh.config.FirstStartup {
if err := f(); err != nil {
return err
}
}
}
return nil
}
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
// connections. It's used by ListenAndServe and ListenAndServeTLS so
// dead TCP connections (e.g. closing laptop mid-download) eventually