diff --git a/caddyhttp/fastcgi/fastcgi.go b/caddyhttp/fastcgi/fastcgi.go index 28ea55f9..54eb4e36 100644 --- a/caddyhttp/fastcgi/fastcgi.go +++ b/caddyhttp/fastcgi/fastcgi.go @@ -33,8 +33,11 @@ import ( "sync/atomic" "time" + "crypto/tls" + "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/httpserver" + "github.com/mholt/caddy/caddytls" ) // Handler is a middleware type that can handle requests as a FastCGI client. @@ -323,6 +326,19 @@ func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string] // Some web apps rely on knowing HTTPS or not if r.TLS != nil { env["HTTPS"] = "on" + // and pass the protocol details in a manner compatible with apache's mod_ssl + // (which is why they have a SSL_ prefix and not TLS_). + v, ok := tlsProtocolStringToMap[r.TLS.Version] + if ok { + env["SSL_PROTOCOL"] = v + } + // and pass the cipher suite in a manner compatible with apache's mod_ssl + for k, v := range caddytls.SupportedCiphersMap { + if v == r.TLS.CipherSuite { + env["SSL_CIPHER"] = k + break + } + } } // Add env variables from config (with support for placeholders in values) @@ -465,3 +481,11 @@ type LogError string func (l LogError) Error() string { return string(l) } + +// Map of supported protocols to Apache ssl_mod format +// Note that these are slightly different from SupportedProtocols in caddytls/config.go's +var tlsProtocolStringToMap = map[uint16]string{ + tls.VersionTLS10: "TLSv1", + tls.VersionTLS11: "TLSv1.1", + tls.VersionTLS12: "TLSv1.2", +} diff --git a/caddyhttp/httpserver/replacer.go b/caddyhttp/httpserver/replacer.go index d6b658f2..05fd1895 100644 --- a/caddyhttp/httpserver/replacer.go +++ b/caddyhttp/httpserver/replacer.go @@ -29,6 +29,7 @@ import ( "time" "github.com/mholt/caddy" + "github.com/mholt/caddy/caddytls" ) // requestReplacer is a strings.Replacer which is used to @@ -140,6 +141,14 @@ func canLogRequest(r *http.Request) bool { return false } +// unescapeBraces finds escaped braces in s and returns +// a string with those braces unescaped. +func unescapeBraces(s string) string { + s = strings.Replace(s, "\\{", "{", -1) + s = strings.Replace(s, "\\}", "}", -1) + return s +} + // Replace performs a replacement of values on s and returns // the string with the replaced values. func (r *replacer) Replace(s string) string { @@ -149,32 +158,59 @@ func (r *replacer) Replace(s string) string { } result := "" +Placeholders: // process each placeholder in sequence for { - idxStart := strings.Index(s, "{") - if idxStart == -1 { - // no placeholder anymore - break - } - idxEnd := strings.Index(s[idxStart:], "}") - if idxEnd == -1 { - // unpaired placeholder - break - } - idxEnd += idxStart + var idxStart, idxEnd int - // get a replacement - placeholder := s[idxStart : idxEnd+1] + idxOffset := 0 + for { // find first unescaped opening brace + searchSpace := s[idxOffset:] + idxStart = strings.Index(searchSpace, "{") + if idxStart == -1 { + // no more placeholders + break Placeholders + } + if idxStart == 0 || searchSpace[idxStart-1] != '\\' { + // preceding character is not an escape + idxStart += idxOffset + break + } + // the brace we found was escaped + // search the rest of the string next + idxOffset += idxStart + 1 + } + + idxOffset = 0 + for { // find first unescaped closing brace + searchSpace := s[idxStart+idxOffset:] + idxEnd = strings.Index(searchSpace, "}") + if idxEnd == -1 { + // unpaired placeholder + break Placeholders + } + if idxEnd == 0 || searchSpace[idxEnd-1] != '\\' { + // preceding character is not an escape + idxEnd += idxOffset + idxStart + break + } + // the brace we found was escaped + // search the rest of the string next + idxOffset += idxEnd + 1 + } + + // get a replacement for the unescaped placeholder + placeholder := unescapeBraces(s[idxStart : idxEnd+1]) replacement := r.getSubstitution(placeholder) - // append prefix + replacement - result += s[:idxStart] + replacement + // append unescaped prefix + replacement + result += strings.TrimPrefix(unescapeBraces(s[:idxStart]), "\\") + replacement // strip out scanned parts s = s[idxEnd+1:] } // append unscanned parts - return result + s + return result + unescapeBraces(s) } func roundDuration(d time.Duration) time.Duration { @@ -375,6 +411,26 @@ func (r *replacer) getSubstitution(key string) string { } elapsedDuration := time.Since(r.responseRecorder.start) return strconv.FormatInt(convertToMilliseconds(elapsedDuration), 10) + case "{tls_protocol}": + if r.request.TLS != nil { + for k, v := range caddytls.SupportedProtocols { + if v == r.request.TLS.Version { + return k + } + } + return "tls" // this should never happen, but guard in case + } + return r.emptyValue // because not using a secure channel + case "{tls_cipher}": + if r.request.TLS != nil { + for k, v := range caddytls.SupportedCiphersMap { + if v == r.request.TLS.CipherSuite { + return k + } + } + return "UNKNOWN" // this should never happen, but guard in case + } + return r.emptyValue default: // {labelN} if strings.HasPrefix(key, "{label") { @@ -394,7 +450,7 @@ func (r *replacer) getSubstitution(key string) string { return r.emptyValue } -//convertToMilliseconds returns the number of milliseconds in the given duration +// convertToMilliseconds returns the number of milliseconds in the given duration func convertToMilliseconds(d time.Duration) int64 { return d.Nanoseconds() / 1e6 } diff --git a/caddyhttp/httpserver/replacer_test.go b/caddyhttp/httpserver/replacer_test.go index 54a935d0..fe1e67c6 100644 --- a/caddyhttp/httpserver/replacer_test.go +++ b/caddyhttp/httpserver/replacer_test.go @@ -114,6 +114,7 @@ func TestReplace(t *testing.T) { {"Missing query string argument is {?missing}", "Missing query string argument is "}, {"{label1} {label2} {label3} {label4}", "localhost local - -"}, {"Label with missing number is {label} or {labelQQ}", "Label with missing number is - or -"}, + {"\\{ 'hostname': '{hostname}' \\}", "{ 'hostname': '" + hostname + "' }"}, } for _, c := range testCases { @@ -146,6 +147,70 @@ func TestReplace(t *testing.T) { } } +func BenchmarkReplace(b *testing.B) { + w := httptest.NewRecorder() + recordRequest := NewResponseRecorder(w) + reader := strings.NewReader(`{"username": "dennis"}`) + + request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader) + if err != nil { + b.Fatalf("Failed to make request: %v", err) + } + ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL) + request = request.WithContext(ctx) + + request.Header.Set("Custom", "foobarbaz") + request.Header.Set("ShorterVal", "1") + repl := NewReplacer(request, recordRequest, "-") + // add some headers after creating replacer + request.Header.Set("CustomAdd", "caddy") + request.Header.Set("Cookie", "foo=bar; taste=delicious") + + // add some respons headers + recordRequest.Header().Set("Custom", "CustomResponseHeader") + + now = func() time.Time { + return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + repl.Replace("This hostname is {hostname}") + } +} + +func BenchmarkReplaceEscaped(b *testing.B) { + w := httptest.NewRecorder() + recordRequest := NewResponseRecorder(w) + reader := strings.NewReader(`{"username": "dennis"}`) + + request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader) + if err != nil { + b.Fatalf("Failed to make request: %v", err) + } + ctx := context.WithValue(request.Context(), OriginalURLCtxKey, *request.URL) + request = request.WithContext(ctx) + + request.Header.Set("Custom", "foobarbaz") + request.Header.Set("ShorterVal", "1") + repl := NewReplacer(request, recordRequest, "-") + // add some headers after creating replacer + request.Header.Set("CustomAdd", "caddy") + request.Header.Set("Cookie", "foo=bar; taste=delicious") + + // add some respons headers + recordRequest.Header().Set("Custom", "CustomResponseHeader") + + now = func() time.Time { + return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + repl.Replace("\\{ 'hostname': '{hostname}' \\}") + } +} + func TestResponseRecorderNil(t *testing.T) { reader := strings.NewReader(`{"username": "dennis"}`) diff --git a/caddyhttp/httpserver/server.go b/caddyhttp/httpserver/server.go index ab65d955..a98a8fdf 100644 --- a/caddyhttp/httpserver/server.go +++ b/caddyhttp/httpserver/server.go @@ -422,13 +422,21 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) (int, error) func trimPathPrefix(u *url.URL, prefix string) *url.URL { // We need to use URL.EscapedPath() when trimming the pathPrefix as // URL.Path is ambiguous about / or %2f - see docs. See #1927 - trimmed := strings.TrimPrefix(u.EscapedPath(), prefix) - if !strings.HasPrefix(trimmed, "/") { - trimmed = "/" + trimmed + trimmedPath := strings.TrimPrefix(u.EscapedPath(), prefix) + if !strings.HasPrefix(trimmedPath, "/") { + trimmedPath = "/" + trimmedPath } - trimmedURL, err := url.Parse(trimmed) + // After trimming path reconstruct uri string with Query before parsing + trimmedURI := trimmedPath + if u.RawQuery != "" || u.ForceQuery == true { + trimmedURI = trimmedPath + "?" + u.RawQuery + } + if u.Fragment != "" { + trimmedURI = trimmedURI + "#" + u.Fragment + } + trimmedURL, err := url.Parse(trimmedURI) if err != nil { - log.Printf("[ERROR] Unable to parse trimmed URL %s: %v", trimmed, err) + log.Printf("[ERROR] Unable to parse trimmed URL %s: %v", trimmedURI, err) return u } return trimmedURL diff --git a/caddyhttp/httpserver/server_test.go b/caddyhttp/httpserver/server_test.go index 82926851..afd29a98 100644 --- a/caddyhttp/httpserver/server_test.go +++ b/caddyhttp/httpserver/server_test.go @@ -129,88 +129,108 @@ func TestMakeHTTPServerWithTimeouts(t *testing.T) { func TestTrimPathPrefix(t *testing.T) { for i, pt := range []struct { - path string + url string prefix string expected string shouldFail bool }{ { - path: "/my/path", + url: "/my/path", prefix: "/my", expected: "/path", shouldFail: false, }, { - path: "/my/%2f/path", + url: "/my/%2f/path", prefix: "/my", expected: "/%2f/path", shouldFail: false, }, { - path: "/my/path", + url: "/my/path", prefix: "/my/", expected: "/path", shouldFail: false, }, { - path: "/my///path", + url: "/my///path", prefix: "/my", expected: "/path", shouldFail: true, }, { - path: "/my///path", + url: "/my///path", prefix: "/my", expected: "///path", shouldFail: false, }, { - path: "/my/path///slash", + url: "/my/path///slash", prefix: "/my", expected: "/path///slash", shouldFail: false, }, { - path: "/my/%2f/path/%2f", + url: "/my/%2f/path/%2f", prefix: "/my", expected: "/%2f/path/%2f", shouldFail: false, }, { - path: "/my/%20/path", + url: "/my/%20/path", prefix: "/my", expected: "/%20/path", shouldFail: false, }, { - path: "/path", + url: "/path", prefix: "", expected: "/path", shouldFail: false, }, { - path: "/path/my/", + url: "/path/my/", prefix: "/my", expected: "/path/my/", shouldFail: false, }, { - path: "", + url: "", prefix: "/my", expected: "/", shouldFail: false, }, { - path: "/apath", + url: "/apath", prefix: "", expected: "/apath", shouldFail: false, + }, { + url: "/my/path/page.php?akey=value", + prefix: "/my", + expected: "/path/page.php?akey=value", + shouldFail: false, + }, { + url: "/my/path/page?key=value#fragment", + prefix: "/my", + expected: "/path/page?key=value#fragment", + shouldFail: false, + }, { + url: "/my/path/page#fragment", + prefix: "/my", + expected: "/path/page#fragment", + shouldFail: false, + }, { + url: "/my/apath?", + prefix: "/my", + expected: "/apath?", + shouldFail: false, }, } { - u, _ := url.Parse(pt.path) - if got, want := trimPathPrefix(u, pt.prefix), pt.expected; got.EscapedPath() != want { + u, _ := url.Parse(pt.url) + if got, want := trimPathPrefix(u, pt.prefix), pt.expected; got.String() != want { if !pt.shouldFail { - t.Errorf("Test %d: Expected='%s', but was '%s' ", i, want, got.EscapedPath()) + t.Errorf("Test %d: Expected='%s', but was '%s' ", i, want, got.String()) } } else if pt.shouldFail { - t.Errorf("SHOULDFAIL Test %d: Expected='%s', and was '%s' but should fail", i, want, got.EscapedPath()) + t.Errorf("SHOULDFAIL Test %d: Expected='%s', and was '%s' but should fail", i, want, got.String()) } } } diff --git a/caddytls/config.go b/caddytls/config.go index cdcd6041..808ed527 100644 --- a/caddytls/config.go +++ b/caddytls/config.go @@ -569,7 +569,8 @@ var supportedKeyTypes = map[string]acme.KeyType{ // Map of supported protocols. // HTTP/2 only supports TLS 1.2 and higher. -var supportedProtocols = map[string]uint16{ +// If updating this map, also update tlsProtocolStringToMap in caddyhttp/fastcgi/fastcgi.go +var SupportedProtocols = map[string]uint16{ "tls1.0": tls.VersionTLS10, "tls1.1": tls.VersionTLS11, "tls1.2": tls.VersionTLS12, @@ -585,7 +586,7 @@ var supportedProtocols = map[string]uint16{ // it is always added (even though it is not technically a cipher suite). // // This map, like any map, is NOT ORDERED. Do not range over this map. -var supportedCiphersMap = map[string]uint16{ +var SupportedCiphersMap = map[string]uint16{ "ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, diff --git a/caddytls/setup.go b/caddytls/setup.go index 857f198f..11a5e45d 100644 --- a/caddytls/setup.go +++ b/caddytls/setup.go @@ -106,19 +106,19 @@ func setupTLS(c *caddy.Controller) error { case "protocols": args := c.RemainingArgs() if len(args) == 1 { - value, ok := supportedProtocols[strings.ToLower(args[0])] + value, ok := SupportedProtocols[strings.ToLower(args[0])] if !ok { return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0]) } config.ProtocolMinVersion, config.ProtocolMaxVersion = value, value } else { - value, ok := supportedProtocols[strings.ToLower(args[0])] + value, ok := SupportedProtocols[strings.ToLower(args[0])] if !ok { return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0]) } config.ProtocolMinVersion = value - value, ok = supportedProtocols[strings.ToLower(args[1])] + value, ok = SupportedProtocols[strings.ToLower(args[1])] if !ok { return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[1]) } @@ -129,7 +129,7 @@ func setupTLS(c *caddy.Controller) error { } case "ciphers": for c.NextArg() { - value, ok := supportedCiphersMap[strings.ToUpper(c.Val())] + value, ok := SupportedCiphersMap[strings.ToUpper(c.Val())] if !ok { return c.Errf("Wrong cipher name or cipher not supported: '%s'", c.Val()) } diff --git a/plugins.go b/plugins.go index 75f10e47..b0011009 100644 --- a/plugins.go +++ b/plugins.go @@ -39,7 +39,7 @@ var ( // eventHooks is a map of hook name to Hook. All hooks plugins // must have a name. - eventHooks = sync.Map{} + eventHooks = &sync.Map{} // parsingCallbacks maps server type to map of directive // to list of callback functions. These aren't really @@ -271,6 +271,36 @@ func EmitEvent(event EventName, info interface{}) { }) } +// cloneEventHooks return a clone of the event hooks *sync.Map +func cloneEventHooks() *sync.Map { + c := &sync.Map{} + eventHooks.Range(func(k, v interface{}) bool { + c.Store(k, v) + return true + }) + return c +} + +// purgeEventHooks purges all event hooks from the map +func purgeEventHooks() { + eventHooks.Range(func(k, _ interface{}) bool { + eventHooks.Delete(k) + return true + }) +} + +// restoreEventHooks restores eventHooks with a provided *sync.Map +func restoreEventHooks(m *sync.Map) { + // Purge old event hooks + purgeEventHooks() + + // Restore event hooks + m.Range(func(k, v interface{}) bool { + eventHooks.Store(k, v) + return true + }) +} + // ParsingCallback is a function that is called after // a directive's setup functions have been executed // for all the server blocks. diff --git a/sigtrap_posix.go b/sigtrap_posix.go index cc65ccb4..a14f9d35 100644 --- a/sigtrap_posix.go +++ b/sigtrap_posix.go @@ -76,9 +76,17 @@ func trapSignalsPosix() { caddyfileToUse = newCaddyfile } + // Backup old event hooks + oldEventHooks := cloneEventHooks() + + // Purge the old event hooks + purgeEventHooks() + // Kick off the restart; our work is done _, err = inst.Restart(caddyfileToUse) if err != nil { + restoreEventHooks(oldEventHooks) + log.Printf("[ERROR] SIGUSR1: %v", err) }