diff --git a/.gitignore b/.gitignore index 4f3845ed4..425a29cf3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ Caddyfile og_static/ -.vscode/ \ No newline at end of file +.vscode/ + +*.bat \ No newline at end of file diff --git a/README.md b/README.md index 2d42d2e6c..bdbffd006 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Caddy + Caddy

Every Site on HTTPS

Caddy is a general-purpose HTTP/2 web server that serves HTTPS by default.

diff --git a/caddy/caddymain/run.go b/caddy/caddymain/run.go index 81f97f2a8..e6faa0513 100644 --- a/caddy/caddymain/run.go +++ b/caddy/caddymain/run.go @@ -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) diff --git a/caddy/caddymain/run_test.go b/caddy/caddymain/run_test.go index 141efe208..c26a54a9e 100644 --- a/caddy/caddymain/run_test.go +++ b/caddy/caddymain/run_test.go @@ -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 { diff --git a/caddyhttp/browse/setup.go b/caddyhttp/browse/setup.go index 6979308ba..a7cef6aa1 100644 --- a/caddyhttp/browse/setup.go +++ b/caddyhttp/browse/setup.go @@ -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); diff --git a/caddyhttp/fastcgi/fastcgi.go b/caddyhttp/fastcgi/fastcgi.go index ee466a3e8..28ea55f9f 100644 --- a/caddyhttp/fastcgi/fastcgi.go +++ b/caddyhttp/fastcgi/fastcgi.go @@ -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: diff --git a/caddyhttp/fastcgi/fcgiclient.go b/caddyhttp/fastcgi/fcgiclient.go index adf37d09a..b5fd1d9ea 100644 --- a/caddyhttp/fastcgi/fcgiclient.go +++ b/caddyhttp/fastcgi/fcgiclient.go @@ -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. diff --git a/caddyhttp/fastcgi/fcgiclient_test.go b/caddyhttp/fastcgi/fcgiclient_test.go index ef4981d48..9c5237f20 100644 --- a/caddyhttp/fastcgi/fcgiclient_test.go +++ b/caddyhttp/fastcgi/fcgiclient_test.go @@ -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: diff --git a/caddyhttp/httpserver/recorder.go b/caddyhttp/httpserver/recorder.go index b0f396218..f903f924d 100644 --- a/caddyhttp/httpserver/recorder.go +++ b/caddyhttp/httpserver/recorder.go @@ -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 { diff --git a/caddyhttp/proxy/reverseproxy.go b/caddyhttp/proxy/reverseproxy.go index 2fac5aabc..d48894ff1 100644 --- a/caddyhttp/proxy/reverseproxy.go +++ b/caddyhttp/proxy/reverseproxy.go @@ -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") { diff --git a/caddyhttp/requestid/requestid.go b/caddyhttp/requestid/requestid.go index c3f69267f..b03c449f6 100644 --- a/caddyhttp/requestid/requestid.go +++ b/caddyhttp/requestid/requestid.go @@ -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) diff --git a/caddyhttp/requestid/requestid_test.go b/caddyhttp/requestid/requestid_test.go index 80968221f..e68c8d2c0 100644 --- a/caddyhttp/requestid/requestid_test.go +++ b/caddyhttp/requestid/requestid_test.go @@ -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) } diff --git a/caddyhttp/requestid/setup.go b/caddyhttp/requestid/setup.go index 4da5a3683..689f99e33 100644 --- a/caddyhttp/requestid/setup.go +++ b/caddyhttp/requestid/setup.go @@ -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 diff --git a/caddyhttp/requestid/setup_test.go b/caddyhttp/requestid/setup_test.go index aea123694..9c420787b 100644 --- a/caddyhttp/requestid/setup_test.go +++ b/caddyhttp/requestid/setup_test.go @@ -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) diff --git a/caddyhttp/staticfiles/fileserver.go b/caddyhttp/staticfiles/fileserver.go index 2b38212ea..91fb1a7f5 100644 --- a/caddyhttp/staticfiles/fileserver.go +++ b/caddyhttp/staticfiles/fileserver.go @@ -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 } diff --git a/caddyhttp/staticfiles/fileserver_test.go b/caddyhttp/staticfiles/fileserver_test.go index 9cce77057..80d8f1a40 100644 --- a/caddyhttp/staticfiles/fileserver_test.go +++ b/caddyhttp/staticfiles/fileserver_test.go @@ -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: "

file1.html

", webrootNotIndexHTML: "

notindex.html

", webrootDirFile2HTML: "

dir/file2.html

", - webrootDirwithindexIndeHTML: "

dirwithindex/index.html

", + webrootDirwithindexIndexHTML: "

dirwithindex/index.html

", webrootDirHiddenHTML: "

dir/hidden.html

", webrootSubGzippedHTML: "

gzipped.html

", webrootSubGzippedHTMLGz: "1.gzipped.html.gz", diff --git a/caddyhttp/templates/templates_test.go b/caddyhttp/templates/templates_test.go index 839c39f76..289f1a85d 100644 --- a/caddyhttp/templates/templates_test.go +++ b/caddyhttp/templates/templates_test.go @@ -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 := `test page

Header title

- -` - - 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 = `img

Header title

- -` - - 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 = `img{{.Include "header.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 = `root

Header title

+ for _, c := range []struct { + tpl Templates + req string + respCode int + res string + }{ + { + tpl: tmpl, + req: "/photos/test.html", + respCode: http.StatusOK, + res: `test page

Header title

-` +`, + }, - 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: `img

Header title

+ +`, + }, + + { + tpl: tmpl, + req: "/images/img2.htm", + respCode: http.StatusOK, + res: `img{{.Include "header.html"}} +`, + }, + + { + tpl: tmplroot, + req: "/root.html", + respCode: http.StatusOK, + res: `root

Header title

+ +`, + }, + + // test extension filter + { + tpl: tmplroot, + req: "/as_it_is.txt", + respCode: http.StatusOK, + res: `as it is{{.Include "header.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) + } + }) } } diff --git a/caddyhttp/templates/testdata/as_it_is.txt b/caddyhttp/templates/testdata/as_it_is.txt new file mode 100644 index 000000000..487ee7273 --- /dev/null +++ b/caddyhttp/templates/testdata/as_it_is.txt @@ -0,0 +1 @@ +as it is{{.Include "header.html"}} diff --git a/dist/init/linux-systemd/README.md b/dist/init/linux-systemd/README.md index 70d269a7c..be548ae4d 100644 --- a/dist/init/linux-systemd/README.md +++ b/dist/init/linux-systemd/README.md @@ -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 diff --git a/dist/init/linux-systemd/caddy.service b/dist/init/linux-systemd/caddy.service index e0ab5643e..61b70b1f3 100644 --- a/dist/init/linux-systemd/caddy.service +++ b/dist/init/linux-systemd/caddy.service @@ -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 diff --git a/dist/init/linux-sysvinit/README.md b/dist/init/linux-sysvinit/README.md index f5ce4de09..c2a8fced2 100644 --- a/dist/init/linux-sysvinit/README.md +++ b/dist/init/linux-sysvinit/README.md @@ -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`) diff --git a/dist/init/linux-sysvinit/caddy b/dist/init/linux-sysvinit/caddy index 88fc96398..b52a8345b 100644 --- a/dist/init/linux-sysvinit/caddy +++ b/dist/init/linux-sysvinit/caddy @@ -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() { diff --git a/plugins.go b/plugins.go index ba1114034..75f10e47f 100644 --- a/plugins.go +++ b/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 diff --git a/sigtrap_posix.go b/sigtrap_posix.go index 38aaa774c..2a0a0de57 100644 --- a/sigtrap_posix.go +++ b/sigtrap_posix.go @@ -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 } } }()