From 8aef859a5510e883a70fb562d5fb83c7585cc301 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 9 Jan 2020 12:35:53 -0700 Subject: caddyfile: Less strict URL parsing; allows placeholders See https://caddy.community/t/caddy-v2-reusable-snippets/6744/11?u=matt --- caddyconfig/httpcaddyfile/addresses.go | 74 +++++++++++++++++++---------- caddyconfig/httpcaddyfile/addresses_test.go | 15 ++++-- 2 files changed, 59 insertions(+), 30 deletions(-) (limited to 'caddyconfig') diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go index deb27c2..431bb75 100644 --- a/caddyconfig/httpcaddyfile/addresses.go +++ b/caddyconfig/httpcaddyfile/addresses.go @@ -17,7 +17,6 @@ package httpcaddyfile import ( "fmt" "net" - "net/url" "reflect" "strconv" "strings" @@ -209,44 +208,69 @@ type Address struct { // ParseAddress parses an address string into a structured format with separate // scheme, host, port, and path portions, as well as the original input string. func ParseAddress(str string) (Address, error) { - httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort) - - input := str - - // Split input into components (prepend with // to force host portion by default) - if !strings.Contains(str, "//") && !strings.HasPrefix(str, "/") { - str = "//" + str + const maxLen = 4096 + if len(str) > maxLen { + str = str[:maxLen] } - - u, err := url.Parse(str) - if err != nil { - return Address{}, err + remaining := strings.TrimSpace(str) + a := Address{Original: remaining} + + // extract scheme + splitScheme := strings.SplitN(remaining, "://", 2) + switch len(splitScheme) { + case 0: + return a, nil + case 1: + remaining = splitScheme[0] + case 2: + a.Scheme = splitScheme[0] + remaining = splitScheme[1] } - // separate host and port - host, port, err := net.SplitHostPort(u.Host) - if err != nil { - host, port, err = net.SplitHostPort(u.Host + ":") + // extract host and port + hostSplit := strings.SplitN(remaining, "/", 2) + if len(hostSplit) > 0 { + host, port, err := net.SplitHostPort(hostSplit[0]) if err != nil { - host = u.Host + host, port, err = net.SplitHostPort(hostSplit[0] + ":") + if err != nil { + host = hostSplit[0] + } } + a.Host = host + a.Port = port + } + if len(hostSplit) == 2 { + // all that remains is the path + a.Path = "/" + hostSplit[1] } + httpPort, httpsPort := strconv.Itoa(certmagic.HTTPPort), strconv.Itoa(certmagic.HTTPSPort) + // see if we can set port based off scheme - if port == "" { - if u.Scheme == "http" { - port = httpPort - } else if u.Scheme == "https" { - port = httpsPort + if a.Port == "" { + if a.Scheme == "http" { + a.Port = httpPort + } else if a.Scheme == "https" { + a.Port = httpsPort + } + } + + // make sure port is valid + if a.Port != "" { + if portNum, err := strconv.Atoi(a.Port); err != nil { + return Address{}, fmt.Errorf("invalid port '%s': %v", a.Port, err) + } else if portNum < 0 || portNum > 65535 { + return Address{}, fmt.Errorf("port %d is out of range", portNum) } } // error if scheme and port combination violate convention - if (u.Scheme == "http" && port == httpsPort) || (u.Scheme == "https" && port == httpPort) { - return Address{}, fmt.Errorf("[%s] scheme and port violate convention", input) + if (a.Scheme == "http" && a.Port == httpsPort) || (a.Scheme == "https" && a.Port == httpPort) { + return Address{}, fmt.Errorf("[%s] scheme and port violate convention", str) } - return Address{Original: input, Scheme: u.Scheme, Host: host, Port: port, Path: u.Path}, err + return a, nil } // TODO: which of the methods on Address are even used? diff --git a/caddyconfig/httpcaddyfile/addresses_test.go b/caddyconfig/httpcaddyfile/addresses_test.go index d6aa6f6..70577f1 100644 --- a/caddyconfig/httpcaddyfile/addresses_test.go +++ b/caddyconfig/httpcaddyfile/addresses_test.go @@ -11,6 +11,7 @@ func TestParseAddress(t *testing.T) { scheme, host, port, path string shouldErr bool }{ + {``, "", "", "", "", false}, {`localhost`, "", "localhost", "", "", false}, {`localhost:1234`, "", "localhost", "1234", "", false}, {`localhost:`, "", "localhost", "", "", false}, @@ -31,6 +32,10 @@ func TestParseAddress(t *testing.T) { {`https://localhost:80`, "", "", "", "", true}, // not conventional {`http://localhost`, "http", "localhost", "80", "", false}, {`https://localhost`, "https", "localhost", "443", "", false}, + {`http://{env.APP_DOMAIN}`, "http", "{env.APP_DOMAIN}", "80", "", false}, + {`{env.APP_DOMAIN}:80`, "", "{env.APP_DOMAIN}", "80", "", false}, + {`{env.APP_DOMAIN}/path`, "", "{env.APP_DOMAIN}", "", "/path", false}, + {`example.com/{env.APP_PATH}`, "", "example.com", "", "/{env.APP_PATH}", false}, {`http://127.0.0.1`, "http", "127.0.0.1", "80", "", false}, {`https://127.0.0.1`, "https", "127.0.0.1", "443", "", false}, {`http://[::1]`, "http", "::1", "80", "", false}, @@ -38,12 +43,12 @@ func TestParseAddress(t *testing.T) { {`https://127.0.0.1:1234`, "https", "127.0.0.1", "1234", "", false}, {`http://[::1]:1234`, "http", "::1", "1234", "", false}, {``, "", "", "", "", false}, - {`::1`, "", "::1", "", "", true}, - {`localhost::`, "", "localhost::", "", "", true}, - {`#$%@`, "", "", "", "", true}, + {`::1`, "", "::1", "", "", false}, + {`localhost::`, "", "localhost::", "", "", false}, + {`#$%@`, "", "#$%@", "", "", false}, // don't want to presume what the hostname could be {`host/path`, "", "host", "", "/path", false}, {`http://host/`, "http", "host", "80", "/", false}, - {`//asdf`, "", "asdf", "", "", false}, + {`//asdf`, "", "", "", "//asdf", false}, {`:1234/asdf`, "", "", "1234", "/asdf", false}, {`http://host/path`, "http", "host", "80", "/path", false}, {`https://host:443/path/foo`, "https", "host", "443", "/path/foo", false}, @@ -56,7 +61,7 @@ func TestParseAddress(t *testing.T) { t.Errorf("Test %d (%s): Expected no error, but had error: %v", i, test.input, err) } if err == nil && test.shouldErr { - t.Errorf("Test %d (%s): Expected error, but had none", i, test.input) + t.Errorf("Test %d (%s): Expected error, but had none (%#v)", i, test.input, actual) } if !test.shouldErr && actual.Original != test.input { -- cgit v1.2.3