请求匹配器
请求匹配器 可用于按各种条件过滤(或分类)请求。
语法
在 Caddyfile 中,紧跟在指令之后的 匹配器标记(matcher token) 可以限制该指令的作用范围。匹配器标记可以采用以下形式之一:
如果指令支持匹配器,它将在其语法文档中显示为 [<matcher>]。匹配器标记通常是可选的(见 可选语法),用 [ ] 表示。如果省略匹配器标记,则等同于通配符匹配器(*)。
示例
此指令适用于 所有 HTTP 请求:
reverse_proxy localhost:9000
这与下面相同(此处 * 不必要):
reverse_proxy * localhost:9000
但此指令仅适用于路径以 /api/ 开头的请求:
reverse_proxy /api/* localhost:9000
要匹配除路径之外的其他条件,请定义一个 命名匹配器 并使用 @name 引用它:
@postfoo {
method POST
path /foo/*
}
reverse_proxy @postfoo localhost:9000
通配符匹配器
通配符(或“通用”)匹配器 * 匹配所有请求,仅在需要匹配器标记时才需要。例如,如果你想给指令的第一个参数也是一个路径,那么它看起来会跟路径匹配器一模一样!因此可以使用通配符匹配器来消除歧义,例如:
root * /home/www/mysite
否则,这个匹配器不常使用。我们通常建议如果语法不要求,就省略它。
路径匹配器
按 URI 路径匹配是匹配请求最常见的方式,因此匹配器可以内联,如下所示:
redir /old.html /new.html
路径匹配器标记必须以正斜杠 / 开头。
路径匹配 默认是精确匹配,而不是前缀匹配。 必须在末尾追加 * 才能进行快速前缀匹配。注意 /foo* 将匹配 /foo 和 /foo/ 以及 /foobar;你可能实际上想要使用 /foo/*。
命名匹配器
所有不是路径或通配符的匹配器都必须是命名匹配器。命名匹配器是在任何特定指令之外定义的匹配器,可以重复使用。
使用唯一名称定义匹配器可提供更多灵活性,允许你将 任何可用匹配器 组合成一组:
@name {
...
}
或者,如果集合中只有一个匹配器,你可以将其放在同一行:
@name ...
然后你可以像这样使用该匹配器,将其作为指令的第一个参数指定:
directive @name
例如,下面的配置将 HTTP/1.1 的 websocket 请求代理到 localhost:6001,其他请求代理到 localhost:8080。它匹配具有名为 Connection 的头字段并且包含 Upgrade 的请求,并且 另一个名为 Upgrade 的字段精确等于 websocket:
example.com {
@websockets {
header Connection *Upgrade*
header Upgrade websocket
}
reverse_proxy @websockets localhost:6001
reverse_proxy localhost:8080
}
如果匹配器集合只包含一个匹配器,也可以使用一行语法:
@post method POST
reverse_proxy @post localhost:6001
作为特殊情况,只要 expression 匹配器后跟一个 带引号的 参数(即 CEL 表达式本身),就可以不指定其名称使用:
@not-found `{err.status_code} == 404`
像指令一样,命名匹配器定义必须放在使用它们的 站点块 内。
命名匹配器定义构成一个 匹配器集合。集合内的匹配器用 AND 连接;即必须全部匹配。例如,如果集合中同时包含 header 和 path 匹配器,则两者都必须匹配。
同一类型的多个匹配器可以合并(例如同一集合中的多个 path 匹配器),使用布尔代数(AND/OR),详见各自部分的说明。
对于更复杂的布尔匹配逻辑,推荐使用 expression 匹配器 编写 CEL 表达式,它支持 and &&、or || 和 括号 ( )。
标准匹配器
完整的匹配器文档可在 各匹配器模块的文档 中找到。
请求可以通过以下方式匹配:
client_ip
client_ip <ranges...>
expression client_ip('<ranges...>')
按客户端 IP 地址匹配。接受精确 IP 或 CIDR 范围。支持 IPv6 区域(zones)。
当全局选项中配置了 trusted_proxies 时,此匹配器最为有用,否则它的行为与 remote_ip 匹配器完全相同。只有来自受信任代理的请求才会在请求开始时解析其客户端 IP;不受信任的请求将使用直接对等方的远程 IP 地址。
作为快捷方式,可以使用 private_ranges 来匹配所有私有 IPv4 和 IPv6 范围。相当于指定以下所有范围:192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 127.0.0.1/8 fd00::/8 ::1
每个命名匹配器中可以有多个 client_ip 匹配器,它们的范围会合并并以 OR 连接。
示例:
匹配来自私有 IPv4 地址的请求:
@private-ipv4 client_ip 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 127.0.0.1/8
该匹配器通常与 not 匹配器配合使用以取反匹配。例如,要中止所有来自公网 IPv4 和 IPv6 地址的连接(即所有私有范围的反集):
example.com {
@denied not client_ip private_ranges
abort @denied
respond "Hello, you must be from a private network!"
}
在 CEL 表达式 中,会是这样的:
@my-friends `client_ip('12.23.34.45', '23.34.45.56')`
expression
expression <cel...>
按任何返回 true 或 false 的 CEL(Common Expression Language) 表达式匹配。
Caddy 的 占位符(或 Caddyfile 简写)可以用于这些 CEL 表达式,因为它们会在传入 CEL 环境解释之前被预处理并转换为常规的 CEL 函数调用。
大多数其他请求匹配器也可以在表达式中作为函数使用,这允许比表达式外部更灵活的布尔逻辑。请参阅各匹配器的文档以了解在 CEL 表达式中支持的语法。
为了方便,如果定义的命名匹配器仅由一个 CEL 表达式构成,则可以省略匹配器名称。CEL 表达式必须是 带引号的(推荐使用反引号或 heredoc)。这样读起来很直观:
@mutable `{method}.startsWith("P")`
在这种情况下将假定为 CEL 匹配器。
示例:
匹配方法以 P 开头的请求,例如 PUT 或 POST:
@methods expression {method}.startsWith("P")
匹配处理器返回错误状态码为 404 的请求,通常与 handle_errors 指令 一起使用:
@404 expression {err.status_code} == 404
匹配路径符合两个不同正则表达式之一的请求;这只能使用表达式来编写,因为 path_regexp 匹配器通常每个命名匹配器只能存在一次:
@user expression path_regexp('^/user/(\w*)') || path_regexp('^/(\w*)')
或者同样的内容,省略匹配器名称,并使用 反引号 将其解析为单个标记:
@user `path_regexp('^/user/(\w*)') || path_regexp('^/(\w*)')`
你可以使用 heredoc 语法 来编写多行 CEL 表达式:
@api <<CEL
{method} == "GET"
&& {path}.startsWith("/api/")
CEL
respond @api "Hello, API!"
file
file {
root <path>
try_files <files...>
try_policy first_exist|first_exist_fallback|smallest_size|largest_size|most_recently_modified
split_path <delims...>
}
file <files...>
expression `file({
'root': '<path>',
'try_files': ['<files...>'],
'try_policy': 'first_exist|first_exist_fallback|smallest_size|largest_size|most_recently_modified',
'split_path': ['<delims...>']
})`
expression file('<files...>')
按文件匹配。
-
root定义查找文件的目录。默认是当前工作目录,或者如果设置了root变量(可以通过root指令 设置),则为该变量的值。 -
try_files按其列出的文件并依据 try_policy 进行检查。要匹配目录,请在路径后追加斜杠
/。所有文件路径均相对于站点 根目录,并且会展开 glob 模式。如果
try_policy为first_exist(默认),列表中的最后一项可以是以=为前缀的数字(例如=404),作为回退,将发出带该代码的错误;该错误可以被handle_errors捕获并处理。 -
try_policy指定如何选择文件。默认是first_exist。-
first_exist检查文件是否存在。第一个存在的文件被选中。 -
first_exist_fallback与first_exist类似,但假定列表中的最后一个元素始终存在以避免磁盘访问。 -
smallest_size选择文件大小最小的文件。 -
largest_size选择文件大小最大的文件。 -
most_recently_modified选择最近修改的文件。
-
-
split_path将导致在尝试的每个文件路径中,以列表中第一个出现在路径中的分隔符进行分割。对于每个分割值,分割的左侧(包括分隔符本身)将作为尝试的文件路径。例如,使用分隔符.php时,/remote.php/dav/会尝试文件/remote.php。每个分隔符必须出现在 URI 路径组件的末尾才能作为分隔符使用。这是一个小众设置,主要在托管 PHP 站点时使用。
由于 try_files 与 first_exist 策略非常常见,因此存在一个单行快捷方式:
file <files...>
一个空的 file 匹配器(后面没有列出文件的 file)将检查请求的文件(直接从 URI 原样取出,相对于站点 根目录)是否存在。这实际上等同于 file {path}。
匹配时,会提供四个新占位符:
{file_match.relative}匹配文件的相对于根的路径。重写请求时常用。{file_match.absolute}匹配文件的绝对路径,包括根目录。{file_match.type}文件类型,file或directory。{file_match.remainder}在拆分文件路径后剩余的部分(如果配置了split_path)
示例:
匹配路径对应的文件确实存在的请求:
@file file
匹配路径加上 .html 后是存在的文件,或者如果不存在则路径本身是存在的文件:
@html file {
try_files {path}.html {path}
}
与上例相同,但使用单行快捷方式,并在未找到文件时回退为发出 404 错误:
@html-or-error file {path}.html {path} =404
使用 CEL 表达式 的更多示例。请记住,占位符会在解释前被预处理并转换为常规的 CEL 函数调用,因此此处使用拼接。此外,如果与占位符进行拼接,必须使用长格式(long-form),因为当前解析有限制:
@file `file()`
@first `file({'try_files': [{path}, {path} + '/', 'index.html']})`
@smallest `file({'try_policy': 'smallest_size', 'try_files': ['a.txt', 'b.txt']})`
header
header <field> [<value> ...]
expression header({'<field>': '<value>'})
按请求头字段匹配。
<field>是要检查的 HTTP 头字段的名称。- 如果以
!为前缀,则该字段必须不存在才能匹配(省略值参数)。
- 如果以
<value>是该字段必须具有的匹配值。可以指定一个或多个值。- 如果以
*为前缀,则执行快速后缀匹配(出现在末尾)。 - 如果以
*为后缀,则执行快速前缀匹配(出现在开头)。 - 如果两侧都用
*包裹,则执行快速子字符串匹配(出现在任意位置)。 - 否则,为快速精确匹配。
- 如果以
同一集合内不同的头字段使用 AND 连接。每个字段的多个值使用 OR 连接。
注意头字段可以重复并具有不同的值。后端应用必须将头字段值视为数组而非单一值,Caddy 不会在这种情况下解释其含义。
示例:
匹配 Connection 头包含 Upgrade 的请求:
@upgrade header Connection *Upgrade*
匹配 Foo 头包含 bar 或 baz 的请求:
@foo {
header Foo bar
header Foo baz
}
匹配完全没有 Foo 头字段的请求:
@not_foo header !Foo
使用 CEL 表达式,通过检查 Connection 头包含 Upgrade 且 Upgrade 头等于 websocket(HTTP/2 则使用 :protocol 头)来匹配 WebSocket 请求:
@websockets `header({'Connection':'*Upgrade*','Upgrade':'websocket'}) || header({':protocol': 'websocket'})`
header_regexp
header_regexp [<name>] <field> <regexp>
expression header_regexp('<name>', '<field>', '<regexp>')
expression header_regexp('<field>', '<regexp>')
类似于 header,但支持正则表达式。
所使用的正则表达式语言为 RE2,包含在 Go 中。参见 RE2 语法参考 和 Go regexp 语法概览。
从 v2.8.0 开始,如果未提供 name,则名称将取自命名匹配器的名称。例如命名匹配器 @foo 将使此匹配器命名为 foo。指定名称的主要优点是在同一命名匹配器中使用多个 regexp 匹配器(例如 header_regexp 和 path_regexp,或多个不同头字段)时能区分它们。
捕获组可以在匹配后通过 占位符 在指令中访问:
-
{re.<name>.<capture_group>},其中:<name>是正则表达式的名称,<capture_group>是表达式中捕获组的名称或编号。
-
为方便起见,也会填充
{re.<capture_group>}(无名称)。但如果按顺序使用多个 regexp 匹配器,后者的占位符值将被下一个匹配器覆盖,这是需要注意的。
捕获组 0 是完整的正则匹配,1 是第一个捕获组,2 是第二个,以此类推。因此 {re.foo.1} 或 {re.1} 都将保存第一个捕获组的值。
每个头字段只支持一个正则表达式,因为 regexp 模式不能合并;如果需要多个,请考虑使用 expression 匹配器。对多个不同头字段的匹配将使用 AND 连接。
示例:
匹配 Cookie 头包含 login_ 后跟十六进制字符串的请求,捕获组可通过 {re.login.1} 或 {re.1} 访问。
@login header_regexp login Cookie login_([a-f0-9]+)
可以通过省略名称简化,名称将从命名匹配器推断:
@login header_regexp Cookie login_([a-f0-9]+)
或使用 CEL 表达式 的等价写法:
@login `header_regexp('login', 'Cookie', 'login_([a-f0-9]+)')`
host
host <hosts...>
expression host('<hosts...>')
按请求的 Host 头字段匹配。
由于大多数站点块在站点地址中已指明主机名,因此该匹配器更常用于使用通配主机名的站点块(见 通配证书模式),但需要基于主机名做特定逻辑时使用。
多个 host 匹配器将以 OR 连接。
示例:
匹配一个子域名:
@sub host sub.example.com
匹配根域名和一个子域名:
@site host example.com www.example.com
使用 CEL 表达式 匹配多个子域名:
@app `host('app1.example.com', 'app2.example.com')`
method
method <verbs...>
expression method('<verbs...>')
按 HTTP 请求的方法(动词)匹配。方法应为大写,例如 POST。可匹配一个或多个方法。
多个 method 匹配器将以 OR 连接。
示例:
匹配 GET 方法的请求:
@get method GET
匹配 PUT 或 DELETE 方法的请求:
@put-delete method PUT DELETE
使用 CEL 表达式 匹配只读方法:
@read `method('GET', 'HEAD', 'OPTIONS')`
not
not <matcher>
或者,要对多个被 AND 连接的匹配器取反,可以打开一个块:
not {
<matchers...>
}
括内匹配器的结果将被取反。
示例:
匹配路径既不以 /css/ 开头也不以 /js/ 开头的请求。
@not-assets {
not path /css/* /js/*
}
匹配同时不具备以下任一项的请求:
/api/路径前缀,或POST请求方法
即必须二者都不具备才能匹配:
@with-neither {
not path /api/*
not method POST
}
匹配不同时具备以下两项的请求:
/api/路径前缀,和POST请求方法
即可以不具备任一项或只具备其中一项才匹配:
@without-both {
not {
path /api/*
method POST
}
}
此匹配器没有单独的 CEL 表达式,因为你可以在表达式中使用 ! 运算符进行取反。例如:
@without-both `!path('/api*') && !method('POST')`
这与下面的带括号写法等价:
@without-both `!(path('/api*') || method('POST'))`
path
path <paths...>
expression path('<paths...>')
按请求路径(请求 URI 的路径部分)匹配。路径匹配是精确匹配但不区分大小写。可以使用通配符 *:
- 仅在末尾,用于前缀匹配(
/prefix/*) - 仅在开头,用于后缀匹配(
*.suffix) - 仅在两侧,用于子字符串匹配(
*/contains/*) - 仅在中间,用于通配匹配(
/accounts/*/info)
斜杠是有意义的。例如,/foo* 将匹配 /foo、/foobar、/foo/ 和 /foo/bar,但 /foo/* 将不会匹配 /foo 或 /foobar。
请求路径在匹配之前会被清理以解析目录遍历点(.、..)。此外,除非匹配模式中存在多个斜杠,否则连续的多个斜杠会合并。换言之,/foo 会匹配 /foo 和 //foo,但 //foo 只能匹配 //foo。
由于任何给定 URI 存在多个转义形式,请求路径会被归一化(URL 解码、取消转义),但在匹配模式中在相同位置显式包含转义序列的那些位置除外。例如,/foo/bar 匹配 /foo/bar 和 /foo%2Fbar,但 /foo%2Fbar 仅匹配 /foo%2Fbar,因为配置中显式给出了转义序列。
特殊的通配符转义 %* 也可用于替代 *,以保留其匹配跨度为转义形式。例如,/bands/*/* 不会匹配 /bands/AC%2FDC/T.N.T,因为路径将在归一化空间中比较,看起来像 /bands/AC/DC/T.N.T,这与模式不匹配;然而,/bands/%*/* 会匹配 /bands/AC%2FDC/T.N.T,因为 %* 表示的跨度在比较时不会解码转义序列。
多个路径将以 OR 连接。
示例:
匹配多个目录及其内容:
@assets path /js/* /css/* /images/*
匹配特定文件:
@favicon path /favicon.ico
匹配文件扩展名:
@extensions path *.js *.css
使用 CEL 表达式:
@assets `path('/js/*', '/css/*', '/images/*')`
path_regexp
path_regexp [<name>] <regexp>
expression path_regexp('<name>', '<regexp>')
expression path_regexp('<regexp>')
类似于 path,但支持正则表达式。针对 URI 解码/取消转义后的路径运行匹配。
所使用的正则表达式语言为 RE2,包含在 Go 中。参见 RE2 语法参考 和 Go regexp 语法概览。
从 v2.8.0 开始,如果未提供 name,则名称将取自命名匹配器的名称。例如命名匹配器 @foo 将使此匹配器命名为 foo。指定名称的主要优点是在同一命名匹配器中使用多个 regexp 匹配器(例如 path_regexp 和 header_regexp)时能区分它们。
捕获组可以在匹配后通过 占位符 在指令中访问:
-
{re.<name>.<capture_group>},其中:<name>是正则表达式的名称,<capture_group>是表达式中捕获组的名称或编号。
-
为方便起见,也会填充
{re.<capture_group>}(无名称)。但如果按顺序使用多个 regexp 匹配器,后者的占位符值将被下一个匹配器覆盖,这是需要注意的。
捕获组 0 是完整的正则匹配,1 是第一个捕获组,2 是第二个,以此类推。因此 {re.foo.1} 或 {re.1} 都将保存第一个捕获组的值。
每个命名匹配器中只能有一个 path_regexp 模式,因为该匹配器不能与自身合并;如果需要多个,请考虑使用 expression 匹配器。
示例:
匹配路径以 6 位十六进制字符串结尾并以 .css 或 .js 为扩展名的请求,带有可以通过 {re.static.1} 和 {re.static.2}(或 {re.1} 与 {re.2})访问的捕获组:
@static path_regexp static \.([a-f0-9]{6})\.(css|js)$
可以通过省略名称简化,名称将从命名匹配器推断:
@static path_regexp \.([a-f0-9]{6})\.(css|js)$
或使用 CEL 表达式 的等价写法,同时验证磁盘上是否存在该 file:
@static `path_regexp('\.([a-f0-9]{6})\.(css|js)$') && file()`
protocol
protocol http|https|grpc|http/<version>[+]
expression protocol('http|https|grpc|http/<version>[+]')
按请求协议匹配。可以使用广义协议名如 http、https 或 grpc;也可以使用特定或最小的 HTTP 版本,例如 http/1.1 或 http/2+。
每个命名匹配器中最多只能有一个 protocol 匹配器。
示例:
匹配使用 HTTP/2 的请求:
@http2 protocol http/2+
使用 CEL 表达式:
@http2 `protocol('http/2+')`
query
query <key>=<val>...
query ""
expression query({'<key>': '<val>'})
expression query({'<key>': ['<vals...>']})
按查询字符串参数匹配。应为一系列 key=value 对,或一个空字符串 ""。键是精确匹配(区分大小写),但也支持 * 来匹配任何值。值可以使用占位符。空字符串匹配没有查询参数的 HTTP 请求。
每个命名匹配器中可以有多个 query 匹配器,相同键的对会以 OR 连接。不同的键将以 AND 连接。因此,匹配器中列出的所有键必须至少有一个匹配值。
非法查询字符串(语法错误、未转义的分号等)将解析失败,因此不会匹配。
注意:查询字符串参数是数组,而非单一值。这是因为查询字符串中允许重复键,每个键可能有不同的值。如果查询字符串中任一值匹配配置值,则该键被视为匹配。使用查询字符串的后端应用必须考虑到查询字符串值是数组且可能有多个值。
示例:
匹配 q 查询参数具有任意值:
@search query q=*
匹配 sort 查询参数值为 asc 或 desc:
@sorted query sort=asc sort=desc
匹配同时存在 q 和 sort,使用 CEL 表达式:
@search-sort `query({'sort': ['asc', 'desc'], 'q': '*'})`
remote_ip
remote_ip <ranges...>
expression remote_ip('<ranges...>')
按远程 IP 地址匹配(即直接对等方的 IP 地址)。接受精确 IP 或 CIDR 范围。支持 IPv6 区域(zones)。
作为快捷方式,可以使用 private_ranges 来匹配所有私有 IPv4 和 IPv6 范围。相当于指定以下所有范围:192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 127.0.0.1/8 fd00::/8 ::1
如果你想匹配客户端的“真实 IP”(从 HTTP 头解析),请改用 client_ip 匹配器。
每个命名匹配器中可以有多个 remote_ip 匹配器,它们的范围会合并并以 OR 连接。
示例:
匹配来自私有 IPv4 地址的请求:
@private-ipv4 remote_ip 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 127.0.0.1/8
该匹配器通常与 not 匹配器配合使用以取反匹配。例如,要中止所有来自公网 IPv4 和 IPv6 地址的连接(即所有私有范围的反集):
example.com {
@denied not remote_ip private_ranges
abort @denied
respond "Hello, you must be from a private network!"
}
在 CEL 表达式 中,会是这样的:
@my-friends `remote_ip('12.23.34.45', '23.34.45.56')`
vars
vars <variable> <values...>
按请求上下文中变量的值或占位符的值匹配。可指定多个值以匹配任一可能的值(OR)。
第一个参数 { } 中的占位符。(在第一个参数中占位符不会被展开。)
当与设置输出的 map 指令 配合使用,或与在请求上下文中设置信息的插件配合使用时,此匹配器最为有用。
示例:
匹配 map 指令输出名为 magic_number 的结果为 3 或 5:
vars {magic_number} 3 5
匹配任意占位符的值,例如认证用户的 ID 为 Bob 或 Alice:
vars {http.auth.user.id} Bob Alice
vars_regexp
vars_regexp [<name>] <variable> <regexp>
类似于 vars,但支持正则表达式。
所使用的正则表达式语言为 RE2,包含在 Go 中。参见 RE2 语法参考 和 Go regexp 语法概览。
从 v2.8.0 开始,如果未提供 name,则名称将取自命名匹配器的名称。例如命名匹配器 @foo 将使此匹配器命名为 foo。指定名称的主要优点是在同一命名匹配器中使用多个 regexp 匹配器(例如 vars_regexp 和 header_regexp)时能区分它们。
捕获组可以在匹配后通过 占位符 在指令中访问:
-
{re.<name>.<capture_group>},其中:<name>是正则表达式的名称,<capture_group>是表达式中捕获组的名称或编号。
-
为方便起见,也会填充
{re.<capture_group>}(无名称)。但如果按顺序使用多个 regexp 匹配器,后者的占位符值将被下一个匹配器覆盖,这是需要注意的。
捕获组 0 是完整的正则匹配,1 是第一个捕获组,2 是第二个,以此类推。因此 {re.foo.1} 或 {re.1} 都将保存第一个捕获组的值。
每个变量名只支持一个正则表达式,因为 regexp 模式不能合并;如果需要多个,请考虑使用 expression 匹配器。对多个不同变量的匹配将使用 AND 连接。
示例:
匹配 map 指令输出名为 magic_number 的值以 4 开头的情况,将该值捕获在捕获组中,可通过 {re.magic.1} 或 {re.1} 访问:
@magic vars_regexp magic {magic_number} ^(4.*)
可以通过省略名称简化,名称将从命名匹配器推断:
@magic vars_regexp {magic_number} ^(4.*)