Commit 2ed1dd6a authored by Matthew Holt's avatar Matthew Holt

Merge branch 'master' into acmev2

# Conflicts:
#	caddyhttp/httpserver/replacer.go
#	caddyhttp/httpserver/replacer_test.go
parents ef40659c f1eaae9b
...@@ -33,8 +33,11 @@ import ( ...@@ -33,8 +33,11 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"crypto/tls"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddyhttp/httpserver" "github.com/mholt/caddy/caddyhttp/httpserver"
"github.com/mholt/caddy/caddytls"
) )
// Handler is a middleware type that can handle requests as a FastCGI client. // 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] ...@@ -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 // Some web apps rely on knowing HTTPS or not
if r.TLS != nil { if r.TLS != nil {
env["HTTPS"] = "on" 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) // Add env variables from config (with support for placeholders in values)
...@@ -465,3 +481,11 @@ type LogError string ...@@ -465,3 +481,11 @@ type LogError string
func (l LogError) Error() string { func (l LogError) Error() string {
return string(l) 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",
}
...@@ -29,6 +29,7 @@ import ( ...@@ -29,6 +29,7 @@ import (
"time" "time"
"github.com/mholt/caddy" "github.com/mholt/caddy"
"github.com/mholt/caddy/caddytls"
) )
// requestReplacer is a strings.Replacer which is used to // requestReplacer is a strings.Replacer which is used to
...@@ -140,6 +141,14 @@ func canLogRequest(r *http.Request) bool { ...@@ -140,6 +141,14 @@ func canLogRequest(r *http.Request) bool {
return false 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 // Replace performs a replacement of values on s and returns
// the string with the replaced values. // the string with the replaced values.
func (r *replacer) Replace(s string) string { func (r *replacer) Replace(s string) string {
...@@ -149,32 +158,59 @@ func (r *replacer) Replace(s string) string { ...@@ -149,32 +158,59 @@ func (r *replacer) Replace(s string) string {
} }
result := "" result := ""
Placeholders: // process each placeholder in sequence
for { for {
idxStart := strings.Index(s, "{") var idxStart, idxEnd int
if idxStart == -1 {
// no placeholder anymore idxOffset := 0
break 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
} }
idxEnd := strings.Index(s[idxStart:], "}")
if idxEnd == -1 { idxOffset = 0
// unpaired placeholder for { // find first unescaped closing brace
break 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
} }
idxEnd += idxStart
// get a replacement // get a replacement for the unescaped placeholder
placeholder := s[idxStart : idxEnd+1] placeholder := unescapeBraces(s[idxStart : idxEnd+1])
replacement := r.getSubstitution(placeholder) replacement := r.getSubstitution(placeholder)
// append prefix + replacement // append unescaped prefix + replacement
result += s[:idxStart] + replacement result += strings.TrimPrefix(unescapeBraces(s[:idxStart]), "\\") + replacement
// strip out scanned parts // strip out scanned parts
s = s[idxEnd+1:] s = s[idxEnd+1:]
} }
// append unscanned parts // append unscanned parts
return result + s return result + unescapeBraces(s)
} }
func roundDuration(d time.Duration) time.Duration { func roundDuration(d time.Duration) time.Duration {
...@@ -375,6 +411,26 @@ func (r *replacer) getSubstitution(key string) string { ...@@ -375,6 +411,26 @@ func (r *replacer) getSubstitution(key string) string {
} }
elapsedDuration := time.Since(r.responseRecorder.start) elapsedDuration := time.Since(r.responseRecorder.start)
return strconv.FormatInt(convertToMilliseconds(elapsedDuration), 10) 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: default:
// {labelN} // {labelN}
if strings.HasPrefix(key, "{label") { if strings.HasPrefix(key, "{label") {
...@@ -394,7 +450,7 @@ func (r *replacer) getSubstitution(key string) string { ...@@ -394,7 +450,7 @@ func (r *replacer) getSubstitution(key string) string {
return r.emptyValue 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 { func convertToMilliseconds(d time.Duration) int64 {
return d.Nanoseconds() / 1e6 return d.Nanoseconds() / 1e6
} }
......
...@@ -114,6 +114,7 @@ func TestReplace(t *testing.T) { ...@@ -114,6 +114,7 @@ func TestReplace(t *testing.T) {
{"Missing query string argument is {?missing}", "Missing query string argument is "}, {"Missing query string argument is {?missing}", "Missing query string argument is "},
{"{label1} {label2} {label3} {label4}", "localhost local - -"}, {"{label1} {label2} {label3} {label4}", "localhost local - -"},
{"Label with missing number is {label} or {labelQQ}", "Label with missing number is - or -"}, {"Label with missing number is {label} or {labelQQ}", "Label with missing number is - or -"},
{"\\{ 'hostname': '{hostname}' \\}", "{ 'hostname': '" + hostname + "' }"},
} }
for _, c := range testCases { for _, c := range testCases {
...@@ -146,6 +147,70 @@ func TestReplace(t *testing.T) { ...@@ -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) { func TestResponseRecorderNil(t *testing.T) {
reader := strings.NewReader(`{"username": "dennis"}`) reader := strings.NewReader(`{"username": "dennis"}`)
......
...@@ -422,13 +422,21 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) (int, error) ...@@ -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 { func trimPathPrefix(u *url.URL, prefix string) *url.URL {
// We need to use URL.EscapedPath() when trimming the pathPrefix as // We need to use URL.EscapedPath() when trimming the pathPrefix as
// URL.Path is ambiguous about / or %2f - see docs. See #1927 // URL.Path is ambiguous about / or %2f - see docs. See #1927
trimmed := strings.TrimPrefix(u.EscapedPath(), prefix) trimmedPath := strings.TrimPrefix(u.EscapedPath(), prefix)
if !strings.HasPrefix(trimmed, "/") { if !strings.HasPrefix(trimmedPath, "/") {
trimmed = "/" + trimmed 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 { 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 u
} }
return trimmedURL return trimmedURL
......
...@@ -129,88 +129,108 @@ func TestMakeHTTPServerWithTimeouts(t *testing.T) { ...@@ -129,88 +129,108 @@ func TestMakeHTTPServerWithTimeouts(t *testing.T) {
func TestTrimPathPrefix(t *testing.T) { func TestTrimPathPrefix(t *testing.T) {
for i, pt := range []struct { for i, pt := range []struct {
path string url string
prefix string prefix string
expected string expected string
shouldFail bool shouldFail bool
}{ }{
{ {
path: "/my/path", url: "/my/path",
prefix: "/my", prefix: "/my",
expected: "/path", expected: "/path",
shouldFail: false, shouldFail: false,
}, },
{ {
path: "/my/%2f/path", url: "/my/%2f/path",
prefix: "/my", prefix: "/my",
expected: "/%2f/path", expected: "/%2f/path",
shouldFail: false, shouldFail: false,
}, },
{ {
path: "/my/path", url: "/my/path",
prefix: "/my/", prefix: "/my/",
expected: "/path", expected: "/path",
shouldFail: false, shouldFail: false,
}, },
{ {
path: "/my///path", url: "/my///path",
prefix: "/my", prefix: "/my",
expected: "/path", expected: "/path",
shouldFail: true, shouldFail: true,
}, },
{ {
path: "/my///path", url: "/my///path",
prefix: "/my", prefix: "/my",
expected: "///path", expected: "///path",
shouldFail: false, shouldFail: false,
}, },
{ {
path: "/my/path///slash", url: "/my/path///slash",
prefix: "/my", prefix: "/my",
expected: "/path///slash", expected: "/path///slash",
shouldFail: false, shouldFail: false,
}, },
{ {
path: "/my/%2f/path/%2f", url: "/my/%2f/path/%2f",
prefix: "/my", prefix: "/my",
expected: "/%2f/path/%2f", expected: "/%2f/path/%2f",
shouldFail: false, shouldFail: false,
}, { }, {
path: "/my/%20/path", url: "/my/%20/path",
prefix: "/my", prefix: "/my",
expected: "/%20/path", expected: "/%20/path",
shouldFail: false, shouldFail: false,
}, { }, {
path: "/path", url: "/path",
prefix: "", prefix: "",
expected: "/path", expected: "/path",
shouldFail: false, shouldFail: false,
}, { }, {
path: "/path/my/", url: "/path/my/",
prefix: "/my", prefix: "/my",
expected: "/path/my/", expected: "/path/my/",
shouldFail: false, shouldFail: false,
}, { }, {
path: "", url: "",
prefix: "/my", prefix: "/my",
expected: "/", expected: "/",
shouldFail: false, shouldFail: false,
}, { }, {
path: "/apath", url: "/apath",
prefix: "", prefix: "",
expected: "/apath", expected: "/apath",
shouldFail: false, 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) u, _ := url.Parse(pt.url)
if got, want := trimPathPrefix(u, pt.prefix), pt.expected; got.EscapedPath() != want { if got, want := trimPathPrefix(u, pt.prefix), pt.expected; got.String() != want {
if !pt.shouldFail { 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 { } 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())
} }
} }
} }
......
...@@ -569,7 +569,8 @@ var supportedKeyTypes = map[string]acme.KeyType{ ...@@ -569,7 +569,8 @@ var supportedKeyTypes = map[string]acme.KeyType{
// Map of supported protocols. // Map of supported protocols.
// HTTP/2 only supports TLS 1.2 and higher. // 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.0": tls.VersionTLS10,
"tls1.1": tls.VersionTLS11, "tls1.1": tls.VersionTLS11,
"tls1.2": tls.VersionTLS12, "tls1.2": tls.VersionTLS12,
...@@ -585,7 +586,7 @@ var supportedProtocols = map[string]uint16{ ...@@ -585,7 +586,7 @@ var supportedProtocols = map[string]uint16{
// it is always added (even though it is not technically a cipher suite). // 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. // 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-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-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, "ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
......
...@@ -106,19 +106,19 @@ func setupTLS(c *caddy.Controller) error { ...@@ -106,19 +106,19 @@ func setupTLS(c *caddy.Controller) error {
case "protocols": case "protocols":
args := c.RemainingArgs() args := c.RemainingArgs()
if len(args) == 1 { if len(args) == 1 {
value, ok := supportedProtocols[strings.ToLower(args[0])] value, ok := SupportedProtocols[strings.ToLower(args[0])]
if !ok { if !ok {
return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0]) return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
} }
config.ProtocolMinVersion, config.ProtocolMaxVersion = value, value config.ProtocolMinVersion, config.ProtocolMaxVersion = value, value
} else { } else {
value, ok := supportedProtocols[strings.ToLower(args[0])] value, ok := SupportedProtocols[strings.ToLower(args[0])]
if !ok { if !ok {
return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0]) return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
} }
config.ProtocolMinVersion = value config.ProtocolMinVersion = value
value, ok = supportedProtocols[strings.ToLower(args[1])] value, ok = SupportedProtocols[strings.ToLower(args[1])]
if !ok { if !ok {
return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[1]) return c.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
} }
...@@ -129,7 +129,7 @@ func setupTLS(c *caddy.Controller) error { ...@@ -129,7 +129,7 @@ func setupTLS(c *caddy.Controller) error {
} }
case "ciphers": case "ciphers":
for c.NextArg() { for c.NextArg() {
value, ok := supportedCiphersMap[strings.ToUpper(c.Val())] value, ok := SupportedCiphersMap[strings.ToUpper(c.Val())]
if !ok { if !ok {
return c.Errf("Wrong cipher name or cipher not supported: '%s'", c.Val()) return c.Errf("Wrong cipher name or cipher not supported: '%s'", c.Val())
} }
......
...@@ -39,7 +39,7 @@ var ( ...@@ -39,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 = sync.Map{} 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
...@@ -271,6 +271,36 @@ func EmitEvent(event EventName, info interface{}) { ...@@ -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 // ParsingCallback is a function that is called after
// a directive's setup functions have been executed // a directive's setup functions have been executed
// for all the server blocks. // for all the server blocks.
......
...@@ -76,9 +76,17 @@ func trapSignalsPosix() { ...@@ -76,9 +76,17 @@ func trapSignalsPosix() {
caddyfileToUse = newCaddyfile caddyfileToUse = newCaddyfile
} }
// Backup old event hooks
oldEventHooks := cloneEventHooks()
// Purge the old event hooks
purgeEventHooks()
// Kick off the restart; our work is done // Kick off the restart; our work is done
_, err = inst.Restart(caddyfileToUse) _, err = inst.Restart(caddyfileToUse)
if err != nil { if err != nil {
restoreEventHooks(oldEventHooks)
log.Printf("[ERROR] SIGUSR1: %v", err) log.Printf("[ERROR] SIGUSR1: %v", err)
} }
......
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