mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d907d57fa | |||
| 24352e799a | |||
| e17d43b58a | |||
| 580b50ea20 | |||
| 659df6967e | |||
| b9244cdf2e | |||
| a2ba00bdc8 | |||
| 1d47e590e5 | |||
| 280ba9db85 | |||
| 7f98a6cccf | |||
| a5b117fcdf | |||
| f56d2090b6 | |||
| 37e3cf684d | |||
| 7949388da8 | |||
| dd119e04b1 | |||
| f7cfe79905 | |||
| 3dc5e0e181 | |||
| 1ca34c4ecf | |||
| 837ee9f042 | |||
| d448c919e8 | |||
| 7d5b6b96ea | |||
| 7b064535bf | |||
| b42334eb91 | |||
| 94c746c44f | |||
| 7d46a7d5f4 | |||
| 9e2cef38f6 | |||
| e166ebf68b | |||
| 33b1d4c55d | |||
| ae2e0900c1 | |||
| 91ac2c58fa | |||
| 69662d4d7d | |||
| fc6afe2a8b | |||
| 51d2ff4e47 | |||
| d46967d1e2 | |||
| 4d78013646 | |||
| 5cced604e4 | |||
| a39ed2823e | |||
| 4bed399ca4 | |||
| 93c330c4ce | |||
| 76ec785e87 | |||
| e9b9432da5 | |||
| c31e86db02 | |||
| 13557eb5ef | |||
| 02213402e8 | |||
| e1f23a1eb7 | |||
| 485af2c6ba | |||
| 171fd34b3c | |||
| 1017142d9b | |||
| be9f644425 | |||
| 2b1cc77f4b | |||
| 8774c90709 | |||
| 01465932e7 | |||
| 72c0527b7d |
@@ -2,7 +2,7 @@
|
||||
|
||||
[](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.
|
||||
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
@@ -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
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package caddy
|
||||
|
||||
func trapSignalsPosix() {}
|
||||
Vendored
+5
@@ -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
|
||||
|
||||
Vendored
+2
-1
@@ -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
|
||||
|
||||
@@ -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
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user