mirror of
https://github.com/caddyserver/caddy.git
synced 2025-02-22 21:53:10 +08:00
Merge branch 'master' into cert-cache
# Conflicts: # sigtrap_posix.go
This commit is contained in:
commit
f26447e2fb
4
.gitignore
vendored
4
.gitignore
vendored
@ -16,4 +16,6 @@ Caddyfile
|
||||
|
||||
og_static/
|
||||
|
||||
.vscode/
|
||||
.vscode/
|
||||
|
||||
*.bat
|
@ -1,5 +1,5 @@
|
||||
<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>
|
||||
<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>
|
||||
|
@ -170,10 +170,18 @@ func confLoader(serverType string) (caddy.Input, error) {
|
||||
return caddy.CaddyfileFromPipe(os.Stdin, serverType)
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var contents []byte
|
||||
if strings.Contains(conf, "*") {
|
||||
// Let caddyfile.doImport logic handle the globbed path
|
||||
contents = []byte("import " + conf)
|
||||
} else {
|
||||
var err error
|
||||
contents, err = ioutil.ReadFile(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return caddy.CaddyfileInput{
|
||||
Contents: contents,
|
||||
Filepath: conf,
|
||||
@ -221,6 +229,8 @@ func setVersion() {
|
||||
// setCPU parses string cpu and sets GOMAXPROCS
|
||||
// according to its value. It accepts either
|
||||
// a number (e.g. 3) or a percent (e.g. 50%).
|
||||
// If the percent resolves to less than a single
|
||||
// GOMAXPROCS, it rounds it up to GOMAXPROCS=1.
|
||||
func setCPU(cpu string) error {
|
||||
var numCPU int
|
||||
|
||||
@ -236,6 +246,9 @@ func setCPU(cpu string) error {
|
||||
}
|
||||
percent = float32(pctInt) / 100
|
||||
numCPU = int(float32(availCPU) * percent)
|
||||
if numCPU < 1 {
|
||||
numCPU = 1
|
||||
}
|
||||
} else {
|
||||
// Number
|
||||
num, err := strconv.Atoi(cpu)
|
||||
|
@ -41,6 +41,7 @@ func TestSetCPU(t *testing.T) {
|
||||
{"invalid input", currentCPU, true},
|
||||
{"invalid input%", currentCPU, true},
|
||||
{"9999", maxCPU, false}, // over available CPU
|
||||
{"1%", 1, false}, // under a single CPU; assume maxCPU < 100
|
||||
} {
|
||||
err := setCPU(test.input)
|
||||
if test.shouldErr && err == nil {
|
||||
|
@ -499,7 +499,7 @@ footer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.textContent = d.toLocaleString();
|
||||
e.textContent = d.toLocaleString([], {day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
}
|
||||
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
|
||||
timeList.forEach(localizeDatetime);
|
||||
|
@ -148,7 +148,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
case "HEAD":
|
||||
resp, err = fcgiBackend.Head(env)
|
||||
case "GET":
|
||||
resp, err = fcgiBackend.Get(env)
|
||||
resp, err = fcgiBackend.Get(env, r.Body, contentLength)
|
||||
case "OPTIONS":
|
||||
resp, err = fcgiBackend.Options(env)
|
||||
default:
|
||||
|
@ -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.
|
||||
func (c *FCGIClient) Get(p map[string]string) (resp *http.Response, err error) {
|
||||
func (c *FCGIClient) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) {
|
||||
|
||||
p["REQUEST_METHOD"] = "GET"
|
||||
p["CONTENT_LENGTH"] = "0"
|
||||
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
|
||||
|
||||
return c.Request(p, nil)
|
||||
return c.Request(p, body)
|
||||
}
|
||||
|
||||
// Head issues a HEAD request to the fcgi responder.
|
||||
|
@ -140,7 +140,8 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[
|
||||
}
|
||||
resp, err = fcgi.PostForm(fcgiParams, values)
|
||||
} else {
|
||||
resp, err = fcgi.Get(fcgiParams)
|
||||
rd := bytes.NewReader(data)
|
||||
resp, err = fcgi.Get(fcgiParams, rd, int64(rd.Len()))
|
||||
}
|
||||
|
||||
default:
|
||||
|
@ -115,6 +115,7 @@ type ResponseBuffer struct {
|
||||
shouldBuffer func(status int, header http.Header) bool
|
||||
stream bool
|
||||
rw http.ResponseWriter
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
// NewResponseBuffer returns a new ResponseBuffer that will
|
||||
@ -152,6 +153,11 @@ func (rb *ResponseBuffer) Header() http.Header {
|
||||
// upcoming body should be buffered, and then writes
|
||||
// the header to the response.
|
||||
func (rb *ResponseBuffer) WriteHeader(status int) {
|
||||
if rb.wroteHeader {
|
||||
return
|
||||
}
|
||||
rb.wroteHeader = true
|
||||
|
||||
rb.status = status
|
||||
rb.stream = !rb.shouldBuffer(status, rb.header)
|
||||
if rb.stream {
|
||||
@ -163,6 +169,10 @@ func (rb *ResponseBuffer) WriteHeader(status int) {
|
||||
// Write writes buf to rb.Buffer if buffering, otherwise
|
||||
// to the ResponseWriter directly if streaming.
|
||||
func (rb *ResponseBuffer) Write(buf []byte) (int, error) {
|
||||
if !rb.wroteHeader {
|
||||
rb.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
if rb.stream {
|
||||
return rb.ResponseWriterWrapper.Write(buf)
|
||||
}
|
||||
@ -190,6 +200,10 @@ func (rb *ResponseBuffer) CopyHeader() {
|
||||
// from ~8,200 to ~9,600 on templated files by ensuring that this type
|
||||
// implements io.ReaderFrom.
|
||||
func (rb *ResponseBuffer) ReadFrom(src io.Reader) (int64, error) {
|
||||
if !rb.wroteHeader {
|
||||
rb.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
if rb.stream {
|
||||
// first see if we can avoid any allocations at all
|
||||
if wt, ok := src.(io.WriterTo); ok {
|
||||
|
@ -240,6 +240,7 @@ func NewSingleHostReverseProxy(target *url.URL, without string, keepalive int) *
|
||||
rp.Transport = &h2quic.RoundTripper{
|
||||
QuicConfig: &quic.Config{
|
||||
HandshakeTimeout: defaultCryptoHandshakeTimeout,
|
||||
KeepAlive: true,
|
||||
},
|
||||
}
|
||||
} else if keepalive != http.DefaultMaxIdleConnsPerHost || strings.HasPrefix(target.Scheme, "srv") {
|
||||
|
@ -16,6 +16,7 @@ package requestid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -24,12 +25,29 @@ import (
|
||||
|
||||
// Handler is a middleware handler
|
||||
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) {
|
||||
reqid := uuid.New().String()
|
||||
c := context.WithValue(r.Context(), httpserver.RequestIDCtxKey, reqid)
|
||||
var reqid uuid.UUID
|
||||
|
||||
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)
|
||||
|
||||
return h.Next.ServeHTTP(w, r)
|
||||
|
@ -15,34 +15,53 @@
|
||||
package requestid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||
)
|
||||
|
||||
func TestRequestID(t *testing.T) {
|
||||
request, err := http.NewRequest("GET", "http://localhost/", nil)
|
||||
func TestRequestIDHandler(t *testing.T) {
|
||||
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 {
|
||||
t.Fatal("Could not create HTTP request:", err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
reqid := uuid.New().String()
|
||||
|
||||
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 == "" {
|
||||
t.Fatal("Request ID should not be empty")
|
||||
}
|
||||
|
||||
if value != reqid {
|
||||
t.Fatal("Request ID does not match")
|
||||
}
|
||||
handler.ServeHTTP(rec, req)
|
||||
}
|
||||
|
||||
func TestRequestIDFromHeader(t *testing.T) {
|
||||
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,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://localhost/", nil)
|
||||
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() {
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
var headerName string
|
||||
|
||||
for c.Next() {
|
||||
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 {
|
||||
return Handler{Next: next}
|
||||
return Handler{Next: next, HeaderName: headerName}
|
||||
})
|
||||
|
||||
return nil
|
||||
|
@ -45,7 +45,15 @@ func TestSetup(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)
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error, got: %v", err)
|
||||
|
@ -107,6 +107,10 @@ func (fs FileServer) serveFile(w http.ResponseWriter, r *http.Request) (int, err
|
||||
if d.IsDir() {
|
||||
// ensure there is a trailing slash
|
||||
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 += "/"
|
||||
http.Redirect(w, r, urlCopy.String(), http.StatusMovedPermanently)
|
||||
return http.StatusMovedPermanently, nil
|
||||
@ -131,6 +135,10 @@ func (fs FileServer) serveFile(w http.ResponseWriter, r *http.Request) (int, err
|
||||
}
|
||||
|
||||
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)
|
||||
return http.StatusMovedPermanently, nil
|
||||
}
|
||||
|
@ -77,9 +77,9 @@ func TestServeHTTP(t *testing.T) {
|
||||
{
|
||||
url: "https://foo/dirwithindex/",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBodyContent: testFiles[webrootDirwithindexIndeHTML],
|
||||
expectedBodyContent: testFiles[webrootDirwithindexIndexHTML],
|
||||
expectedEtag: `"2n9cw"`,
|
||||
expectedContentLength: strconv.Itoa(len(testFiles[webrootDirwithindexIndeHTML])),
|
||||
expectedContentLength: strconv.Itoa(len(testFiles[webrootDirwithindexIndexHTML])),
|
||||
},
|
||||
// Test 4 - access folder with index file without trailing slash
|
||||
{
|
||||
@ -235,16 +235,38 @@ func TestServeHTTP(t *testing.T) {
|
||||
expectedBodyContent: movedPermanently,
|
||||
},
|
||||
{
|
||||
// Test 27 - Check etag
|
||||
url: "https://foo/notindex.html",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBodyContent: testFiles[webrootNotIndexHTML],
|
||||
expectedEtag: `"2n9cm"`,
|
||||
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 {
|
||||
// set up response writer and rewuest
|
||||
// set up response writer and request
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
request, err := http.NewRequest("GET", test.url, nil)
|
||||
if err != nil {
|
||||
@ -518,7 +540,7 @@ var (
|
||||
webrootNotIndexHTML = filepath.Join(webrootName, "notindex.html")
|
||||
webrootDirFile2HTML = filepath.Join(webrootName, "dir", "file2.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")
|
||||
webrootSubGzippedHTMLGz = filepath.Join(webrootName, "sub", "gzipped.html.gz")
|
||||
webrootSubGzippedHTMLBr = filepath.Join(webrootName, "sub", "gzipped.html.br")
|
||||
@ -544,7 +566,7 @@ var testFiles = map[string]string{
|
||||
webrootFile1HTML: "<h1>file1.html</h1>",
|
||||
webrootNotIndexHTML: "<h1>notindex.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>",
|
||||
webrootSubGzippedHTML: "<h1>gzipped.html</h1>",
|
||||
webrootSubGzippedHTMLGz: "1.gzipped.html.gz",
|
||||
|
@ -62,100 +62,79 @@ func TestTemplates(t *testing.T) {
|
||||
BufPool: &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }},
|
||||
}
|
||||
|
||||
// Test tmpl on /photos/test.html
|
||||
req, err := http.NewRequest("GET", "/photos/test.html", 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()
|
||||
|
||||
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>test page</title></head><body><h1>Header title</h1>
|
||||
</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>
|
||||
</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)
|
||||
}
|
||||
|
||||
respBody = rec.Body.String()
|
||||
expectedBody = `<!DOCTYPE html><html><head><title>root</title></head><body><h1>Header title</h1>
|
||||
for _, c := range []struct {
|
||||
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>
|
||||
`
|
||||
`,
|
||||
},
|
||||
|
||||
if respBody != expectedBody {
|
||||
t.Fatalf("Test: the expected body %v is different from the response one: %v", expectedBody, respBody)
|
||||
{
|
||||
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>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
tpl: tmpl,
|
||||
req: "/images/img2.htm",
|
||||
respCode: http.StatusOK,
|
||||
res: `<!DOCTYPE html><html><head><title>img</title></head><body>{{.Include "header.html"}}</body></html>
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
tpl: tmplroot,
|
||||
req: "/root.html",
|
||||
respCode: http.StatusOK,
|
||||
res: `<!DOCTYPE html><html><head><title>root</title></head><body><h1>Header title</h1>
|
||||
</body></html>
|
||||
`,
|
||||
},
|
||||
|
||||
// test extension filter
|
||||
{
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
1
caddyhttp/templates/testdata/as_it_is.txt
vendored
Normal file
1
caddyhttp/templates/testdata/as_it_is.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
<!DOCTYPE html><html><head><title>as it is</title></head><body>{{.Include "header.html"}}</body></html>
|
3
dist/init/linux-systemd/README.md
vendored
3
dist/init/linux-systemd/README.md
vendored
@ -46,7 +46,7 @@ sudo useradd \
|
||||
sudo mkdir /etc/caddy
|
||||
sudo chown -R root:www-data /etc/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
|
||||
```
|
||||
|
||||
@ -91,6 +91,7 @@ Install the systemd service unit configuration file, reload the systemd daemon,
|
||||
and start caddy:
|
||||
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/mholt/caddy/master/dist/init/linux-systemd/caddy.service
|
||||
sudo cp caddy.service /etc/systemd/system/
|
||||
sudo chown root:root /etc/systemd/system/caddy.service
|
||||
sudo chmod 644 /etc/systemd/system/caddy.service
|
||||
|
6
dist/init/linux-systemd/caddy.service
vendored
6
dist/init/linux-systemd/caddy.service
vendored
@ -30,8 +30,8 @@ LimitNPROC=512
|
||||
|
||||
; Use private /tmp and /var/tmp, which are discarded after caddy stops.
|
||||
PrivateTmp=true
|
||||
; Use a minimal /dev
|
||||
PrivateDevices=true
|
||||
; 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=false
|
||||
; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
|
||||
ProtectHome=true
|
||||
; Make /usr, /boot, /etc and possibly some more folders read-only.
|
||||
@ -41,7 +41,7 @@ ProtectSystem=full
|
||||
ReadWriteDirectories=/etc/ssl/caddy
|
||||
|
||||
; 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.
|
||||
;CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
;AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
16
dist/init/linux-sysvinit/README.md
vendored
16
dist/init/linux-sysvinit/README.md
vendored
@ -9,3 +9,19 @@ Usage
|
||||
* 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`
|
||||
* 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`)
|
||||
|
18
dist/init/linux-sysvinit/caddy
vendored
18
dist/init/linux-sysvinit/caddy
vendored
@ -20,18 +20,30 @@ DAEMONUSER=www-data
|
||||
PIDFILE=/var/run/$NAME.pid
|
||||
LOGFILE=/var/log/$NAME.log
|
||||
CONFIGFILE=/etc/caddy/Caddyfile
|
||||
DAEMONOPTS="-agree=true -log=$LOGFILE -conf=$CONFIGFILE"
|
||||
|
||||
USERBIND="setcap cap_net_bind_service=+ep"
|
||||
STOP_SCHEDULE="${STOP_SCHEDULE:-QUIT/5/TERM/5/KILL/5}"
|
||||
CADDYPATH=/etc/ssl/caddy
|
||||
ULIMIT=8192
|
||||
|
||||
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.
|
||||
export CADDYPATH=/etc/ssl/caddy
|
||||
export CADDYPATH
|
||||
|
||||
# Set the ulimits
|
||||
ulimit -n 8192
|
||||
ulimit -n ${ULIMIT}
|
||||
|
||||
|
||||
start() {
|
||||
|
30
plugins.go
30
plugins.go
@ -19,6 +19,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
)
|
||||
@ -38,7 +39,7 @@ var (
|
||||
|
||||
// eventHooks is a map of hook name to Hook. All hooks plugins
|
||||
// must have a name.
|
||||
eventHooks = make(map[string]EventHook)
|
||||
eventHooks = sync.Map{}
|
||||
|
||||
// parsingCallbacks maps server type to map of directive
|
||||
// to list of callback functions. These aren't really
|
||||
@ -67,12 +68,15 @@ func DescribePlugins() string {
|
||||
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"
|
||||
for hookPlugin := range eventHooks {
|
||||
str += " hook." + hookPlugin + "\n"
|
||||
}
|
||||
str += hooks
|
||||
}
|
||||
|
||||
// Let's alphabetize the rest of these...
|
||||
@ -248,23 +252,23 @@ func RegisterEventHook(name string, hook EventHook) {
|
||||
if 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")
|
||||
}
|
||||
eventHooks[name] = hook
|
||||
}
|
||||
|
||||
// EmitEvent executes the different hooks passing the EventType as an
|
||||
// argument. This is a blocking function. Hook developers should
|
||||
// use 'go' keyword if they don't want to block Caddy.
|
||||
func EmitEvent(event EventName, info interface{}) {
|
||||
for name, hook := range eventHooks {
|
||||
err := hook(event, info)
|
||||
|
||||
eventHooks.Range(func(k, v interface{}) bool {
|
||||
err := v.(EventHook)(event, info)
|
||||
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
|
||||
|
@ -31,19 +31,19 @@ func trapSignalsPosix() {
|
||||
|
||||
for sig := range sigchan {
|
||||
switch sig {
|
||||
case syscall.SIGTERM:
|
||||
log.Println("[INFO] SIGTERM: Terminating process")
|
||||
case syscall.SIGQUIT:
|
||||
log.Println("[INFO] SIGQUIT: Quitting process immediately")
|
||||
for _, f := range OnProcessExit {
|
||||
f() // only perform important cleanup actions
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
case syscall.SIGQUIT:
|
||||
log.Println("[INFO] SIGQUIT: Shutting down")
|
||||
exitCode := executeShutdownCallbacks("SIGQUIT")
|
||||
case syscall.SIGTERM:
|
||||
log.Println("[INFO] SIGTERM: Shutting down servers then terminating")
|
||||
exitCode := executeShutdownCallbacks("SIGTERM")
|
||||
err := Stop()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] SIGQUIT stop: %v", err)
|
||||
log.Printf("[ERROR] SIGTERM stop: %v", err)
|
||||
exitCode = 3
|
||||
}
|
||||
for _, f := range OnProcessExit {
|
||||
@ -51,13 +51,6 @@ func trapSignalsPosix() {
|
||||
}
|
||||
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:
|
||||
log.Println("[INFO] SIGUSR1: Reloading")
|
||||
|
||||
@ -94,6 +87,9 @@ func trapSignalsPosix() {
|
||||
if err := Upgrade(); err != nil {
|
||||
log.Printf("[ERROR] SIGUSR2: upgrading: %v", err)
|
||||
}
|
||||
|
||||
case syscall.SIGHUP:
|
||||
// ignore; this signal is sometimes sent outside of the user's control
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
Loading…
x
Reference in New Issue
Block a user