Commit f26447e2 authored by Matthew Holt's avatar Matthew Holt

Merge branch 'master' into cert-cache

# Conflicts:
#	sigtrap_posix.go
parents 08028714 2de49500
...@@ -16,4 +16,6 @@ Caddyfile ...@@ -16,4 +16,6 @@ Caddyfile
og_static/ og_static/
.vscode/ .vscode/
\ No newline at end of file
*.bat
\ No newline at end of file
<p align="center"> <p align="center">
<a href="https://caddyserver.com"><img src="https://cloud.githubusercontent.com/assets/1128849/25305033/12916fce-2731-11e7-86ec-580d4d31cb16.png" alt="Caddy" width="400"></a> <a href="https://caddyserver.com"><img src="https://user-images.githubusercontent.com/1128849/36137292-bebc223a-1051-11e8-9a81-4ea9054c96ac.png" alt="Caddy" width="400"></a>
</p> </p>
<h3 align="center">Every Site on HTTPS <!-- Serve Confidently --></h3> <h3 align="center">Every Site on HTTPS <!-- Serve Confidently --></h3>
<p align="center">Caddy is a general-purpose HTTP/2 web server that serves HTTPS by default.</p> <p align="center">Caddy is a general-purpose HTTP/2 web server that serves HTTPS by default.</p>
......
...@@ -170,10 +170,18 @@ func confLoader(serverType string) (caddy.Input, error) { ...@@ -170,10 +170,18 @@ func confLoader(serverType string) (caddy.Input, error) {
return caddy.CaddyfileFromPipe(os.Stdin, serverType) return caddy.CaddyfileFromPipe(os.Stdin, serverType)
} }
contents, err := ioutil.ReadFile(conf) var contents []byte
if err != nil { if strings.Contains(conf, "*") {
return nil, err // Let caddyfile.doImport logic handle the globbed path
contents = []byte("import " + conf)
} else {
var err error
contents, err = ioutil.ReadFile(conf)
if err != nil {
return nil, err
}
} }
return caddy.CaddyfileInput{ return caddy.CaddyfileInput{
Contents: contents, Contents: contents,
Filepath: conf, Filepath: conf,
...@@ -221,6 +229,8 @@ func setVersion() { ...@@ -221,6 +229,8 @@ func setVersion() {
// setCPU parses string cpu and sets GOMAXPROCS // setCPU parses string cpu and sets GOMAXPROCS
// according to its value. It accepts either // according to its value. It accepts either
// a number (e.g. 3) or a percent (e.g. 50%). // a number (e.g. 3) or a percent (e.g. 50%).
// If the percent resolves to less than a single
// GOMAXPROCS, it rounds it up to GOMAXPROCS=1.
func setCPU(cpu string) error { func setCPU(cpu string) error {
var numCPU int var numCPU int
...@@ -236,6 +246,9 @@ func setCPU(cpu string) error { ...@@ -236,6 +246,9 @@ func setCPU(cpu string) error {
} }
percent = float32(pctInt) / 100 percent = float32(pctInt) / 100
numCPU = int(float32(availCPU) * percent) numCPU = int(float32(availCPU) * percent)
if numCPU < 1 {
numCPU = 1
}
} else { } else {
// Number // Number
num, err := strconv.Atoi(cpu) num, err := strconv.Atoi(cpu)
......
...@@ -41,6 +41,7 @@ func TestSetCPU(t *testing.T) { ...@@ -41,6 +41,7 @@ func TestSetCPU(t *testing.T) {
{"invalid input", currentCPU, true}, {"invalid input", currentCPU, true},
{"invalid input%", currentCPU, true}, {"invalid input%", currentCPU, true},
{"9999", maxCPU, false}, // over available CPU {"9999", maxCPU, false}, // over available CPU
{"1%", 1, false}, // under a single CPU; assume maxCPU < 100
} { } {
err := setCPU(test.input) err := setCPU(test.input)
if test.shouldErr && err == nil { if test.shouldErr && err == nil {
......
...@@ -499,7 +499,7 @@ footer { ...@@ -499,7 +499,7 @@ footer {
return; return;
} }
} }
e.textContent = d.toLocaleString(); e.textContent = d.toLocaleString([], {day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit"});
} }
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time")); var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
timeList.forEach(localizeDatetime); timeList.forEach(localizeDatetime);
......
...@@ -148,7 +148,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) ...@@ -148,7 +148,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
case "HEAD": case "HEAD":
resp, err = fcgiBackend.Head(env) resp, err = fcgiBackend.Head(env)
case "GET": case "GET":
resp, err = fcgiBackend.Get(env) resp, err = fcgiBackend.Get(env, r.Body, contentLength)
case "OPTIONS": case "OPTIONS":
resp, err = fcgiBackend.Options(env) resp, err = fcgiBackend.Options(env)
default: default:
......
...@@ -460,12 +460,12 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res ...@@ -460,12 +460,12 @@ func (c *FCGIClient) Request(p map[string]string, req io.Reader) (resp *http.Res
} }
// Get issues a GET request to the fcgi responder. // Get issues a GET request to the fcgi responder.
func (c *FCGIClient) Get(p map[string]string) (resp *http.Response, err error) { func (c *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) {
p["REQUEST_METHOD"] = "GET" p["REQUEST_METHOD"] = "GET"
p["CONTENT_LENGTH"] = "0" p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
return c.Request(p, nil) return c.Request(p, body)
} }
// Head issues a HEAD request to the fcgi responder. // Head issues a HEAD request to the fcgi responder.
......
...@@ -140,7 +140,8 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[ ...@@ -140,7 +140,8 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
} }
resp, err = fcgi.PostForm(fcgiParams, values) resp, err = fcgi.PostForm(fcgiParams, values)
} else { } else {
resp, err = fcgi.Get(fcgiParams) rd := bytes.NewReader(data)
resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len()))
} }
default: default:
......
...@@ -115,6 +115,7 @@ type ResponseBuffer struct { ...@@ -115,6 +115,7 @@ type ResponseBuffer struct {
shouldBuffer func(status int, header http.Header) bool shouldBuffer func(status int, header http.Header) bool
stream bool stream bool
rw http.ResponseWriter rw http.ResponseWriter
wroteHeader bool
} }
// NewResponseBuffer returns a new ResponseBuffer that will // NewResponseBuffer returns a new ResponseBuffer that will
...@@ -152,6 +153,11 @@ func (rb *ResponseBuffer) Header() http.Header { ...@@ -152,6 +153,11 @@ func (rb *ResponseBuffer) Header() http.Header {
// upcoming body should be buffered, and then writes // upcoming body should be buffered, and then writes
// the header to the response. // the header to the response.
func (rb *ResponseBuffer) WriteHeader(status int) { func (rb *ResponseBuffer) WriteHeader(status int) {
if rb.wroteHeader {
return
}
rb.wroteHeader = true
rb.status = status rb.status = status
rb.stream = !rb.shouldBuffer(status, rb.header) rb.stream = !rb.shouldBuffer(status, rb.header)
if rb.stream { if rb.stream {
...@@ -163,6 +169,10 @@ func (rb *ResponseBuffer) WriteHeader(status int) { ...@@ -163,6 +169,10 @@ func (rb *ResponseBuffer) WriteHeader(status int) {
// Write writes buf to rb.Buffer if buffering, otherwise // Write writes buf to rb.Buffer if buffering, otherwise
// to the ResponseWriter directly if streaming. // to the ResponseWriter directly if streaming.
func (rb *ResponseBuffer) Write(buf []byte) (int, error) { func (rb *ResponseBuffer) Write(buf []byte) (int, error) {
if !rb.wroteHeader {
rb.WriteHeader(http.StatusOK)
}
if rb.stream { if rb.stream {
return rb.ResponseWriterWrapper.Write(buf) return rb.ResponseWriterWrapper.Write(buf)
} }
...@@ -190,6 +200,10 @@ func (rb *ResponseBuffer) CopyHeader() { ...@@ -190,6 +200,10 @@ func (rb *ResponseBuffer) CopyHeader() {
// from ~8,200 to ~9,600 on templated files by ensuring that this type // from ~8,200 to ~9,600 on templated files by ensuring that this type
// implements io.ReaderFrom. // implements io.ReaderFrom.
func (rb *ResponseBuffer) ReadFrom(src io.Reader) (int64, error) { func (rb *ResponseBuffer) ReadFrom(src io.Reader) (int64, error) {
if !rb.wroteHeader {
rb.WriteHeader(http.StatusOK)
}
if rb.stream { if rb.stream {
// first see if we can avoid any allocations at all // first see if we can avoid any allocations at all
if wt, ok := src.(io.WriterTo); ok { if wt, ok := src.(io.WriterTo); ok {
......
...@@ -240,6 +240,7 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) * ...@@ -240,6 +240,7 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) *
rp.Transport = &h2quic.RoundTripper{ rp.Transport = &h2quic.RoundTripper{
QuicConfig: &quic.Config{ QuicConfig: &quic.Config{
HandshakeTimeout: defaultCryptoHandshakeTimeout, HandshakeTimeout: defaultCryptoHandshakeTimeout,
KeepAlive: true,
}, },
} }
} else if keepalive != http.DefaultMaxIdleConnsPerHost || strings.HasPrefix(target.Scheme, "srv") { } else if keepalive != http.DefaultMaxIdleConnsPerHost || strings.HasPrefix(target.Scheme, "srv") {
......
...@@ -16,6 +16,7 @@ package requestid ...@@ -16,6 +16,7 @@ package requestid
import ( import (
"context" "context"
"log"
"net/http" "net/http"
"github.com/google/uuid" "github.com/google/uuid"
...@@ -24,12 +25,29 @@ import ( ...@@ -24,12 +25,29 @@ import (
// Handler is a middleware handler // Handler is a middleware handler
type Handler struct { type Handler struct {
Next httpserver.Handler Next httpserver.Handler
HeaderName string // (optional) header from which to read an existing ID
} }
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
reqid := uuid.New().String() var reqid uuid.UUID
c := context.WithValue(r.Context(), httpserver.RequestIDCtxKey, reqid)
uuidFromHeader := r.Header.Get(h.HeaderName)
if h.HeaderName != "" && uuidFromHeader != "" {
// use the ID in the header field if it exists
var err error
reqid, err = uuid.Parse(uuidFromHeader)
if err != nil {
log.Printf("[NOTICE] Parsing request ID from %s header: %v", h.HeaderName, err)
reqid = uuid.New()
}
} else {
// otherwise, create a new one
reqid = uuid.New()
}
// set the request ID on the context
c := context.WithValue(r.Context(), httpserver.RequestIDCtxKey, reqid.String())
r = r.WithContext(c) r = r.WithContext(c)
return h.Next.ServeHTTP(w, r) return h.Next.ServeHTTP(w, r)
......
...@@ -15,34 +15,53 @@ ...@@ -15,34 +15,53 @@
package requestid package requestid
import ( import (
"context"
"net/http" "net/http"
"net/http/httptest"
"testing" "testing"
"github.com/google/uuid"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
) )
func TestRequestID(t *testing.T) { func TestRequestIDHandler(t *testing.T) {
request, err := http.NewRequest("GET", "http://localhost/", nil) handler := Handler{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
value, _ := r.Context().Value(httpserver.RequestIDCtxKey).(string)
if value == "" {
t.Error("Request ID should not be empty")
}
return 0, nil
}),
}
req, err := http.NewRequest("GET", "http://localhost/", nil)
if err != nil { if err != nil {
t.Fatal("Could not create HTTP request:", err) t.Fatal("Could not create HTTP request:", err)
} }
rec := httptest.NewRecorder()
reqid := uuid.New().String() handler.ServeHTTP(rec, req)
}
c := context.WithValue(request.Context(), httpserver.RequestIDCtxKey, reqid)
request = request.WithContext(c)
// See caddyhttp/replacer.go
value, _ := request.Context().Value(httpserver.RequestIDCtxKey).(string)
if value == "" { func TestRequestIDFromHeader(t *testing.T) {
t.Fatal("Request ID should not be empty") headerName := "X-Request-ID"
headerValue := "71a75329-d9f9-4d25-957e-e689a7b68d78"
handler := Handler{
Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
value, _ := r.Context().Value(httpserver.RequestIDCtxKey).(string)
if value != headerValue {
t.Errorf("Request ID should be '%s' but got '%s'", headerValue, value)
}
return 0, nil
}),
HeaderName: headerName,
} }
if value != reqid { req, err := http.NewRequest("GET", "http://localhost/", nil)
t.Fatal("Request ID does not match") if err != nil {
t.Fatal("Could not create HTTP request:", err)
} }
req.Header.Set(headerName, headerValue)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
} }
...@@ -27,14 +27,19 @@ func init() { ...@@ -27,14 +27,19 @@ func init() {
} }
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
var headerName string
for c.Next() { for c.Next() {
if c.NextArg() { if c.NextArg() {
return c.ArgErr() //no arg expected. headerName = c.Val()
}
if c.NextArg() {
return c.ArgErr()
} }
} }
httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
return Handler{Next: next} return Handler{Next: next, HeaderName: headerName}
}) })
return nil return nil
......
...@@ -45,7 +45,15 @@ func TestSetup(t *testing.T) { ...@@ -45,7 +45,15 @@ func TestSetup(t *testing.T) {
} }
func TestSetupWithArg(t *testing.T) { func TestSetupWithArg(t *testing.T) {
c := caddy.NewTestController("http", `requestid abc`) c := caddy.NewTestController("http", `requestid X-Request-ID`)
err := setup(c)
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
}
func TestSetupWithTooManyArgs(t *testing.T) {
c := caddy.NewTestController("http", `requestid foo bar`)
err := setup(c) err := setup(c)
if err == nil { if err == nil {
t.Errorf("Expected an error, got: %v", err) t.Errorf("Expected an error, got: %v", err)
......
...@@ -107,6 +107,10 @@ func (fs FileServer) serveFile(w http.ResponseWriter, r *http.Request) (int, err ...@@ -107,6 +107,10 @@ func (fs FileServer) serveFile(w http.ResponseWriter, r *http.Request) (int, err
if d.IsDir() { if d.IsDir() {
// ensure there is a trailing slash // ensure there is a trailing slash
if urlCopy.Path[len(urlCopy.Path)-1] != '/' { if urlCopy.Path[len(urlCopy.Path)-1] != '/' {
for strings.HasPrefix(urlCopy.Path, "//") {
// prevent path-based open redirects
urlCopy.Path = strings.TrimPrefix(urlCopy.Path, "/")
}
urlCopy.Path += "/" urlCopy.Path += "/"
http.Redirect(w, r, urlCopy.String(), http.StatusMovedPermanently) http.Redirect(w, r, urlCopy.String(), http.StatusMovedPermanently)
return http.StatusMovedPermanently, nil return http.StatusMovedPermanently, nil
...@@ -131,6 +135,10 @@ func (fs FileServer) serveFile(w http.ResponseWriter, r *http.Request) (int, err ...@@ -131,6 +135,10 @@ func (fs FileServer) serveFile(w http.ResponseWriter, r *http.Request) (int, err
} }
if redir { if redir {
for strings.HasPrefix(urlCopy.Path, "//") {
// prevent path-based open redirects
urlCopy.Path = strings.TrimPrefix(urlCopy.Path, "/")
}
http.Redirect(w, r, urlCopy.String(), http.StatusMovedPermanently) http.Redirect(w, r, urlCopy.String(), http.StatusMovedPermanently)
return http.StatusMovedPermanently, nil return http.StatusMovedPermanently, nil
} }
......
...@@ -77,9 +77,9 @@ func TestServeHTTP(t *testing.T) { ...@@ -77,9 +77,9 @@ func TestServeHTTP(t *testing.T) {
{ {
url: "https://foo/dirwithindex/", url: "https://foo/dirwithindex/",
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedBodyContent: testFiles[webrootDirwithindexIndeHTML], expectedBodyContent: testFiles[webrootDirwithindexIndexHTML],
expectedEtag: `"2n9cw"`, expectedEtag: `"2n9cw"`,
expectedContentLength: strconv.Itoa(len(testFiles[webrootDirwithindexIndeHTML])), expectedContentLength: strconv.Itoa(len(testFiles[webrootDirwithindexIndexHTML])),
}, },
// Test 4 - access folder with index file without trailing slash // Test 4 - access folder with index file without trailing slash
{ {
...@@ -235,16 +235,38 @@ func TestServeHTTP(t *testing.T) { ...@@ -235,16 +235,38 @@ func TestServeHTTP(t *testing.T) {
expectedBodyContent: movedPermanently, expectedBodyContent: movedPermanently,
}, },
{ {
// Test 27 - Check etag
url: "https://foo/notindex.html", url: "https://foo/notindex.html",
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedBodyContent: testFiles[webrootNotIndexHTML], expectedBodyContent: testFiles[webrootNotIndexHTML],
expectedEtag: `"2n9cm"`, expectedEtag: `"2n9cm"`,
expectedContentLength: strconv.Itoa(len(testFiles[webrootNotIndexHTML])), expectedContentLength: strconv.Itoa(len(testFiles[webrootNotIndexHTML])),
}, },
{
// Test 28 - Prevent path-based open redirects (directory)
url: "https://foo//example.com%2f..",
expectedStatus: http.StatusMovedPermanently,
expectedLocation: "https://foo/example.com/../",
expectedBodyContent: movedPermanently,
},
{
// Test 29 - Prevent path-based open redirects (file)
url: "https://foo//example.com%2f../dirwithindex/index.html",
expectedStatus: http.StatusMovedPermanently,
expectedLocation: "https://foo/example.com/../dirwithindex/",
expectedBodyContent: movedPermanently,
},
{
// Test 29 - Prevent path-based open redirects (extra leading slashes)
url: "https://foo///example.com%2f..",
expectedStatus: http.StatusMovedPermanently,
expectedLocation: "https://foo/example.com/../",
expectedBodyContent: movedPermanently,
},
} }
for i, test := range tests { for i, test := range tests {
// set up response writer and rewuest // set up response writer and request
responseRecorder := httptest.NewRecorder() responseRecorder := httptest.NewRecorder()
request, err := http.NewRequest("GET", test.url, nil) request, err := http.NewRequest("GET", test.url, nil)
if err != nil { if err != nil {
...@@ -518,7 +540,7 @@ var ( ...@@ -518,7 +540,7 @@ var (
webrootNotIndexHTML = filepath.Join(webrootName, "notindex.html") webrootNotIndexHTML = filepath.Join(webrootName, "notindex.html")
webrootDirFile2HTML = filepath.Join(webrootName, "dir", "file2.html") webrootDirFile2HTML = filepath.Join(webrootName, "dir", "file2.html")
webrootDirHiddenHTML = filepath.Join(webrootName, "dir", "hidden.html") webrootDirHiddenHTML = filepath.Join(webrootName, "dir", "hidden.html")
webrootDirwithindexIndeHTML = filepath.Join(webrootName, "dirwithindex", "index.html") webrootDirwithindexIndexHTML = filepath.Join(webrootName, "dirwithindex", "index.html")
webrootSubGzippedHTML = filepath.Join(webrootName, "sub", "gzipped.html") webrootSubGzippedHTML = filepath.Join(webrootName, "sub", "gzipped.html")
webrootSubGzippedHTMLGz = filepath.Join(webrootName, "sub", "gzipped.html.gz") webrootSubGzippedHTMLGz = filepath.Join(webrootName, "sub", "gzipped.html.gz")
webrootSubGzippedHTMLBr = filepath.Join(webrootName, "sub", "gzipped.html.br") webrootSubGzippedHTMLBr = filepath.Join(webrootName, "sub", "gzipped.html.br")
...@@ -544,7 +566,7 @@ var testFiles = map[string]string{ ...@@ -544,7 +566,7 @@ var testFiles = map[string]string{
webrootFile1HTML: "<h1>file1.html</h1>", webrootFile1HTML: "<h1>file1.html</h1>",
webrootNotIndexHTML: "<h1>notindex.html</h1>", webrootNotIndexHTML: "<h1>notindex.html</h1>",
webrootDirFile2HTML: "<h1>dir/file2.html</h1>", webrootDirFile2HTML: "<h1>dir/file2.html</h1>",
webrootDirwithindexIndeHTML: "<h1>dirwithindex/index.html</h1>", webrootDirwithindexIndexHTML: "<h1>dirwithindex/index.html</h1>",
webrootDirHiddenHTML: "<h1>dir/hidden.html</h1>", webrootDirHiddenHTML: "<h1>dir/hidden.html</h1>",
webrootSubGzippedHTML: "<h1>gzipped.html</h1>", webrootSubGzippedHTML: "<h1>gzipped.html</h1>",
webrootSubGzippedHTMLGz: "1.gzipped.html.gz", webrootSubGzippedHTMLGz: "1.gzipped.html.gz",
......
...@@ -62,100 +62,79 @@ func TestTemplates(t *testing.T) { ...@@ -62,100 +62,79 @@ func TestTemplates(t *testing.T) {
BufPool: &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}, BufPool: &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }},
} }
// Test tmpl on /photos/test.html // register custom function which is used in template
req, err := http.NewRequest("GET", "/photos/test.html", nil) httpserver.TemplateFuncs["root"] = func() string { return "root" }
if err != nil {
t.Fatalf("Test: Could not create HTTP request: %v", err)
}
req = req.WithContext(context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL))
rec := httptest.NewRecorder()
tmpl.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
}
respBody := rec.Body.String() for _, c := range []struct {
expectedBody := `<!DOCTYPE html><html><head><title>test page</title></head><body><h1>Header title</h1> tpl Templates
req string
respCode int
res string
}{
{
tpl: tmpl,
req: "/photos/test.html",
respCode: http.StatusOK,
res: `<!DOCTYPE html><html><head><title>test page</title></head><body><h1>Header title</h1>
</body></html> </body></html>
` `,
},
if respBody != expectedBody {
t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
}
// Test tmpl on /images/img.htm
req, err = http.NewRequest("GET", "/images/img.htm", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
req = req.WithContext(context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL))
rec = httptest.NewRecorder()
tmpl.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
}
respBody = rec.Body.String() {
expectedBody = `<!DOCTYPE html><html><head><title>img</title></head><body><h1>Header title</h1> tpl: tmpl,
req: "/images/img.htm",
respCode: http.StatusOK,
res: `<!DOCTYPE html><html><head><title>img</title></head><body><h1>Header title</h1>
</body></html> </body></html>
` `,
},
if respBody != expectedBody {
t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
}
// Test tmpl on /images/img2.htm
req, err = http.NewRequest("GET", "/images/img2.htm", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
req = req.WithContext(context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL))
rec = httptest.NewRecorder()
tmpl.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK)
}
respBody = rec.Body.String()
expectedBody = `<!DOCTYPE html><html><head><title>img</title></head><body>{{.Include "header.html"}}</body></html>
`
if respBody != expectedBody {
t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
}
// Test tmplroot on /root.html
req, err = http.NewRequest("GET", "/root.html", nil)
if err != nil {
t.Fatalf("Could not create HTTP request: %v", err)
}
req = req.WithContext(context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL))
rec = httptest.NewRecorder()
// register custom function which is used in template
httpserver.TemplateFuncs["root"] = func() string { return "root" }
tmplroot.ServeHTTP(rec, req)
if rec.Code != http.StatusOK { {
t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, http.StatusOK) tpl: tmpl,
} req: "/images/img2.htm",
respCode: http.StatusOK,
res: `<!DOCTYPE html><html><head><title>img</title></head><body>{{.Include "header.html"}}</body></html>
`,
},
respBody = rec.Body.String() {
expectedBody = `<!DOCTYPE html><html><head><title>root</title></head><body><h1>Header title</h1> tpl: tmplroot,
req: "/root.html",
respCode: http.StatusOK,
res: `<!DOCTYPE html><html><head><title>root</title></head><body><h1>Header title</h1>
</body></html> </body></html>
` `,
},
if respBody != expectedBody { // test extension filter
t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody) {
tpl: tmplroot,
req: "/as_it_is.txt",
respCode: http.StatusOK,
res: `<!DOCTYPE html><html><head><title>as it is</title></head><body>{{.Include "header.html"}}</body></html>
`,
},
} {
c := c
t.Run("", func(t *testing.T) {
req, err := http.NewRequest("GET", c.req, nil)
if err != nil {
t.Fatalf("Test: Could not create HTTP request: %v", err)
}
req = req.WithContext(context.WithValue(req.Context(), httpserver.OriginalURLCtxKey, *req.URL))
rec := httptest.NewRecorder()
c.tpl.ServeHTTP(rec, req)
if rec.Code != c.respCode {
t.Fatalf("Test: Wrong response code: %d, should be %d", rec.Code, c.respCode)
}
respBody := rec.Body.String()
if respBody != c.res {
t.Fatalf("Test: the expected body %v is different from the response one: %v", c.res, respBody)
}
})
} }
} }
<!DOCTYPE html><html><head><title>as it is</title></head><body>{{.Include "header.html"}}</body></html>
...@@ -46,7 +46,7 @@ sudo useradd \ ...@@ -46,7 +46,7 @@ sudo useradd \
sudo mkdir /etc/caddy sudo mkdir /etc/caddy
sudo chown -R root:www-data /etc/caddy sudo chown -R root:www-data /etc/caddy
sudo mkdir /etc/ssl/caddy sudo mkdir /etc/ssl/caddy
sudo chown -R www-data:root /etc/ssl/caddy sudo chown -R root:www-data /etc/ssl/caddy
sudo chmod 0770 /etc/ssl/caddy sudo chmod 0770 /etc/ssl/caddy
``` ```
...@@ -91,6 +91,7 @@ Install the systemd service unit configuration file, reload the systemd daemon, ...@@ -91,6 +91,7 @@ Install the systemd service unit configuration file, reload the systemd daemon,
and start caddy: and start caddy:
```bash ```bash
wget https://raw.githubusercontent.com/mholt/caddy/master/dist/init/linux-systemd/caddy.service
sudo cp caddy.service /etc/systemd/system/ sudo cp caddy.service /etc/systemd/system/
sudo chown root:root /etc/systemd/system/caddy.service sudo chown root:root /etc/systemd/system/caddy.service
sudo chmod 644 /etc/systemd/system/caddy.service sudo chmod 644 /etc/systemd/system/caddy.service
......
...@@ -30,8 +30,8 @@ LimitNPROC=512 ...@@ -30,8 +30,8 @@ LimitNPROC=512
; Use private /tmp and /var/tmp, which are discarded after caddy stops. ; Use private /tmp and /var/tmp, which are discarded after caddy stops.
PrivateTmp=true PrivateTmp=true
; Use a minimal /dev ; Use a minimal /dev (May bring additional security if switched to 'true', but it may not work on Raspberry Pi's or other devices, so it has been disabled in this dist.)
PrivateDevices=true PrivateDevices=false
; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys. ; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
ProtectHome=true ProtectHome=true
; Make /usr, /boot, /etc and possibly some more folders read-only. ; Make /usr, /boot, /etc and possibly some more folders read-only.
...@@ -41,7 +41,7 @@ ProtectSystem=full ...@@ -41,7 +41,7 @@ ProtectSystem=full
ReadWriteDirectories=/etc/ssl/caddy ReadWriteDirectories=/etc/ssl/caddy
; The following additional security directives only work with systemd v229 or later. ; The following additional security directives only work with systemd v229 or later.
; They further retrict privileges that can be gained by caddy. Uncomment if you like. ; They further restrict privileges that can be gained by caddy. Uncomment if you like.
; Note that you may have to add capabilities required by any plugins in use. ; Note that you may have to add capabilities required by any plugins in use.
;CapabilityBoundingSet=CAP_NET_BIND_SERVICE ;CapabilityBoundingSet=CAP_NET_BIND_SERVICE
;AmbientCapabilities=CAP_NET_BIND_SERVICE ;AmbientCapabilities=CAP_NET_BIND_SERVICE
......
...@@ -9,3 +9,19 @@ Usage ...@@ -9,3 +9,19 @@ Usage
* Ensure that the folder `/etc/caddy` exists and that the folder `/etc/ssl/caddy` is owned by `www-data`. * Ensure that the folder `/etc/caddy` exists and that the folder `/etc/ssl/caddy` is owned by `www-data`.
* Create a Caddyfile in `/etc/caddy/Caddyfile` * Create a Caddyfile in `/etc/caddy/Caddyfile`
* Now you can use `service caddy start|stop|restart|reload|status` as `root`. * Now you can use `service caddy start|stop|restart|reload|status` as `root`.
Init script manipulation
-----
The init script supports configuration via the following files:
* `/etc/default/caddy` ( Debian based https://www.debian.org/doc/manuals/debian-reference/ch03.en.html#_the_default_parameter_for_each_init_script )
* `/etc/sysconfig/caddy` ( CentOS based https://www.centos.org/docs/5/html/5.2/Deployment_Guide/s1-sysconfig-files.html )
The following variables can be changed:
* DAEMON: path to the caddy binary file (default: `/usr/local/bin/caddy`)
* DAEMONUSER: user used to run caddy (default: `www-data`)
* PIDFILE: path to the pidfile (default: `/var/run/$NAME.pid`)
* LOGFILE: path to the log file for caddy daemon (not for access logs) (default: `/var/log/$NAME.log`)
* CONFIGFILE: path to the caddy configuration file (default: `/etc/caddy/Caddyfile`)
* CADDYPATH: path for SSL certificates managed by caddy (default: `/etc/ssl/caddy`)
* ULIMIT: open files limit (default: `8192`)
...@@ -20,18 +20,30 @@ DAEMONUSER=www-data ...@@ -20,18 +20,30 @@ DAEMONUSER=www-data
PIDFILE=/var/run/$NAME.pid PIDFILE=/var/run/$NAME.pid
LOGFILE=/var/log/$NAME.log LOGFILE=/var/log/$NAME.log
CONFIGFILE=/etc/caddy/Caddyfile CONFIGFILE=/etc/caddy/Caddyfile
DAEMONOPTS="-agree=true -log=$LOGFILE -conf=$CONFIGFILE"
USERBIND="setcap cap_net_bind_service=+ep" USERBIND="setcap cap_net_bind_service=+ep"
STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/5/KILL/5}" STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/5/KILL/5}"
CADDYPATH=/etc/ssl/caddy
ULIMIT=8192
test -x $DAEMON || exit 0 test -x $DAEMON || exit 0
# allow overwriting variables
# Debian based
[ -e "/etc/default/caddy" ] && . /etc/default/caddy
# CentOS based
[ -e "/etc/sysconfig/caddy" ] && . /etc/sysconfig/caddy
if [ -z "$DAEMONOPTS" ]; then
# daemon options
DAEMONOPTS="-agree=true -log=$LOGFILE -conf=$CONFIGFILE"
fi
# Set the CADDYPATH; Let's Encrypt certificates will be written to this directory. # Set the CADDYPATH; Let's Encrypt certificates will be written to this directory.
export CADDYPATH=/etc/ssl/caddy export CADDYPATH
# Set the ulimits # Set the ulimits
ulimit -n 8192 ulimit -n ${ULIMIT}
start() { start() {
......
...@@ -19,6 +19,7 @@ import ( ...@@ -19,6 +19,7 @@ import (
"log" "log"
"net" "net"
"sort" "sort"
"sync"
"github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyfile"
) )
...@@ -38,7 +39,7 @@ var ( ...@@ -38,7 +39,7 @@ var (
// eventHooks is a map of hook name to Hook. All hooks plugins // eventHooks is a map of hook name to Hook. All hooks plugins
// must have a name. // must have a name.
eventHooks = make(map[string]EventHook) eventHooks = sync.Map{}
// parsingCallbacks maps server type to map of directive // parsingCallbacks maps server type to map of directive
// to list of callback functions. These aren't really // to list of callback functions. These aren't really
...@@ -67,12 +68,15 @@ func DescribePlugins() string { ...@@ -67,12 +68,15 @@ func DescribePlugins() string {
str += " " + defaultCaddyfileLoader.name + "\n" str += " " + defaultCaddyfileLoader.name + "\n"
} }
if len(eventHooks) > 0 { // List the event hook plugins
// List the event hook plugins hooks := ""
eventHooks.Range(func(k, _ interface{}) bool {
hooks += " hook." + k.(string) + "\n"
return true
})
if hooks != "" {
str += "\nEvent hook plugins:\n" str += "\nEvent hook plugins:\n"
for hookPlugin := range eventHooks { str += hooks
str += " hook." + hookPlugin + "\n"
}
} }
// Let's alphabetize the rest of these... // Let's alphabetize the rest of these...
...@@ -248,23 +252,23 @@ func RegisterEventHook(name string, hook EventHook) { ...@@ -248,23 +252,23 @@ func RegisterEventHook(name string, hook EventHook) {
if name == "" { if name == "" {
panic("event hook must have a name") panic("event hook must have a name")
} }
if _, dup := eventHooks[name]; dup { _, dup := eventHooks.LoadOrStore(name, hook)
if dup {
panic("hook named " + name + " already registered") panic("hook named " + name + " already registered")
} }
eventHooks[name] = hook
} }
// EmitEvent executes the different hooks passing the EventType as an // EmitEvent executes the different hooks passing the EventType as an
// argument. This is a blocking function. Hook developers should // argument. This is a blocking function. Hook developers should
// use 'go' keyword if they don't want to block Caddy. // use 'go' keyword if they don't want to block Caddy.
func EmitEvent(event EventName, info interface{}) { func EmitEvent(event EventName, info interface{}) {
for name, hook := range eventHooks { eventHooks.Range(func(k, v interface{}) bool {
err := hook(event, info) err := v.(EventHook)(event, info)
if err != nil { if err != nil {
log.Printf("error on '%s' hook: %v", name, err) log.Printf("error on '%s' hook: %v", k.(string), err)
} }
} return true
})
} }
// ParsingCallback is a function that is called after // ParsingCallback is a function that is called after
......
...@@ -31,19 +31,19 @@ func trapSignalsPosix() { ...@@ -31,19 +31,19 @@ func trapSignalsPosix() {
for sig := range sigchan { for sig := range sigchan {
switch sig { switch sig {
case syscall.SIGTERM: case syscall.SIGQUIT:
log.Println("[INFO] SIGTERM: Terminating process") log.Println("[INFO] SIGQUIT: Quitting process immediately")
for _, f := range OnProcessExit { for _, f := range OnProcessExit {
f() // only perform important cleanup actions f() // only perform important cleanup actions
} }
os.Exit(0) os.Exit(0)
case syscall.SIGQUIT: case syscall.SIGTERM:
log.Println("[INFO] SIGQUIT: Shutting down") log.Println("[INFO] SIGTERM: Shutting down servers then terminating")
exitCode := executeShutdownCallbacks("SIGQUIT") exitCode := executeShutdownCallbacks("SIGTERM")
err := Stop() err := Stop()
if err != nil { if err != nil {
log.Printf("[ERROR] SIGQUIT stop: %v", err) log.Printf("[ERROR] SIGTERM stop: %v", err)
exitCode = 3 exitCode = 3
} }
for _, f := range OnProcessExit { for _, f := range OnProcessExit {
...@@ -51,13 +51,6 @@ func trapSignalsPosix() { ...@@ -51,13 +51,6 @@ func trapSignalsPosix() {
} }
os.Exit(exitCode) os.Exit(exitCode)
case syscall.SIGHUP:
log.Println("[INFO] SIGHUP: Hanging up")
err := Stop()
if err != nil {
log.Printf("[ERROR] SIGHUP stop: %v", err)
}
case syscall.SIGUSR1: case syscall.SIGUSR1:
log.Println("[INFO] SIGUSR1: Reloading") log.Println("[INFO] SIGUSR1: Reloading")
...@@ -94,6 +87,9 @@ func trapSignalsPosix() { ...@@ -94,6 +87,9 @@ func trapSignalsPosix() {
if err := Upgrade(); err != nil { if err := Upgrade(); err != nil {
log.Printf("[ERROR] SIGUSR2: upgrading: %v", err) log.Printf("[ERROR] SIGUSR2: upgrading: %v", err)
} }
case syscall.SIGHUP:
// ignore; this signal is sometimes sent outside of the user's control
} }
} }
}() }()
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment