mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
Merge branch 'master' into acmev2
# Conflicts: # caddyhttp/httpserver/replacer.go # caddyhttp/httpserver/replacer_test.go
This commit is contained in:
commit
2ed1dd6afc
9 changed files with 258 additions and 46 deletions
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"}`)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
32
plugins.go
32
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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue