mirror of
https://github.com/caddyserver/caddy.git
synced 2026-06-24 01:50:19 +00:00
* caddyhttp: add url_pattern request matcher Match requests against a URLPattern (https://urlpattern.spec.whatwg.org/), supporting named groups, wildcards and regexp components beyond the path matcher. Relative patterns match any origin; absolute patterns or base_url scope to scheme and host. Exposes a url_pattern CEL function and publishes captured groups as {http.url_pattern.<component>.<group>} placeholders. * caddyhttp: add Caddyfile adapt test for url_pattern matcher
241 lines
5.8 KiB
Go
241 lines
5.8 KiB
Go
package caddyhttp
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
)
|
|
|
|
func TestURLPatternMatcher(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
match MatchURLPattern
|
|
host string
|
|
tls bool
|
|
input string
|
|
expect bool
|
|
provisionErr bool
|
|
}{
|
|
{
|
|
name: "literal path matches",
|
|
match: MatchURLPattern{Pattern: "/foo"},
|
|
host: "example.com",
|
|
input: "/foo",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "literal path mismatch",
|
|
match: MatchURLPattern{Pattern: "/foo"},
|
|
host: "example.com",
|
|
input: "/bar",
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "named group matches",
|
|
match: MatchURLPattern{Pattern: "/books/:id"},
|
|
host: "example.com",
|
|
input: "/books/123",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "named group requires segment",
|
|
match: MatchURLPattern{Pattern: "/books/:id"},
|
|
host: "example.com",
|
|
input: "/books",
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "wildcard spans segments",
|
|
match: MatchURLPattern{Pattern: "/files/*"},
|
|
host: "example.com",
|
|
input: "/files/a/b/c",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "absolute pattern matches host and scheme",
|
|
match: MatchURLPattern{Pattern: "https://example.com/foo"},
|
|
host: "example.com",
|
|
tls: true,
|
|
input: "/foo",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "absolute pattern rejects scheme mismatch",
|
|
match: MatchURLPattern{Pattern: "https://example.com/foo"},
|
|
host: "example.com",
|
|
input: "/foo",
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "absolute pattern rejects host mismatch",
|
|
match: MatchURLPattern{Pattern: "https://example.com/foo"},
|
|
host: "other.com",
|
|
tls: true,
|
|
input: "/foo",
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "ignore_case matches mixed case",
|
|
match: MatchURLPattern{Pattern: "/foo", IgnoreCase: true},
|
|
host: "example.com",
|
|
input: "/FOO",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "case sensitive by default",
|
|
match: MatchURLPattern{Pattern: "/foo"},
|
|
host: "example.com",
|
|
input: "/FOO",
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "base_url scopes to host",
|
|
match: MatchURLPattern{Pattern: "/search", BaseURL: "https://example.com"},
|
|
host: "example.com",
|
|
tls: true,
|
|
input: "/search?q=caddy",
|
|
expect: true,
|
|
},
|
|
{
|
|
name: "base_url rejects other host",
|
|
match: MatchURLPattern{Pattern: "/search", BaseURL: "https://example.com"},
|
|
host: "other.com",
|
|
tls: true,
|
|
input: "/search",
|
|
expect: false,
|
|
},
|
|
{
|
|
name: "invalid pattern fails provisioning",
|
|
match: MatchURLPattern{Pattern: "https://[invalid"},
|
|
provisionErr: true,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.match.Provision(caddy.Context{})
|
|
if tc.provisionErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
u, err := url.ParseRequestURI(tc.input)
|
|
require.NoError(t, err)
|
|
|
|
req := &http.Request{URL: u, Host: tc.host}
|
|
if tc.tls {
|
|
req.TLS = &tls.ConnectionState{}
|
|
}
|
|
|
|
actual, err := tc.match.MatchWithError(req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.expect, actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestURLPatternMatcherUnmarshalCaddyfile(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
input string
|
|
expect MatchURLPattern
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "pattern only",
|
|
input: `url_pattern /books/:id`,
|
|
expect: MatchURLPattern{Pattern: "/books/:id"},
|
|
},
|
|
{
|
|
name: "base_url and ignore_case",
|
|
input: `url_pattern /search {
|
|
base_url https://example.com
|
|
ignore_case
|
|
}`,
|
|
expect: MatchURLPattern{Pattern: "/search", BaseURL: "https://example.com", IgnoreCase: true},
|
|
},
|
|
{
|
|
name: "missing pattern",
|
|
input: `url_pattern`,
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "unknown option",
|
|
input: `url_pattern /foo {
|
|
nope
|
|
}`,
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "base_url without value",
|
|
input: `url_pattern /foo {
|
|
base_url
|
|
}`,
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "ignore_case with stray arg",
|
|
input: `url_pattern /foo {
|
|
ignore_case yes
|
|
}`,
|
|
expectErr: true,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var m MatchURLPattern
|
|
err := m.UnmarshalCaddyfile(caddyfile.NewTestDispenser(tc.input))
|
|
if tc.expectErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.expect, m)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestURLPatternMatcherGroups checks that captured groups are exposed as
|
|
// component-scoped placeholders, mirroring the URLPattern result object.
|
|
func TestURLPatternMatcherGroups(t *testing.T) {
|
|
m := MatchURLPattern{Pattern: "/books/:id/chapters/:chapter"}
|
|
require.NoError(t, m.Provision(caddy.Context{}))
|
|
|
|
u, err := url.ParseRequestURI("/books/42/chapters/7")
|
|
require.NoError(t, err)
|
|
|
|
repl := caddy.NewReplacer()
|
|
ctx := context.WithValue(context.Background(), caddy.ReplacerCtxKey, repl)
|
|
req := (&http.Request{URL: u, Host: "example.com"}).WithContext(ctx)
|
|
|
|
ok, err := m.MatchWithError(req)
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
id, _ := repl.GetString("http.url_pattern.pathname.id")
|
|
assert.Equal(t, "42", id)
|
|
chapter, _ := repl.GetString("http.url_pattern.pathname.chapter")
|
|
assert.Equal(t, "7", chapter)
|
|
}
|
|
|
|
// TestURLPatternMatcherRelative checks that a relative pattern matches the
|
|
// request path regardless of the request's host.
|
|
func TestURLPatternMatcherRelative(t *testing.T) {
|
|
m := MatchURLPattern{Pattern: "/books/:id"}
|
|
require.NoError(t, m.Provision(caddy.Context{}))
|
|
|
|
for _, host := range []string{"example.com", "other.org", "192.0.2.1:8080"} {
|
|
u, err := url.ParseRequestURI("/books/42")
|
|
require.NoError(t, err)
|
|
|
|
ok, err := m.MatchWithError(&http.Request{URL: u, Host: host})
|
|
require.NoError(t, err)
|
|
assert.Truef(t, ok, "expected match on host %q", host)
|
|
}
|
|
}
|