mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-13 10:42:16 -04:00
Merge branch 'master' into telemetry
# Conflicts: # caddy/caddymain/run.go # caddyhttp/httpserver/plugin.go # caddytls/client.go
This commit is contained in:
@@ -100,8 +100,8 @@ func enableAutoHTTPS(configs []*SiteConfig, loadCertificates bool) error {
|
||||
}
|
||||
cfg.TLS.Enabled = true
|
||||
cfg.Addr.Scheme = "https"
|
||||
if loadCertificates && caddytls.HostQualifies(cfg.Addr.Host) {
|
||||
_, err := cfg.TLS.CacheManagedCertificate(cfg.Addr.Host)
|
||||
if loadCertificates && caddytls.HostQualifies(cfg.TLS.Hostname) {
|
||||
_, err := cfg.TLS.CacheManagedCertificate(cfg.TLS.Hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ type Logger struct {
|
||||
V4ipMask net.IPMask
|
||||
V6ipMask net.IPMask
|
||||
IPMaskExists bool
|
||||
Exceptions []string
|
||||
}
|
||||
|
||||
// NewTestLogger creates logger suitable for testing purposes
|
||||
@@ -84,6 +85,17 @@ func (l Logger) MaskIP(ip string) string {
|
||||
|
||||
}
|
||||
|
||||
// ShouldLog returns true if the path is not exempted from
|
||||
// being logged (i.e. it is not found in l.Exceptions).
|
||||
func (l Logger) ShouldLog(path string) bool {
|
||||
for _, exc := range l.Exceptions {
|
||||
if Path(path).Matches(exc) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Attach binds logger Start and Close functions to
|
||||
// controller's OnStartup and OnShutdown hooks.
|
||||
func (l *Logger) Attach(controller *caddy.Controller) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -123,15 +124,17 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
|
||||
// For each address in each server block, make a new config
|
||||
for _, sb := range serverBlocks {
|
||||
for _, key := range sb.Keys {
|
||||
key = strings.ToLower(key)
|
||||
if _, dup := h.keysToSiteConfigs[key]; dup {
|
||||
return serverBlocks, fmt.Errorf("duplicate site key: %s", key)
|
||||
}
|
||||
addr, err := standardizeAddress(key)
|
||||
if err != nil {
|
||||
return serverBlocks, err
|
||||
}
|
||||
|
||||
addr = addr.Normalize()
|
||||
key = addr.Key()
|
||||
if _, dup := h.keysToSiteConfigs[key]; dup {
|
||||
return serverBlocks, fmt.Errorf("duplicate site key: %s", key)
|
||||
}
|
||||
|
||||
// Fill in address components from command line so that middleware
|
||||
// have access to the correct information during setup
|
||||
if addr.Host == "" && Host != DefaultHost {
|
||||
@@ -146,7 +149,7 @@ func (h *httpContext) InspectServerBlocks(sourceFile string, serverBlocks []cadd
|
||||
if addrCopy.Port == "" && Port == DefaultPort {
|
||||
addrCopy.Port = Port
|
||||
}
|
||||
addrStr := strings.ToLower(addrCopy.String())
|
||||
addrStr := addrCopy.String()
|
||||
if otherSiteKey, dup := siteAddrs[addrStr]; dup {
|
||||
err := fmt.Errorf("duplicate site address: %s", addrStr)
|
||||
if (addrCopy.Host == Host && Host != DefaultHost) ||
|
||||
@@ -218,6 +221,13 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate each site configuration and make sure that:
|
||||
// 1) TLS is disabled for explicitly-HTTP sites (necessary
|
||||
// when an HTTP address shares a block containing tls)
|
||||
// 2) if QUIC is enabled, TLS ClientAuth is not, because
|
||||
// currently, QUIC does not support ClientAuth (TODO:
|
||||
// revisit this when our QUIC implementation supports it)
|
||||
// 3) if TLS ClientAuth is used, StrictHostMatching is on
|
||||
var atLeastOneSiteLooksLikeProduction bool
|
||||
for _, cfg := range h.siteConfigs {
|
||||
// see if all the addresses (both sites and
|
||||
@@ -254,6 +264,17 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
|
||||
// instead of 443 because it doesn't know about TLS.
|
||||
cfg.Addr.Port = HTTPSPort
|
||||
}
|
||||
if cfg.TLS.ClientAuth != tls.NoClientCert {
|
||||
if QUIC {
|
||||
return nil, fmt.Errorf("cannot enable TLS client authentication with QUIC, because QUIC does not yet support it")
|
||||
}
|
||||
// this must be enabled so that a client cannot connect
|
||||
// using SNI for another site on this listener that
|
||||
// does NOT require ClientAuth, and then send HTTP
|
||||
// requests with the Host header of this site which DOES
|
||||
// require client auth, thus bypassing it...
|
||||
cfg.StrictHostMatching = true
|
||||
}
|
||||
}
|
||||
|
||||
// we must map (group) each config to a bind address
|
||||
@@ -287,12 +308,22 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// normalizedKey returns "normalized" key representation:
|
||||
// scheme and host names are lowered, everything else stays the same
|
||||
func normalizedKey(key string) string {
|
||||
addr, err := standardizeAddress(key)
|
||||
if err != nil {
|
||||
return key
|
||||
}
|
||||
return addr.Normalize().Key()
|
||||
}
|
||||
|
||||
// GetConfig gets the SiteConfig that corresponds to c.
|
||||
// If none exist (should only happen in tests), then a
|
||||
// new, empty one will be created.
|
||||
func GetConfig(c *caddy.Controller) *SiteConfig {
|
||||
ctx := c.Context().(*httpContext)
|
||||
key := strings.ToLower(c.Key)
|
||||
key := normalizedKey(c.Key)
|
||||
if cfg, ok := ctx.keysToSiteConfigs[key]; ok {
|
||||
return cfg
|
||||
}
|
||||
@@ -396,6 +427,43 @@ func (a Address) VHost() string {
|
||||
return a.Original
|
||||
}
|
||||
|
||||
// Normalize normalizes URL: turn scheme and host names into lower case
|
||||
func (a Address) Normalize() Address {
|
||||
path := a.Path
|
||||
if !CaseSensitivePath {
|
||||
path = strings.ToLower(path)
|
||||
}
|
||||
return Address{
|
||||
Original: a.Original,
|
||||
Scheme: strings.ToLower(a.Scheme),
|
||||
Host: strings.ToLower(a.Host),
|
||||
Port: a.Port,
|
||||
Path: path,
|
||||
}
|
||||
}
|
||||
|
||||
// Key is similar to String, just replaces scheme and host values with modified values.
|
||||
// Unlike String it doesn't add anything default (scheme, port, etc)
|
||||
func (a Address) Key() string {
|
||||
res := ""
|
||||
if a.Scheme != "" {
|
||||
res += a.Scheme + "://"
|
||||
}
|
||||
if a.Host != "" {
|
||||
res += a.Host
|
||||
}
|
||||
if a.Port != "" {
|
||||
if strings.HasPrefix(a.Original[len(res):], ":"+a.Port) {
|
||||
// insert port only if the original has its own explicit port
|
||||
res += ":" + a.Port
|
||||
}
|
||||
}
|
||||
if a.Path != "" {
|
||||
res += a.Path
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// standardizeAddress parses an address string into a structured format with separate
|
||||
// scheme, host, port, and path portions, as well as the original input string.
|
||||
func standardizeAddress(str string) (Address, error) {
|
||||
@@ -523,6 +591,7 @@ var directives = []string{
|
||||
"startup", // TODO: Deprecate this directive
|
||||
"shutdown", // TODO: Deprecate this directive
|
||||
"on",
|
||||
"supervisor", // github.com/lucaslorentz/caddy-supervisor
|
||||
"request_id",
|
||||
"realip", // github.com/captncraig/caddy-realip
|
||||
"git", // github.com/abiosoft/caddy-git
|
||||
@@ -538,13 +607,13 @@ var directives = []string{
|
||||
"ext",
|
||||
"gzip",
|
||||
"header",
|
||||
"geoip", // github.com/kodnaplakal/caddy-geoip
|
||||
"errors",
|
||||
"authz", // github.com/casbin/caddy-authz
|
||||
"filter", // github.com/echocat/caddy-filter
|
||||
"minify", // github.com/hacdias/caddy-minify
|
||||
"ipfilter", // github.com/pyed/ipfilter
|
||||
"ratelimit", // github.com/xuqingfeng/caddy-rate-limit
|
||||
"search", // github.com/pedronasser/caddy-search
|
||||
"expires", // github.com/epicagency/caddy-expires
|
||||
"forwardproxy", // github.com/caddyserver/forwardproxy
|
||||
"basicauth",
|
||||
|
||||
@@ -18,6 +18,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sort"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/mholt/caddy"
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
)
|
||||
@@ -147,7 +151,20 @@ func TestInspectServerBlocksWithCustomDefaultPort(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Didn't expect an error, but got: %v", err)
|
||||
}
|
||||
addr := ctx.keysToSiteConfigs["localhost"].Addr
|
||||
localhostKey := "localhost"
|
||||
item, ok := ctx.keysToSiteConfigs[localhostKey]
|
||||
if !ok {
|
||||
availableKeys := make(sort.StringSlice, len(ctx.keysToSiteConfigs))
|
||||
i := 0
|
||||
for key := range ctx.keysToSiteConfigs {
|
||||
availableKeys[i] = fmt.Sprintf("'%s'", key)
|
||||
i++
|
||||
}
|
||||
availableKeys.Sort()
|
||||
t.Errorf("`%s` not found within registered keys, only these are available: %s", localhostKey, strings.Join(availableKeys, ", "))
|
||||
return
|
||||
}
|
||||
addr := item.Addr
|
||||
if addr.Port != Port {
|
||||
t.Errorf("Expected the port on the address to be set, but got: %#v", addr)
|
||||
}
|
||||
@@ -184,6 +201,64 @@ func TestInspectServerBlocksCaseInsensitiveKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyNormalization(t *testing.T) {
|
||||
originalCaseSensitivePath := CaseSensitivePath
|
||||
defer func() {
|
||||
CaseSensitivePath = originalCaseSensitivePath
|
||||
}()
|
||||
CaseSensitivePath = true
|
||||
|
||||
caseSensitiveData := []struct {
|
||||
orig string
|
||||
res string
|
||||
}{
|
||||
{
|
||||
orig: "HTTP://A/ABCDEF",
|
||||
res: "http://a/ABCDEF",
|
||||
},
|
||||
{
|
||||
orig: "A/ABCDEF",
|
||||
res: "a/ABCDEF",
|
||||
},
|
||||
{
|
||||
orig: "A:2015/Port",
|
||||
res: "a:2015/Port",
|
||||
},
|
||||
}
|
||||
for _, item := range caseSensitiveData {
|
||||
v := normalizedKey(item.orig)
|
||||
if v != item.res {
|
||||
t.Errorf("Normalization of `%s` with CaseSensitivePath option set to true must be equal to `%s`, got `%s` instead", item.orig, item.res, v)
|
||||
}
|
||||
}
|
||||
|
||||
CaseSensitivePath = false
|
||||
caseInsensitiveData := []struct {
|
||||
orig string
|
||||
res string
|
||||
}{
|
||||
{
|
||||
orig: "HTTP://A/ABCDEF",
|
||||
res: "http://a/abcdef",
|
||||
},
|
||||
{
|
||||
orig: "A/ABCDEF",
|
||||
res: "a/abcdef",
|
||||
},
|
||||
{
|
||||
orig: "A:2015/Port",
|
||||
res: "a:2015/port",
|
||||
},
|
||||
}
|
||||
for _, item := range caseInsensitiveData {
|
||||
v := normalizedKey(item.orig)
|
||||
if v != item.res {
|
||||
t.Errorf("Normalization of `%s` with CaseSensitivePath option set to false must be equal to `%s`, got `%s` instead", item.orig, item.res, v)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetConfig(t *testing.T) {
|
||||
// case insensitivity for key
|
||||
con := caddy.NewTestController("http", "")
|
||||
@@ -201,6 +276,14 @@ func TestGetConfig(t *testing.T) {
|
||||
if cfg == cfg3 {
|
||||
t.Errorf("Expected different configs using when key is different; got %p and %p", cfg, cfg3)
|
||||
}
|
||||
|
||||
con.Key = "foo/foobar"
|
||||
cfg4 := GetConfig(con)
|
||||
con.Key = "foo/Foobar"
|
||||
cfg5 := GetConfig(con)
|
||||
if cfg4 == cfg5 {
|
||||
t.Errorf("Expected different cases in path to differentiate keys in general")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectivesList(t *testing.T) {
|
||||
|
||||
@@ -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 {
|
||||
@@ -224,6 +260,16 @@ func (r *replacer) getSubstitution(key string) string {
|
||||
}
|
||||
}
|
||||
}
|
||||
// search response headers then
|
||||
if r.responseRecorder != nil && key[1] == '<' {
|
||||
want := key[2 : len(key)-1]
|
||||
for key, values := range r.responseRecorder.Header() {
|
||||
// Header placeholders (case-insensitive)
|
||||
if strings.EqualFold(key, want) {
|
||||
return strings.Join(values, ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
// next check for cookies
|
||||
if key[1] == '~' {
|
||||
name := key[2 : len(key)-1]
|
||||
@@ -365,12 +411,46 @@ 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") {
|
||||
nStr := key[6 : len(key)-1] // get the integer N in "{labelN}"
|
||||
n, err := strconv.Atoi(nStr)
|
||||
if err != nil || n < 1 {
|
||||
return r.emptyValue
|
||||
}
|
||||
labels := strings.Split(r.request.Host, ".")
|
||||
if n > len(labels) {
|
||||
return r.emptyValue
|
||||
}
|
||||
return labels[n-1]
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestReplace(t *testing.T) {
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
reader := strings.NewReader(`{"username": "dennis"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
|
||||
request, err := http.NewRequest("POST", "http://localhost.local/?foo=bar", reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
@@ -67,6 +67,9 @@ func TestReplace(t *testing.T) {
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
// add some respons headers
|
||||
recordRequest.Header().Set("Custom", "CustomResponseHeader")
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to determine hostname: %v", err)
|
||||
@@ -84,7 +87,7 @@ func TestReplace(t *testing.T) {
|
||||
expect string
|
||||
}{
|
||||
{"This hostname is {hostname}", "This hostname is " + hostname},
|
||||
{"This host is {host}.", "This host is localhost."},
|
||||
{"This host is {host}.", "This host is localhost.local."},
|
||||
{"This request method is {method}.", "This request method is POST."},
|
||||
{"The response status is {status}.", "The response status is 200."},
|
||||
{"{when}", "02/Jan/2006:15:04:05 +0000"},
|
||||
@@ -92,10 +95,13 @@ func TestReplace(t *testing.T) {
|
||||
{"{when_unix}", "1136214252"},
|
||||
{"The Custom header is {>Custom}.", "The Custom header is foobarbaz."},
|
||||
{"The CustomAdd header is {>CustomAdd}.", "The CustomAdd header is caddy."},
|
||||
{"The request is {request}.", "The request is POST /?foo=bar HTTP/1.1\\r\\nHost: localhost\\r\\n" +
|
||||
{"The Custom response header is {<Custom}.", "The Custom response header is CustomResponseHeader."},
|
||||
{"Bad {>Custom placeholder", "Bad {>Custom placeholder"},
|
||||
{"The request is {request}.", "The request is POST /?foo=bar HTTP/1.1\\r\\nHost: localhost.local\\r\\n" +
|
||||
"Cookie: foo=bar; taste=delicious\\r\\nCustom: foobarbaz\\r\\nCustomadd: caddy\\r\\n" +
|
||||
"Shorterval: 1\\r\\n\\r\\n."},
|
||||
{"The cUsToM header is {>cUsToM}...", "The cUsToM header is foobarbaz..."},
|
||||
{"The cUsToM response header is {<CuSTom}.", "The cUsToM response header is CustomResponseHeader."},
|
||||
{"The Non-Existent header is {>Non-Existent}.", "The Non-Existent header is -."},
|
||||
{"Bad {host placeholder...", "Bad {host placeholder..."},
|
||||
{"Bad {>Custom placeholder", "Bad {>Custom placeholder"},
|
||||
@@ -106,6 +112,9 @@ func TestReplace(t *testing.T) {
|
||||
{"Query string is {query}", "Query string is foo=bar"},
|
||||
{"Query string value for foo is {?foo}", "Query string value for foo is bar"},
|
||||
{"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 {
|
||||
@@ -138,6 +147,107 @@ 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"}`)
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost/?foo=bar", reader)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Custom", "foobarbaz")
|
||||
repl := NewReplacer(request, nil, "-")
|
||||
// add some headers after creating replacer
|
||||
request.Header.Set("CustomAdd", "caddy")
|
||||
request.Header.Set("Cookie", "foo=bar; taste=delicious")
|
||||
|
||||
old := now
|
||||
now = func() time.Time {
|
||||
return time.Date(2006, 1, 2, 15, 4, 5, 02, time.FixedZone("hardcoded", -7))
|
||||
}
|
||||
defer func() {
|
||||
now = old
|
||||
}()
|
||||
testCases := []struct {
|
||||
template string
|
||||
expect string
|
||||
}{
|
||||
{"The Custom response header is {<Custom}.", "The Custom response header is -."},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
if expected, actual := c.expect, repl.Replace(c.template); expected != actual {
|
||||
t.Errorf("for template '%s', expected '%s', got '%s'", c.template, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
recordRequest := NewResponseRecorder(w)
|
||||
|
||||
@@ -320,6 +320,9 @@ func (s *Server) Serve(ln net.Listener) error {
|
||||
}
|
||||
|
||||
err := s.Server.Serve(ln)
|
||||
if err == http.ErrServerClosed {
|
||||
err = nil // not an error worth reporting since closing a server is intentional
|
||||
}
|
||||
if s.quicServer != nil {
|
||||
s.quicServer.Close()
|
||||
}
|
||||
@@ -421,19 +424,39 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||
r.URL = trimPathPrefix(r.URL, pathPrefix)
|
||||
}
|
||||
|
||||
// enforce strict host matching, which ensures that the SNI
|
||||
// value (if any), matches the Host header; essential for
|
||||
// sites that rely on TLS ClientAuth sharing a port with
|
||||
// sites that do not - if mismatched, close the connection
|
||||
if vhost.StrictHostMatching && r.TLS != nil &&
|
||||
strings.ToLower(r.TLS.ServerName) != strings.ToLower(hostname) {
|
||||
r.Close = true
|
||||
log.Printf("[ERROR] %s - strict host matching: SNI (%s) and HTTP Host (%s) values differ",
|
||||
vhost.Addr, r.TLS.ServerName, hostname)
|
||||
return http.StatusForbidden, nil
|
||||
}
|
||||
|
||||
return vhost.middlewareChain.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,16 @@ type SiteConfig struct {
|
||||
// TLS configuration
|
||||
TLS *caddytls.Config
|
||||
|
||||
// If true, the Host header in the HTTP request must
|
||||
// match the SNI value in the TLS handshake (if any).
|
||||
// This should be enabled whenever a site relies on
|
||||
// TLS client authentication, for example; or any time
|
||||
// you want to enforce that THIS site's TLS config
|
||||
// is used and not the TLS config of any other site
|
||||
// on the same listener. TODO: Check how relevant this
|
||||
// is with TLS 1.3.
|
||||
StrictHostMatching bool
|
||||
|
||||
// Uncompiled middleware stack
|
||||
middleware []Middleware
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ func TestHostname(t *testing.T) {
|
||||
// // Test 3 - ipv6 without port and brackets
|
||||
// {"2001:4860:4860::8888", "google-public-dns-a.google.com."},
|
||||
// Test 4 - no hostname available
|
||||
{"1.1.1.1", "1.1.1.1"},
|
||||
{"0.0.0.0", "0.0.0.0"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
||||
Reference in New Issue
Block a user