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 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: "