mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 00:32:31 -04:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad3a83fb91 | |||
| 53c4d788d4 | |||
| d6bc9e0b5c | |||
| 54d1923ccb | |||
| c0f76e9ed4 | |||
| f259ed52bb | |||
| 8bac134f26 | |||
| 412dcc07d3 | |||
| 660c59b6f3 | |||
| 58e05cab15 | |||
| 10f85558ea | |||
| 98468af8b6 | |||
| 25f10511e7 | |||
| b6e96fa3c5 | |||
| 56013934a4 | |||
| 0b6f764356 | |||
| 050d6e0aeb | |||
| 0bcd02d5f6 | |||
| c82fe91104 | |||
| f9b42c3772 |
@@ -25,6 +25,8 @@ import (
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"hash"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -894,16 +896,36 @@ func (h adminHandler) originAllowed(origin *url.URL) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// etagHasher returns a the hasher we used on the config to both
|
||||
// produce and verify ETags.
|
||||
func etagHasher() hash.Hash32 { return fnv.New32a() }
|
||||
|
||||
// makeEtag returns an Etag header value (including quotes) for
|
||||
// the given config path and hash of contents at that path.
|
||||
func makeEtag(path string, hash hash.Hash) string {
|
||||
return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil))
|
||||
}
|
||||
|
||||
func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// Set the ETag as a trailer header.
|
||||
// The alternative is to write the config to a buffer, and
|
||||
// then hash that.
|
||||
w.Header().Set("Trailer", "ETag")
|
||||
|
||||
err := readConfig(r.URL.Path, w)
|
||||
hash := etagHasher()
|
||||
configWriter := io.MultiWriter(w, hash)
|
||||
err := readConfig(r.URL.Path, configWriter)
|
||||
if err != nil {
|
||||
return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
|
||||
}
|
||||
|
||||
// we could consider setting up a sync.Pool for the summed
|
||||
// hashes to reduce GC pressure.
|
||||
w.Header().Set("Etag", makeEtag(r.URL.Path, hash))
|
||||
|
||||
return nil
|
||||
|
||||
case http.MethodPost,
|
||||
@@ -937,7 +959,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
forceReload := r.Header.Get("Cache-Control") == "must-revalidate"
|
||||
|
||||
err := changeConfig(r.Method, r.URL.Path, body, forceReload)
|
||||
err := changeConfig(r.Method, r.URL.Path, body, r.Header.Get("If-Match"), forceReload)
|
||||
if err != nil && !errors.Is(err, errSameConfig) {
|
||||
return err
|
||||
}
|
||||
|
||||
+50
-1
@@ -16,6 +16,8 @@ package caddy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -139,10 +141,57 @@ func TestLoadConcurrent(t *testing.T) {
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
type fooModule struct {
|
||||
IntField int
|
||||
StrField string
|
||||
}
|
||||
|
||||
func (fooModule) CaddyModule() ModuleInfo {
|
||||
return ModuleInfo{
|
||||
ID: "foo",
|
||||
New: func() Module { return new(fooModule) },
|
||||
}
|
||||
}
|
||||
func (fooModule) Start() error { return nil }
|
||||
func (fooModule) Stop() error { return nil }
|
||||
|
||||
func TestETags(t *testing.T) {
|
||||
RegisterModule(fooModule{})
|
||||
|
||||
if err := Load([]byte(`{"apps": {"foo": {"strField": "abc", "intField": 0}}}`), true); err != nil {
|
||||
t.Fatalf("loading: %s", err)
|
||||
}
|
||||
|
||||
const key = "/" + rawConfigKey + "/apps/foo"
|
||||
|
||||
// try update the config with the wrong etag
|
||||
err := changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}}`), fmt.Sprintf(`"/%s not_an_etag"`, rawConfigKey), false)
|
||||
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
|
||||
t.Fatalf("expected precondition failed; got %v", err)
|
||||
}
|
||||
|
||||
// get the etag
|
||||
hash := etagHasher()
|
||||
if err := readConfig(key, hash); err != nil {
|
||||
t.Fatalf("reading: %s", err)
|
||||
}
|
||||
|
||||
// do the same update with the correct key
|
||||
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 1}`), makeEtag(key, hash), false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected update to work; got %v", err)
|
||||
}
|
||||
|
||||
// now try another update. The hash should no longer match and we should get precondition failed
|
||||
err = changeConfig(http.MethodPost, key, []byte(`{"strField": "abc", "intField": 2}`), makeEtag(key, hash), false)
|
||||
if apiErr, ok := err.(APIError); !ok || apiErr.HTTPStatus != http.StatusPreconditionFailed {
|
||||
t.Fatalf("expected precondition failed; got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLoad(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Load(testCfg, true)
|
||||
|
||||
@@ -17,6 +17,7 @@ package caddy
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -111,7 +112,7 @@ func Load(cfgJSON []byte, forceReload bool) error {
|
||||
}
|
||||
}()
|
||||
|
||||
err := changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, forceReload)
|
||||
err := changeConfig(http.MethodPost, "/"+rawConfigKey, cfgJSON, "", forceReload)
|
||||
if errors.Is(err, errSameConfig) {
|
||||
err = nil // not really an error
|
||||
}
|
||||
@@ -125,7 +126,12 @@ func Load(cfgJSON []byte, forceReload bool) error {
|
||||
// occur unless forceReload is true. If the config is unchanged and not
|
||||
// forcefully reloaded, then errConfigUnchanged This function is safe for
|
||||
// concurrent use.
|
||||
func changeConfig(method, path string, input []byte, forceReload bool) error {
|
||||
// The ifMatchHeader can optionally be given a string of the format:
|
||||
// "<path> <hash>"
|
||||
// where <path> is the absolute path in the config and <hash> is the expected hash of
|
||||
// the config at that path. If the hash in the ifMatchHeader doesn't match
|
||||
// the hash of the config, then an APIError with status 412 will be returned.
|
||||
func changeConfig(method, path string, input []byte, ifMatchHeader string, forceReload bool) error {
|
||||
switch method {
|
||||
case http.MethodGet,
|
||||
http.MethodHead,
|
||||
@@ -138,6 +144,40 @@ func changeConfig(method, path string, input []byte, forceReload bool) error {
|
||||
currentCfgMu.Lock()
|
||||
defer currentCfgMu.Unlock()
|
||||
|
||||
if ifMatchHeader != "" {
|
||||
// expect the first and last character to be quotes
|
||||
if len(ifMatchHeader) < 2 || ifMatchHeader[0] != '"' || ifMatchHeader[len(ifMatchHeader)-1] != '"' {
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("malformed If-Match header; expect quoted string"),
|
||||
}
|
||||
}
|
||||
|
||||
// read out the parts
|
||||
parts := strings.Fields(ifMatchHeader[1 : len(ifMatchHeader)-1])
|
||||
if len(parts) != 2 {
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("malformed If-Match header; expect format \"<path> <hash>\""),
|
||||
}
|
||||
}
|
||||
|
||||
// get the current hash of the config
|
||||
// at the given path
|
||||
hash := etagHasher()
|
||||
err := unsyncedConfigAccess(http.MethodGet, parts[0], nil, hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hex.EncodeToString(hash.Sum(nil)) != parts[1] {
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusPreconditionFailed,
|
||||
Err: fmt.Errorf("If-Match header did not match current config hash"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := unsyncedConfigAccess(method, path, input, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -500,7 +540,7 @@ func finishSettingUp(ctx Context, cfg *Config) error {
|
||||
|
||||
runLoadedConfig := func(config []byte) error {
|
||||
logger.Info("applying dynamically-loaded config")
|
||||
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, false)
|
||||
err := changeConfig(http.MethodPost, "/"+rawConfigKey, config, "", false)
|
||||
if errors.Is(err, errSameConfig) {
|
||||
return err
|
||||
}
|
||||
|
||||
+46
-1
@@ -58,6 +58,10 @@ func (al adminLoad) Routes() []caddy.AdminRoute {
|
||||
Pattern: "/load",
|
||||
Handler: caddy.AdminHandlerFunc(al.handleLoad),
|
||||
},
|
||||
{
|
||||
Pattern: "/adapt",
|
||||
Handler: caddy.AdminHandlerFunc(al.handleAdapt),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +126,48 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contenType.
|
||||
// handleAdapt adapts the given Caddy config to JSON and responds with the result.
|
||||
func (adminLoad) handleAdapt(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.Method != http.MethodPost {
|
||||
return caddy.APIError{
|
||||
HTTPStatus: http.StatusMethodNotAllowed,
|
||||
Err: fmt.Errorf("method not allowed"),
|
||||
}
|
||||
}
|
||||
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
defer bufPool.Put(buf)
|
||||
|
||||
_, err := io.Copy(buf, r.Body)
|
||||
if err != nil {
|
||||
return caddy.APIError{
|
||||
HTTPStatus: http.StatusBadRequest,
|
||||
Err: fmt.Errorf("reading request body: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
result, warnings, err := adaptByContentType(r.Header.Get("Content-Type"), buf.Bytes())
|
||||
if err != nil {
|
||||
return caddy.APIError{
|
||||
HTTPStatus: http.StatusBadRequest,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
out := struct {
|
||||
Warnings []Warning `json:"warnings,omitempty"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
}{
|
||||
Warnings: warnings,
|
||||
Result: result,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
return json.NewEncoder(w).Encode(out)
|
||||
}
|
||||
|
||||
// adaptByContentType adapts body to Caddy JSON using the adapter specified by contentType.
|
||||
// If contentType is empty or ends with "/json", the input will be returned, as a no-op.
|
||||
func adaptByContentType(contentType string, body []byte) ([]byte, []Warning, error) {
|
||||
// assume JSON as the default
|
||||
|
||||
@@ -63,32 +63,6 @@ app.example.com {
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"exclude": [
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Te",
|
||||
"Trailers",
|
||||
"Transfer-Encoding",
|
||||
"Upgrade"
|
||||
],
|
||||
"handler": "copy_response_headers"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "copy_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"handler": "reverse_proxy",
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
:8881
|
||||
|
||||
forward_auth localhost:9000 {
|
||||
uri /auth
|
||||
copy_headers A>1 B C>3 {
|
||||
D
|
||||
E>5
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8881"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handle_response": [
|
||||
{
|
||||
"match": {
|
||||
"status_code": [
|
||||
2
|
||||
]
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "headers",
|
||||
"request": {
|
||||
"set": {
|
||||
"1": [
|
||||
"{http.reverse_proxy.header.A}"
|
||||
],
|
||||
"3": [
|
||||
"{http.reverse_proxy.header.C}"
|
||||
],
|
||||
"5": [
|
||||
"{http.reverse_proxy.header.E}"
|
||||
],
|
||||
"B": [
|
||||
"{http.reverse_proxy.header.B}"
|
||||
],
|
||||
"D": [
|
||||
"{http.reverse_proxy.header.D}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"handler": "reverse_proxy",
|
||||
"headers": {
|
||||
"request": {
|
||||
"set": {
|
||||
"X-Forwarded-Method": [
|
||||
"{http.request.method}"
|
||||
],
|
||||
"X-Forwarded-Uri": [
|
||||
"{http.request.uri}"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"rewrite": {
|
||||
"method": "GET",
|
||||
"uri": "/auth"
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:9000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,9 @@ https://example.com {
|
||||
max_conns_per_host 5
|
||||
keepalive_idle_conns_per_host 2
|
||||
keepalive_interval 30s
|
||||
renegotiation freely
|
||||
|
||||
tls_renegotiation freely
|
||||
tls_except_ports 8181 8182
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +95,10 @@ https://example.com {
|
||||
},
|
||||
"response_header_timeout": 8000000000,
|
||||
"tls": {
|
||||
"except_ports": [
|
||||
"8181",
|
||||
"8182"
|
||||
],
|
||||
"renegotiation": "freely"
|
||||
},
|
||||
"versions": [
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
// 3. Run `go mod init caddy`
|
||||
// 4. Run `go install` or `go build` - you now have a custom binary!
|
||||
//
|
||||
// Or you can use xcaddy which does it all for you as a command:
|
||||
// https://github.com/caddyserver/xcaddy
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -10,11 +10,11 @@ require (
|
||||
github.com/caddyserver/certmagic v0.16.1
|
||||
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
|
||||
github.com/go-chi/chi v4.1.2+incompatible
|
||||
github.com/google/cel-go v0.7.3
|
||||
github.com/google/cel-go v0.11.4
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/klauspost/compress v1.15.4
|
||||
github.com/klauspost/cpuid/v2 v2.0.12
|
||||
github.com/lucas-clemente/quic-go v0.27.1
|
||||
github.com/klauspost/compress v1.15.6
|
||||
github.com/klauspost/cpuid/v2 v2.0.13
|
||||
github.com/lucas-clemente/quic-go v0.28.0
|
||||
github.com/mholt/acmez v1.0.2
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/smallstep/certificates v0.19.0
|
||||
@@ -30,10 +30,10 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.4.0
|
||||
go.uber.org/zap v1.21.0
|
||||
golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf
|
||||
google.golang.org/protobuf v1.27.1
|
||||
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21
|
||||
google.golang.org/protobuf v1.28.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
@@ -43,7 +43,7 @@ require (
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
@@ -81,8 +81,9 @@ require (
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/marten-seemann/qpack v0.2.1 // indirect
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
||||
github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
@@ -121,11 +122,11 @@ require (
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
|
||||
golang.org/x/tools v0.1.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/grpc v1.44.0 // indirect
|
||||
google.golang.org/grpc v1.46.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
|
||||
@@ -149,8 +149,8 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f h1:0cEys61Sr2hUBEXfNV8eyQP01oZuBgoMeHunebPirK8=
|
||||
github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed h1:ue9pVfIcP+QMEjfgo/Ez4ZjNZfonGgR6NgjMaJMu1Cg=
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220418222510-f25a4f6275ed/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
|
||||
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
|
||||
github.com/apache/beam v2.28.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o=
|
||||
github.com/apache/beam v2.30.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o=
|
||||
@@ -238,6 +238,7 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
@@ -311,6 +312,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE=
|
||||
@@ -390,6 +392,7 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -433,9 +436,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/cel-go v0.7.3 h1:8v9BSN0avuGwrHFKNCjfiQ/CE6+D6sW+BDyOVoEeP6o=
|
||||
github.com/google/cel-go v0.7.3/go.mod h1:4EtyFAHT5xNr0Msu0MJjyGxPUgdr9DlcaPyzLt/kkt8=
|
||||
github.com/google/cel-spec v0.5.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA=
|
||||
github.com/google/cel-go v0.11.4 h1:wWOnKmLxALl3l9Av221MfIOWRiR01sDVljzg6LZ6Zn0=
|
||||
github.com/google/cel-go v0.11.4/go.mod h1:Av7CU6r6X3YmcHR9GXqVDaEJYfEtSxl6wvIjUQTriCw=
|
||||
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||
github.com/google/certificate-transparency-go v1.1.2-0.20210422104406-9f33727a7a18/go.mod h1:6CKh9dscIRoqc2kC6YUFICHZMT9NrClyPrRVFrdw1QQ=
|
||||
github.com/google/certificate-transparency-go v1.1.2-0.20210512142713-bed466244fa6/go.mod h1:aF2dp7Dh81mY8Y/zpzyXps4fQW5zQbDu2CxfpJB6NkI=
|
||||
@@ -689,11 +691,11 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.15.4 h1:1kn4/7MepF/CHmYub99/nNX8az0IJjfSOU/jbnTVfqQ=
|
||||
github.com/klauspost/compress v1.15.4/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY=
|
||||
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.0.13 h1:1XxvOiqXZ8SULZUKim/wncr3wZ38H4yCuVDvKdK9OGs=
|
||||
github.com/klauspost/cpuid/v2 v2.0.13/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -720,8 +722,8 @@ github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
|
||||
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lucas-clemente/quic-go v0.27.1 h1:sOw+4kFSVrdWOYmUjufQ9GBVPqZ+tu+jMtXxXNmRJyk=
|
||||
github.com/lucas-clemente/quic-go v0.27.1/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
|
||||
github.com/lucas-clemente/quic-go v0.28.0 h1:9eXVRgIkMQQyiyorz/dAaOYIx3TFzXsIFkNFz4cxuJM=
|
||||
github.com/lucas-clemente/quic-go v0.28.0/go.mod h1:oGz5DKK41cJt5+773+BSO9BXDsREY4HLf7+0odGAPO0=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
@@ -735,10 +737,12 @@ github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0O
|
||||
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.1 h1:DQjHPq+aOzUeh9/lixAGunn6rIOQyWChPSI4+hgW7jc=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.1/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.1 h1:qp7p7XXUFL7fpBvSS1sWD+uSqPvzNQK43DH+/qEkj0Y=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.1/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.2 h1:JADBlm0LYiVbuSySCHeY863dNkcpMmDR7s0bLKJeYlQ=
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
|
||||
github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1 h1:7m/WlWcSROrcK5NxuXaxYD32BZqe/LEEnBrWcH/cOqQ=
|
||||
github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
@@ -1363,8 +1367,9 @@ golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -1506,8 +1511,9 @@ golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
@@ -1714,7 +1720,6 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
@@ -1754,8 +1759,9 @@ google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ6
|
||||
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf h1:SVYXkUz2yZS9FWb2Gm8ivSlbNQzL2Z/NpPKE3RG2jWk=
|
||||
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I=
|
||||
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
@@ -1793,8 +1799,9 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
|
||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
|
||||
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.44.0 h1:weqSxi/TMs1SqFRMHCtBgXRs8k3X39QIDEZ0pRcttUg=
|
||||
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
|
||||
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
@@ -1809,8 +1816,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -111,8 +111,6 @@ type App struct {
|
||||
// be forcefully closed.
|
||||
GracePeriod caddy.Duration `json:"grace_period,omitempty"`
|
||||
|
||||
Strict *StrictOptions `json:"strict,omitempty"`
|
||||
|
||||
// Servers is the list of servers, keyed by arbitrary names chosen
|
||||
// at your discretion for your own convenience; the keys do not
|
||||
// affect functionality.
|
||||
@@ -129,13 +127,6 @@ type App struct {
|
||||
allCertDomains []string
|
||||
}
|
||||
|
||||
type StrictOptions struct {
|
||||
Disabled bool `json:"disable,omitempty"`
|
||||
LenientQueryStrings bool `json:"lenient_query_strings,omitempty"`
|
||||
LenientPaths bool `json:"lenient_paths,omitempty"`
|
||||
LenientHeaders bool `json:"lenient_headers,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (App) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
@@ -171,7 +162,6 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
srv.tlsApp = app.tlsApp
|
||||
srv.logger = app.logger.Named("log")
|
||||
srv.errorLogger = app.logger.Named("log.error")
|
||||
srv.strict = app.Strict
|
||||
|
||||
// only enable access logs if configured
|
||||
if srv.Logs != nil {
|
||||
@@ -367,12 +357,10 @@ func (app *App) Start() error {
|
||||
return fmt.Errorf("getting HTTP/3 QUIC listener: %v", err)
|
||||
}
|
||||
h3srv := &http3.Server{
|
||||
Server: &http.Server{
|
||||
Addr: hostport,
|
||||
Handler: srv,
|
||||
TLSConfig: tlsCfg,
|
||||
ErrorLog: serverLogger,
|
||||
},
|
||||
Addr: hostport,
|
||||
Handler: srv,
|
||||
TLSConfig: tlsCfg,
|
||||
MaxHeaderBytes: srv.MaxHeaderBytes,
|
||||
}
|
||||
//nolint:errcheck
|
||||
go h3srv.ServeListener(h3ln)
|
||||
|
||||
+470
-11
@@ -17,6 +17,7 @@ package caddyhttp
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
@@ -28,11 +29,15 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/checker/decls"
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/operators"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/common/types/traits"
|
||||
"github.com/google/cel-go/ext"
|
||||
"github.com/google/cel-go/interpreter"
|
||||
"github.com/google/cel-go/interpreter/functions"
|
||||
"github.com/google/cel-go/parser"
|
||||
"go.uber.org/zap"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -96,6 +101,29 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
|
||||
// our type adapter expands CEL's standard type support
|
||||
m.ta = celTypeAdapter{}
|
||||
|
||||
// initialize the CEL libraries from the Matcher implementations which
|
||||
// have been configured to support CEL.
|
||||
matcherLibProducers := []CELLibraryProducer{}
|
||||
for _, info := range caddy.GetModules("http.matchers") {
|
||||
p, ok := info.New().(CELLibraryProducer)
|
||||
if ok {
|
||||
matcherLibProducers = append(matcherLibProducers, p)
|
||||
}
|
||||
}
|
||||
// Assemble the compilation and program options from the different library
|
||||
// producers into a single cel.Library implementation.
|
||||
matcherEnvOpts := []cel.EnvOption{}
|
||||
matcherProgramOpts := []cel.ProgramOption{}
|
||||
for _, producer := range matcherLibProducers {
|
||||
l, err := producer.CELLibrary(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error initializing CEL library for %T: %v", producer, err)
|
||||
}
|
||||
matcherEnvOpts = append(matcherEnvOpts, l.CompileOptions()...)
|
||||
matcherProgramOpts = append(matcherProgramOpts, l.ProgramOptions()...)
|
||||
}
|
||||
matcherLib := cel.Lib(NewMatcherCELLibrary(matcherEnvOpts, matcherProgramOpts))
|
||||
|
||||
// create the CEL environment
|
||||
env, err := cel.NewEnv(
|
||||
cel.Declarations(
|
||||
@@ -107,6 +135,7 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
|
||||
),
|
||||
cel.CustomTypeAdapter(m.ta),
|
||||
ext.Strings(),
|
||||
matcherLib,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up CEL environment: %v", err)
|
||||
@@ -114,7 +143,7 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
|
||||
|
||||
// parse and type-check the expression
|
||||
checked, issues := env.Compile(m.expandedExpr)
|
||||
if issues != nil && issues.Err() != nil {
|
||||
if issues.Err() != nil {
|
||||
return fmt.Errorf("compiling CEL program: %s", issues.Err())
|
||||
}
|
||||
|
||||
@@ -126,6 +155,7 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
|
||||
|
||||
// compile the "program"
|
||||
m.prg, err = env.Program(checked,
|
||||
cel.EvalOptions(cel.OptOptimize),
|
||||
cel.Functions(
|
||||
&functions.Overload{
|
||||
Operator: placeholderFuncName,
|
||||
@@ -133,7 +163,6 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling CEL program: %s", err)
|
||||
}
|
||||
@@ -142,18 +171,17 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
|
||||
|
||||
// Match returns true if r matches m.
|
||||
func (m MatchExpression) Match(r *http.Request) bool {
|
||||
out, _, err := m.prg.Eval(map[string]interface{}{
|
||||
"request": celHTTPRequest{r},
|
||||
})
|
||||
celReq := celHTTPRequest{r}
|
||||
out, _, err := m.prg.Eval(celReq)
|
||||
if err != nil {
|
||||
m.log.Error("evaluating expression", zap.Error(err))
|
||||
SetVar(r.Context(), MatcherErrorVarKey, err)
|
||||
return false
|
||||
}
|
||||
if outBool, ok := out.Value().(bool); ok {
|
||||
return outBool
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||
@@ -175,13 +203,15 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
|
||||
if !ok {
|
||||
return types.NewErr(
|
||||
"invalid request of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)",
|
||||
lhs.Type())
|
||||
lhs.Type(),
|
||||
)
|
||||
}
|
||||
phStr, ok := rhs.(types.String)
|
||||
if !ok {
|
||||
return types.NewErr(
|
||||
"invalid placeholder variable name of type '%v' to "+placeholderFuncName+"(request, placeholderVarName)",
|
||||
rhs.Type())
|
||||
rhs.Type(),
|
||||
)
|
||||
}
|
||||
|
||||
repl := celReq.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
@@ -193,10 +223,23 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
|
||||
// httpRequestCELType is the type representation of a native HTTP request.
|
||||
var httpRequestCELType = types.NewTypeValue("http.Request", traits.ReceiverType)
|
||||
|
||||
// cellHTTPRequest wraps an http.Request with
|
||||
// methods to satisfy the ref.Val interface.
|
||||
// celHTTPRequest wraps an http.Request with ref.Val interface methods.
|
||||
//
|
||||
// This type also implements the interpreter.Activation interface which
|
||||
// drops allocation costs for CEL expression evaluations by roughly half.
|
||||
type celHTTPRequest struct{ *http.Request }
|
||||
|
||||
func (cr celHTTPRequest) ResolveName(name string) (interface{}, bool) {
|
||||
if name == "request" {
|
||||
return cr, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (cr celHTTPRequest) Parent() interpreter.Activation {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
|
||||
return cr.Request, nil
|
||||
}
|
||||
@@ -250,12 +293,428 @@ func (celTypeAdapter) NativeToValue(value interface{}) ref.Val {
|
||||
return types.DefaultTypeAdapter.NativeToValue(value)
|
||||
}
|
||||
|
||||
// CELLibraryProducer provide CEL libraries that expose a Matcher
|
||||
// implementation as a first class function within the CEL expression
|
||||
// matcher.
|
||||
type CELLibraryProducer interface {
|
||||
// CELLibrary creates a cel.Library which makes it possible to use the
|
||||
// target object within CEL expression matchers.
|
||||
CELLibrary(caddy.Context) (cel.Library, error)
|
||||
}
|
||||
|
||||
// CELMatcherImpl creates a new cel.Library based on the following pieces of
|
||||
// data:
|
||||
//
|
||||
// - macroName: the function name to be used within CEL. This will be a macro
|
||||
// and not a function proper.
|
||||
// - funcName: the function overload name generated by the CEL macro used to
|
||||
// represent the matcher.
|
||||
// - matcherDataTypes: the argument types to the macro.
|
||||
// - fac: a matcherFactory implementation which converts from CEL constant
|
||||
// values to a Matcher instance.
|
||||
//
|
||||
// Note, macro names and function names must not collide with other macros or
|
||||
// functions exposed within CEL expressions, or an error will be produced
|
||||
// during the expression matcher plan time.
|
||||
//
|
||||
// The existing CELMatcherImpl support methods are configured to support a
|
||||
// limited set of function signatures. For strong type validation you may need
|
||||
// to provide a custom macro which does a more detailed analysis of the CEL
|
||||
// literal provided to the macro as an argument.
|
||||
func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*exprpb.Type, fac CELMatcherFactory) (cel.Library, error) {
|
||||
requestType := decls.NewObjectType("http.Request")
|
||||
var macro parser.Macro
|
||||
switch len(matcherDataTypes) {
|
||||
case 1:
|
||||
matcherDataType := matcherDataTypes[0]
|
||||
if isCELStringListType(matcherDataType) {
|
||||
macro = parser.NewGlobalVarArgMacro(macroName, celMatcherStringListMacroExpander(funcName))
|
||||
} else if isCELStringType(matcherDataType) {
|
||||
macro = parser.NewGlobalMacro(macroName, 1, celMatcherStringMacroExpander(funcName))
|
||||
} else if isCELJSONType(matcherDataType) {
|
||||
macro = parser.NewGlobalMacro(macroName, 1, celMatcherJSONMacroExpander(funcName))
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported matcher data type: %s", cel.FormatType(matcherDataType))
|
||||
}
|
||||
case 2:
|
||||
if isCELStringType(matcherDataTypes[0]) && isCELStringType(matcherDataTypes[1]) {
|
||||
macro = parser.NewGlobalMacro(macroName, 2, celMatcherStringListMacroExpander(funcName))
|
||||
matcherDataTypes = []*exprpb.Type{CelTypeListString}
|
||||
} else {
|
||||
return nil, fmt.Errorf(
|
||||
"unsupported matcher data type: %s, %s",
|
||||
cel.FormatType(matcherDataTypes[0]), cel.FormatType(matcherDataTypes[1]),
|
||||
)
|
||||
}
|
||||
case 3:
|
||||
if isCELStringType(matcherDataTypes[0]) && isCELStringType(matcherDataTypes[1]) && isCELStringType(matcherDataTypes[2]) {
|
||||
macro = parser.NewGlobalMacro(macroName, 3, celMatcherStringListMacroExpander(funcName))
|
||||
matcherDataTypes = []*exprpb.Type{CelTypeListString}
|
||||
} else {
|
||||
return nil, fmt.Errorf(
|
||||
"unsupported matcher data type: %s, %s, %s",
|
||||
cel.FormatType(matcherDataTypes[0]), cel.FormatType(matcherDataTypes[1]), cel.FormatType(matcherDataTypes[2]),
|
||||
)
|
||||
}
|
||||
}
|
||||
envOptions := []cel.EnvOption{
|
||||
cel.Macros(macro),
|
||||
cel.Declarations(
|
||||
decls.NewFunction(funcName,
|
||||
decls.NewOverload(
|
||||
funcName,
|
||||
append([]*exprpb.Type{requestType}, matcherDataTypes...),
|
||||
decls.Bool,
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
programOptions := []cel.ProgramOption{
|
||||
cel.CustomDecorator(CELMatcherDecorator(funcName, fac)),
|
||||
cel.Functions(
|
||||
&functions.Overload{
|
||||
Operator: funcName,
|
||||
Binary: CELMatcherRuntimeFunction(funcName, fac),
|
||||
},
|
||||
),
|
||||
}
|
||||
return NewMatcherCELLibrary(envOptions, programOptions), nil
|
||||
}
|
||||
|
||||
// CELMatcherFactory converts a constant CEL value into a RequestMatcher.
|
||||
type CELMatcherFactory func(data ref.Val) (RequestMatcher, error)
|
||||
|
||||
// matcherCELLibrary is a simplistic configurable cel.Library implementation.
|
||||
type matcherCELLibary struct {
|
||||
envOptions []cel.EnvOption
|
||||
programOptions []cel.ProgramOption
|
||||
}
|
||||
|
||||
// NewMatcherCELLibrary creates a matcherLibrary from option setes.
|
||||
func NewMatcherCELLibrary(envOptions []cel.EnvOption, programOptions []cel.ProgramOption) cel.Library {
|
||||
return &matcherCELLibary{
|
||||
envOptions: envOptions,
|
||||
programOptions: programOptions,
|
||||
}
|
||||
}
|
||||
|
||||
func (lib *matcherCELLibary) CompileOptions() []cel.EnvOption {
|
||||
return lib.envOptions
|
||||
}
|
||||
|
||||
func (lib *matcherCELLibary) ProgramOptions() []cel.ProgramOption {
|
||||
return lib.programOptions
|
||||
}
|
||||
|
||||
// CELMatcherDecorator matches a call overload generated by a CEL macro
|
||||
// that takes a single argument, and optimizes the implementation to precompile
|
||||
// the matcher and return a function that references the precompiled and
|
||||
// provisioned matcher.
|
||||
func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.InterpretableDecorator {
|
||||
return func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
|
||||
call, ok := i.(interpreter.InterpretableCall)
|
||||
if !ok {
|
||||
return i, nil
|
||||
}
|
||||
if call.OverloadID() != funcName {
|
||||
return i, nil
|
||||
}
|
||||
callArgs := call.Args()
|
||||
reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute)
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'request' argument")
|
||||
}
|
||||
nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute)
|
||||
if !ok {
|
||||
return nil, errors.New("missing 'request' argument")
|
||||
}
|
||||
varNames := nsAttr.CandidateVariableNames()
|
||||
if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != "request" {
|
||||
return nil, errors.New("missing 'request' argument")
|
||||
}
|
||||
matcherData, ok := callArgs[1].(interpreter.InterpretableConst)
|
||||
if !ok {
|
||||
// If the matcher arguments are not constant, then this means
|
||||
// they contain a Caddy placeholder reference and the evaluation
|
||||
// and matcher provisioning should be handled at dynamically.
|
||||
return i, nil
|
||||
}
|
||||
matcher, err := fac(matcherData.Value())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return interpreter.NewCall(
|
||||
i.ID(), funcName, funcName+"_opt",
|
||||
[]interpreter.Interpretable{reqAttr},
|
||||
func(args ...ref.Val) ref.Val {
|
||||
// The request value, guaranteed to be of type celHTTPRequest
|
||||
celReq := args[0]
|
||||
// If needed this call could be changed to convert the value
|
||||
// to a *http.Request using CEL's ConvertToNative method.
|
||||
httpReq := celReq.Value().(celHTTPRequest)
|
||||
return types.Bool(matcher.Match(httpReq.Request))
|
||||
},
|
||||
), nil
|
||||
}
|
||||
}
|
||||
|
||||
// CELMatcherRuntimeFunction creates a function binding for when the input to the matcher
|
||||
// is dynamically resolved rather than a set of static constant values.
|
||||
func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions.BinaryOp {
|
||||
return func(celReq, matcherData ref.Val) ref.Val {
|
||||
matcher, err := fac(matcherData)
|
||||
if err != nil {
|
||||
return types.NewErr(err.Error())
|
||||
}
|
||||
httpReq := celReq.Value().(celHTTPRequest)
|
||||
return types.Bool(matcher.Match(httpReq.Request))
|
||||
}
|
||||
}
|
||||
|
||||
// celMatcherStringListMacroExpander validates that the macro is called
|
||||
// with a variable number of string arguments (at least one).
|
||||
//
|
||||
// The arguments are collected into a single list argument the following
|
||||
// function call returned: <funcName>(request, [args])
|
||||
func celMatcherStringListMacroExpander(funcName string) parser.MacroExpander {
|
||||
return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
|
||||
matchArgs := []*exprpb.Expr{}
|
||||
if len(args) == 0 {
|
||||
return nil, &common.Error{
|
||||
Message: "matcher requires at least one argument",
|
||||
}
|
||||
}
|
||||
for _, arg := range args {
|
||||
if isCELStringExpr(arg) {
|
||||
matchArgs = append(matchArgs, arg)
|
||||
} else {
|
||||
return nil, &common.Error{
|
||||
Location: eh.OffsetLocation(arg.GetId()),
|
||||
Message: "matcher arguments must be string constants",
|
||||
}
|
||||
}
|
||||
}
|
||||
return eh.GlobalCall(funcName, eh.Ident("request"), eh.NewList(matchArgs...)), nil
|
||||
}
|
||||
}
|
||||
|
||||
// celMatcherStringMacroExpander validates that the macro is called a single
|
||||
// string argument.
|
||||
//
|
||||
// The following function call is returned: <funcName>(request, arg)
|
||||
func celMatcherStringMacroExpander(funcName string) parser.MacroExpander {
|
||||
return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
|
||||
if len(args) != 1 {
|
||||
return nil, &common.Error{
|
||||
Message: "matcher requires one argument",
|
||||
}
|
||||
}
|
||||
if isCELStringExpr(args[0]) {
|
||||
return eh.GlobalCall(funcName, eh.Ident("request"), args[0]), nil
|
||||
}
|
||||
return nil, &common.Error{
|
||||
Location: eh.OffsetLocation(args[0].GetId()),
|
||||
Message: "matcher argument must be a string literal",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// celMatcherStringMacroExpander validates that the macro is called a single
|
||||
// map literal argument.
|
||||
//
|
||||
// The following function call is returned: <funcName>(request, arg)
|
||||
func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander {
|
||||
return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
|
||||
if len(args) != 1 {
|
||||
return nil, &common.Error{
|
||||
Message: "matcher requires a map literal argument",
|
||||
}
|
||||
}
|
||||
arg := args[0]
|
||||
switch arg.GetExprKind().(type) {
|
||||
case *exprpb.Expr_StructExpr:
|
||||
structExpr := arg.GetStructExpr()
|
||||
if structExpr.GetMessageName() != "" {
|
||||
return nil, &common.Error{
|
||||
Location: eh.OffsetLocation(arg.GetId()),
|
||||
Message: fmt.Sprintf(
|
||||
"matcher input must be a map literal, not a %s",
|
||||
structExpr.GetMessageName(),
|
||||
),
|
||||
}
|
||||
}
|
||||
for _, entry := range structExpr.GetEntries() {
|
||||
isStringPlaceholder := isCELStringExpr(entry.GetMapKey())
|
||||
if !isStringPlaceholder {
|
||||
return nil, &common.Error{
|
||||
Location: eh.OffsetLocation(entry.GetId()),
|
||||
Message: "matcher map keys must be string literals",
|
||||
}
|
||||
}
|
||||
isStringListPlaceholder := isCELStringExpr(entry.GetValue()) ||
|
||||
isCELStringListLiteral(entry.GetValue())
|
||||
if !isStringListPlaceholder {
|
||||
return nil, &common.Error{
|
||||
Location: eh.OffsetLocation(entry.GetValue().GetId()),
|
||||
Message: "matcher map values must be string or list literals",
|
||||
}
|
||||
}
|
||||
}
|
||||
return eh.GlobalCall(funcName, eh.Ident("request"), arg), nil
|
||||
}
|
||||
|
||||
return nil, &common.Error{
|
||||
Location: eh.OffsetLocation(arg.GetId()),
|
||||
Message: "matcher requires a map literal argument",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CELValueToMapStrList converts a CEL value to a map[string][]string
|
||||
//
|
||||
// Earlier validation stages should guarantee that the value has this type
|
||||
// at compile time, and that the runtime value type is map[string]interface{}.
|
||||
// The reason for the slight difference in value type is that CEL allows for
|
||||
// map literals containing heterogeneous values, in this case string and list
|
||||
// of string.
|
||||
func CELValueToMapStrList(data ref.Val) (map[string][]string, error) {
|
||||
mapStrType := reflect.TypeOf(map[string]interface{}{})
|
||||
mapStrRaw, err := data.ConvertToNative(mapStrType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mapStrIface := mapStrRaw.(map[string]interface{})
|
||||
mapStrListStr := make(map[string][]string, len(mapStrIface))
|
||||
for k, v := range mapStrIface {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
mapStrListStr[k] = []string{val}
|
||||
case types.String:
|
||||
mapStrListStr[k] = []string{string(val)}
|
||||
case []string:
|
||||
mapStrListStr[k] = val
|
||||
case []ref.Val:
|
||||
convVals := make([]string, len(val))
|
||||
for i, elem := range val {
|
||||
strVal, ok := elem.(types.String)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported value type in header match: %T", val)
|
||||
}
|
||||
convVals[i] = string(strVal)
|
||||
}
|
||||
mapStrListStr[k] = convVals
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported value type in header match: %T", val)
|
||||
}
|
||||
}
|
||||
return mapStrListStr, nil
|
||||
}
|
||||
|
||||
// isCELJSONType returns whether the type corresponds to JSON input.
|
||||
func isCELJSONType(t *exprpb.Type) bool {
|
||||
switch t.GetTypeKind().(type) {
|
||||
case *exprpb.Type_MapType_:
|
||||
mapType := t.GetMapType()
|
||||
return isCELStringType(mapType.GetKeyType()) && mapType.GetValueType().GetDyn() != nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCELStringType returns whether the type corresponds to a string.
|
||||
func isCELStringType(t *exprpb.Type) bool {
|
||||
switch t.GetTypeKind().(type) {
|
||||
case *exprpb.Type_Primitive:
|
||||
return t.GetPrimitive() == exprpb.Type_STRING
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCELStringExpr indicates whether the expression is a supported string expression
|
||||
func isCELStringExpr(e *exprpb.Expr) bool {
|
||||
return isCELStringLiteral(e) || isCELCaddyPlaceholderCall(e) || isCELConcatCall(e)
|
||||
}
|
||||
|
||||
// isCELStringLiteral returns whether the expression is a CEL string literal.
|
||||
func isCELStringLiteral(e *exprpb.Expr) bool {
|
||||
switch e.GetExprKind().(type) {
|
||||
case *exprpb.Expr_ConstExpr:
|
||||
constant := e.GetConstExpr()
|
||||
switch constant.GetConstantKind().(type) {
|
||||
case *exprpb.Constant_StringValue:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call.
|
||||
func isCELCaddyPlaceholderCall(e *exprpb.Expr) bool {
|
||||
switch e.GetExprKind().(type) {
|
||||
case *exprpb.Expr_CallExpr:
|
||||
call := e.GetCallExpr()
|
||||
if call.GetFunction() == "caddyPlaceholder" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or
|
||||
// other concat call arguments.
|
||||
func isCELConcatCall(e *exprpb.Expr) bool {
|
||||
switch e.GetExprKind().(type) {
|
||||
case *exprpb.Expr_CallExpr:
|
||||
call := e.GetCallExpr()
|
||||
if call.GetTarget() != nil {
|
||||
return false
|
||||
}
|
||||
if call.GetFunction() != operators.Add {
|
||||
return false
|
||||
}
|
||||
for _, arg := range call.GetArgs() {
|
||||
if !isCELStringExpr(arg) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCELStringListType returns whether the type corresponds to a list of strings.
|
||||
func isCELStringListType(t *exprpb.Type) bool {
|
||||
switch t.GetTypeKind().(type) {
|
||||
case *exprpb.Type_ListType_:
|
||||
return isCELStringType(t.GetListType().GetElemType())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCELStringListLiteral returns whether the expression resolves to a list literal
|
||||
// containing only string constants or a placeholder call.
|
||||
func isCELStringListLiteral(e *exprpb.Expr) bool {
|
||||
switch e.GetExprKind().(type) {
|
||||
case *exprpb.Expr_ListExpr:
|
||||
list := e.GetListExpr()
|
||||
for _, elem := range list.GetElements() {
|
||||
if !isCELStringExpr(elem) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Variables used for replacing Caddy placeholders in CEL
|
||||
// expressions with a proper CEL function call; this is
|
||||
// just for syntactic sugar.
|
||||
var (
|
||||
placeholderRegexp = regexp.MustCompile(`{([\w.-]+)}`)
|
||||
placeholderRegexp = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`)
|
||||
placeholderExpansion = `caddyPlaceholder(request, "${1}")`
|
||||
|
||||
CelTypeListString = decls.NewListType(decls.String)
|
||||
CelTypeJson = decls.NewMapType(decls.String, decls.Dyn)
|
||||
)
|
||||
|
||||
var httpRequestObjectType = decls.NewObjectType("http.Request")
|
||||
|
||||
@@ -19,12 +19,462 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
clientCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
|
||||
eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
|
||||
A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
|
||||
iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
|
||||
z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
|
||||
fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
|
||||
BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
|
||||
AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
|
||||
eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||
3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
|
||||
9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
matcherTests = []struct {
|
||||
name string
|
||||
expression *MatchExpression
|
||||
urlTarget string
|
||||
httpMethod string
|
||||
httpHeader *http.Header
|
||||
wantErr bool
|
||||
wantResult bool
|
||||
clientCertificate []byte
|
||||
}{
|
||||
{
|
||||
name: "boolean matches succeed for placeholder http.request.tls.client.subject",
|
||||
expression: &MatchExpression{
|
||||
Expr: "{http.request.tls.client.subject} == 'CN=client.localdomain'",
|
||||
},
|
||||
clientCertificate: clientCert,
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "header matches (MatchHeader)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `header({'Field': 'foo'})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "header error (MatchHeader)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `header('foo')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "header_regexp matches (MatchHeaderRE)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `header_regexp('Field', 'fo{2}')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "header_regexp matches with name (MatchHeaderRE)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `header_regexp('foo', 'Field', 'fo{2}')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "header_regexp does not match (MatchHeaderRE)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `header_regexp('foo', 'Nope', 'fo{2}')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "header_regexp error (MatchHeaderRE)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `header_regexp('foo')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "host matches localhost (MatchHost)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `host('localhost')`,
|
||||
},
|
||||
urlTarget: "http://localhost",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "host matches (MatchHost)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `host('*.example.com')`,
|
||||
},
|
||||
urlTarget: "https://foo.example.com",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "host does not match (MatchHost)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `host('example.net', '*.example.com')`,
|
||||
},
|
||||
urlTarget: "https://foo.example.org",
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "host error (MatchHost)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `host(80)`,
|
||||
},
|
||||
urlTarget: "http://localhost:80",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "method does not match (MatchMethod)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `method('PUT')`,
|
||||
},
|
||||
urlTarget: "https://foo.example.com",
|
||||
httpMethod: "GET",
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "method matches (MatchMethod)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `method('DELETE', 'PUT', 'POST')`,
|
||||
},
|
||||
urlTarget: "https://foo.example.com",
|
||||
httpMethod: "PUT",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "method error not enough arguments (MatchMethod)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `method()`,
|
||||
},
|
||||
urlTarget: "https://foo.example.com",
|
||||
httpMethod: "PUT",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "path matches substring (MatchPath)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `path('*substring*')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo/substring/bar.txt",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "path does not match (MatchPath)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `path('/foo')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo/bar",
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "path matches end url fragment (MatchPath)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `path('/foo')`,
|
||||
},
|
||||
urlTarget: "https://example.com/FOO",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "path matches end fragment with substring prefix (MatchPath)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `path('/foo*')`,
|
||||
},
|
||||
urlTarget: "https://example.com/FOOOOO",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "path matches one of multiple (MatchPath)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `path('/foo', '/foo/*', '/bar', '/bar/*', '/baz', '/baz*')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "path_regexp with empty regex matches empty path (MatchPathRE)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `path_regexp('')`,
|
||||
},
|
||||
urlTarget: "https://example.com/",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "path_regexp with slash regex matches empty path (MatchPathRE)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `path_regexp('/')`,
|
||||
},
|
||||
urlTarget: "https://example.com/",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "path_regexp matches end url fragment (MatchPathRE)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `path_regexp('^/foo')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo/",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "path_regexp does not match fragment at end (MatchPathRE)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `path_regexp('bar_at_start', '^/bar')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo/bar",
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "protocol matches (MatchProtocol)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `protocol('HTTPs')`,
|
||||
},
|
||||
urlTarget: "https://example.com",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "protocol does not match (MatchProtocol)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `protocol('grpc')`,
|
||||
},
|
||||
urlTarget: "https://example.com",
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "protocol invocation error no args (MatchProtocol)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `protocol()`,
|
||||
},
|
||||
urlTarget: "https://example.com",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "protocol invocation error too many args (MatchProtocol)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `protocol('grpc', 'https')`,
|
||||
},
|
||||
urlTarget: "https://example.com",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "protocol invocation error wrong arg type (MatchProtocol)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `protocol(true)`,
|
||||
},
|
||||
urlTarget: "https://example.com",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "query does not match against a specific value (MatchQuery)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `query({"debug": "1"})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantResult: false,
|
||||
},
|
||||
{
|
||||
name: "query matches against a specific value (MatchQuery)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `query({"debug": "1"})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo/?debug=1",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "query matches against multiple values (MatchQuery)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `query({"debug": ["0", "1", {http.request.uri.query.debug}+"1"]})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo/?debug=1",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "query matches against a wildcard (MatchQuery)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `query({"debug": ["*"]})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo/?debug=something",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "query matches against a placeholder value (MatchQuery)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `query({"debug": {http.request.uri.query.debug}})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo/?debug=1",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "query error bad map key type (MatchQuery)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `query({1: "1"})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "query error typed struct instead of map (MatchQuery)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `query(Message{field: "1"})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "query error bad map value type (MatchQuery)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `query({"debug": 1})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo/?debug=1",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "query error no args (MatchQuery)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `query()`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo/?debug=1",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "remote_ip error no args (MatchRemoteIP)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `remote_ip()`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "remote_ip single IP match (MatchRemoteIP)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `remote_ip('192.0.2.1')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "remote_ip forwarded (MatchRemoteIP)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `remote_ip('forwarded', '192.0.2.1')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "remote_ip forwarded not first (MatchRemoteIP)",
|
||||
expression: &MatchExpression{
|
||||
Expr: `remote_ip('192.0.2.1', 'forwarded')`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestMatchExpressionMatch(t *testing.T) {
|
||||
for _, tst := range matcherTests {
|
||||
tc := tst
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.expression.Provision(caddy.Context{})
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
|
||||
if tc.httpHeader != nil {
|
||||
req.Header = *tc.httpHeader
|
||||
}
|
||||
repl := caddy.NewReplacer()
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
|
||||
|
||||
if tc.clientCertificate != nil {
|
||||
block, _ := pem.Decode(clientCert)
|
||||
if block == nil {
|
||||
t.Fatalf("failed to decode PEM certificate")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode PEM certificate: %v", err)
|
||||
}
|
||||
|
||||
req.TLS = &tls.ConnectionState{
|
||||
PeerCertificates: []*x509.Certificate{cert},
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expression.Match(req) != tc.wantResult {
|
||||
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMatchExpressionMatch(b *testing.B) {
|
||||
for _, tst := range matcherTests {
|
||||
tc := tst
|
||||
if tc.wantErr {
|
||||
continue
|
||||
}
|
||||
b.Run(tst.name, func(b *testing.B) {
|
||||
tc.expression.Provision(caddy.Context{})
|
||||
req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
|
||||
if tc.httpHeader != nil {
|
||||
req.Header = *tc.httpHeader
|
||||
}
|
||||
repl := caddy.NewReplacer()
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
|
||||
if tc.clientCertificate != nil {
|
||||
block, _ := pem.Decode(clientCert)
|
||||
if block == nil {
|
||||
b.Fatalf("failed to decode PEM certificate")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
b.Fatalf("failed to decode PEM certificate: %v", err)
|
||||
}
|
||||
|
||||
req.TLS = &tls.ConnectionState{
|
||||
PeerCertificates: []*x509.Certificate{cert},
|
||||
}
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tc.expression.Match(req)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchExpressionProvision(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -54,71 +504,3 @@ func TestMatchExpressionProvision(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchExpressionMatch(t *testing.T) {
|
||||
|
||||
clientCert := []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk
|
||||
eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG
|
||||
A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
|
||||
iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF
|
||||
z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+
|
||||
fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ
|
||||
BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A
|
||||
AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+
|
||||
eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||
3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH
|
||||
9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g=
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expression *MatchExpression
|
||||
wantErr bool
|
||||
wantResult bool
|
||||
clientCertificate []byte
|
||||
}{
|
||||
{
|
||||
name: "boolean matches succeed for placeholder http.request.tls.client.subject",
|
||||
expression: &MatchExpression{
|
||||
Expr: "{http.request.tls.client.subject} == 'CN=client.localdomain'",
|
||||
},
|
||||
clientCertificate: clientCert,
|
||||
wantResult: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.expression.Provision(caddy.Context{}); (err != nil) != tt.wantErr {
|
||||
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "https://example.com/foo", nil)
|
||||
repl := caddy.NewReplacer()
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
|
||||
|
||||
if tt.clientCertificate != nil {
|
||||
block, _ := pem.Decode(clientCert)
|
||||
if block == nil {
|
||||
t.Fatalf("failed to decode PEM certificate")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode PEM certificate: %v", err)
|
||||
}
|
||||
|
||||
req.TLS = &tls.ConnectionState{
|
||||
PeerCertificates: []*x509.Certificate{cert},
|
||||
}
|
||||
}
|
||||
|
||||
if tt.expression.Match(req) != tt.wantResult {
|
||||
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tt.wantResult, tt.expression.Expr)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +67,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
|
||||
if r.URL.Path == "" || path.Base(origReq.URL.Path) == path.Base(r.URL.Path) {
|
||||
if !strings.HasSuffix(origReq.URL.Path, "/") {
|
||||
fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path))
|
||||
origReq.URL.Path += "/"
|
||||
http.Redirect(w, r, origReq.URL.String(), http.StatusMovedPermanently)
|
||||
return nil
|
||||
return redirect(w, r, origReq.URL.Path+"/")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,14 @@ import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/checker/decls"
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/operators"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/interpreter/functions"
|
||||
"github.com/google/cel-go/parser"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -139,6 +147,110 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
// Example:
|
||||
// expression file({'root': '/srv', 'try_files': [{http.request.uri.path}, '/index.php'], 'try_policy': 'first_exist', 'split_path': ['.php']})
|
||||
func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
requestType := decls.NewObjectType("http.Request")
|
||||
envOptions := []cel.EnvOption{
|
||||
cel.Macros(parser.NewGlobalVarArgMacro("file", celFileMatcherMacroExpander())),
|
||||
cel.Declarations(
|
||||
decls.NewFunction("file",
|
||||
decls.NewOverload("file_request_map",
|
||||
[]*exprpb.Type{requestType, caddyhttp.CelTypeJson},
|
||||
decls.Bool,
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) {
|
||||
values, err := caddyhttp.CELValueToMapStrList(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var root string
|
||||
if len(values["root"]) > 0 {
|
||||
root = values["root"][0]
|
||||
}
|
||||
|
||||
var try_policy string
|
||||
if len(values["try_policy"]) > 0 {
|
||||
root = values["try_policy"][0]
|
||||
}
|
||||
|
||||
m := MatchFile{
|
||||
Root: root,
|
||||
TryFiles: values["try_files"],
|
||||
TryPolicy: try_policy,
|
||||
SplitPath: values["split_path"],
|
||||
}
|
||||
|
||||
err = m.Provision(ctx)
|
||||
return m, err
|
||||
}
|
||||
|
||||
programOptions := []cel.ProgramOption{
|
||||
cel.CustomDecorator(caddyhttp.CELMatcherDecorator("file_request_map", matcherFactory)),
|
||||
cel.Functions(
|
||||
&functions.Overload{
|
||||
Operator: "file_request_map",
|
||||
Binary: caddyhttp.CELMatcherRuntimeFunction("file_request_map", matcherFactory),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
return caddyhttp.NewMatcherCELLibrary(envOptions, programOptions), nil
|
||||
}
|
||||
|
||||
func celFileMatcherMacroExpander() parser.MacroExpander {
|
||||
return func(eh parser.ExprHelper, target *exprpb.Expr, args []*exprpb.Expr) (*exprpb.Expr, *common.Error) {
|
||||
if len(args) == 0 {
|
||||
return nil, &common.Error{
|
||||
Message: "matcher requires at least one argument",
|
||||
}
|
||||
}
|
||||
if len(args) == 1 {
|
||||
arg := args[0]
|
||||
if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) {
|
||||
return eh.GlobalCall("file",
|
||||
eh.Ident("request"),
|
||||
eh.NewMap(
|
||||
eh.NewMapEntry(eh.LiteralString("try_files"), eh.NewList(arg)),
|
||||
),
|
||||
), nil
|
||||
}
|
||||
if isCELTryFilesLiteral(arg) {
|
||||
return eh.GlobalCall("file", eh.Ident("request"), arg), nil
|
||||
}
|
||||
return nil, &common.Error{
|
||||
Location: eh.OffsetLocation(arg.GetId()),
|
||||
Message: "matcher requires either a map or string literal argument",
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
if !(isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg)) {
|
||||
return nil, &common.Error{
|
||||
Location: eh.OffsetLocation(arg.GetId()),
|
||||
Message: "matcher only supports repeated string literal arguments",
|
||||
}
|
||||
}
|
||||
}
|
||||
return eh.GlobalCall("file",
|
||||
eh.Ident("request"),
|
||||
eh.NewMap(
|
||||
eh.NewMapEntry(
|
||||
eh.LiteralString("try_files"), eh.NewList(args...),
|
||||
),
|
||||
),
|
||||
), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up m's defaults.
|
||||
func (m *MatchFile) Provision(_ caddy.Context) error {
|
||||
if m.Root == "" {
|
||||
@@ -359,6 +471,107 @@ func indexFold(haystack, needle string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
// isCELMapLiteral returns whether the expression resolves to a map literal containing
|
||||
// only string keys with or a placeholder call.
|
||||
func isCELTryFilesLiteral(e *exprpb.Expr) bool {
|
||||
switch e.GetExprKind().(type) {
|
||||
case *exprpb.Expr_StructExpr:
|
||||
structExpr := e.GetStructExpr()
|
||||
if structExpr.GetMessageName() != "" {
|
||||
return false
|
||||
}
|
||||
for _, entry := range structExpr.GetEntries() {
|
||||
mapKey := entry.GetMapKey()
|
||||
mapVal := entry.GetValue()
|
||||
if !isCELStringLiteral(mapKey) {
|
||||
return false
|
||||
}
|
||||
mapKeyStr := mapKey.GetConstExpr().GetStringValue()
|
||||
if mapKeyStr == "try_files" || mapKeyStr == "split_path" {
|
||||
if !isCELStringListLiteral(mapVal) {
|
||||
return false
|
||||
}
|
||||
} else if mapKeyStr == "try_policy" || mapKeyStr == "root" {
|
||||
if !(isCELStringExpr(mapVal)) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCELStringExpr indicates whether the expression is a supported string expression
|
||||
func isCELStringExpr(e *exprpb.Expr) bool {
|
||||
return isCELStringLiteral(e) || isCELCaddyPlaceholderCall(e) || isCELConcatCall(e)
|
||||
}
|
||||
|
||||
// isCELStringLiteral returns whether the expression is a CEL string literal.
|
||||
func isCELStringLiteral(e *exprpb.Expr) bool {
|
||||
switch e.GetExprKind().(type) {
|
||||
case *exprpb.Expr_ConstExpr:
|
||||
constant := e.GetConstExpr()
|
||||
switch constant.GetConstantKind().(type) {
|
||||
case *exprpb.Constant_StringValue:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCELCaddyPlaceholderCall returns whether the expression is a caddy placeholder call.
|
||||
func isCELCaddyPlaceholderCall(e *exprpb.Expr) bool {
|
||||
switch e.GetExprKind().(type) {
|
||||
case *exprpb.Expr_CallExpr:
|
||||
call := e.GetCallExpr()
|
||||
if call.GetFunction() == "caddyPlaceholder" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCELConcatCall tests whether the expression is a concat function (+) with string, placeholder, or
|
||||
// other concat call arguments.
|
||||
func isCELConcatCall(e *exprpb.Expr) bool {
|
||||
switch e.GetExprKind().(type) {
|
||||
case *exprpb.Expr_CallExpr:
|
||||
call := e.GetCallExpr()
|
||||
if call.GetTarget() != nil {
|
||||
return false
|
||||
}
|
||||
if call.GetFunction() != operators.Add {
|
||||
return false
|
||||
}
|
||||
for _, arg := range call.GetArgs() {
|
||||
if !isCELStringExpr(arg) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isCELStringListLiteral returns whether the expression resolves to a list literal
|
||||
// containing only string constants or a placeholder call.
|
||||
func isCELStringListLiteral(e *exprpb.Expr) bool {
|
||||
switch e.GetExprKind().(type) {
|
||||
case *exprpb.Expr_ListExpr:
|
||||
list := e.GetListExpr()
|
||||
for _, elem := range list.GetElements() {
|
||||
if !isCELStringExpr(elem) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
tryPolicyFirstExist = "first_exist"
|
||||
tryPolicyLargestSize = "largest_size"
|
||||
@@ -368,6 +581,7 @@ const (
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Validator = (*MatchFile)(nil)
|
||||
_ caddyhttp.RequestMatcher = (*MatchFile)(nil)
|
||||
_ caddy.Validator = (*MatchFile)(nil)
|
||||
_ caddyhttp.RequestMatcher = (*MatchFile)(nil)
|
||||
_ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
|
||||
)
|
||||
|
||||
@@ -15,12 +15,15 @@
|
||||
package fileserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
@@ -259,3 +262,109 @@ func TestFirstSplit(t *testing.T) {
|
||||
t.Errorf("Expected remainder %s but got %s", expectedRemainder, remainder)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
expressionTests = []struct {
|
||||
name string
|
||||
expression *caddyhttp.MatchExpression
|
||||
urlTarget string
|
||||
httpMethod string
|
||||
httpHeader *http.Header
|
||||
wantErr bool
|
||||
wantResult bool
|
||||
clientCertificate []byte
|
||||
}{
|
||||
{
|
||||
name: "file error no args (MatchFile)",
|
||||
expression: &caddyhttp.MatchExpression{
|
||||
Expr: `file()`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "file error bad try files (MatchFile)",
|
||||
expression: &caddyhttp.MatchExpression{
|
||||
Expr: `file({"try_file": ["bad_arg"]})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "file match short pattern index.php (MatchFile)",
|
||||
expression: &caddyhttp.MatchExpression{
|
||||
Expr: `file("index.php")`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "file match short pattern foo.txt (MatchFile)",
|
||||
expression: &caddyhttp.MatchExpression{
|
||||
Expr: `file({http.request.uri.path})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo.txt",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "file match index.php (MatchFile)",
|
||||
expression: &caddyhttp.MatchExpression{
|
||||
Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "file match long pattern foo.txt (MatchFile)",
|
||||
expression: &caddyhttp.MatchExpression{
|
||||
Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo.txt",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "file match long pattern foo.txt with concatenation (MatchFile)",
|
||||
expression: &caddyhttp.MatchExpression{
|
||||
Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`,
|
||||
},
|
||||
urlTarget: "https://example.com/foo.txt",
|
||||
wantResult: true,
|
||||
},
|
||||
{
|
||||
name: "file not match long pattern (MatchFile)",
|
||||
expression: &caddyhttp.MatchExpression{
|
||||
Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`,
|
||||
},
|
||||
urlTarget: "https://example.com/nopenope.txt",
|
||||
wantResult: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestMatchExpressionMatch(t *testing.T) {
|
||||
for _, tst := range expressionTests {
|
||||
tc := tst
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.expression.Provision(caddy.Context{})
|
||||
if err != nil {
|
||||
if !tc.wantErr {
|
||||
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(tc.httpMethod, tc.urlTarget, nil)
|
||||
if tc.httpHeader != nil {
|
||||
req.Header = *tc.httpHeader
|
||||
}
|
||||
repl := caddyhttp.NewTestReplacer(req)
|
||||
repl.Set("http.vars.root", "./testdata")
|
||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
if tc.expression.Match(req) != tc.wantResult {
|
||||
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +118,16 @@ type HeaderOps struct {
|
||||
// Sets HTTP headers; replaces existing header fields.
|
||||
Set http.Header `json:"set,omitempty"`
|
||||
|
||||
// Names of HTTP header fields to delete.
|
||||
// Names of HTTP header fields to delete. Basic wildcards are supported:
|
||||
//
|
||||
// - Start with `*` for all field names with the given suffix;
|
||||
// - End with `*` for all field names with the given prefix;
|
||||
// - Start and end with `*` for all field names containing a substring.
|
||||
Delete []string `json:"delete,omitempty"`
|
||||
|
||||
// Performs substring replacements of HTTP headers in-situ.
|
||||
// Performs in-situ substring replacements of HTTP headers.
|
||||
// Keys are the field names on which to perform the associated replacements.
|
||||
// If the field name is `*`, the replacements are performed on all header fields.
|
||||
Replace map[string][]Replacement `json:"replace,omitempty"`
|
||||
}
|
||||
|
||||
@@ -188,38 +194,60 @@ type RespHeaderOps struct {
|
||||
func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
|
||||
// add
|
||||
for fieldName, vals := range ops.Add {
|
||||
fieldName = repl.ReplaceAll(fieldName, "")
|
||||
fieldName = repl.ReplaceKnown(fieldName, "")
|
||||
for _, v := range vals {
|
||||
hdr.Add(fieldName, repl.ReplaceAll(v, ""))
|
||||
hdr.Add(fieldName, repl.ReplaceKnown(v, ""))
|
||||
}
|
||||
}
|
||||
|
||||
// set
|
||||
for fieldName, vals := range ops.Set {
|
||||
fieldName = repl.ReplaceAll(fieldName, "")
|
||||
fieldName = repl.ReplaceKnown(fieldName, "")
|
||||
var newVals []string
|
||||
for i := range vals {
|
||||
// append to new slice so we don't overwrite
|
||||
// the original values in ops.Set
|
||||
newVals = append(newVals, repl.ReplaceAll(vals[i], ""))
|
||||
newVals = append(newVals, repl.ReplaceKnown(vals[i], ""))
|
||||
}
|
||||
hdr.Set(fieldName, strings.Join(newVals, ","))
|
||||
}
|
||||
|
||||
// delete
|
||||
for _, fieldName := range ops.Delete {
|
||||
hdr.Del(repl.ReplaceAll(fieldName, ""))
|
||||
fieldName = strings.ToLower(repl.ReplaceKnown(fieldName, ""))
|
||||
switch {
|
||||
case strings.HasPrefix(fieldName, "*") && strings.HasSuffix(fieldName, "*"):
|
||||
for existingField := range hdr {
|
||||
if strings.Contains(strings.ToLower(existingField), fieldName[1:len(fieldName)-1]) {
|
||||
delete(hdr, existingField)
|
||||
}
|
||||
}
|
||||
case strings.HasPrefix(fieldName, "*"):
|
||||
for existingField := range hdr {
|
||||
if strings.HasSuffix(strings.ToLower(existingField), fieldName[1:]) {
|
||||
delete(hdr, existingField)
|
||||
}
|
||||
}
|
||||
case strings.HasSuffix(fieldName, "*"):
|
||||
for existingField := range hdr {
|
||||
if strings.HasPrefix(strings.ToLower(existingField), fieldName[:len(fieldName)-1]) {
|
||||
delete(hdr, existingField)
|
||||
}
|
||||
}
|
||||
default:
|
||||
hdr.Del(fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
// replace
|
||||
for fieldName, replacements := range ops.Replace {
|
||||
fieldName = http.CanonicalHeaderKey(repl.ReplaceAll(fieldName, ""))
|
||||
fieldName = http.CanonicalHeaderKey(repl.ReplaceKnown(fieldName, ""))
|
||||
|
||||
// all fields...
|
||||
if fieldName == "*" {
|
||||
for _, r := range replacements {
|
||||
search := repl.ReplaceAll(r.Search, "")
|
||||
replace := repl.ReplaceAll(r.Replace, "")
|
||||
search := repl.ReplaceKnown(r.Search, "")
|
||||
replace := repl.ReplaceKnown(r.Replace, "")
|
||||
for fieldName, vals := range hdr {
|
||||
for i := range vals {
|
||||
if r.re != nil {
|
||||
@@ -235,8 +263,8 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
|
||||
|
||||
// ...or only with the named field
|
||||
for _, r := range replacements {
|
||||
search := repl.ReplaceAll(r.Search, "")
|
||||
replace := repl.ReplaceAll(r.Replace, "")
|
||||
search := repl.ReplaceKnown(r.Search, "")
|
||||
replace := repl.ReplaceKnown(r.Replace, "")
|
||||
for hdrFieldName, vals := range hdr {
|
||||
// see issue #4330 for why we don't simply use hdr[fieldName]
|
||||
if http.CanonicalHeaderKey(hdrFieldName) != fieldName {
|
||||
|
||||
@@ -79,6 +79,26 @@ func TestHandler(t *testing.T) {
|
||||
"Keep-Me": []string{"i swear i'm innocent"},
|
||||
},
|
||||
},
|
||||
{
|
||||
handler: Handler{
|
||||
Request: &HeaderOps{
|
||||
Delete: []string{
|
||||
"*-suffix",
|
||||
"prefix-*",
|
||||
"*_*",
|
||||
},
|
||||
},
|
||||
},
|
||||
reqHeader: http.Header{
|
||||
"Header-Suffix": []string{"lalala"},
|
||||
"Prefix-Test": []string{"asdf"},
|
||||
"Host_Header": []string{"silly django... sigh"}, // see issue #4830
|
||||
"Keep-Me": []string{"foofoofoo"},
|
||||
},
|
||||
expectedReqHeader: http.Header{
|
||||
"Keep-Me": []string{"foofoofoo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
handler: Handler{
|
||||
Request: &HeaderOps{
|
||||
|
||||
+296
-13
@@ -16,6 +16,7 @@ package caddyhttp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -30,7 +32,12 @@ import (
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/checker/decls"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"go.uber.org/zap"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -291,6 +298,29 @@ outer:
|
||||
return false
|
||||
}
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
// Example:
|
||||
// expression host('localhost')
|
||||
func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
return CELMatcherImpl(
|
||||
"host",
|
||||
"host_match_request_list",
|
||||
[]*exprpb.Type{CelTypeListString},
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher := MatchHost(strList.([]string))
|
||||
err = matcher.Provision(ctx)
|
||||
return matcher, err
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// fuzzy returns true if the given hostname h is not a specific
|
||||
// hostname, e.g. has placeholders or wildcards.
|
||||
func (MatchHost) fuzzy(h string) bool { return strings.ContainsAny(h, "{*") }
|
||||
@@ -396,6 +426,33 @@ func (m MatchPath) Match(r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
// Example:
|
||||
// expression path('*substring*', '*suffix')
|
||||
func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
return CELMatcherImpl(
|
||||
// name of the macro, this is the function name that users see when writing expressions.
|
||||
"path",
|
||||
// name of the function that the macro will be rewritten to call.
|
||||
"path_match_request_list",
|
||||
// internal data type of the MatchPath value.
|
||||
[]*exprpb.Type{CelTypeListString},
|
||||
// function to convert a constant list of strings to a MatchPath instance.
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher := MatchPath(strList.([]string))
|
||||
err = matcher.Provision(ctx)
|
||||
return matcher, err
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||
func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
@@ -440,6 +497,50 @@ func (m MatchPathRE) Match(r *http.Request) bool {
|
||||
return m.MatchRegexp.Match(cleanedPath, repl)
|
||||
}
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
// Example:
|
||||
// expression path_regexp('^/bar')
|
||||
func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
unnamedPattern, err := CELMatcherImpl(
|
||||
"path_regexp",
|
||||
"path_regexp_request_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
pattern := data.(types.String)
|
||||
matcher := MatchPathRE{MatchRegexp{Pattern: string(pattern)}}
|
||||
err := matcher.Provision(ctx)
|
||||
return matcher, err
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namedPattern, err := CELMatcherImpl(
|
||||
"path_regexp",
|
||||
"path_regexp_request_string_string",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
params, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
strParams := params.([]string)
|
||||
matcher := MatchPathRE{MatchRegexp{Name: strParams[0], Pattern: strParams[1]}}
|
||||
err = matcher.Provision(ctx)
|
||||
return matcher, err
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...)
|
||||
prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...)
|
||||
return NewMatcherCELLibrary(envOpts, prgOpts), nil
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (MatchMethod) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
@@ -469,6 +570,27 @@ func (m MatchMethod) Match(r *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
// Example:
|
||||
// expression method('PUT', 'POST')
|
||||
func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) {
|
||||
return CELMatcherImpl(
|
||||
"method",
|
||||
"method_request_list",
|
||||
[]*exprpb.Type{CelTypeListString},
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return MatchMethod(strList.([]string)), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (MatchQuery) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
@@ -503,26 +625,41 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// Match returns true if r matches m. An empty m matches an empty query string.
|
||||
func (m MatchQuery) Match(r *http.Request) bool {
|
||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
reqQuery, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for param, vals := range m {
|
||||
param = repl.ReplaceAll(param, "")
|
||||
paramVal, found := reqQuery[param]
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
for _, v := range vals {
|
||||
v = repl.ReplaceAll(v, "")
|
||||
if paramVal[0] == v || v == "*" {
|
||||
return true
|
||||
paramVal, found := r.URL.Query()[param]
|
||||
if found {
|
||||
for _, v := range vals {
|
||||
v = repl.ReplaceAll(v, "")
|
||||
if paramVal[0] == v || v == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(m) == 0 && len(r.URL.Query()) == 0
|
||||
}
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
// Example:
|
||||
// expression query({'sort': 'asc'}) || query({'foo': ['*bar*', 'baz']})
|
||||
func (MatchQuery) CELLibrary(_ caddy.Context) (cel.Library, error) {
|
||||
return CELMatcherImpl(
|
||||
"query",
|
||||
"query_matcher_request_map",
|
||||
[]*exprpb.Type{CelTypeJson},
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
mapStrListStr, err := CELValueToMapStrList(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return MatchQuery(url.Values(mapStrListStr)), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (MatchHeader) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
@@ -578,6 +715,27 @@ func (m MatchHeader) Match(r *http.Request) bool {
|
||||
return matchHeaders(r.Header, http.Header(m), r.Host, repl)
|
||||
}
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
// Example:
|
||||
// expression header({'content-type': 'image/png'})
|
||||
// expression header({'foo': ['bar', 'baz']}) // match bar or baz
|
||||
func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) {
|
||||
return CELMatcherImpl(
|
||||
"header",
|
||||
"header_matcher_request_map",
|
||||
[]*exprpb.Type{CelTypeJson},
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
mapStrListStr, err := CELValueToMapStrList(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return MatchHeader(http.Header(mapStrListStr)), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// getHeaderFieldVals returns the field values for the given fieldName from input.
|
||||
// The host parameter should be obtained from the http.Request.Host field since
|
||||
// net/http removes it from the header map.
|
||||
@@ -715,6 +873,57 @@ func (m MatchHeaderRE) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
// Example:
|
||||
// expression header_regexp('foo', 'Field', 'fo+')
|
||||
func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
unnamedPattern, err := CELMatcherImpl(
|
||||
"header_regexp",
|
||||
"header_regexp_request_string_string",
|
||||
[]*exprpb.Type{decls.String, decls.String},
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
params, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
strParams := params.([]string)
|
||||
matcher := MatchHeaderRE{}
|
||||
matcher[strParams[0]] = &MatchRegexp{Pattern: strParams[1], Name: ""}
|
||||
err = matcher.Provision(ctx)
|
||||
return matcher, err
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namedPattern, err := CELMatcherImpl(
|
||||
"header_regexp",
|
||||
"header_regexp_request_string_string_string",
|
||||
[]*exprpb.Type{decls.String, decls.String, decls.String},
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
params, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
strParams := params.([]string)
|
||||
matcher := MatchHeaderRE{}
|
||||
matcher[strParams[1]] = &MatchRegexp{Pattern: strParams[2], Name: strParams[0]}
|
||||
err = matcher.Provision(ctx)
|
||||
return matcher, err
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...)
|
||||
prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...)
|
||||
return NewMatcherCELLibrary(envOpts, prgOpts), nil
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (MatchProtocol) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
@@ -748,6 +957,26 @@ func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
// Example:
|
||||
// expression protocol('https')
|
||||
func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) {
|
||||
return CELMatcherImpl(
|
||||
"protocol",
|
||||
"protocol_request_string",
|
||||
[]*exprpb.Type{decls.String},
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
protocolStr, ok := data.(types.String)
|
||||
if !ok {
|
||||
return nil, errors.New("protocol argument was not a string")
|
||||
}
|
||||
return MatchProtocol(strings.ToLower(string(protocolStr))), nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (MatchNot) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
@@ -892,6 +1121,46 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CELLibrary produces options that expose this matcher for use in CEL
|
||||
// expression matchers.
|
||||
//
|
||||
// Example:
|
||||
// expression remote_ip('forwarded', '192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8')
|
||||
func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
|
||||
return CELMatcherImpl(
|
||||
// name of the macro, this is the function name that users see when writing expressions.
|
||||
"remote_ip",
|
||||
// name of the function that the macro will be rewritten to call.
|
||||
"remote_ip_match_request_list",
|
||||
// internal data type of the MatchPath value.
|
||||
[]*exprpb.Type{CelTypeListString},
|
||||
// function to convert a constant list of strings to a MatchPath instance.
|
||||
func(data ref.Val) (RequestMatcher, error) {
|
||||
refStringList := reflect.TypeOf([]string{})
|
||||
strList, err := data.ConvertToNative(refStringList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := MatchRemoteIP{}
|
||||
|
||||
for _, input := range strList.([]string) {
|
||||
if input == "forwarded" {
|
||||
if len(m.Ranges) > 0 {
|
||||
return nil, errors.New("if used, 'forwarded' must be first argument")
|
||||
}
|
||||
m.Forwarded = true
|
||||
continue
|
||||
}
|
||||
m.Ranges = append(m.Ranges, input)
|
||||
}
|
||||
|
||||
err = m.Provision(ctx)
|
||||
return m, err
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Provision parses m's IP ranges, either from IP or CIDR expressions.
|
||||
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
|
||||
m.logger = ctx.Logger(m)
|
||||
@@ -1067,7 +1336,9 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var wordRE = regexp.MustCompile(`\w+`)
|
||||
var (
|
||||
wordRE = regexp.MustCompile(`\w+`)
|
||||
)
|
||||
|
||||
const regexpPlaceholderPrefix = "http.regexp"
|
||||
|
||||
@@ -1108,6 +1379,18 @@ var (
|
||||
_ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
|
||||
_ caddyfile.Unmarshaler = (*MatchVarsRE)(nil)
|
||||
|
||||
_ CELLibraryProducer = (*MatchHost)(nil)
|
||||
_ CELLibraryProducer = (*MatchPath)(nil)
|
||||
_ CELLibraryProducer = (*MatchPathRE)(nil)
|
||||
_ CELLibraryProducer = (*MatchMethod)(nil)
|
||||
_ CELLibraryProducer = (*MatchQuery)(nil)
|
||||
_ CELLibraryProducer = (*MatchHeader)(nil)
|
||||
_ CELLibraryProducer = (*MatchHeaderRE)(nil)
|
||||
_ CELLibraryProducer = (*MatchProtocol)(nil)
|
||||
_ CELLibraryProducer = (*MatchRemoteIP)(nil)
|
||||
// _ CELLibraryProducer = (*VarsMatcher)(nil)
|
||||
// _ CELLibraryProducer = (*MatchVarsRE)(nil)
|
||||
|
||||
_ json.Marshaler = (*MatchNot)(nil)
|
||||
_ json.Unmarshaler = (*MatchNot)(nil)
|
||||
)
|
||||
|
||||
@@ -624,18 +624,9 @@ func TestQueryMatcher(t *testing.T) {
|
||||
input: "/?somekey=1",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid query string",
|
||||
match: MatchQuery{"test": []string{"*"}},
|
||||
input: "/?test=1;",
|
||||
expect: false,
|
||||
},
|
||||
} {
|
||||
u, err := url.Parse(tc.input)
|
||||
if err != nil {
|
||||
t.Errorf("Test %d: Parsing URL: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
u, _ := url.Parse(tc.input)
|
||||
|
||||
req := &http.Request{URL: u}
|
||||
repl := caddy.NewReplacer()
|
||||
|
||||
@@ -814,6 +814,8 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error
|
||||
// tls_timeout <duration>
|
||||
// tls_trusted_ca_certs <cert_files...>
|
||||
// tls_server_name <sni>
|
||||
// tls_renegotiation <level>
|
||||
// tls_except_ports <ports...>
|
||||
// keepalive [off|<duration>]
|
||||
// keepalive_interval <interval>
|
||||
// keepalive_idle_conns <max_count>
|
||||
@@ -907,6 +909,11 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
return d.Errf("must specify at least one resolver address")
|
||||
}
|
||||
|
||||
case "tls":
|
||||
if h.TLS == nil {
|
||||
h.TLS = new(TLSConfig)
|
||||
}
|
||||
|
||||
case "tls_client_auth":
|
||||
if h.TLS == nil {
|
||||
h.TLS = new(TLSConfig)
|
||||
@@ -922,25 +929,6 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
case "renegotiation":
|
||||
if h.TLS == nil {
|
||||
h.TLS = new(TLSConfig)
|
||||
}
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
switch renegotiation := d.Val(); renegotiation {
|
||||
case "never", "once", "freely":
|
||||
h.TLS.Renegotiation = renegotiation
|
||||
default:
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
case "tls":
|
||||
if h.TLS == nil {
|
||||
h.TLS = new(TLSConfig)
|
||||
}
|
||||
|
||||
case "tls_insecure_skip_verify":
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
@@ -982,6 +970,29 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
}
|
||||
h.TLS.ServerName = d.Val()
|
||||
|
||||
case "tls_renegotiation":
|
||||
if h.TLS == nil {
|
||||
h.TLS = new(TLSConfig)
|
||||
}
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
switch renegotiation := d.Val(); renegotiation {
|
||||
case "never", "once", "freely":
|
||||
h.TLS.Renegotiation = renegotiation
|
||||
default:
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
case "tls_except_ports":
|
||||
if h.TLS == nil {
|
||||
h.TLS = new(TLSConfig)
|
||||
}
|
||||
h.TLS.ExceptPorts = d.RemainingArgs()
|
||||
if len(h.TLS.ExceptPorts) == 0 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
case "keepalive":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
|
||||
@@ -80,7 +80,7 @@ func (h CopyResponseHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request
|
||||
hrc.isFinalized = true
|
||||
|
||||
// write the response
|
||||
return hrc.handler.finalizeResponse(rw, req, hrc.response, repl, hrc.start, hrc.logger, false)
|
||||
return hrc.handler.finalizeResponse(rw, req, hrc.response, repl, hrc.start, hrc.logger)
|
||||
}
|
||||
|
||||
// CopyResponseHeadersHandler is a special HTTP handler which may
|
||||
|
||||
@@ -17,6 +17,7 @@ package forwardauth
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
@@ -58,13 +59,6 @@ func init() {
|
||||
// Remote-Email {http.reverse_proxy.header.Remote-Email}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// handle_response {
|
||||
// copy_response_headers {
|
||||
// exclude Connection Keep-Alive Te Trailers Transfer-Encoding Upgrade
|
||||
// }
|
||||
// copy_response
|
||||
// }
|
||||
// }
|
||||
//
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
|
||||
@@ -115,7 +109,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
|
||||
// collect the headers to copy from the auth response
|
||||
// onto the original request, so they can get passed
|
||||
// through to a backend app
|
||||
headersToCopy := []string{}
|
||||
headersToCopy := make(map[string]string)
|
||||
|
||||
// read the subdirectives for configuring the forward_auth shortcut
|
||||
// NOTE: we delete the tokens as we go so that the reverse_proxy
|
||||
@@ -141,10 +135,28 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
|
||||
|
||||
case "copy_headers":
|
||||
args := dispenser.RemainingArgs()
|
||||
dispenser.Delete()
|
||||
for _, headerField := range args {
|
||||
hadBlock := false
|
||||
for nesting := dispenser.Nesting(); dispenser.NextBlock(nesting); {
|
||||
hadBlock = true
|
||||
args = append(args, dispenser.Val())
|
||||
}
|
||||
|
||||
dispenser.Delete() // directive name
|
||||
if hadBlock {
|
||||
dispenser.Delete() // opening brace
|
||||
dispenser.Delete() // closing brace
|
||||
}
|
||||
for range args {
|
||||
dispenser.Delete()
|
||||
headersToCopy = append(headersToCopy, headerField)
|
||||
}
|
||||
|
||||
for _, headerField := range args {
|
||||
if strings.Contains(headerField, ">") {
|
||||
parts := strings.Split(headerField, ">")
|
||||
headersToCopy[parts[0]] = parts[1]
|
||||
} else {
|
||||
headersToCopy[headerField] = headerField
|
||||
}
|
||||
}
|
||||
if len(headersToCopy) == 0 {
|
||||
return nil, dispenser.ArgErr()
|
||||
@@ -173,66 +185,40 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
|
||||
},
|
||||
Routes: []caddyhttp.Route{},
|
||||
}
|
||||
if len(headersToCopy) > 0 {
|
||||
handler := &headers.Handler{
|
||||
Request: &headers.HeaderOps{
|
||||
Set: http.Header{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, headerField := range headersToCopy {
|
||||
handler.Request.Set[headerField] = []string{
|
||||
"{http.reverse_proxy.header." + headerField + "}",
|
||||
}
|
||||
}
|
||||
|
||||
goodResponseHandler.Routes = append(
|
||||
goodResponseHandler.Routes,
|
||||
caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
||||
handler,
|
||||
"handler",
|
||||
"headers",
|
||||
nil,
|
||||
)},
|
||||
},
|
||||
)
|
||||
}
|
||||
rpHandler.HandleResponse = append(rpHandler.HandleResponse, goodResponseHandler)
|
||||
|
||||
// set up handler for denial responses; when a response
|
||||
// has any other status than 2xx, then we copy the response
|
||||
// back to the client, and terminate handling.
|
||||
denialResponseHandler := caddyhttp.ResponseHandler{
|
||||
Routes: []caddyhttp.Route{
|
||||
{
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
||||
&reverseproxy.CopyResponseHeadersHandler{
|
||||
Exclude: []string{
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Te",
|
||||
"Trailers",
|
||||
"Transfer-Encoding",
|
||||
"Upgrade",
|
||||
},
|
||||
},
|
||||
"handler",
|
||||
"copy_response_headers",
|
||||
nil,
|
||||
)},
|
||||
},
|
||||
{
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
||||
&reverseproxy.CopyResponseHandler{},
|
||||
"handler",
|
||||
"copy_response",
|
||||
nil,
|
||||
)},
|
||||
},
|
||||
handler := &headers.Handler{
|
||||
Request: &headers.HeaderOps{
|
||||
Set: http.Header{},
|
||||
},
|
||||
}
|
||||
rpHandler.HandleResponse = append(rpHandler.HandleResponse, denialResponseHandler)
|
||||
|
||||
// the list of headers to copy may be empty, but that's okay; we
|
||||
// need at least one handler in the routes for the response handling
|
||||
// logic in reverse_proxy to not skip this entry as empty.
|
||||
for from, to := range headersToCopy {
|
||||
handler.Request.Set[to] = []string{
|
||||
"{http.reverse_proxy.header." + from + "}",
|
||||
}
|
||||
}
|
||||
|
||||
goodResponseHandler.Routes = append(
|
||||
goodResponseHandler.Routes,
|
||||
caddyhttp.Route{
|
||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
||||
handler,
|
||||
"handler",
|
||||
"headers",
|
||||
nil,
|
||||
)},
|
||||
},
|
||||
)
|
||||
|
||||
// note that when a response has any other status than 2xx, then we
|
||||
// use the reverse proxy's default behaviour of copying the response
|
||||
// back to the client, so we don't need to explicitly add a response
|
||||
// handler specifically for that behaviour; we do need the 2xx handler
|
||||
// though, to make handling fall through to handlers deeper in the chain.
|
||||
rpHandler.HandleResponse = append(rpHandler.HandleResponse, goodResponseHandler)
|
||||
|
||||
// the rest of the config is specified by the user
|
||||
// using the reverse_proxy directive syntax
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
@@ -242,9 +243,45 @@ func (h *HTTPTransport) NewTransport(ctx caddy.Context) (*http.Transport, error)
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
// replaceTLSServername checks TLS servername to see if it needs replacing
|
||||
// if it does need replacing, it creates a new cloned HTTPTransport object to avoid any races
|
||||
// and does the replacing of the TLS servername on that and returns the new object
|
||||
// if no replacement is necessary it returns the original
|
||||
func (h *HTTPTransport) replaceTLSServername(repl *caddy.Replacer) *HTTPTransport {
|
||||
// check whether we have TLS and need to replace the servername in the TLSClientConfig
|
||||
if h.TLSEnabled() && strings.Contains(h.TLS.ServerName, "{") {
|
||||
// make a new h, "copy" the parts we don't need to touch, add a new *tls.Config and replace servername
|
||||
newtransport := &HTTPTransport{
|
||||
Resolver: h.Resolver,
|
||||
TLS: h.TLS,
|
||||
KeepAlive: h.KeepAlive,
|
||||
Compression: h.Compression,
|
||||
MaxConnsPerHost: h.MaxConnsPerHost,
|
||||
DialTimeout: h.DialTimeout,
|
||||
FallbackDelay: h.FallbackDelay,
|
||||
ResponseHeaderTimeout: h.ResponseHeaderTimeout,
|
||||
ExpectContinueTimeout: h.ExpectContinueTimeout,
|
||||
MaxResponseHeaderSize: h.MaxResponseHeaderSize,
|
||||
WriteBufferSize: h.WriteBufferSize,
|
||||
ReadBufferSize: h.ReadBufferSize,
|
||||
Versions: h.Versions,
|
||||
Transport: h.Transport.Clone(),
|
||||
h2cTransport: h.h2cTransport,
|
||||
}
|
||||
newtransport.Transport.TLSClientConfig.ServerName = repl.ReplaceAll(newtransport.Transport.TLSClientConfig.ServerName, "")
|
||||
return newtransport
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper.
|
||||
func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
h.SetScheme(req)
|
||||
// Try to replace TLS servername if needed
|
||||
repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
transport := h.replaceTLSServername(repl)
|
||||
|
||||
transport.setScheme(req)
|
||||
|
||||
// if H2C ("HTTP/2 over cleartext") is enabled and the upstream request is
|
||||
// HTTP without TLS, use the alternate H2C-capable transport instead
|
||||
@@ -252,19 +289,37 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return h.h2cTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
return h.Transport.RoundTrip(req)
|
||||
return transport.Transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
// SetScheme ensures that the outbound request req
|
||||
// setScheme ensures that the outbound request req
|
||||
// has the scheme set in its URL; the underlying
|
||||
// http.Transport requires a scheme to be set.
|
||||
func (h *HTTPTransport) SetScheme(req *http.Request) {
|
||||
if req.URL.Scheme == "" {
|
||||
func (h *HTTPTransport) setScheme(req *http.Request) {
|
||||
if req.URL.Scheme != "" {
|
||||
return
|
||||
}
|
||||
if h.shouldUseTLS(req) {
|
||||
req.URL.Scheme = "https"
|
||||
} else {
|
||||
req.URL.Scheme = "http"
|
||||
if h.TLS != nil {
|
||||
req.URL.Scheme = "https"
|
||||
}
|
||||
}
|
||||
|
||||
// shouldUseTLS returns true if TLS should be used for req.
|
||||
func (h *HTTPTransport) shouldUseTLS(req *http.Request) bool {
|
||||
if h.TLS == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
port := req.URL.Port()
|
||||
for i := range h.TLS.ExceptPorts {
|
||||
if h.TLS.ExceptPorts[i] == port {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// TLSEnabled returns true if TLS is enabled.
|
||||
@@ -332,6 +387,13 @@ type TLSConfig struct {
|
||||
// - "once": allows a remote server to request renegotiation once per connection.
|
||||
// - "freely": allows a remote server to repeatedly request renegotiation.
|
||||
Renegotiation string `json:"renegotiation,omitempty"`
|
||||
|
||||
// Skip TLS ports specifies a list of upstream ports on which TLS should not be
|
||||
// attempted even if it is configured. Handy when using dynamic upstreams that
|
||||
// return HTTP and HTTPS endpoints too.
|
||||
// When specified, TLS will automatically be configured on the transport.
|
||||
// The value can be a list of any valid tcp port numbers, default empty.
|
||||
ExceptPorts []string `json:"except_ports,omitempty"`
|
||||
}
|
||||
|
||||
// MakeTLSClientConfig returns a tls.Config usable by a client to a backend.
|
||||
@@ -403,7 +465,7 @@ func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
|
||||
|
||||
// Renegotiation
|
||||
switch t.Renegotiation {
|
||||
case "never":
|
||||
case "never", "":
|
||||
cfg.Renegotiation = tls.RenegotiateNever
|
||||
case "once":
|
||||
cfg.Renegotiation = tls.RenegotiateOnceAsClient
|
||||
|
||||
@@ -477,7 +477,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
|
||||
upstream := h.LoadBalancing.SelectionPolicy.Select(upstreams, r, w)
|
||||
if upstream == nil {
|
||||
if proxyErr == nil {
|
||||
proxyErr = fmt.Errorf("no upstreams available")
|
||||
proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, fmt.Errorf("no upstreams available"))
|
||||
}
|
||||
if !h.LoadBalancing.tryAgain(h.ctx, start, proxyErr, r) {
|
||||
return true, proxyErr
|
||||
@@ -784,18 +784,14 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
|
||||
res.Body = h.bufferedBody(res.Body)
|
||||
}
|
||||
|
||||
// the response body may get closed by a response handler,
|
||||
// and we need to keep track to make sure we don't try to copy
|
||||
// the response if it was already closed
|
||||
bodyClosed := false
|
||||
|
||||
// see if any response handler is configured for this response from the backend
|
||||
for i, rh := range h.HandleResponse {
|
||||
if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) {
|
||||
continue
|
||||
}
|
||||
|
||||
// if configured to only change the status code, do that then continue regular proxy response
|
||||
// if configured to only change the status code,
|
||||
// do that then continue regular proxy response
|
||||
if statusCodeStr := rh.StatusCode.String(); statusCodeStr != "" {
|
||||
statusCode, err := strconv.Atoi(repl.ReplaceAll(statusCodeStr, ""))
|
||||
if err != nil {
|
||||
@@ -840,33 +836,29 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
|
||||
// pass the request through the response handler routes
|
||||
routeErr := rh.Routes.Compile(next).ServeHTTP(rw, origReq.WithContext(ctx))
|
||||
|
||||
// if the response handler routes already finalized the response,
|
||||
// we can return early. It should be finalized if the routes executed
|
||||
// included a copy_response handler. If a fresh response was written
|
||||
// by the routes instead, then we still need to finalize the response
|
||||
// without copying the body.
|
||||
if routeErr == nil && hrc.isFinalized {
|
||||
return nil
|
||||
// close the response body afterwards, since we don't need it anymore;
|
||||
// either a route had 'copy_response' which already consumed the body,
|
||||
// or some other terminal handler ran which doesn't need the response
|
||||
// body after that point (e.g. 'file_server' for X-Accel-Redirect flow),
|
||||
// or we fell through to subsequent handlers past this proxy
|
||||
// (e.g. forward auth's 2xx response flow).
|
||||
if !hrc.isFinalized {
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
// always close the response body afterwards, since it's expected
|
||||
// that the response handler routes will have written to the
|
||||
// response writer with a new body, if it wasn't already finalized.
|
||||
res.Body.Close()
|
||||
bodyClosed = true
|
||||
|
||||
// wrap any route error in roundtripSucceeded so caller knows that
|
||||
// the roundtrip was successful and to not retry
|
||||
if routeErr != nil {
|
||||
// wrap error in roundtripSucceeded so caller knows that
|
||||
// the roundtrip was successful and to not retry
|
||||
return roundtripSucceeded{routeErr}
|
||||
}
|
||||
|
||||
// we've already closed the body, so there's no use allowing
|
||||
// another response handler to run as well
|
||||
break
|
||||
// we're done handling the response, and we don't want to
|
||||
// fall through to the default finalize/copy behaviour
|
||||
return nil
|
||||
}
|
||||
|
||||
return h.finalizeResponse(rw, req, res, repl, start, logger, bodyClosed)
|
||||
// copy the response body and headers back to the upstream client
|
||||
return h.finalizeResponse(rw, req, res, repl, start, logger)
|
||||
}
|
||||
|
||||
// finalizeResponse prepares and copies the response.
|
||||
@@ -877,7 +869,6 @@ func (h Handler) finalizeResponse(
|
||||
repl *caddy.Replacer,
|
||||
start time.Time,
|
||||
logger *zap.Logger,
|
||||
bodyClosed bool,
|
||||
) error {
|
||||
// deal with 101 Switching Protocols responses: (WebSocket, h2c, etc)
|
||||
if res.StatusCode == http.StatusSwitchingProtocols {
|
||||
@@ -891,13 +882,6 @@ func (h Handler) finalizeResponse(
|
||||
res.Header.Del(h)
|
||||
}
|
||||
|
||||
// remove the content length if we're not going to be copying
|
||||
// from the response, because otherwise there'll be a mismatch
|
||||
// between bytes written and the advertised length
|
||||
if bodyClosed {
|
||||
res.Header.Del("Content-Length")
|
||||
}
|
||||
|
||||
// apply any response header operations
|
||||
if h.Headers != nil && h.Headers.Response != nil {
|
||||
if h.Headers.Response.Require == nil ||
|
||||
@@ -920,17 +904,16 @@ func (h Handler) finalizeResponse(
|
||||
}
|
||||
|
||||
rw.WriteHeader(res.StatusCode)
|
||||
if !bodyClosed {
|
||||
err := h.copyResponse(rw, res.Body, h.flushInterval(req, res))
|
||||
res.Body.Close() // close now, instead of defer, to populate res.Trailer
|
||||
if err != nil {
|
||||
// we're streaming the response and we've already written headers, so
|
||||
// there's nothing an error handler can do to recover at this point;
|
||||
// the standard lib's proxy panics at this point, but we'll just log
|
||||
// the error and abort the stream here
|
||||
h.logger.Error("aborting with incomplete response", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
err := h.copyResponse(rw, res.Body, h.flushInterval(req, res))
|
||||
res.Body.Close() // close now, instead of defer, to populate res.Trailer
|
||||
if err != nil {
|
||||
// we're streaming the response and we've already written headers, so
|
||||
// there's nothing an error handler can do to recover at this point;
|
||||
// the standard lib's proxy panics at this point, but we'll just log
|
||||
// the error and abort the stream here
|
||||
h.logger.Error("aborting with incomplete response", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(res.Trailer) > 0 {
|
||||
|
||||
@@ -132,7 +132,6 @@ type Server struct {
|
||||
primaryHandlerChain Handler
|
||||
errorHandlerChain Handler
|
||||
listenerWrappers []caddy.ListenerWrapper
|
||||
strict *StrictOptions
|
||||
|
||||
tlsApp *caddytls.TLS
|
||||
logger *zap.Logger
|
||||
@@ -316,46 +315,9 @@ func (s *Server) enforcementHandler(w http.ResponseWriter, r *http.Request, next
|
||||
return Error(http.StatusMisdirectedRequest, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.strict.enforce(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (strict *StrictOptions) enforce(r *http.Request) error {
|
||||
if strict != nil && strict.Disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reject query strings with unencoded ;
|
||||
if strict == nil || !strict.LenientQueryStrings {
|
||||
_, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return Error(http.StatusBadRequest, fmt.Errorf("invalid query string: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Reject header fields with _ in name (#4830)
|
||||
if strict == nil || !strict.LenientHeaders {
|
||||
for field := range r.Header {
|
||||
if strings.Contains(field, "_") {
|
||||
return Error(http.StatusBadRequest, fmt.Errorf("invalid header field name: %s", field))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reject paths with // or ..
|
||||
if strict == nil || !strict.LenientPaths {
|
||||
if strings.Contains(r.URL.Path, "//") || strings.Contains(r.URL.Path, "..") || strings.Contains(r.URL.Path, "\x00") {
|
||||
return Error(http.StatusBadRequest, fmt.Errorf("invalid request path: %s", r.URL.RawPath))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// listenersUseAnyPortOtherThan returns true if there are any
|
||||
// listeners in s that use a port which is not otherPort.
|
||||
func (s *Server) listenersUseAnyPortOtherThan(otherPort int) bool {
|
||||
|
||||
@@ -85,9 +85,11 @@ type ACMEIssuer struct {
|
||||
PreferredChains *ChainPreference `json:"preferred_chains,omitempty"`
|
||||
|
||||
rootPool *x509.CertPool
|
||||
template certmagic.ACMEIssuer
|
||||
magic *certmagic.Config
|
||||
logger *zap.Logger
|
||||
|
||||
template certmagic.ACMEIssuer // set at Provision
|
||||
magic *certmagic.Config // set at PreCheck
|
||||
issuer *certmagic.ACMEIssuer // set at PreCheck; result of template + magic
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
@@ -217,30 +219,27 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) {
|
||||
// the ConfigSetter interface.
|
||||
func (iss *ACMEIssuer) SetConfig(cfg *certmagic.Config) {
|
||||
iss.magic = cfg
|
||||
iss.issuer = certmagic.NewACMEIssuer(cfg, iss.template)
|
||||
}
|
||||
|
||||
// TODO: I kind of hate how each call to these methods needs to
|
||||
// make a new ACME manager to fill in defaults before using; can
|
||||
// we find the right place to do that just once and then re-use?
|
||||
|
||||
// PreCheck implements the certmagic.PreChecker interface.
|
||||
func (iss *ACMEIssuer) PreCheck(ctx context.Context, names []string, interactive bool) error {
|
||||
return certmagic.NewACMEIssuer(iss.magic, iss.template).PreCheck(ctx, names, interactive)
|
||||
return iss.issuer.PreCheck(ctx, names, interactive)
|
||||
}
|
||||
|
||||
// Issue obtains a certificate for the given csr.
|
||||
func (iss *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
|
||||
return certmagic.NewACMEIssuer(iss.magic, iss.template).Issue(ctx, csr)
|
||||
return iss.issuer.Issue(ctx, csr)
|
||||
}
|
||||
|
||||
// IssuerKey returns the unique issuer key for the configured CA endpoint.
|
||||
func (iss *ACMEIssuer) IssuerKey() string {
|
||||
return certmagic.NewACMEIssuer(iss.magic, iss.template).IssuerKey()
|
||||
return iss.issuer.IssuerKey()
|
||||
}
|
||||
|
||||
// Revoke revokes the given certificate.
|
||||
func (iss *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource, reason int) error {
|
||||
return certmagic.NewACMEIssuer(iss.magic, iss.template).Revoke(ctx, cert, reason)
|
||||
return iss.issuer.Revoke(ctx, cert, reason)
|
||||
}
|
||||
|
||||
// GetACMEIssuer returns iss. This is useful when other types embed ACMEIssuer, because
|
||||
|
||||
@@ -336,7 +336,7 @@ func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
|
||||
for _, iss := range ap.magic.Issuers {
|
||||
if am, ok := iss.(acmeCapable); ok {
|
||||
iss := am.GetACMEIssuer()
|
||||
if certmagic.NewACMEIssuer(iss.magic, iss.template).HandleHTTPChallenge(w, r) {
|
||||
if iss.issuer.HandleHTTPChallenge(w, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,8 +162,8 @@ func (iss *ZeroSSLIssuer) generateEABCredentials(ctx context.Context, acct acme.
|
||||
func (iss *ZeroSSLIssuer) initialize() {
|
||||
iss.mu.Lock()
|
||||
defer iss.mu.Unlock()
|
||||
if iss.template.NewAccountFunc == nil {
|
||||
iss.template.NewAccountFunc = iss.newAccountCallback
|
||||
if iss.ACMEIssuer.issuer.NewAccountFunc == nil {
|
||||
iss.ACMEIssuer.issuer.NewAccountFunc = iss.newAccountCallback
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user