mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8cc5d1a7d | |||
| 8d304a4566 | |||
| 65e33fc1ee | |||
| 9f34383c02 | |||
| b07b198764 | |||
| 51b1bfb125 | |||
| c049bab458 |
@@ -109,7 +109,7 @@ jobs:
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist --timeout 60m
|
||||
args: release --clean --timeout 60m
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ steps.vars.outputs.version_tag }}
|
||||
|
||||
@@ -93,6 +93,11 @@ func makeArgsReplacer(args []string) *caddy.Replacer {
|
||||
// TODO: Remove the deprecated {args.*} placeholder
|
||||
// support at some point in the future
|
||||
if matches := argsRegexpIndexDeprecated.FindStringSubmatch(key); len(matches) > 0 {
|
||||
// What's matched may be a substring of the key
|
||||
if matches[0] != key {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
caddy.Log().Named("caddyfile").Warn(
|
||||
@@ -111,6 +116,11 @@ func makeArgsReplacer(args []string) *caddy.Replacer {
|
||||
|
||||
// Handle args[*] form
|
||||
if matches := argsRegexpIndex.FindStringSubmatch(key); len(matches) > 0 {
|
||||
// What's matched may be a substring of the key
|
||||
if matches[0] != key {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if strings.Contains(matches[1], ":") {
|
||||
caddy.Log().Named("caddyfile").Warn(
|
||||
"Variadic placeholder {args[" + matches[1] + "]} must be a token on its own")
|
||||
|
||||
@@ -718,6 +718,36 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportReplacementInJSONWithBrace(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
args []string
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
args: []string{"123"},
|
||||
input: "{args[0]}",
|
||||
expect: "123",
|
||||
},
|
||||
{
|
||||
args: []string{"123"},
|
||||
input: `{"key":"{args[0]}"}`,
|
||||
expect: `{"key":"123"}`,
|
||||
},
|
||||
{
|
||||
args: []string{"123", "123"},
|
||||
input: `{"key":[{args[0]},{args[1]}]}`,
|
||||
expect: `{"key":[123,123]}`,
|
||||
},
|
||||
} {
|
||||
repl := makeArgsReplacer(test.args)
|
||||
actual := repl.ReplaceKnown(test.input, "")
|
||||
if actual != test.expect {
|
||||
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnippets(t *testing.T) {
|
||||
p := testParser(`
|
||||
(common) {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
*.sandbox.localhost {
|
||||
@sandboxPort {
|
||||
header_regexp first_label Host ^([0-9]{3})\.sandbox\.
|
||||
}
|
||||
handle @sandboxPort {
|
||||
reverse_proxy {re.first_label.1}
|
||||
}
|
||||
handle {
|
||||
redir {scheme}://application.localhost
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"*.sandbox.localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "{http.regexp.first_label.1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"header_regexp": {
|
||||
"Host": {
|
||||
"name": "first_label",
|
||||
"pattern": "^([0-9]{3})\\.sandbox\\."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.scheme}://application.localhost"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
*.sandbox.localhost {
|
||||
@sandboxPort {
|
||||
header_regexp port Host ^([0-9]{3})\.sandbox\.
|
||||
}
|
||||
handle @sandboxPort {
|
||||
reverse_proxy app:6{re.port.1}
|
||||
}
|
||||
handle {
|
||||
redir {scheme}://application.localhost
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"*.sandbox.localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "app:6{http.regexp.port.1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"header_regexp": {
|
||||
"Host": {
|
||||
"name": "port",
|
||||
"pattern": "^([0-9]{3})\\.sandbox\\."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.scheme}://application.localhost"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
*.sandbox.localhost {
|
||||
@sandboxPort {
|
||||
header_regexp port Host ^([0-9]{3})\.sandbox\.
|
||||
}
|
||||
handle @sandboxPort {
|
||||
reverse_proxy app:{re.port.1}
|
||||
}
|
||||
handle {
|
||||
redir {scheme}://application.localhost
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"*.sandbox.localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "app:{http.regexp.port.1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"header_regexp": {
|
||||
"Host": {
|
||||
"name": "port",
|
||||
"pattern": "^([0-9]{3})\\.sandbox\\."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.scheme}://application.localhost"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -611,6 +612,16 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
|
||||
origin := "http://" + parsedAddr.JoinHostPort(0)
|
||||
if parsedAddr.IsUnixNetwork() {
|
||||
origin = "http://127.0.0.1" // bogus host is a hack so that http.NewRequest() is happy
|
||||
|
||||
// the unix address at this point might still contain the optional
|
||||
// unix socket permissions, which are part of the address/host.
|
||||
// those need to be removed first, as they aren't part of the
|
||||
// resulting unix file path
|
||||
addr, _, err := internal.SplitUnixSocketPermissionsBits(parsedAddr.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsedAddr.Host = addr
|
||||
}
|
||||
|
||||
// form the request
|
||||
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/mastercactapus/proxyprotocol v0.0.4
|
||||
github.com/mholt/acmez v1.2.0
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/quic-go/quic-go v0.37.1
|
||||
github.com/quic-go/quic-go v0.37.3
|
||||
github.com/smallstep/certificates v0.24.3-rc.5
|
||||
github.com/smallstep/nosql v0.6.0
|
||||
github.com/smallstep/truststore v0.12.1
|
||||
@@ -58,7 +58,7 @@ require (
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.1 // indirect
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
|
||||
@@ -861,10 +861,10 @@ github.com/pseudomuto/protoc-gen-doc v1.5.0/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr
|
||||
github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.0 h1:NrCXmDl8BddZwO67vlvEpBTwT89bJfKYygxv4HQvuDk=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.0/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.37.1 h1:M+mcsFq9KoxVjCetIwH65TvusW1UdRBc6zmxI6pkeD0=
|
||||
github.com/quic-go/quic-go v0.37.1/go.mod h1:XtCUOCALTTWbPyd0IxFfHf6h0sEMubRFvEYHl3QxKw8=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.1 h1:O4BLOM3hwfVF3AcktIylQXyl7Yi2iBNVy5QsV+ySxbg=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.37.3 h1:pkHH3xaMNUNAh6OtgEV/0K6Fz+YIJXhPzgd/ShiRDm4=
|
||||
github.com/quic-go/quic-go v0.37.3/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SplitUnixSocketPermissionsBits takes a unix socket address in the
|
||||
// unusual "path|bits" format (e.g. /run/caddy.sock|0222) and tries
|
||||
// to split it into socket path (host) and permissions bits (port).
|
||||
// Colons (":") can't be used as separator, as socket paths on Windows
|
||||
// may include a drive letter (e.g. `unix/c:\absolute\path.sock`).
|
||||
// Permission bits will default to 0200 if none are specified.
|
||||
// Throws an error, if the first carrying bit does not
|
||||
// include write perms (e.g. `0422` or `022`).
|
||||
// Symbolic permission representation (e.g. `u=w,g=w,o=w`)
|
||||
// is not supported and will throw an error for now!
|
||||
func SplitUnixSocketPermissionsBits(addr string) (path string, fileMode fs.FileMode, err error) {
|
||||
addrSplit := strings.SplitN(addr, "|", 2)
|
||||
|
||||
if len(addrSplit) == 2 {
|
||||
// parse octal permission bit string as uint32
|
||||
fileModeUInt64, err := strconv.ParseUint(addrSplit[1], 8, 32)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("could not parse octal permission bits in %s: %v", addr, err)
|
||||
}
|
||||
fileMode = fs.FileMode(fileModeUInt64)
|
||||
|
||||
// FileMode.String() returns a string like `-rwxr-xr--` for `u=rwx,g=rx,o=r` (`0754`)
|
||||
if string(fileMode.String()[2]) != "w" {
|
||||
return "", 0, fmt.Errorf("owner of the socket requires '-w-' (write, octal: '2') permissions at least; got '%s' in %s", fileMode.String()[1:4], addr)
|
||||
}
|
||||
|
||||
return addrSplit[0], fileMode, nil
|
||||
}
|
||||
|
||||
// default to 0200 (symbolic: `u=w,g=,o=`)
|
||||
// if no permission bits are specified
|
||||
return addr, 0200, nil
|
||||
}
|
||||
+3
-36
@@ -31,6 +31,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"go.uber.org/zap"
|
||||
@@ -157,7 +158,7 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
|
||||
// is independent of permissions bits
|
||||
if na.IsUnixNetwork() {
|
||||
var err error
|
||||
address, unixFileMode, err = splitUnixSocketPermissionsBits(na.Host)
|
||||
address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -319,40 +320,6 @@ func IsUnixNetwork(netw string) bool {
|
||||
return strings.HasPrefix(netw, "unix")
|
||||
}
|
||||
|
||||
// Takes a unix socket address in the unusual "path|bits" format
|
||||
// (e.g. /run/caddy.sock|0222) and tries to split it into
|
||||
// socket path (host) and permissions bits (port). Colons (":")
|
||||
// can't be used as separator, as socket paths on Windows may
|
||||
// include a drive letter (e.g. `unix/c:\absolute\path.sock`).
|
||||
// Permission bits will default to 0200 if none are specified.
|
||||
// Throws an error, if the first carrying bit does not
|
||||
// include write perms (e.g. `0422` or `022`).
|
||||
// Symbolic permission representation (e.g. `u=w,g=w,o=w`)
|
||||
// is not supported and will throw an error for now!
|
||||
func splitUnixSocketPermissionsBits(addr string) (path string, fileMode fs.FileMode, err error) {
|
||||
addrSplit := strings.SplitN(addr, "|", 2)
|
||||
|
||||
if len(addrSplit) == 2 {
|
||||
// parse octal permission bit string as uint32
|
||||
fileModeUInt64, err := strconv.ParseUint(addrSplit[1], 8, 32)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("could not parse octal permission bits in %s: %v", addr, err)
|
||||
}
|
||||
fileMode = fs.FileMode(fileModeUInt64)
|
||||
|
||||
// FileMode.String() returns a string like `-rwxr-xr--` for `u=rwx,g=rx,o=r` (`0754`)
|
||||
if string(fileMode.String()[2]) != "w" {
|
||||
return "", 0, fmt.Errorf("owner of the socket requires '-w-' (write, octal: '2') permissions at least; got '%s' in %s", fileMode.String()[1:4], addr)
|
||||
}
|
||||
|
||||
return addrSplit[0], fileMode, nil
|
||||
}
|
||||
|
||||
// default to 0200 (symbolic: `u=w,g=,o=`)
|
||||
// if no permission bits are specified
|
||||
return addr, 0200, nil
|
||||
}
|
||||
|
||||
// ParseNetworkAddress parses addr into its individual
|
||||
// components. The input string is expected to be of
|
||||
// the form "network/host:port-range" where any part is
|
||||
@@ -377,7 +344,7 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
|
||||
network = defaultNetwork
|
||||
}
|
||||
if IsUnixNetwork(network) {
|
||||
_, _, err := splitUnixSocketPermissionsBits(host)
|
||||
_, _, err := internal.SplitUnixSocketPermissionsBits(host)
|
||||
return NetworkAddress{
|
||||
Network: network,
|
||||
Host: host,
|
||||
|
||||
+3
-1
@@ -17,6 +17,8 @@ package caddy
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
func TestSplitNetworkAddress(t *testing.T) {
|
||||
@@ -634,7 +636,7 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) {
|
||||
expectErr: true,
|
||||
},
|
||||
} {
|
||||
actualPath, actualFileMode, err := splitUnixSocketPermissionsBits(tc.input)
|
||||
actualPath, actualFileMode, err := internal.SplitUnixSocketPermissionsBits(tc.input)
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but got: %v", i, err)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<path d="M9 7l4 0"></path>
|
||||
<path d="M9 11l4 0"></path>
|
||||
</svg>
|
||||
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic"}}
|
||||
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}}
|
||||
{{- if eq .Tpl.Layout "grid"}}
|
||||
<img loading="lazy" src="{{html .Name}}">
|
||||
{{- else}}
|
||||
|
||||
@@ -23,11 +23,43 @@ import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
type parsedAddr struct {
|
||||
network, scheme, host, port string
|
||||
valid bool
|
||||
}
|
||||
|
||||
func (p parsedAddr) dialAddr() string {
|
||||
if !p.valid {
|
||||
return ""
|
||||
}
|
||||
// for simplest possible config, we only need to include
|
||||
// the network portion if the user specified one
|
||||
if p.network != "" {
|
||||
return caddy.JoinNetworkAddress(p.network, p.host, p.port)
|
||||
}
|
||||
|
||||
// if the host is a placeholder, then we don't want to join with an empty port,
|
||||
// because that would just append an extra ':' at the end of the address.
|
||||
if p.port == "" && strings.Contains(p.host, "{") {
|
||||
return p.host
|
||||
}
|
||||
return net.JoinHostPort(p.host, p.port)
|
||||
}
|
||||
func (p parsedAddr) rangedPort() bool {
|
||||
return strings.Contains(p.port, "-")
|
||||
}
|
||||
func (p parsedAddr) replaceablePort() bool {
|
||||
return strings.Contains(p.port, "{") && strings.Contains(p.port, "}")
|
||||
}
|
||||
func (p parsedAddr) isUnix() bool {
|
||||
return caddy.IsUnixNetwork(p.network)
|
||||
}
|
||||
|
||||
// parseUpstreamDialAddress parses configuration inputs for
|
||||
// the dial address, including support for a scheme in front
|
||||
// as a shortcut for the port number, and a network type,
|
||||
// for example 'unix' to dial a unix socket.
|
||||
func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
|
||||
func parseUpstreamDialAddress(upstreamAddr string) (parsedAddr, error) {
|
||||
var network, scheme, host, port string
|
||||
|
||||
if strings.Contains(upstreamAddr, "://") {
|
||||
@@ -35,7 +67,7 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
|
||||
// so we return a more user-friendly error message instead
|
||||
// to explain what to do instead
|
||||
if strings.Contains(upstreamAddr, "{") {
|
||||
return "", "", fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
|
||||
return parsedAddr{}, fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
|
||||
}
|
||||
|
||||
toURL, err := url.Parse(upstreamAddr)
|
||||
@@ -46,19 +78,19 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
|
||||
if strings.Contains(err.Error(), "invalid port") && strings.Contains(err.Error(), "-") {
|
||||
index := strings.LastIndex(upstreamAddr, ":")
|
||||
if index == -1 {
|
||||
return "", "", fmt.Errorf("parsing upstream URL: %v", err)
|
||||
return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
|
||||
}
|
||||
portRange := upstreamAddr[index+1:]
|
||||
if strings.Count(portRange, "-") != 1 {
|
||||
return "", "", fmt.Errorf("parsing upstream URL: parse \"%v\": port range invalid: %v", upstreamAddr, portRange)
|
||||
return parsedAddr{}, fmt.Errorf("parsing upstream URL: parse \"%v\": port range invalid: %v", upstreamAddr, portRange)
|
||||
}
|
||||
toURL, err = url.Parse(strings.ReplaceAll(upstreamAddr, portRange, "0"))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("parsing upstream URL: %v", err)
|
||||
return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
|
||||
}
|
||||
port = portRange
|
||||
} else {
|
||||
return "", "", fmt.Errorf("parsing upstream URL: %v", err)
|
||||
return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
|
||||
}
|
||||
}
|
||||
if port == "" {
|
||||
@@ -69,18 +101,18 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
|
||||
// a backend and proxying to it, so we cannot allow extra components
|
||||
// in backend URLs
|
||||
if toURL.Path != "" || toURL.RawQuery != "" || toURL.Fragment != "" {
|
||||
return "", "", fmt.Errorf("for now, URLs for proxy upstreams only support scheme, host, and port components")
|
||||
return parsedAddr{}, fmt.Errorf("for now, URLs for proxy upstreams only support scheme, host, and port components")
|
||||
}
|
||||
|
||||
// ensure the port and scheme aren't in conflict
|
||||
if toURL.Scheme == "http" && port == "443" {
|
||||
return "", "", fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
|
||||
return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
|
||||
}
|
||||
if toURL.Scheme == "https" && port == "80" {
|
||||
return "", "", fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
|
||||
return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
|
||||
}
|
||||
if toURL.Scheme == "h2c" && port == "443" {
|
||||
return "", "", fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
|
||||
return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
|
||||
}
|
||||
|
||||
// if port is missing, attempt to infer from scheme
|
||||
@@ -112,18 +144,5 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
|
||||
network = "unix"
|
||||
scheme = "h2c"
|
||||
}
|
||||
|
||||
// for simplest possible config, we only need to include
|
||||
// the network portion if the user specified one
|
||||
if network != "" {
|
||||
return caddy.JoinNetworkAddress(network, host, port), scheme, nil
|
||||
}
|
||||
|
||||
// if the host is a placeholder, then we don't want to join with an empty port,
|
||||
// because that would just append an extra ':' at the end of the address.
|
||||
if port == "" && strings.Contains(host, "{") {
|
||||
return host, scheme, nil
|
||||
}
|
||||
|
||||
return net.JoinHostPort(host, port), scheme, nil
|
||||
return parsedAddr{network, scheme, host, port, true}, nil
|
||||
}
|
||||
|
||||
@@ -265,18 +265,18 @@ func TestParseUpstreamDialAddress(t *testing.T) {
|
||||
expectScheme: "h2c",
|
||||
},
|
||||
} {
|
||||
actualHostPort, actualScheme, err := parseUpstreamDialAddress(tc.input)
|
||||
actualAddr, err := parseUpstreamDialAddress(tc.input)
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but got %v", i, err)
|
||||
}
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error but got %v", i, err)
|
||||
}
|
||||
if actualHostPort != tc.expectHostPort {
|
||||
t.Errorf("Test %d: Expected host and port '%s' but got '%s'", i, tc.expectHostPort, actualHostPort)
|
||||
if actualAddr.dialAddr() != tc.expectHostPort {
|
||||
t.Errorf("Test %d: input %s: Expected host and port '%s' but got '%s'", i, tc.input, tc.expectHostPort, actualAddr.dialAddr())
|
||||
}
|
||||
if actualScheme != tc.expectScheme {
|
||||
t.Errorf("Test %d: Expected scheme '%s' but got '%s'", i, tc.expectScheme, actualScheme)
|
||||
if actualAddr.scheme != tc.expectScheme {
|
||||
t.Errorf("Test %d: Expected scheme '%s' but got '%s'", i, tc.expectScheme, actualAddr.scheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// appendUpstream creates an upstream for address and adds
|
||||
// it to the list.
|
||||
appendUpstream := func(address string) error {
|
||||
dialAddr, scheme, err := parseUpstreamDialAddress(address)
|
||||
pa, err := parseUpstreamDialAddress(address)
|
||||
if err != nil {
|
||||
return d.WrapErr(err)
|
||||
}
|
||||
@@ -154,21 +154,27 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// the underlying JSON does not yet support different
|
||||
// transports (protocols or schemes) to each backend,
|
||||
// so we remember the last one we see and compare them
|
||||
if commonScheme != "" && scheme != commonScheme {
|
||||
if commonScheme != "" && pa.scheme != commonScheme {
|
||||
return d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'",
|
||||
commonScheme, scheme)
|
||||
commonScheme, pa.scheme)
|
||||
}
|
||||
commonScheme = scheme
|
||||
commonScheme = pa.scheme
|
||||
|
||||
parsedAddr, err := caddy.ParseNetworkAddress(dialAddr)
|
||||
// if the port of upstream address contains a placeholder, only wrap it with the `Upstream` struct,
|
||||
// delaying actual resolution of the address until request time.
|
||||
if pa.replaceablePort() {
|
||||
h.Upstreams = append(h.Upstreams, &Upstream{Dial: pa.dialAddr()})
|
||||
return nil
|
||||
}
|
||||
parsedAddr, err := caddy.ParseNetworkAddress(pa.dialAddr())
|
||||
if err != nil {
|
||||
return d.WrapErr(err)
|
||||
}
|
||||
|
||||
if parsedAddr.StartPort == 0 && parsedAddr.EndPort == 0 {
|
||||
if pa.isUnix() || !pa.rangedPort() {
|
||||
// unix networks don't have ports
|
||||
h.Upstreams = append(h.Upstreams, &Upstream{
|
||||
Dial: dialAddr,
|
||||
Dial: pa.dialAddr(),
|
||||
})
|
||||
} else {
|
||||
// expand a port range into multiple upstreams
|
||||
|
||||
@@ -132,14 +132,14 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||
toAddresses := make([]string, len(to))
|
||||
var toScheme string
|
||||
for i, toLoc := range to {
|
||||
addr, scheme, err := parseUpstreamDialAddress(toLoc)
|
||||
addr, err := parseUpstreamDialAddress(toLoc)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err)
|
||||
}
|
||||
if scheme != "" && toScheme == "" {
|
||||
toScheme = scheme
|
||||
if addr.scheme != "" && toScheme == "" {
|
||||
toScheme = addr.scheme
|
||||
}
|
||||
toAddresses[i] = addr
|
||||
toAddresses[i] = addr.dialAddr()
|
||||
}
|
||||
|
||||
// proceed to build the handler and server
|
||||
|
||||
Reference in New Issue
Block a user