Edgelang 语言用户手册

目录

描述

本手册是 Edge 语言的用户端文档。

部分本手册内记录的特性在当前 edgelang 版本中可能还未实现,edgelang 开发团队正在快速赶上。如果有疑问,请与 OpenResty Inc. 公司直接联系。

本手册目前依然为草案。有些细节还会更新。

有些大的特性尚未在本手册中介绍,比如针对很大的请求和响应体的无缓冲的模式匹配和替换。

面向通用 TCP/UDP 代理和 DNS 服务器的内置的判断函数和动作函数尚未列出。

习惯用法

为了便于表达,本文中用 edgelang 来代表 Edge 语言。

含有问题的样例代码将在每行开头前置一个问号 ?。比如:

? true =>
?    say($a = 3);

零件与组件

标识符

edgelang 里面的标识符是一个或多个划线连接的。一个是一个字母、数字和下划线组成的序列。不过,下划线不能出现在字的开头。下面是一些有效的 edgelang 标识符的例子:

foo
hisName
uri-prefix
Your_Name1234
a-b_1-c

Edgelang 是大小写敏感的语言。所以像 fooFoo 这样的标识符表示完全不同的东西。

标识符不能是本语言的关键字,但是变量名例外。

回到目录

关键字

Edgelang 有下列关键字

ge      gt         le      lt        eq        ne
contains           contains-word     suffix    prefix
my      our        macro   use       INIT      as
rx      wc         qw      phase     END       defer
func    action

回到目录

变量

Edge 语言变量命名由两部分组成:一个叫变量字首的特殊字符,后跟一个标识符。 变量字首用于表示变量的类型。Edge 语言支持下列三种不同的变量字首:

  • $ 用于 标量类型
  • @ 用于 数组类型
  • % 用于 哈希/散列类型

标量变量保存简单的数值,比如数字,字串,布尔以及数量等。

数组变量是包含简单值或包含其它数组或哈希变量的有序的列表。

哈希变量是一些键值对的无序列表。

变量通常用 my 关键字声明。

用户程序里定义的每个标量变量,在其生命期里,只能有一个值类型。 每个变量的值都必须在声明时显式指定。下面是七种标量值:

  • Str
  • Num
  • Bool
  • Time
  • Size
  • SizeRate
  • CountRate

用户必须显式声明一个标量变量的类型,如下所示:

my Num $count;
my Str $name;

数组类型变量必须在声明时显式指定其元素的数据类型,如下所示:

my Bool @bool-list;
my Time @time-array;

哈希类型变量必须在声明时显式指定其哈希 - 键类型与哈希 - 值类型。 哈希 - 值类型紧跟变量作用域关键词后指定,哈希 - 键类型则在变量名后指定,如下所示:

my Num $city-weight{Str};
my Time $uri-released{Str};

变量声明的时候可以赋予一个初始值,像下面这样:

my Num $count = 0;
my Str @domains = qw/ foo.com bar.blah.org /;
my Num %city-weight{Str} = (ShenZhen: 100, Beijing: 50, Shanghai: 30, Guangzhou: 10);

回到目录

请求范围内的变量

my 声明的变量都只有代码块内的作用范围。 每个请求处理的生命期中的运行中的阶段都会有自己的范围。 如果需要在一个请求的生命期中的多个阶段中共享变量,我们可以用 our 替换 my 来声明客户的变量。 如下所示:

our Bool $is-mobile;
our Num $my-count;

回到目录

规则范围内的变量

用户可以通过As 表达式引入客户化定制的规则范围内的变量。

特殊的子模式捕获变量,比如 $1$2,$3 这些,也可以通过在规则条件的正则文本内部的捕获组 (...) 隐含地引入。 跟通过As 表达式引入的变量一样,这些捕获变量的作用范围也是所包含的规则的范围。

下面是一些例子:

uri-prefix(rx/ \/ ( [a-z]{2} ) \/ ( [a-z]{2} ) /) =>
    say("country: $1, lang: $2");

对于请求 URI /us/en/read.html,这条规则将被触发并且生成下面的响应体输出:

country: us, lang: en

对于多条件规则,每个条件都有自己的子模式捕获变量($1$2 等等)。比如:

uri(rx{ /([a-z]*) });
uri(rx{ /([0-9]*) })
=>
    say("result: $1"),

对于请求 GET /foo,我们会得到

result: foo

而对于请求 GET /123,我们会得到

result: 123

在上面两种场合,我们都会在特殊变量 $ 里得到一个有意义的数值。

回到目录

函数名称和函数调用

Edgelang 大量使用了函数做为规则内的判断和动作。如果需要,用户可以定义自己的函数。

函数名称使用标识符直接表示,不需要声明类型印记。

函数调用是以函数名跟着一对儿圆括号和圆括号中包围的任意参数表示的,像下面这样

say("hello, ", "world")

没有参数的函数调用可以省略圆括号。比如:

uri()

可以简化成:

uri

参数可以用位置或者名字来传递。比如,内置动作函数 say(),像前面的例子说的那样, 可以接受位置参数,并且当作响应体的一部分输出。命名参数是通过参数名跟一个冒号做前缀传递的,如下例

redirect(uri: "/foo", code: 302)

内置函数可能需要以命名参数的方式传递一部分参数,以位置参数的方式传递另外一部分参数。 具体用法请参考对应内置函数的文档。

至少有一个参数的函数调用可以用“方法调用”的形式书写,这个时候第一个参数表现为调用者。比如:

say("hello")

可以写成

"hello".say()

甚至是

"hello".say

多个参数的函数调用也可以用类似形式书写,比如,

say("hello", "world")

语法上等同于:

"hello".say("world")

回到目录

字串文本

字串文本可以用单引号(') 或者双引号(")包围,像下面这样:

"hello, world!"
'foo 1234'

标量和数组类型可以在双引号包围的字串文本中展开,如:

"Hello, $name!"

所以在双引号字串里的文本 $ 需要用 \ 逃逸,以避免多余的展开,比如:

"Hello, \$name!"

在双引号字串中支持下列逃逸序列:

\a
\b
\f
\n
\r
\t
\\
\0
\$
\@
\%
\'
\"

单引号包围的字串内不支持变量展开。另外,它们也只识别下列逃逸序列:

\'
\\

在单引号包围的文本中出现的任何其它 \ 字符,都会被展开成一个文本的 \

回到目录

数值常量

数值常量可以用下列方式之一书写:

1527
3.14159
-32
-3.14
78e-3      (带一个十进制指数)
0xBEFF     (十六进制)
0157       (八进制)

回到目录

正则文本

正则文本用语声明一个 Perl 兼容的正则表达式值。 它是通过关键字 rx 跟着一个引号结构书写的。 下面是一些例子:

rx{ hello, \w+}
rx/ [0-9]+ /
rx( [a-zA-Z]* )
rx[ ^/abc ]
rx" \d+ - \d+ "
rx' ([^a-z][a-z]+) '
rx! ([^a-z][a-z]+) !

用户可以随意使用花括弧,斜杠,圆括弧,中括号,双引号或者单引号或者感叹号来包围正则文本。 它们的效果都是一样的,只是在正则文本里对应需要转义的字符不同。比如,在 rx(...) 里用斜杠(/) 是不需要转义的。

缺省情况下,除了在字符表结构里(比如 [a-z]),在正则文本值里使用空白字符是不影响结果的。 这是为了方便用户把正则格式化得更容易阅读。

在正则里可以声明一个或多个选项,比如

rx:i/hello/

表示一个对模式 hello 的大小写无关的匹配。类似的:

rx:s/hello, world/

则令模式字串里的空白字符影响结果。 多个选项可以用同样方法堆叠起来使用,比如:

rx:i:s/hello, world/

如果没有选项,前面的 rx 也可以省略,比如:

/\w+/
/hello world/

在 edge 语言的正则文本中,字符 . 会匹配包含换行符 \n 的任何字符,字符 \s 会匹配包含换行符的任意空字符。

在 edge 语言的正则文本中,变量的插值也是支持的。比如:

my Str $foo = "hello";

rx:s/$foo, world/;

其中需要注意的是,在变量中出现捕获动作会导致非预期的结果,避免这种用法。

回到目录

通配符文本

通配符文本是使用 UNIX 风格的通配符,与字符串发生匹配模式的文本串。 它是通过关键字 wc 跟着一个引号结构书写的,比如下面这样:

wc{foo???}
wc/*.foo.com/;
wc(/country/??/);
wc"[a-z].bar.org"
wc'[a-z].*.gov'
wc![a-z].*.gov!

和正则文本一样,通配符文本也可以使用灵活的引号字符。

我们支持三种通配符元模式:* 可以用于匹配任意子字串, ? 用于匹配任意单字符,而 [...] 匹配字符表。

通配符上可以声明一个活多个选项,比如:

wc:i/hello/

表示一个模式 hello 的大小写无关的匹配。

回到目录

引用字

引用字提供了声明一个字串文本列表值而不需要敲入太多次包围字串的引号的简洁方法。

它使用关键字 qw 跟着一系列灵活的引号结构的方式书写的。 比如:

qw/ foo bar baz /

等效于

"foo", "bar", "baz"

和正则文本、通配符文本类似,用户可以在引用字的引号结构里选择各种不同的引号字符, 比如 /, {,(, ", 和 '

回到目录

量纲

带单位的量纲是 edgelang 内部的一等公民。量纲是通过一个数字跟着包围在一对方括号里的单位来声明的。 比如,

32 [kB/s]

是“每秒 32K 字节“的数量。

支持下列时间单位:

  • ssec,或 second

  • ms

    毫秒

  • us

    微秒

  • ns

    纳秒

  • min

    分钟

  • h or hour

    小时

  • d or day

  • month

  • year

rreq 的单位是请求数。

支持下列数据大小的单位:

  • B or Byte

    字节

  • b or bit

数据大小单位可以接受下列幅度前缀:

  • k

    1000 倍

  • K or Ki

    1024 倍

  • m

    1000 * 1000 倍

  • M or Mi

    1024 * 1024 倍

  • g

    1000 1000 1000 倍

  • G or Gi

    1024 1024 1024 倍

  • t

    1000 1000 1000 * 1000 倍

  • T or Ti 1024 1024 1024 * 1024 倍

像数据传输速率这样的组合单位可以用斜杠字符连接数据尺寸单位和时间单位来实现。

数量值可以直接转换成文本,比如下面这个动作

say(32 [hour])

给出下面的响应输出:

32 [hour]

我们可以在单位之前使用任意算术表达式,比如:

(1.5 + 2) [kB/s]

输出一个等于 3.5 [kB/s] 的数量。

内置函数 convert-unit() 可用于转换量纲的单位,只要新的单位不改变原来量纲的物理含义, 就可以转换。比如:

convert-unit(1 [hour], 'sec')

会得出新的量纲, 3600 [sec],逻辑上是相等的。

内置的 to-num() 函数可以用于从量纲中抽取数字部分。比如:

to-num(32 [hour])

会返回数字 32

回到目录

布尔值

布尔值是使用对内置函数 true()false() 的调用分别表示的。 所有关系表达式也会计算得出布尔值。

在 edgelang 里,下列数值会被认为是“条件为假”:

  • 数字 0
  • 字串 “0”
  • false() 的值
  • 一个空字串
  • 一个空列表或者数组
  • 一个空哈希表

所有其它数值都被认为是“条件为真”。

函数调用 true()false() 经常被简写为 truefalse

回到目录

网络地址

Netaddr/网络地址常量支持 CIDR 格式,可以用下列形式之一书写:

192.168.1.1
192.168.1.1/32  -- 跟 192.168.1.1 一样
192.168.1.0/24
::ffff:192.1.56.10/96

网络地址也可以使用 关系操作符

回到目录

任意串

特殊术语 * 表示一个任意 文本。有些内置函数接受任意文本作为参数。

回到目录

注释

注释以字符 # 开头,直到当前行行尾。比如:

# 这是一个注释

也支持块注释,比如:

#`(
This is a block
comment...
)

注意,反引号和括号必须是紧跟在 # 后面,注释块里也可以使用括号,比如:

#`(
3 * (2 - (5 - 3))
)

回到目录

操作符

支持下列操作符,按照优先级排序:

优先级              操作符
0                   后环绕 [], {}, <>
1                   **
2                   单目 +/-/~, as
3                   * / % x
4                   + - ~
5                   << >>
6                   &
7                   | ^
8                   单目 !, > < <= >= == !=
                    contains, contains-word, prefix, suffix
                    eq ne lt le gt ge
9                   ..
10                  ?:

用户可以在一个表达式中用圆括号,(),明确地修改操作符相关的优先级或者明确地修改操作符关联顺序。

回到目录

算术操作符

Edgelang 支持下面的二进制算术操作符:

**      幂运算
*       乘
/       除
%       模除
+       加
-       减

比如:

2 ** (3 * 2)        # 得出 64
(7 - 2) * 5         # 得出 25

还支持单目前缀操作符 +- 比如:

+(32 + 1)           # 等于 33
-(3.15 * 2)         # 等于 -6.3

回到目录

字串操作符

Edgelang 支持下面的二目字串操作符:

x       重复一个字串若干次并且把他们连接起来
~       字串连接

比如:

"abc" x 3           # 得出 "abcabcabc"
"hello" ~ "world"   # 得出 "helloworld"

回到目录

位操作符

支持下列位操作符:

<<          左移
>>          右移
&           位 AND
|           位 OR
^           位 XOR

单目前缀操作符 ~ 用于位的 NOT(非操作)。请不要把它和字串连接的二目操作符 ~ 搞混了。

回到目录

关系操作符

当前的表达式中,使用关系操作符都会得到一个布尔结果。 使用关系操作符的表达式叫做关系表达式

下列二目操作符用于匹配网络地址:

~~          包含
!~~         不包含

例如:

client-addr !~~ 192.168.10.0/24 =>
    say("it's not come from internal network");

first-x-forwarded-addr ~~ any(12.34.56.1/24, 23.45.1.1/16) =>
    say("it comes from the backlist");

下列二目操作符按数字含义对比两个操作数:

>           大于
<           小于
<=          小于等于
>=          大于等于
==          等于
!=          不等于

下面的二目操作符按照文本含义比较两个字串值:

gt          大于
lt          小于
le          小于等于
ge          大于等于
eq          等于
ne          不等于

还有 3 个特殊的二目字串操作符用于字串值内部的模式匹配:

contains            如果右侧的操作数“包含于”左侧的操作数,那么为真

contains-word       如果右侧的操作数作为一个字“包含于”左侧的操作数种,那么为真

prefix              如果右侧的操作数是左侧操作数的“前缀”,那么为真

suffix              如果右侧操作数是左侧操作数的“后缀”,那么为真

单目前缀操作符 ! 对布尔操作数取反

如果比较操作符的右侧操作数看着像一个模式,比如说通配符或者正则值,那么会假设在模式中有匹配锚存在。 比如:

uri eq rx{ /foo } =>
    say("hit");

等效于:

uri contains rx{ \A /foo \z } =>
    say("hit");

这里的正则模式 \A 只匹配字串开头,而 \z 只匹配结束。 另外,操作符 contains 不会有任何匹配锚的假设。

类似, contains-word 操作符假设在用户的正则周围包含 \b 正则锚。

回到目录

范围操作符

二目操作符 .. 可用于形成一个范围表达式,如下所示

1 .. 5          # 等效于 1, 2, 3, 4, 5
'a'..'d'        # 等效于 `a`, 'b', 'c', 'd'

范围表达式的值是一个在该范围内所有独立值的平面列表。

回到目录

三元操作符

三元关系操作符 ?: 可以用于根据用户的条件,在两个用户表达式之间选择。

比如:

$a < 3 ? $a + 1 : $a

这个表达式在 $a < 3 为真时计算出 $a +1 的值,否则给出 $a 的值。

回到目录

下标操作符

后环绕操作符 [] 可以用于给一个数组取下标。 比如:

my Str @names = ('Tom', 'Bob', 'John');

true =>
    say(@names[0]),  # 输出 Tom
    say(@names[1]),  # 输出 Bob
    say(@names[2]);  # 输出 John

负数下标用于从数组尾部访问元素,比如,-1 是最后一个元素,-2 倒数第二个,以此类推。

类似地,后环绕操作符 {} 用于访问哈希表,比如:

my Num %scores{Str} = (Tom: 78, Bob: 100, John: 91);

true =>
    say(%scores{'Bob'});    # output 100

后环绕操作符 {}<> 用于以文本字串键字名的方式访问哈希表,比如 %scores{"John"} 等效于 %scores<John>, 用法详见 下标操作符

回到目录

规则

Edgelang 是一种以规则为基础的语言。实际上每一个 edgelang 程序都是由一组规则组成。

回到目录

基础的规则布局

edgelang 规则由两部分组成,一个条件,以及一个结果。条件和结果用 => 连接, 整个规则用一个分号字符终止。基本的规则长得像下面这样:

<condition> => <consequence>;

规则的条件部分可以使用一个或多个关系表达式,像 resp-status == 200。 所有关系表达式用逗号字符连接(,),这样的意思是把所有关系表达式在一起,也就是说, 所有关系表达式都为真的时候,整个条件为真。条件不能有副作用,这个效果由 edgelang 编译器强制要求。 因为这个原因,同一个条件中的关系表达式的计算顺序的改变并不影响整个条件的结果。

结果部分通常包含一个或多个动作。每个动作都可以有一些副作用,比如修改一些请求的字段, 执行一个 302 重定向,或者修改当前请求在后台的路由等等。我们可以为单个动作声明一大块的规则 (参阅动作块一节)。

下面是一个简单的 edgelang 规则:

uri("/foo") =>
    redirect(uri: "/bar", code: 302);

在条件部分, uri("/foo") 是关系表达式。接受一些参数的 uri() 函数是一个判断,意思是它只返回真或者假。 uri("/foo") 执行下列判断:如果当前请求 URI 精确匹配文本字串 /foo,那么就返回真; 否则返回假。我们在结果中有一个动作。也就是 redirect() 函数调用。 这个函数生成一个 302 HTTP 响应,发起一个外部的重定向到同主机的 /bar URI 上头。 值得说明的是 uri() 函数接受一个位置参数,而 redirect() 函数接受两个命名参数。 edgelang 内置的函数可以自行判断它们是接受位置参数还是命名参数,抑或是两者皆要。

Edgelang 是自由格式的语言,所以你可以自由使用空白字符。 上面例子中结果前面缩进是不影响语义的,只是为了更佳美观而采用的。 我们完全可以在一行中写整个规则,比如:

uri("/foo") => redirect(uri: "/bar", code: 302);

这里 uri() 函数也可以不接受参数,它返回当前请求 URI 的字串形式,比如:

uri() eq "/foo" =>
    redirect(uri: "/bar", code: 302);

这个例子中的关系表达式 uri() eq "/foo" 等同于前面使用的 uri("/foo") 判断。 eq 部分是一个二目比较操作符,用于比较两边的字串是否完全一样。

值得指出的一点是,不带参数的 edgelang 函数调用可以省略圆括弧,所以 uri() 可以简写成 uri, 像下面这样

uri eq "/foo" =>
    redirect(uri: "/bar", code: 302);

回到目录

多个关系表达式

用户也可以在一个条件中声明多个关系表达式,比如:

uri("/foo"), uri-arg("n") < 1 =>
    exit(403);

在这里我们在条件里多了一个关系表达式,也就是 uri-arg("n") < 1,它在 URI 的参数 n 收到一个小于数字 1 的时候返回真。在这个例子里我们用了另外一个动作,exit(403),在执行的时候立即给客户端发送一个 “403 Permission Denied” 响应。请注意在两个关系表达式中间的逗号的意思是, 意思是如果要整个条件为真,那么逗号两侧的关系表达式必须都为真。

用户可以甚至可以在同一个条件里声明更多的关系表达式,比如:

uri("/foo"), uri-arg("n") < 1, user-agent() contains "Chrome" =>
    exit(403);

这里我们有第三个关系表达式,测试了请求头中的 User-Agent 是否包含子字串 Chrome

回到目录

多个条件

Edgelang 规则实际上可以执行多个并行的条件,用分号操作符链接。这些条件逻辑上是为当前规则在一起的。

比如:

uri("/foo"), uri-arg("n") < 1;
uri("/bar"), uri-arg("n") >= 4
=>
    exit(403);

这里只要两个条件之一为真,那么就规则就算匹配上了。当然,两个条件都匹配的话,规则也算匹配上。

回到目录

多个动作

我们可以在一个结果里声明多个动作,参考下面的例子,

uri("/foo") =>
    errlog(level: "warn", "rule matched!"),
    say("response body data with an automatic trailing newline"),
    say("more body data...");

这个例子在结果里有 3 个动作。第一个调用内置的 errlog() 函数向服务器的错误日志 中写一行日志,其级别是 warn。后面两个动作调用 say() 函数为当前请求输出响应体。

回到目录

无条件规则

有些规则会选择无条件运行它们的动作。不过 edgelang 规则,总是要求一个条件部分。 要实现这个无条件出发规则的效果,用户可以使用总是返回真的判断函数 true() 作为 条件里的唯一的关系表达式,比如:

true() =>
    say("hello world");

在这个规则里,动作 say() 是无条件执行的。

因为无参数函数调用可以省略圆括弧,我们更喜欢写成 true,而不是 true()。 像下面这样:

true =>
    say("hello world");

回到目录

多个规则

同一个块中的多个规则是按顺序执行的。先写的规则先执行。

观察下面的例子:

uri("/foo") =>
    say("hello");

uri-arg("n") > 3 =>
    say("world");

对于请求 GET /foo?n=4,我们可以得到一个 200 的 HTTP 响应,响应体数据是:

hello
world

不过,多规则的条件部分可能会被 edgelang 编译器优化,这样就可以同时匹配,甚至可能在规则实际执行之前计算。 这些情况会在 edgelang 编译器认为安全的时候发生。

回到目录

语句块

语句块通常是用一对花括弧 ({}) 构成,同时也给变量生成一个新的范围。 每个 edgelang 程序都有一个隐含的顶级语句块。

在下面的例子里,我们有两个独立的 $a 变量,因为它们属于不同的语句块(或者说范围)。

my Num $a = 3;

{
    my Str $a = "hello";
}

true =>
    say("a = $a");   # output `3` instead of `hello`

规则和变量一样,也是所在语句块的词法对象。语句块可以用来把相关的规则组合在一起成为一个整体。 在这种设置中,哪些更早执行的规则可以使用特殊动作 done()忽略所有同语句块中其他后续的规则。 如下例所示:

{
    uri("/test") =>
        print("hello"),
        done;

    true =>
        print("howdy");
}

true =>
    say(", outside!");

对于请求 GET /test,响应体应该是 hello, outside!。 请注意第一个规则中的 done 动作忽略了第二个规则的执行。换句话说, 请求 GET /foo" 会生成输出 howdy, outside!,因为第一条规则不匹配。

不过,在一个规则结果的中间使用 done() 动作并不忽略同结果之后的动作。它只影响同一语句块中的后续规则。

语句块可以嵌套任意层深度,如下

uri-arg("a") => say("outer-most");

{
    true => say("2nd level");

    {
        uri("/foo") => say("3rd level");
    }
}

回到目录

动作块

在规则结果中,语句块也可以用做动作。这样的语句块叫动作块。 这个结构可以用于声明嵌套块。比如:

uri-prefix("/foo/") =>
    {
        uri-arg("a") < 0 =>
            say("negative"),
            done;

        uri-arg("a") == 0 =>
            say("zero"),
            done;

        true =>
            say("positive");
    };

在这个规则里,如果匹配上条件 uri-prefix("/foo/"),那么在动作块中的 3 个规则将顺序执行。 换句话说,如果最外层的条件不匹配,那么执行流是根本不会看其中的规则的。 这个方法可以很方便的把几个规则规则的公共条件组合起来。这么写也可以帮助编译器生成更有效的机器码。

其它类型的动作可以跟同一个规则结果中的这样的动作块混合在一起,比如:

uri-prefix("/foo/") =>
    {
        uri-arg("a") < 0 => say("negative!");
    },
    done;

回到目录

As 表达式

用户可以在规则条件中使用 as 表达式 把表达式的结果定义成一个别名状的用户变量。 这些变量可以在规则的条件和/或结果(也就是说,在动作里)的后面部分使用。

这些变量的可见范围由包含他们的规则限制。

比如:

uri-prefix("/security01/" as $prefix) =>
    rm-uri-prefix($prefix),
    set-req-host("sec.foo.com");

在上面这里我们把表达式 "/security01/" 的值别名成了标量变量 $prefix, 然后在我们的 rm-uri-prefix() 动作中不再重复使用前面那个常量字串,而是使用了这个别名变量, 这样就减少了常量字串值敲错的风险。如果我们敲错了变量名,那么我们会在编译的时候收到一个编译错误 (缺少变量定义)。所以使用 as 表达式做变量别名,不仅让代码更短,也让它更安全。

我们也可以用“as 表达式”代表任意表达式的值。比如

uri-arg("uid") as $uid, looks-like-num($uid), $uid > 0 =>
    say("found uid: $uid");

在这个规则里,我们把 uri-arg("uid") 这个动态表达式的值别名给了用户定义变量 $uid, 然后在条件后面的关系表达式和规则动作里引用了这个值。

回到目录

赋值动作

赋值操作符 = 用于声明给一个变量或者一个可以是左值的表达式赋值的动作。比如,

my Num $a;

true =>
    $a = 3;

和所有其他动作一样,赋值表达式本身没有值。 所以不允许在其他表达式中嵌入一个赋值表达式。比如,下面的例子会生成一个编译时错误:

? my Num $a;
?
? true =>
?     say($a = 3);

这是因为赋值 $a = 3 不返回值并且只能用于一个独立的动作中。

下面的赋值

$a = $a + 3

可以简化成使用操作符 +=:

$a += 3

类似的,提供了 *=/=%=x=~= 用于跟二目操作符 */%x,和 ~ 对应。

最后,后缀操作符 ++ 可以用于简化 +=1 的场合。比如:

$a++

等效于 $a += 1 或者 $a = $a + 1。对应的还有 -- 用于做 -= 1 的简写。

和标准 = 操作符类似,所有这些赋值的形式自己并不接受任何数值,只能用于独立的动作中。

回到目录

运行时阶段

OpenResty® 在不同的运行时阶段处理每个客户端请求。

目前支持下列阶段,默认会运行在 rewrite 阶段,其他阶段需要配合 Defer 块 来使用。

  • rewrite

    请求改写和重定向的时候

  • resp-header

    在响应头准备好的时候

  • resp-body

    响应体处理阶段

未来可能会添加更多运行时阶段。

回到目录

Defer 块

defer 块是一个特殊的代码块,他们会被延后到指定阶段的开头执行。当前支持的阶段有:resp-header, resp-body

注意:使用了 resp-body 的 defer 块之后,响应头的 Content-Length 会被清空,强制使用 chunked 模式。并且 defer 块中不可以嵌套 defer 块。

看看下面例子:

true =>
    defer resp-header {
        errlog(level: 'error', 'defer log in resp-header');
    };

true =>
    defer resp-body {
        errlog(level: 'error', 'defer log in resp-body');
    };

回到目录

连接

一个连接是一个单值等效于多值。内置的函数 anyall,和 none 可以用于从一个列数值或者一个哈希构造连接。 连接提供了一个可以用来表示列表和数值之间关系约束的非常简单的方法。比如, 如果要测试数值 @foo 中有没有某个元素大于 3,我们可以写成

any(@foo) > 3

或者我们想判断是否所有元素都大于 3:

all(@foo) > 3

用户也可以直接声明多个离散的数值,比如:

any(1, 3, 5) <= 1

我们也可以在关系操作符两边使用连接:

any(2, 3) > all(-1, 1)

测试一个数值是否未出现在一列数值中,我们可以写:

$a eq none('foo', 'bar', 'baz')

连接只能用于关系表达式中。

当我们在关系操作符的一边使用多个数值的时候,可以通过通过 any() 函数自动创建隐含的连接。 比如,当一个数组值出现在关系操作符的一边的时候:

@foo > 3

它等效于

any(@foo) > 3

类似的,对于 uri-arg() 这样可能返回多个值的函数调用:

uri-arg("name") eq 'admin'

等效于:

any(uri-arg("name")) eq 'admin'

在连接操作的中使用求反关系操作符有潜在的问题,尤其是翻译成本地语言之后。 比如下面这个例子:

$a != any(1, 2, 3)

它实际上相当于这样:

!($a == any(1, 2, 3))

为了避免这种用英语语序理解容易出现的误会, edgelang 在关系操作符是 ne 或者 !=,并且在右边使用了 any() 这种场合下, 为用户自动做这样的转换。

连接只能在关系表达式的顶层使用。在函数参数中使用连接是禁止的。

目前还不支持嵌套连接。

回到目录

虚拟服务器

一个虚拟服务器是一个域名或者一个通配符的域名,用于表示一个独立的“主机”。 大量虚拟服务器或者域名共享同一个 OpenResty® 服务器实例是很常见的。

每个 edgelang 程序都跟一个虚拟服务器关联,虚拟服务器之间通常都是分离且独立的。 虚拟服务器通常并不直接在 edgelang 源代码内部生命,而是在调用 edgelang 编译器的时候在外部声明。 在 OpenResty® Edge 平台的环境下,虚拟服务器是用应用的概念表示的。在那里, 当 OpenResty® Edge 调用的时候,这些信息会自动提交给 edgelang 编译器。

如果给虚拟服务器声明了一个通配符域名,比如 *.foo.com,那么用户可以使用内置判断函数 host() 指定具体的子域名。 比如:

#  *.foo.com 的公共规则放在这里。。。

host("api.foo.com") => {
    # 子域名 api.foo.com 的规则放在这里
}, done;

host("blog.foo.com") => {
    # rules for the sub-domain blog.foo.com go here...
}, done;

回到目录

用户定义动作

用户可以定义他们自己的参数化动作,方法是把其他一些动作组合起来。 定义客户化动作的常用语法像下面这样:

action <name>(<arg>...) =
    <action1>,
    <action2>,
    ...
    <actionN>;

参数必须要指定数据类型,就像变量声明时那样。

比如:

action say-hi(Str $who) =
    say("hi, $who!"),
    exit(200);

然后一个 HTTP 请求会触发一个 HTTP 200 响应,响应体如下:

hi, Tom!

我们也可以声明多个参数。

用户定义动作是一个非常好的在动作中引入你自己的词汇的方法,这些动作可以用于规则结果中。

也可以定义定义递归动作,如:

action count-down(Num $n) =
    say($n),
    $n > 0 ? count-down($n - 1) : say("done");

true => count-down(5);

这样会生成下面的响应体:

5
4
3
2
1
0
done

递归的最大深度会由编译器限制,以避免无限递归。

回到目录

用户定义函数

用户可以定义自己的函数,可以用于规则条件和结果。定义客户化函数的语法释这样的:

func <name>(<arg>...) = <expression>

参数必须要指定数据类型,就像变量声明时那样。

= 符号后面的 <expression> 的值是整个函数的值。

参看下面的例子:

func x-powered-by () =
    resp-header("X-Powered-By");

x-powered-by contains rx:i/\b php \b/ =>
    errlog(level: "notice", "found a PHP server: ", x-powered-by);

这个例子定义了自己的函数 x-powered-by,它不接受参数,并且执行计算出表达式 resp-header("X-Powered-By" ) 的值。

用户定义函数也可以接受参数。参考下面的例子

func bit-is-set(Num $num, Num $pos) =
    $num & (1 << ($pos - 1))

check-bit(3, 1)
=>
    say("the 1st bit is set!");

bit-is-set 函数接受一个数字作为第一个参数,另外接受一个位的位置用于测试该数字的指定位。 如果置顶的二进制位是 1 则返回真,否则返回假。

需要提醒的是我们有一个内置判断函数 test-bit,它的功能就是用户定义函数 bit-is-set() 的功能。

回到目录

模块

Edge 模块是可以在不同 edgelang 程序之间共享的可重用的 edgelang 源代码文件。 模块通常包含各种用户定义动作和/或用户定义函数的定义。

要装在一个模块,用户的 edgelang 程序可以用 use 语句,像下面这样:

use Foo;

edgelang 编译器将在模块搜索路径中搜索一个名字为 Foo.edge 的文件。 用户可以在 edgelang 编译器命令行声明 -I PATH 选项向缺省模块搜索路径中增加用户定制路径。 比如:

edgelang -I /foo/bar -I /baz/blah test.edge

用户还可以在命令行上用 -M NAME 选项声明预装载的 edge 模块, 像下面这样:

edgelang -I /path/to/modules -M Foo -M Bar test.edge

回到目录

调用外部代码

我们还支持目标语言写的外部库。 比如,如果目标语言是 lua,那么只要用户具备足够的权限,那么她就可以在自己的 edgelang 程序中 调用任意 Lua 模块。

调用外部代码通常是用内置函数 foreign-call()。 它接受下列命名参数:

  • module

    外部模块名。在 Lua 的场合下,它是 Lua 模块的名字。这个参数是可选的。

  • func

    该模块内、或者目标语言缺省的名字空间内的函数名。这个参数是必须的。

位置参数(如果有的话)将被肢解传递给指定模块(如果有的话 0 的指定函数。

下面是一个调用标准 Lua 模块 mathrandom 函数的例子:

true =>
    say(foreign-call(module: "math", func: "random", 1, 10));

这个例子中的 foreign-call() 等效于下面 Lua 表达式:

math.random(1, 10)

要调用外部的 C 库代码,用户可以使用 LuaJIT 中高效的 FFI 简单封装一个 Lua 模块,然后在像普通 Lua 模块一样去调用。

外部代码的默认路径

请参考这篇文档创建全局 Lua 模块。

回到目录

内置判断函数

Edge 语言提供了下列内置判断函数。

回到目录

cache-status

语法: cache-status()

返回上游缓存状态。

比如:

true =>
    defer resp-header {
        set-resp-header("Cache-Status", cache-status);
    };

回到目录

cache-creation-time

语法: cache-creation-time()

阶段: resp-header

返回上游自缓存创建以来的时间

回到目录

client-addr

语法: client-addr()

返回客户端地址。

回到目录

client-asn

语法: client-asn()

语法: client-asn(asn1, asn2, ...)

当客户端地址属于参数中指定的任一自治系统号码 (autonomous system number) 时返回 true;否则返回 false。

举例如下:

client-asn("7018", "8023") =>
    say("Welcome, our dear guest!");

当未指定任何参数时,它将返回客户端的当前自治系统编号 (autonomous system number)。

举例如下:

client-asn eq "7018" =>
    say("Welcome, our dear guest!");

回到目录

client-continent

语法: client-continent()

语法: client-continent(continent1, continent2, ...)

如果客户端地址来自参数声明的大洲之一,返回真,否则返回假。

下面是所有大洲编码:

AF = Africa(非洲)
AS = Asia(亚洲)
EU = Europe(欧洲)
NA = North America(北美)
SA = South America(南美)
OC = Oceania(大洋洲)
AN = Antarctica(南极洲)

比如:

client-continent("AS") =>
    say("Welcome, our dear guest from Asia Region!");

如果不声明任何参数,它会返回客户端当前的洲名称:

client-continent eq "AS" =>
    say("Welcome, our dear guest from Asia Region!");

回到目录

client-country

语法: client-country()

语法: client-country(country1, country2, ...)

如果客户端地址是参数中声明的城市之一,则返回真;否则返回假。

你可以从下列地址获取所有两字母国家编码 wikipedia

下面是一个典型的国家代码列表:

US = United States of America
CA = Canada
CN = China
RU = Russian Federation
JP = Japan
IN = India
FR = France
DE = Germany

比如:

client-country("CN") =>
    say("Welcome, our dear guest from China!");

如果不声明任何参数,它会返回客户端的当前国家名:

client-country eq "CN" =>
    say("Welcome, our dear guest from China!");

回到目录

client-port

语法: client-port()

返回客户端端口。

client-province

语法: client-province()

语法: client-province(province1, province2, ...)

如果客户端地址来自参数声明的省份之一,则返回真;否则返回假。

比如:

client-province("California") =>
    say("Welcome, our dear guest from California!");

如果不声明任何参数,它会返回客户端当前的省份名:

client-province eq "California" =>
    say("Welcome, our dear guest from California!");

查看所有中国省份名称代号。

回到目录

client-city

语法: client-city()

语法: client-city(city1, city2, ...)

如果客户端地址是来自参数声明的城市列表之一,则返回真;否则返回假。

比如:

client-city("Los Angeles") =>
    say("Welcome, our dear guest from Los Angeles!");

如果不声明任何参数,它会返回客户端所在的当前城市:

client-city eq "Los Angeles" =>
    say("Welcome, our dear guest from Los Angeles!");

回到目录

client-isp

语法: client-isp()

语法: client-isp(isp1, isp2, ...)

如果客户端的 ISP 来自参数列表声明的 ISP 之一,则返回真;否则返回假。

比如:

client-isp("ChinaTelecom") =>
    say("our guest's ISP is ChinaTelecom!");

如果不声明任何参数,它会返回客户端当前 ISP 名:、

client-isp eq "ChinaTelecom" =>
    say("our guest's ISP is ChinaTelecom!");

查看所有中国 ISP。

回到目录

client-org

语法: client-org() 语法: client-org(org1, org2)

当客户端地址属于参数中指定的任一自治系统组织(autonomous system organization)时返回 true;否则返回 false。

举例如下:

client-org("Korea Telecom", "AT&T Services, Inc.") =>
    say("Welcome, our dear guest!");

当未指定任何参数时,它将返回客户端的当前自治系统编号。

举例如下:

client-org eq "AT&T Services, Inc." =>
    say("Welcome, our dear guest!");

回到目录

client-subnet

语法: client-subnet()

子系统: dns

返回 DNS 查询里面客户端的子网,如果在 DNS 查询里没有发现子网,则返回 nil

它支持 netaddr 常量,比如:

client-subnet ~~ 127.0.0.1/24 =>
    errlog("match");

注意 现在在 DNS 里只解析 IPv4。

回到目录

decode-base64

语法: decode-base64(digest)

把输入字串当作 base64 的编码解码。

回到目录

decode-hex

语法: decode-hex(str)

把输入字串单数当作一个十六进制进行解码。

回到目录

defined

语法: defined(val)

如果参数值被定义了,返回真;否则返回假。

回到目录

disable-convert-head-method-to-get

语法: disable-convert-head-method-get()

禁止将 HEAD 请求方法转换为 GET 方法用于回源。

回到目录

encode-base64

语法: encode-base64(str)

把输入的字串参数编码为 base64 数据。

回到目录

false

语法: false()

返回布尔假值。

回到目录

first-x-forwarded-addr

语法: first-x-forwarded-addr()

返回请求头 X-Forwarded-For 中的第一个地址。

回到目录

host

语法: host()

语法: host(pattern...)

如果不声明参数,返回请求指定的主机名。

如果声明了参数,这个函数在请求的主机名匹配任意参数指定的模式的时候返回真,这里匹配的操作符是 eq。 参数的模式可以是正则,文本串,或者通配符。

下面是例子:

host("foo.com", wc"*.foo.com") =>
    say("hit!");

这个等效于下面的形式:

host eq any("foo.com", wc"*.foo.com") =>
    say("hit!");

我们推荐用前者,因为它更简单。

回到目录

http-time

语法: http-time()

语法: http-time(quantity-val)

Last-ModifiedExpires 这样的响应头数值生成 HTTP 时间格式串。

如果没有声明参数,它会使用当前时间为缺省值。

如果声明了参数,那么只接受带时间单位的量词。

下面是一些例子:

# 1st
true =>
    http-time.say;

# 2nd
true =>
    say(http-time(now));

# 3rd
true =>
    say(http-time(1513068009 [s]));

返回结果是一个字传,比如 Tue, 12 Dec 2017 08:40:09 GMT

回到目录

ip-asn

语法: ip-asn(netaddr)

返回指定的 IP 地址的自治系统编号(autonomous system number

举例如下:

ip-asn("127.0.0.1") eq "7018" =>
    say("Welcome, our dear guest!");

回到目录

ip-continent

语法: ip-continent(netaddr)

子系统: dns

为指定的网络地址返回大洲名称,大洲名称可以用于解析 DNS 查询。

下面是例子:

ip-continent(client-subnet) eq 'AP' =>
    errlog("match");

回到目录

ip-country

语法: ip-country(netaddr)

子系统: dns

为指定的网络地址返回国家名称,国家名称可以用于解析 DNS 查询。

下面是例子:

ip-country(client-subnet) eq 'CN' =>
    errlog("match");

回到目录

ip-province

语法: ip-province(netaddr)

子系统: dns

为指定的网络地址返回省份名称,省份名称可以用于解析 DNS 查询。

下面是例子:

ip-province(client-subnet) eq 'Guangdong' =>
    errlog("match");

回到目录

ip-city

语法: ip-city(netaddr)

子系统: dns

为指定的网络地址返回城市名称,城市名称可以用于解析 DNS 查询。

下面是例子:

ip-city(client-subnet) eq 'Zhuhai' =>
    errlog("match");

回到目录

ip-isp

语法: ip-isp(netaddr)

子系统: dns

为指定的网络地址返回 ISP 名称,ISP 名称可以用于解析 DNS 查询。

下面是例子:

ip-isp(client-subnet) eq 'ChinaTelecom' =>
    errlog("match");

回到目录

ip-org

syntax: ip-org(netaddr)

返回指定的 IP 地址的自治系统组织名称(autonomous system organization)。

ip-org("127.0.0.1") eq "AT&T Services, Inc." =>
    say("Welcome, our dear guest from AT&T!");

回到目录

is-empty

语法: is-empty(value)

如果参数值是空(未定义,空字传或者 true 值)返回真;否则返回假。

回到目录

inject-csrf-token

语法: inject-csrf-token()

注意:该功能仅适用于表单请求,如果 HTML 页面上使用 AJAX 请求,CSRF token 将无法成功注入。

该动作会在 Content-Typetext/html 的响应内容末尾添加一段 JavaScript 代码。这段代码会自动为页面中的表单请求参数添加 _edge_csrf_token 参数,以便在发起表单请求时携带该参数。配合 validate-csrf-token 动作,可以实现 CSRF 防护的效果。只能在 resp-bodydefer 块中使用该动作。由于要修改响应体,还需要移除 Accept-Encoding 请求头,以免受到编码的影响。

下面是一个实现了 CSRF 防护功能的例子:

my Str $csrf-res;

true =>
    rm-req-header("Accept-Encoding"),
    defer resp-body {
        inject-csrf-token();
    },
    $csrf-res = validate-csrf-token(3600),
    {
        $csrf-res ne "ok" =>
            waflog($csrf-res, action: "block", rule-name: "csrf_protection"),
            exit(403);
    };

回到目录

last-x-forwarded-addr

语法: last-x-forwarded-addr()

返回请求头 X-Forwarded-For 中的最后一个地址。

回到目录

looks-like-int

语法: looks-like-int(value)

在参数看上去像整数的时候返回真,也就是说, 要么是一个看上去像整数的字串,或者是数值本身是一个整数值,或者是一个小数位为 0 的数值。 否则返回假。

下列调用会返回真:

looks-like-int(32)
looks-like-int(3.00)
looks-like-int("561")
looks-like-int('0')

回到目录

looks-like-num

语法: looks-like-num(value)

如果参数值看上去像一个数字,返回真,也就是说要么是一个像数字的字串, 要么是是一个数字值。

下列调用返回真:

looks-like-num(3.14)
looks-like-num("-532.3")

回到目录

lower-case

语法: lower-case(value)

返回一个参数的所有字母都转成小写字母的字串。

回到目录

md5-hex

语法: md5-hex(value)

返回一个参数值的十六进制表现的 MD5 摘要。

回到目录

escape-uri

语法: escape-uri(str)

返回 str URI 参数编码后的值。

回到目录

unescape-uri

语法: unescape-uri(str)

返回 str URI 参数解码后的值。

回到目录

str-len

语法: str-len(str)

返回 str 的长度。

回到目录

modsec-amp

语法: modsec-amp(var)

功能与 Modsecurity 规则里的 “&” 操作符一致,规则如下:

  • 当传入变量为空时,返回 0
  • 当传入变量为标量类型且不为空时,返回 1
  • 当传入变量为数组类型且不为空时,返回数组的长度
my Num $a;
my Str $b = "hello";
my Str @c = ("hello", "world");

true =>
    say(modsec-amp($a)),
    say(modsec-amp($b)),
    say(modsec-amp(@c)),
    done;

回到目录

match-ip-list

语法: match-ip-list(name: IP_LIST_NAME, IP)

如果 IP 地址符合 IP 列表中的其中一条,则返回真。否则返回假。

IP 列表需要在应用中或者全局配置中提前创建。指定应用 IP 列表的时候,需要在 IP 列表名称前加上 app: 的前缀。 当指定全局 IP 列表的时候,需要在 IP 列表名称前加上 global: 的前缀。

使用应用 IP 列表匹配:

match-ip-list(name: "app:ip-list-1", client-addr) =>
    say("matched"),
    done;

使用全局 IP 列表匹配:

match-ip-list(name: "global:ip-list-1", client-addr) =>
    say("matched"),
    done;

回到目录

now

语法: now()

从 OpenResty 缓冲的时间(因此不像 Lua 的 date 库那样需要系统调用)中返回一个纪元开始以来到现在的浮点数表示的秒数(小数部分是毫秒数)。

回到目录

now-secs

语法: now-secs()

返回标准的 Unix 时间戳,单位为秒。(与 Lua 的 os.date 库不同,这里无系统调用)

回到目录

post-arg

语法: post-arg(pattern...)

语法: post-arg(&ast;)

返回使用 pattern 通过 eq 操作符匹配到的 POST 表单参数的值。

参数 pattern 可以是正则,文本串或者通配符之一。

post-arg 不支持 multipart/form-data。

下面是一个是用 POST 表单参数 limitrate 的值限制响应体数据发送率的例子。

post-arg("limitrate") as $rate, looks-like-int($rate) =>
    limit-resp-data-rate($rate [Kb/s], after: 1 [MB]);

如果参数是一个任意值,也就是 *,那么它返回请求中所有的 POST 表单参数的值。 在布尔值上下文中,只要存在 POST 表单参数,其返回值即为真。

回到目录

random-pick

语法: random-pick(value...)

等概率随机选出参数值给予返回。

比如,

random-pick("foo", "bar", "baz")

会以相同概率返回 "foo""bar",或者 "baz"

回到目录

rand

语法: rand()

返回一个范围在 [0,1) 之间的随机数。

回到目录

rand-bytes

语法: rand-bytes(len)

返回一个长度为 len 的随机字符串。

回到目录

random-hit

语法: random-hit(ratio)

根据 ratio 参数声明的可能性,返回随机数的真假。这里 ratio 的取值必须在范围 [0,1] 之间。 这里的 0 意思是永不,而 1 意思是 100%,也就是说,永远。比如,如果 ratio 是 0.2,那么这个函数有 20% 的机会返回真, 而其它场合返回假。

回到目录

referer

语法: referer()

语法: referer(pattern...)

如果不加任何参数调用,它返回函数调用 req-header("Referer") 的值。

如果声明了一些参数,那么这些参数就会被当作模式使用。 如果引用者 referer 的值可以用 eq 操作符匹配任意这些模式,就会返回真。

参数 pattern 可以是正则,文本串,也可以是通配符。

比如:

referer(wc{*/search.html}, rx{.*?/find\.html}) =>
    say("hit!");

这个规则等效于下面这个无参数调用 referer 的形式:

referer-host eq any(wc{*/search.html}, rx{.*?/find\.html}) =>
    say("hit!");

我们推荐前面的形式,因为更简单。

回到目录

reg-domain

语法: reg-domain()

语法: reg-domain(pattern...)

如果没有给出参数,它返回客户端正在请求的服务器的注册域名。比如,像 www.openresty.org 这样的不是注册域名,而 openresty.org 是。

如果声明了一些参数,那么这些参数将会被当作模式看待。 如果引用者的主机名可以用 eq 操作符匹配任意其中的模式,则返回真。 参数 pattern 可以是正则,文本串,也可以是通配符。

比如:

reg-domain("openresty.org", "agentzh.org") =>
    say("hit!");

等效于:

reg-domain eq any("openresty.org", "agentzh.org") =>
    say("hit!");

我见建议使用前者,因为前者可以用引用字的语法进一步简化成下面这样:

reg-domain(qw/openresty.org agentzh.org/) =>
    say("hit!");

回到目录

req-charset

语法: req-charset()

返回请求头 Content-Type 里面的 charset 参数值(如果有的话)。

回到目录

语法: req-cookie(pattern...)

语法: req-cookie(&ast;)

返回 cookie 名可以用 eq 操作符和参数 pattern 匹配的 cookie 值。

在布尔环境里,它只会计算出真假两个值,分别对应 cookie 名匹配上和没有匹配上参数的情况。

参数 pattern 可以是正则,文本串或者通配符之一。

下面是一些例子:

req-cookie("mobile_type") =>
    say("cookie mobile_type is present!");

req-cookie("mobile_type") > 0 =>
    say("cookie mobile_type takes a value greater than 0!");

如果参数是任意值,也就是 *,那么它返回在请求中的所有 cookie。在布尔环境中, 只要请求中有 cookie,就会返回真。

cookie 的名字也可以是类似正则和通配符那样的模式。 在这种情况下,名字匹配任意一个这些模式的 cookie 的值都会被返回。

回到目录

req-header

语法: req-header(pattern...)

返回名字可以使用 eq 操作符匹配上参数 pattern 的对应请求头的值。

在布尔环境里,只要有任何请求头名字匹配上参数 pattern,就返回真,否则返回假。

参数 pattern 可以是正则,文本串或者通配符之一。

下面是一些例子:

req-header("X-WAP-Profile", "WAP-Profile") =>
    say("either header X-WAF-Profile or header WAF-Profile is present!");

请求头的名字也可以是像正则或者通配符那样的模式。在这种场合下, 匹配任意这些模式的请求头名字都会被选中,并且将其值返回。

回到目录

duplicate-req-header

语法: duplicate-req-header()

如果存在重复的请求头名字,这个动作会返回 true,否则返回 false

下面是一些例子:

duplicate-req-header =>
    say("duplicate request headers found!");

回到目录

max-req-header-name-len

语法: max-req-header-name-len()

返回最长的请求头名字的长度。

下面是一些例子:

max-req-header-name-len > 100 =>
    say("Found a request header name longer than 100");

回到目录

max-req-header-value-len

语法: max-req-header-value-len()

返回最长的请求头值的长度。

下面是一些例子:

max-req-header-value-len > 100 =>
    say("Found a request header value longer than 100");

回到目录

req-id

语法: req-id()

为当前请求返回请求 id 的值。请求 id 包含一些可以唯一标识一个 OpenResty Edge 中的某个请求的信息。

请求 id 总是一个 24 字符的字串。

下面是例子:

true =>
    add-resp-header("X-Request-Id", req-id)

回到目录

req-latency

语法: req-latency()

返回请求的时延,这是一个数量类型的值。

比如 0.01 [s] 意思是 0.01 second

回到目录

req-bytes

语法: req-bytes()

返回请求的字节数 (包含请求行/请求头/请求体)

回到目录

resp-bytes

语法: resp-bytes()

返回响应的字节数

回到目录

req-method

语法: req-method(pattern...)

如果不声明参数,返回请求方法的字串,比如 GETPOSTDELETE

如果声明了一些参数,那么这些参数就会被当作模式使用。这个模式用于匹配当前请求方法字串。如果任何其中的用户模式匹配上了就返回 true;否则返回 false

回到目录

req-uri

语法: req-uri()

带有参数的解码后的请求 URI

下面的例子返回了请求的 URI:

true =>
    say(req-uri());

回到目录

resp-header

语法: resp-header(pattern...)

返回响应头中的名字可以用 eq 操作符匹配上参数 pattern 的响应头数值。

在布尔环境中,如果有匹配的响应头名字,它就只是返回真,否则返回假。

参数 pattern 可以是正则,文本串,或者通配符。

比如:

resp-header("X-WAP-Profile", "WAP-Profile") =>
    say("either header X-WAF-Profile or header WAF-Profile is present!");

响应头名字也可以是类似模式或者通配符的东西。在这种场合下, 匹配任何这样的模式的头部名字都会被选中,并且返回他们的值。

回到目录

resp-header-param

语法: resp-header-param(header-name, param-name)

返回指定响应头中指定头参数的值。

比如,

resp-header-param("Cache-Control", "s-maxage") =>
   rm-resp-header-param("Cache-Control", "s-maxage");

这段代码在响应头 Cache-Control 里面出现了 s-maxage 参数的时候就把它删除。

回到目录

resp-body

语法: resp-body()

返回响应体的内容。只有在 resp-body 的 defer 块中可以使用。

比如,

true =>
    defer resp-body {
        errlog("body: ", resp-body);
    };

回到目录

resp-mime-type

语法: resp-mime-type()

语法: reps-mime-type(pattern...)

如果不声明参数,则返回响应的 MIME-type,也就是响应头 Content-Type,但是不包含 任何其它参数,比如 charset=utf-8

如果声明了一些参数,就会把这些参数当作模式看待。 如果响应 MIME-type 数值可以用 eq 操作符匹配任何模式,就会返回真。

参数 pattern 可以是,文本串或者通配符之一。

比如:

resp-mime-type("text/html", wc"*javascript") =>
    say("hit!");

这条规则等效于下面的无参数的 resp-mime-type 调用:

resp-mime-type eq any("text/html", wc"*javascript") =>
    say("hit!");

我们推荐用前者,因为更简单。

回到目录

resp-status

语法: resp-status()

语法: resp-status(code...)

如果没有声明参数,它返回当前响应的状态码。

如果给出了参数,那么参数会被用于跟当前响应状态进行比较的代码。 如果匹配上了任何特定的代码,则返回真,否则返回假。

比如:

resp-status(404, 500, 502, 503) =>
    say("found a known bad response status code: ", resp-status);

回到目录

scheme

语法: scheme()

返货当前请求的协议模式,比如 http 或者 https

比如:

scheme() eq "http" =>
    redirect(scheme: "https", host: host(), uri: req-uri(), code: 307);

回到目录

server-addr

语法: server-addr()

阶段: rewrite resp-header resp-body

返回接受了当前请求的服务器地址。

注意 此函数不能在 ssl-cert 阶段使用,其他阶段是可以的。

下面是例子:

true => say("address: ", server-addr);

因你的服务器监听地址不同,我们可能得到下列回答:

# IPv4
address: 127.0.0.1

# IPv6
address: ::1

# Unix domain
address: unix:/tmp/nginx.sock

回到目录

server-port

语法: server-port()

返回接受当前请求的服务器的端口。

回到目录

substr

语法: substr(str, start[, end])

返回从角标 start(从 1 开始)开始,到 end 结束指定的子串。

负数角标表示从字串尾部开始定位。比如,-1 表示最后一个字符,-2 表示倒数第二个,以此类推。

如果省略了 end 参数,它意味着所有直到字串尾部的字符。

下面是一些例子:

my Str $s = "hello world";

true =>
    say(substr($s, 1, 5)),       #  输出: hello
    say(substr($s, 7)),          #  输出: world
    say(substr($s, -5, -2)),      #  输出: worl
    say(substr($s, -5));         #  输出: world

回到目录

subst

语法: subst(subject, regex, replacement,) 语法: subst(subject, regex, replacement, g: BOOL)

字符串替换,将字符串 subject 中,正则 regex 匹配的部分替换成 replacement, 默认情况下,只替换第一个命中的。如果需要全局替换需要指定 g: true

下面是一些例子:

my Str $s = "hello world";

true =>
    say(subst($s, rx/l/, "g")),          #  输出: heglo world
    say(subst($s, rx/l/, "g", g: true)); #  输出: heggo worgd

回到目录

system-hostname

语法: host_name = system-hostname()

返回系统的主机名,与命令行 hostname 返回值相同

例:

true =>
     say("host name: ", system-hostname);

回到目录

ssl-client-s-dn

语法: client_subject_dn = ssl-client-s-dn()

返回客户端证书中的“subject DN”字符串,比如: CN=client.com,OU=dev,O=orinc,L=xm,ST=fj,C=cn

例如:

true =>
     say("client subject dn: ", ssl-client-s-dn);

回到目录

ssl-client-i-dn

语法: issuer_subject_dn = ssl-client-i-dn()

返回客户端证书中的“issuer DN”字符串,比如: CN=rootca.com,OU=dev,O=orinc,L=xm,ST=fj,C=cn

例如:

true =>
     say("issuer subject dn: ", ssl-client-i-dn);

回到目录

ssl-client-serial

语法: client_serial = ssl-client-serial()

返回已经建立的 SSL 链接的客户端证书的序列号,比如:

045CA7F023CAC0FD592B4D5DE5E7C6AF

例如:

true =>
     say("ssl client serial: ", ssl-client-serial);

回到目录

ssl-client-verify-result

语法: result = ssl-client-verify-result()

返回客户端证书认证的结果,结果的返回值包括:

NONE, SUCCESS, FAILED:unable to verify the first certificate

例如:

true =>
     say("result: ", ssl-client-verify-result);

回到目录

to-int

语法: to-int(value)

语法: to-int(value, method: METHOD)

将数字转化为整型。字符串将会被转化成 10 进制整型。纲量将会被移除单位。

其中参数 ceil / floor / round 将会决定其取整算法。默认为 floor

ceil 表示向上取整,floor 表示向下取整,round 表示四舍六入五成双。

例如:

true =>
    to-int("10.1", method: "ceil"), # 11
    to-int("10.1", method: "floor"); # 10

回到目录

to-num

语法: to-num(value)

把参数值转换成一个数值。字串会以 10 进制的形式转换成数字。量纲 会被删去单位部分。数字会直接通过。

回到目录

to-hex

语法: to-hex(value)

把参数值转换成一个十六进制编码的字符串。

回到目录

true

语法: true()

返回布尔值:真。

回到目录

ua-contains

语法: ua-contains(pattern...)

这个只是表达式 user-agent contains any(pattern1, pattern2, ...) 的缩写。

回到目录

ua-is-mobile

语法: ua-is-mobile()

如果客户端看上去像一个移动设备,则返回真,否则返回假。这个是通过客户端检查请求头的 User-Agent 字段来实现的。

回到目录

upper-case

语法: upper-case(value)

把一个字串的所有字符都转换成大写字母返回。

回到目录

upstream-addr

语法: $addr = upstream-addr()

返回字符串格式的上游地址,形如:192.168.0.1:8080

比如:

true =>
    defer resp-header {
        errlog(level: "warn", upstream-addr);
    };

回到目录

uri

语法: uri()

语法: uri(pattern...)

如果没有声明参数,那么返回请求的 URI。请注意这个 URI 串不包括任何 URI 参数。

如果声明了一些参数,那么这些参数会被当作模式看待,如果 URI 值可以用 eq 操作符匹配任意其中的模式, 就返回真。

参数 pattern 可以是正则,文本串或者通配符之一。

比如:

# 对请求 URIs `/foo/`, `/bar/`, 和 `/bar/blah`, 条件为真
# 但是对 `/blah/foo/`, `/blah/bar/`, 和 `/bar` 为假:
uri("/foo/", wc"/bar/*") =>
    say("hit!");

这条规则等效于下面形式不带参数的 uri 判断函数调用:

uri eq any("/foo/", wc"/bar/*") =>
    say("hit!");

我们建议用前者,因为他更简单。

回到目录

uri-arg

语法: uri-arg(name)

获取指定 URI 参数的值。

下面是一个是用 URI 参数 limitrate 的值限制响应体数据发送率的例子。

uri-arg("limitrate") as $rate, looks-like-int($rate) =>
    limit-resp-data-rate($rate [Kb/s], after: 1 [MB]);

回到目录

duplicate-uri-arg

语法: duplicate-uri-arg()

如果请求参数中有相同的名字,该动作会返回 true,否则返回 false

duplicate-uri-arg =>
    say("duplicate URI arguments found!");

回到目录

query-string

语法: query-string()

返回请求中的 查询字符串

例如下面这段 edge,如果请求是 GET /uri?foo=bar&a=b,我们将得到 foo=bar&a=b

true =>
    say(query-string);

回到目录

sorted-query-string

语法: sorted-query-string()

返回请求中排序后的 查询字符串

例如下面这段 edge,如果请求是 GET /uri?b=2&a=1&c=3,我们将得到 a=1&b=2&c=3

true =>
    say(sorted-query-string);

回到目录

uri-basename

语法: uri-basename()

语法: uri-basename(pattern...)

如果没有参数,它返回请求 URI 声明的基础名。比如,对于 URI /en/company/about-us.html,它返回 about-us 为基础名,对于 /static/download/foo.tar.gz, 则返回 foo

如果声明了参数,那么这些参数会被当成模式对待。 如果 URI 的值可以用 eq 操作符匹配任意其中的参数的话,就返回真。

uri-basename("foo", rx/bar\w+/) =>
    say("hit!");

回到目录

uri-contains

语法: uri-contains(pattern...)

这是表达式 uri contains any(pattern1, pattern2, ...) 的简写。

回到目录

uri-prefix

语法: uri-prefix(pattern...)

在布尔环境里,它是表达式 uri prefix any(pattern1, pattern2, ...) 的简写。

在字串环境里(比如在 as 表达式),它返回实际可以匹配上的第一个模式的子串。

回到目录

uri-seg

语法: uri-seg(index...)

语法: uri-seg(&ast;)

这个函数把 URI 的路径串当作用斜杠(/)分隔的多个,然后返回指定索引的段。 这个段索引从 1 开始,在 URI 里头从左到右递增。

比如,对于请求 URI /foo/bar/baz, uri-seg(1) 返回 foouri-seg(2) 返回 bar,而 uri-seg(3) 返回 baz。可以同时声明多个索引, 像 uri-seg(2, 5) 里头。

如果声明了任意值:* 为唯一的参数,这个函数返回所有 URI 段段路径值。

回到目录

uri-suffix

语法: uri-suffix(pattern...)

在布尔环境里,这是 uri suffix any(pattern1, pattern2, ...) 的简写。

在字串环境里 (比如在 as 表达式 里),它返回实际匹配第一个参数的子串。

回到目录

user-agent

语法: user-agent()

语法: user-agent(pattern...)

不带参数的时候,这个函数只是 req-header("User-Agent") 的简写。

如果带参数,则其调用等效于 user-agent eq any(pattern1, pattern2, ...),也就是说,检查用户的浏览器串是否可以用 eq 操作符匹配任意输入的模式。

回到目录

uuid-v4

语法: uuid-v4()

生成一个 UUID 版本 4 字串值。

回到目录

userid

语法: userid()

生成一个用户 id 的字串值。

回到目录

内置动作函数

add-req-header

语法: add-req-header(name, value)

语法: add-req-header(name1, value1, name2, value2, ...)

语法: add-req-header(%name-value-pairs)

添加新的请求头,但不会覆盖现存请求头中同名头。

比如:

true =>
    add-req-header("X-Foo", 1234);

如果你想覆盖现存同名请求头,请使用内置动作函数 set-req-header

回到目录

add-resp-header

语法: add-resp-header(header, value)

语法: add-resp-header(header1, value1, header2, value2, ...)

语法: add-resp-header(%name-value-pairs)

给当前请求增加新的响应头。不会影响现存请求里面同名头。如果你想覆盖现存头里头同名头, 请使用 set-resp-header

下面是一些例子:

true =>
    add-resp-header("X-Powered-By", "OpenResty Edge");

给当前请求增加新的响应头。不会影响现存请求里面同名头。如果你想覆盖现存头里头同名头, 请使用 set-resp-header

回到目录

add-uri-arg

语法: add-uri-arg(name, value)

语法: add-uri-arg(name1, value1, name2, value2, ...)

语法: add-uri-arg(%name-value-pairs)

给当前请求增加新的 URI 参数。不会影响 URI 中现存同名参数。如果想覆盖同名 URI 参数, 请使用 set-uri-arg

下面是一些例子:

true =>
    add-uri-arg("uid", "1234");

回到目录

add-uri-prefix

语法: add-uri-prefix(prefix)

为当前请求的 URI 添加一个前缀字符串。

请注意,前缀值不需要以斜杠(/)结尾,因为现有的 URI 字符串无论如何都必须以斜杠开头。

下面是一个例子:

true =>
    add-uri-prefix("/en/us");

对于请求 GET /install.html,这条规则会使 URI 变成 /en/us/install.html

回到目录

apply-std-mime-types

语法: apply-std-mime-types()

语法: apply-std-mime-types(force: true)

根据文件后缀名设置标准的 Content-Type 响应头。

默认情况下,仅当源响应头中无 Content-Type 响应头时生效。 但用户可以使用 force: true 选项来覆盖原有的 Content-Type 响应头。

回到目录

basic-authenticate

语法: basic-authenticate(auth-id: AUTH-ID)

启用 HTTP 的基本认证功能。

auth_id 参数是由授权列表类型和授权列表 ID 组成的。

如果这个授权列表是在应用内配置的,这个参数应该这样组合 app-auth:<list_id>

如果这个授权列表是在全局配置中的,那么参数应该是 global-auth:<list_id>

下面是一个例子:

basic-authenticate(auth-id: "app-auth:1") =>
    say("ok");

basic-authenticate(auth-id: "global-auth:1") =>
    say("ok");

回到目录

foreign-call

语法: foreign-call(module: <module>, func: <func>, arg...)

语法: foreign-call(func: <func>, arg...)

语法: foreign-call(func: <func>)

发起一个用目标语言(比如 Lua)对外部函数的调用。

可选的命名参数 module 声明外部模块名。 如果省略了这个参数,缺省是外部语言的标准名字空间。

func 名参数生命外部调用的函数名。这个参数是必须的。

任何其它位置参数都会当作传递给外部函数调用。

参考 调用外部代码 获取更多细节。

回到目录

enable-edge-captcha

语法: enable-edge-captcha(clearance-time: CLEARANCE-TIME)

语法: enable-edge-captcha(clearance-time: CLEARANCE-TIME, page-template-id: PAGE-TEMPLATE-ID)

为当前请求启用 Edge 验证码。如果请求已经通过验证,将被允许通过。

clearance-time 参数指定成功验证的有效期(以秒为单位)。

可选的 page-template-id 参数指定自定义验证码页面模板的 ID。此模板必须预先在全局页面模板中配置。

以下是示例:

使用默认模板,不需要指定 page-template-id 参数。

true =>
    enable-edge-captcha(clearance-time: 30),
    done;

指定页面模板:

true =>
    enable-edge-captcha(clearance-time: 60, page-template-id: 1),
    done;

回到目录

enable-hcaptcha

语法: enable-hcaptcha(clearance-time: CLEARANCE-TIME)

语法: enable-hcaptcha(clearance-time: CLEARANCE-TIME, page-template-id: PAGE-TEMPLATE-ID)

在使用 hCaptcha 之前,您需要在全局配置中配置 hCaptcha 的站点密钥和密钥:hCaptcha

为当前请求启用 hCaptcha 验证。如果请求已经通过验证,将被允许通过。

clearance-time 参数指定成功验证的有效期(以秒为单位)。

可选的 page-template-id 参数指定自定义 hCaptcha 页面模板的 ID。此模板必须预先在全局页面模板中配置。

以下是示例:

使用默认模板,不需要指定 page-template-id 参数。

true =>
    enable-hcaptcha(clearance-time: 30),
    done;

指定页面模板:

true =>
    enable-hcaptcha(clearance-time: 60, page-template-id: 1),
    done;

回到目录

enable-otel-trace

语法: enable-otel-trace()

该命令用来启用 otel trace 功能。

举例如下:

random-hit(0.05) =>
    enable-otel-trace();

回到目录

enable-proxy-cache

语法: enable-proxy-cache(key: KEY)

为当前请求打开用户提供缓存键字的代理缓存,缺省的时候代理缓存是关闭的。

回到目录

enable-global-cache

语法: enable-global-cache()

使用全局缓存。全局缓存将在不同的 APP 中共享,缺省的时候全局缓存是关闭的。

回到目录

enable-gateway-gzip

语法: enable-gateway-gzip()

语法: enable-gateway-gzip(enabled)

阶段: resp-header

为当前请求动态打开/关闭 gzip 压缩。

如果忽略参数,则默认打开网关 gzip。

如果使用了一个 bool 参数,意思是打开或关闭网关 gzip。

下面是例子:

uri-prefix("/css/") =>
    defer resp-header {
        enable-gateway-gzip;
    };

回到目录

enable-proxy-cache-revalidate

语法: enable-proxy-cache-revalidate()

语法: enable-proxy-cache-revalidate(enabled)

阶段: rewrite

是否启用 proxy_cache_revalidate 功能。

如果忽略参数,则默认打开 proxy_cache_revalidate 功能。

下面是例子:

true =>
    enable-proxy-cache-revalidate(true);

回到目录

enable-ssl-client-verify

语法: enable-ssl-client-verify()

开启客户端证书认证,如果认证失败,会返回 400 状态码并退出当前请求。

例如:

true =>
    enable-ssl-client-verify();

回到目录

enforce-proxy-cache

语法: enforce-proxy-cache(time)

set-proxy-cache-default-ttl 类似, 但将忽略响应头行为,强制缓存当前响应 (也就是说,忽略 Cache-Control,Set-Cookie,Expires 等等)。

回到目录

errlog

语法: errlog(level: LEVEL, msg...)

语法: errlog(msg...)

通过命名参数 level 指定的日志级别产生一条错误信息。 缺省是 error 错误级别。

level 可以是下面列表中的值:

  • error
  • warn
  • stderr
  • emerg
  • alert
  • crit
  • notice
  • info
  • debug

错误信息可以是多个字串参数。这个函数会自动把他们连接起来。

一些例子:

true =>
    errlog(level: "alert", "Something bad", " just happened!"),
    errlog("The user is not authorized");

回到目录

exit

语法: exit(code)

以状态码 code 退出当前请求处理。 如果到这个函数调用为止还没有响应信息发送出去,那么这个调用会为当前这个状态码生成一个缺省的错误页面————前提是这个状态码可以被识别。

如果想立即终止连接,请使用特殊的退出码 444

回到目录

expires

语法: expires(time)

语法: expires(time, force: true)

给响应头增加或者修改 ExpiresCache-Control 的设置为指定的过期时间。

缺省的嘶吼,这个函数只应用于状态码是 200, 201, 204, 206, 301, 302, 303, 304, 307 或者 308 的响应。 用户可以通过声明命名参数 force:true 来强制用于任何状态码。

time 位置参数必须是一个 量纲,接受一个时间单位, 像 [sec] (秒),[min](分钟),[hour] (小时),或者 [day] (天)。

下面是一个例子:

uri-prefix("/css/") =>
    expires(1 [day]);

这个动作不影响 proxy cache 过期时间。请参考 cache-expires

回到目录

limit-req-concurrency

语法: limit-req-concurrency(key: KEY, target-n: COUNT, reject-n: COUNT, log-headers: BOOL)

在用户声明的键字上限制到来请求大并发程度。

在运行这个设置之后,世纪的并发级别将保证不超过有名参数 target-n 的值。如果到来的并发程度在 target-nreject-n 之间,那么当前的请求将会被延迟一个合适的时间以满足目标并发级别。

如果到来的请求并发程度已经超过了 reject-n 值,那么当前请求将立刻被以一个 503 错误页面拒绝(针对 HTTP/HTTPS 应用)或者直接丢包(针对 DNS 应用)。

log-headers 设置为 true 时,错误日志中会记录请求头,默认为 false。

下面是例子:

true =>
    limit-req-concurrency(key: client-addr, target-n: 100, reject-n: 200);

回到目录

limit-req-count

语法: limit-req-count(key: KEY, target-n: NUM, reset-time: SECONDS, log-headers: BOOL)

在用户声明的键字上,限制在指定时间窗口 SECONDS 内的最多请求数量 NUM

有名参数 key 是可选的,可以是任意的字符串。如果省略之,则等效于一个常量键字,意味着针对整个应用的限制。

如果请求数超过 NUM,那么请求句柄将会立即以 503 错误页面(对 HTTP/HTTPS 应用)拒绝当前请求,或者是立即丢包(对 DNS 应用)。

log-headers 设置为 true 时,错误日志中会记录请求头,默认为 false。

下面是例子:

true =>
    limit-req-count(key: client-addr, target-n: 10, reset-time: 60);

回到目录

limit-req-rate

语法:

limit-req-rate(key: KEY, target-rate: RATE, reject-rate: RATE,
    reject-action: ACTION,
    hcaptcha-clearance-time: HCAPTCHA-CLEARANCE-TIME,
    edge-captcha-clearance-time: EDGE-CAPTCHA-CLEARANCE-TIME,
    redirect-validate-clearance-time: REDIRECT-VALIDATE-CLEARANCE-TIME,
    error-page-status-code: STATUS-CODE,
    log-headers: BOOL,
    page-template-id: PAGE-TEMPLATE-ID)

在用户声明的键字上限制请求速率。

有名参数 key 是可选的,可以是任意的字符串。如果省略之,则等效于一个常量键字,意味着针对整个应用的限制。

有名参数 target-rate 是我们想约束的最大速率。

如果到来的速率大于 reject-rate,那么请求句柄将会立即以 503 错误页面(对 HTTP/HTTPS 应用)拒绝当前请求,或者是立即丢包(对 DNS 应用)。

如果到来的速率在 target-ratereject-rate 之间,这个动作会等待应用一段时间以便让当前请求匹配 target-rate

reject-rate 的值不能小于 target-rate

两个速率的数值都必须接受一个类似 [r/s][r/min] 这样的单位。

有名参数 reject-action 是可选的,支持以下动作:

  • enable_hcaptcha 表示触发 hCaptcha,参数 HCAPTCHA-CLEARANCE-TIME 表示校验通过后多长时间不需要再次校验。

  • enable_edge_captcha 表示触发 edge captcha,参数 EDGE-CAPTCHA-CLEARANCE-TIME 表示校验通过后多长时间不需要再次校验。

  • error_page 表示返回自定义的错误页面,参数 STATUS-CODE 表示返回的状态码。

  • close_connection 表示直接断开连接。与 STATE-CODE 为 444 的 error_page 等价。

  • redirect_validate 表示进行重定向验证。

  • js_challenge 表示进行 js 挑战。

  • page_template 表示返回页面模板,页面模板通过 ID 进行指定。

log-headers 设置为 true 时,错误日志中会记录请求头,默认为 false。

下面是例子:

true =>
    limit-req-rate(key: client-addr,
        target-rate: 10 [r/s],
        reject-rate: 20 [r/s],
        reject-action: "enable_hcaptcha",
        hcaptcha-clearance-time: 50);

用户可以在一个请求头里头为不同的键字发起多个 limit-req-rate 调用。

回到目录

limit-resp-data-rate

语法: limit-resp-data-rate(rate)

语法: limit-resp-data-rate(rate, after: size)

在发送响应(体)数据的时候限制速率。 位置参数 rate 声明最高速度的速率。它必须是一个量纲, 接受一个速率单位,比如 [kB/s]

可选的命名参数 after 接收一个带着尺寸单位(比如 kBmB)的参数。

下面是一个例子:

true =>
    limit-resp-data-rate(100 [kB/s], after 200 [kB]);

请注意小写的 k 前缀代表的是 1000,而大写的 K 前缀的意思是 1024。类似的是,小写的 b 单位意思是 bit, 而大写的 B 意思是 byte,意思是一个八位元。

回到目录

local-time

语法: local-time()

语法: local-time(year: YEAR, month: MONTH, day: MDAY, hour: HOUR, min: MINUTE, sec: SECOND)

返回指定时间(当前时区)的 Unix 时间戳(从协调世界时 1970 年 1 月 1 日 0 时 0 分 0 秒起至现在的总秒数), 注意返回值为有单位的数值。

如东八区的 2019-01-01 00:00:00 会返回 1546272000 [s]

如果调用时没有带参数,则会返回当前 Unix 时间戳(从协调世界时 1970 年 1 月 1 日 0 时 0 分 0 秒起至现在的总秒数), 返回值同样是有单位的数值。

如果带了部分参数,则剩余的部分将会被默认值填充:

  • YEAR: 0
  • MONTH: 1
  • MDAY: 1
  • HOUR: 0
  • MINUTE: 0
  • SECOND: 0

例:

true =>
    local-time().say; # current timestamp (quantity typed value)

# 1546272000 [s]
true =>
    local-time(year: 2019, month: 1, day: 1, hour: 0, min: 0, sec: 0).say,
    local-time(year: 2019).say;

回到目录

local-time-day

语法: local-time-day()

返回当前日期中的几号。

2019-01-02 03:04:05 中会返回 2

true =>
    local-time-day().say;

回到目录

local-time-hour

语法: local-time-hour()

返回当前时间中的小时数。

例如 2019-01-02 03:04:05 将会返回 3

true =>
    local-time-hour().say;

回到目录

local-time-min

语法: local-time-min()

返回当前时间的分钟数。

例如 2019-01-02 03:04:05 将会返回 4

true =>
    local-time-min().say;

回到目录

local-time-sec

语法: local-time-sec()

返回当前时间的秒数。

例如 2019-01-02 03:04:05 将会返回 5

true =>
    local-time-sec().say;

回到目录

print

语法: print(msg...)

生成客户化的响应体数据片段。如果响应头还没发送,那么它会在响应体之前自动发送 ———— 这个原因很明显哈。

say 动作不一样,这个动作不会在每条用户消息后头附加一个新行字符。

比如:

true =>
    print("hello", ", world!"),
    print(" oh, yeah");

回到目录

redirect

语法: redirect(uri: URI)

语法: redirect(host: HOST, uri: URI, args: ARGS)

语法: redirect(scheme: SCHEME, host: HOST, uri: URI, code: CODE)

阶段: rewrite

发送 HTTP 重定向响应。它接受下列命名参数:

  • uri

    URI 字串,去掉了所有查询串后缀,以及去掉了所有主机/模式前缀。

  • args

    URI 查询串或者一个带着参数键值对的表。缺省是没有。

  • host

    将要重定向的主机名。这是可选的,缺省是当前服务器。

  • scheme

    协议模式,比如 httphttps。缺省是当前请求的协议模式。

  • code

    准备使用的状态码。可以是 301, 302, 303 或者 307。缺省是 302。

比如:

uri("/foo") =>
    redirect(uri: "/blah/bah.html");

uri("/foo") =>
    redirect(scheme: "https", host: "a.foo.com", uri: "/blah/bah.html",
             args: "a=1&b=4", code: 301);

回到目录

replace-resp-filter

语法: replace-resp-filter(string, replacement)

语法: replace-resp-filter(string, replacement, g: BOOL)

语法: replace-resp-filter(regex, replacement)

语法: replace-resp-filter(regex, replacement, g: BOOL)

这个函数用于替换响应体中的内容,可以匹配固定字符串或正则表达式模式。它只能在 resp-body 的 defer 块中使用。

参数说明:

  • stringregex:要匹配的字符串或正则表达式。
  • replacement:用于替换匹配内容的字符串或用户定义函数。
    • 如果是字符串,可以包含子模式捕获变量(如 $1$2 等)。
  • g:可选布尔参数,默认为 false。
    • 当设为 true 时,替换所有匹配项。
    • 当为 false 时,只替换第一个匹配项。

示例:

  • replace-resp-filter("hello", "hi") - 将第一个 “hello” 替换为 “hi”
  • replace-resp-filter("hello", "hi", g: true) - 将所有 “hello” 替换为 “hi”
  • replace-resp-filter(/\d+/, "number") - 将第一个数字替换为 “number”

这个函数主要用于在将响应发送给客户端之前修改响应内容。比如:

host('test1.com') =>
    defer resp-body {
        replace-resp-filter(/\d+/, "number", g: true);
    };

host('test2.com') =>
    defer resp-body {
        replace-resp-filter(/(\d+)/, "number: $1", g: true);
    };

高级用法:

对于更复杂的替换需求,例如需要对子模式捕获变量进行转换,可以使用用户定义函数。以下示例展示了如何将捕获的文本转换为小写:

func transform(Str $full-match, Str @groups) =
    "before: $full-match" ~ ', after: ' ~ lower-case(@groups[0]) ~ ' ' ~ lower-case(@groups[1]) ~ @groups[2];

true =>
    defer resp-body {
        replace-resp-filter(rx:i/(hello)\s*(world)(!)/, transform);
    },
    say("HELLO WORLD!");

在这个例子中:

  • transform 函数接收两个参数:
    • $full-match:完整的匹配内容
    • @groups:子模式捕获变量数组
  • @groups[0] 代表第一个捕获组(HELLO),@groups[1] 代表第二个捕获组(WORLD),以此类推。
  • 这个示例最终会返回:before: HELLO WORLD!, after: hello world!

通过使用用户定义函数,您可以实现更灵活和强大的内容替换逻辑。

回到目录

rewrite-uri-seg

语法: rewrite-uri-seg(index, replacement)

语法: rewrite-uri-seg(index1, replacement1, index2, replacement2, ...)

这个函数把 URI 路径字串当作一个用斜杠(/)分隔的,然后用指定的下标和在参数 replacement 里头的值替换这些段。段下表从 1 开始计,并且在 URI 路径中从左向右增长。

比如,对于请求 URI /foo/bar/bazrewrite-uri-seg(1, "qux") 生成一个新的 URI /qux/bar/baz,而 rewrite-uri-seg(2, "qux") 生成 /foo/qux/baz。 多个下标可以同时声明,比如 rewrite-uri-seg(2, "qux", 3, "foo") 生成 /foo/qux/foo

回到目录

语法: rm-req-cookie(name)

语法: rm-req-cookie(name1, name2, ...)

删除请求里 cookie 名和参数匹配的 cookie。

下面是一个例子:

true =>
    rm-req-cookie("foo", "bar");

回到目录

rm-req-header

语法: rm-req-header(pattern...)

如果请求里头的某个字段的名字可以用 eq 关系操作符匹配上位置参数给出的那些模式,就删除它们。

模式可以是一个文本串,一个正则或者一个通配符。

下面是一个例子:

true =>
    rm-req-header("Authorization", rx/X-.*/, wc/Internal-*/);

这条规则无条件删除任何名字是 Authorization、任何名字以 X- 开头或者任何名字以 Internal- 开头的请求头。

回到目录

语法: rm-resp-cookie(name...)

删除响应头中一个或多个指定名称的 Cookie。

如果响应里头的 cookie 名可以用 eq 关系操作符匹配上位置参数给出的那些模式,就删除它们。

这里是一个例子:

true =>
    rm-resp-cookie("_uid","foo");

上面这条规则无条件删除响应里头名字是 _uidfoo 的 cookie。

如果想要删除所有 Cookie,可以使用 rm-resp-header("Set-Cookie")

回到目录

rm-resp-header

语法: rm-resp-header(pattern...)

如果响应头里头的某个字段的名字可以用 eq 关系操作符匹配上位置参数给出的那些模式,就删除它们。

模式可以是一个文本串,一个正则,一个通配符,或者一个任意值。

下面是一个例子:

true =>
    rm-resp-header("Set-Cookie", rx/X-.*/, wc/Internal-*/);

这条规则无条件删除任何响应头名字是 Set-Cookie 、或者名字以 X- 开头、以 Internal- 开头的响应头。

回到目录

rm-uri-arg

语法: rm-uri-arg(name...)

删除指定名字的 URI 参数。

true =>
    rm-uri-arg("foo");

回到目录

rm-uri-prefix

语法: rm-uri-prefix(pattern...)

删除那些能够匹配用户通过参数声明的第一个模式的 URI 前缀。

比如:

true =>
    rm-uri-prefix("/foo/", rx{/foo\d+/});

对请求 URI /foo/hello,这条规则会把 URI 变成 /hello。而对请求 /foo1234/, 这条规则会生成新 URI /

回到目录

rm-uri-seg

语法: rm-uri-seg(index...)

这个函数把 URI 路径当作一个用斜杠(/)分隔的多个组成的数组,然后删除指定下标的段。 段索引从 1 开始计,在 URI 中从左向右递增。

比如,对于请求 URI /foo/bar/bazrm-uri-seg(1) 会生成一个新的 URI /bar/bazrm-uri-seg(2) 生成 /foo/baz,而 rm-uri-seg(3) 返回 /foo/bar/。 一次可以声明多个下标,比如 rm-uri-seg(2,5)

回到目录

say

语法: say(msg...)

生成客户化定制的响应体数据块,并且自动后缀一个新行。 显然,如果还没有发送响应头,那么会在发送响应体数据之前自动发送响应头。

如果你不想要背后的新行,那么可以用 print

比如:

true =>
    say("hello", ", world!"),
    say(" oh, yeah");

回到目录

set-error-page

语法: set-error-page(resp-body: CONTENT, content-type: CONTENT-TYPE, error_code…)

语法: set-error-page(refetch-url: URL, content-type: CONTENT-TYPE, error_code…)

语法: set-error-page(page-template-id: ID, content-type: CONTENT-TYPE, error_code…)

对指定的错误响应码设置错误页,其中 content-type 字段可选。

错误页支持两种设置形式:

  1. resp-body: HTML 内容
  2. refetch-url: 静态资源 URL
  3. page-template-id: 页面模板的 ID

注意:不支持同时使用两个或以上的方式设置错误页,否则会报错。

支持以下的错误响应码:

  • 403
  • 404
  • 500
  • 501
  • 502
  • 503
  • 504

例:

true =>
   set-error-page(404, resp-body: "<h1>Not Found</h1>");

true =>
   set-error-page(500, refetch-url: "http://example.com/error.html")

回到目录

set-otel-span-name

语法: set-otel-span-name('name')

phase: rewrite

默认的 span 名称是 URI。使用该接口设置新的 span 名称。

举例如下:

true =>
    set-otel-span-name('new-span-name');

回到目录

set-proxy-cache-default-ttl

语法: set-proxy-cache-default-ttl(time, status: STATUS)

设置代理缓存缺省的过期时间,当响应状态码为 STATUS 时生效。

默认的 status200,目前仅支持 200, 301, 302

位置参数 time 必须是一个量纲,接收一个时间单位,比如 [sec] (秒),[min] (分钟),[hour](小时),以及 [day] (天)。

下面是例子:

uri-prefix("/css/") =>
    set-proxy-cache-default-ttl(1 [day]);

这个动作不影响当前响应的 ExpiresCache-Control 响应头。如果您需要覆盖浏览器的缓存时间,可以这样使用:

uri-prefix("/css/") =>
    set-proxy-cache-default-ttl(1 [day]);
    expires(12 [hour]);

我们可以给节点缓存(通过 set-proxy-cache-default-ttl) 和浏览器(通过 expires)声明不同的过期时间。

又见 expires.

回到目录

set-proxy-cache-use-stale

语法: set-proxy-cache-use-stale('off')

语法: set-proxy-cache-use-stale('http_500', 'invalid_header', ...)

阶段: rewrite

修改当前请求的 proxy_cache_use_stale 配置,如果传入 off 则会关闭 proxy-cache-use-stale 功能。

下面是例子:

true =>
    set-proxy-cache-use-stale('http_500', 'invalid_header');

回到目录

语法: set-req-cookie(name, value)

语法: set-req-cookie(name1, value1, name2, value2, ...)

设置新的请求 cookie,会覆盖任何现有同名的 cookie。

比如:

true
=>
    set-req-cookie("foo", "foo", "bar", "bar"),
    say("foo:" ~ req-cookie("foo")),
    say("bar:" ~ req-cookie("bar"));

回到目录

set-req-header

语法: set-req-header(name, value)

语法: set-req-header(name1, value1, name2, value2, ...)

语法: set-req-header(%name-value-pairs)

设置请求头,覆盖任何现有同名的请求头。

比如:

uri-prefix("/foo/") =>
    set-req-header("X-Debug", 1);

如果你只想添加新的请求头而不是覆盖现有的,那么请使用内置动作 add-req-header

回到目录

req-body

语法: req-body()

获取请求体。

比如:

req-body =>
    req-body.say;

回到目录

set-req-body

语法: set-req-body(body)

设置请求体内容,会覆盖当前的请求体。

比如:

uri-prefix("/foo/") =>
    set-req-body("foo");

回到目录

set-proxy-host

语法: set-proxy-host(host)

缺省代理注意是当前请求主机。

回到目录

set-proxy-header

语法: set-proxy-header(header, value)

语法: set-proxy-header(header1, value1, header2, value2, ...)

给代理的服务器设置代理头。现有同名头会被删除。

你可以用这个函数把一个客户端和服务器之间的连接,从 HTTP/1.1 改成 WebSocket,下面是例子:

true =>
    set-proxy-header("Upgrade", "WebSocket",
                     "Connection", "Upgrade");

用这个 API 不能设置的头列表如下:

  • Content-Length
  • Transfer-Encoding
  • If-Modified-Since
  • If-None-Match

回到目录

set-proxy-uri

语法: set-proxy-uri(uri, [query-string: QUERY-STRING])

将上游代理服务器收到的请求 URI 设置为一个新数值,不改变当前 URI。 这里的 URI 不应该包含任何查询串或者任何主机/端口部分。query-string 参数是可选的。

true =>
    set-proxy-uri("/foo.html");

true =>
    set-proxy-uri("/foo.html", query-string: "foo=bar");

回到目录

append-proxy-header-value

语法: append-proxy-header-value(header, value)

语法: append-proxy-header-value(header1, value1, header2, value2, ...)

给代理服务器追加代理头。 若对应代理头字段非空,则会用 value 追加到代理头字段现有数值后,用逗号分隔。 若代理头字段为空,代理头字段值将会是 value

比如:

true =>
    appear-proxy-header-value("X-Forwarded-For", client-addr);
    # 当原始 `X-Forwarded-For` 请求头内容为 `192.168.1.1`
    # 且客户端地址为 `10.10.1.1` 时
    # 代理服务器收到的 `X-Forwarded-For` 请求头内容被设置为 `192.168.1.1,10.10.1.1`

回到目录

block-req

语法:

block-req(key: KEY, target-rate: RATE, reject-rate: RATE,
                    block-threshold: COUNT,
                    observe-interval: COUNT,
                    block-time: TIME,
                    log-headers: BOOL,
                    reject-action: REJECT-ACTION,
                    status-code: STATUS-CODE,
                    clearance-time: CLEARANCE-TIME,
                    page-template-id: PAGE-TEMPLATE-ID)

根据指定的关键字来限制请求的频率。

其中,参数 key 可选。当未设置 key 时,语句就等价于使用常量作为关键字。

target-rate 参数代表想要设置的频率上限。

当请求频率达到 reject-rate 时,当前请求立即会被拒绝并:

  • HTTP/HTTPS 应用:响应一个 503 页面
  • DNS 应用:立即丢包

当请求频率大于 target-rate 且小于 reject-rate 时,当前请求会被延迟处理, 去试图达到 target-rate 设定的值。

注意:reject-rate 必须大于 target-rate,且两者都需要带上类似 [r/s][r/min] 的单位。

observe-interval: 检测间隔的时间窗口大小,单位为秒 (s) block-threshold: 连续检测的次数

当请求率在一个检测间隔内达到了 reject-rate,在接下来的 block-time 时间内的请求, 都会被拒绝并响应一个 503 页面。

log-headers 设置为 true 时,错误日志中会记录请求头,默认为 false。

有名参数 reject-action 是可选的,支持以下动作:

  • enable_hcaptcha 表示触发 hCaptcha,参数 CLEARANCE-TIME 表示校验通过后多长时间不需要再次校验。

  • enable_edge_captcha 表示触发 edge captcha,参数 CLEARANCE-TIME 表示校验通过后多长时间不需要再次校验。

  • error_page 表示返回自定义的错误页面,参数 STATUS-CODE 表示返回的状态码。

  • close_connection 表示直接断开连接。与 STATE-CODE 为 444 的 error_page 等价。

  • redirect_validate 表示进行重定向校验。参数 CLEARANCE-TIME 表示校验通过后多长时间不需要再次校验。

  • js_challenge 表示进行 js 挑战。

  • page_template 表示返回页面模板,页面模板通过 ID 进行指定。

示例:

true =>
    block-req(key: client-addr, target-rate: 10 [r/s], reject-rate: 20 [r/s],
              block-threshold: 2, observe-interval: 30, block-time: 60);

用户在一个请求处理中,可以调用多次 block-req,实现多个约束的请求限制。

回到目录

这个 API 不能使用的头的的列表:

  • Host
  • Connection
  • Upgrade
  • Content-Length
  • Transfer-Encoding
  • If-Modified-Since
  • If-None-Match

回到目录

set-req-host

语法: set-req-host(host)

把请求的 Host 请求头设置成位置参数 host 的值。 它只是 set-req-header("host", host) 的缩写。

这个动作不回让当前请求重新匹配新的虚拟服务器。它通常只是影响转发给上级服务器的 Host 请求头。

比如:

host("images.foo.com") =>
    set-req-host("images.foo.com.s3.amazonaws.com");

回到目录

语法: set-resp-cookie(name, value, domain: DOMAIN, path: PATH, http-only: BOOL, expires: TIME, max-age: TIME)

设置一个新的响应 cookie,覆盖任何现有同名 cookie。

回到目录

语法: set-resp-cookie-samesite(value)

语法: set-resp-cookie-samesite(value, names: NAMES)

设置响应 cookie 的 SameSite 属性,value 可以为 StrictLax。默认会修改所有的响应 cookie,可以通过 names 参数来指定要修改的响应 cookie。

下面是一个例子:

true =>
    set-resp-cookie-samesite("Lax", names: ("cookie1", "cookie2"));

回到目录

set-resp-header

语法: set-resp-header(header, value)

语法: set-resp-header(header1, value1, header2, value2, ...)

语法: set-resp-header(%name-value-pairs)

给当前响应设置响应头。现存同名响应头会呗删除。如果你不想覆盖现存同名响应头, 请使用add-resp-header

下面是一个例子:

true =>
    set-resp-header("X-Powered-By", "OpenResty Edge");

回到目录

set-resp-body

语法: set-resp-body(value)

给当前响应设置响应体,只能在 resp-body 的 defer 块中使用。

下面是一个例子:

true =>
    defer resp-body {
        set-resp-body("hello world");
    };

回到目录

capture-resp-body

语法: capture-resp-body(size)

阶段: rewrite resp-header

捕获响应体到日志变量 $response_body 中,需要设置最大值,单位为字节。 如果响应体大小超过设置的最大值,只会捕获部分响应体内容。

接受一个带 kBmB 这样的尺寸单位的 size 参数。

下面是一个例子:

true =>
    capture-resp-body(1 [kB]);

true =>
    defer resp-header {
        {
            resp-status(403) =>
                capture-resp-body(8192);
        };
    };

回到目录

set-resp-status

语法: set-resp-status(code)

把当前响应的状态码设置为参数 code 的值。

不要在响应头已经发出之后调用这个动作(比如响应头已经被 一个 print 或者 say 调用触发)。

下面是一个例子:

req-header("User-Agent") eq "" =>
    set-resp-status(450),
    say("custom body message for 450 status"),
    exit(450),
    done;

回到目录

set-proxy-retry-condition

语法: set-proxy-retry-condition(...)

阶段: rewrite

设置上游重试条件,默认值:error, timeout。 支持的值有: error, timeout, invalid_header, http_500, http_502, http_503, http_504, http_403, http_404, http_429, non_idempotent。

  • error:与服务器建立连接、向其传递请求或读取响应标头时发生错误;
  • timeout:在与服务器建立连接、向其传递请求或读取响应标头时发生超时;
  • invalid_header:服务器返回了一个空的或无效的响应;
  • http_CODE:服务器返回了指定的 HTTP 状态码;
  • non_idempotent:通常非幂等方法(POST、LOCK、PATCH)的请求只会传递给一个服务器,启用此选项明确允许重试此类请求;
true =>
    set-proxy-retry-condition('http_404'),
    set-proxy-retries(1),
    done;

示例中如果上游服务器返回 404 状态码,则会进行 1 次重试。

回到目录

set-proxy-retries

语法: set-proxy-retries(num)

设定重试次数,默认为 0,意味着默认情况下不会重试。

回到目录

set-proxy-timeouts

语法: set-proxy-timeouts(connect: TIMEOUT?, send: TIMEOUT?, read: TIMEOUT?)

语法: set-proxy-timeouts(connect: TIMEOUT)

语法: set-proxy-timeouts(send: TIMEOUT)

语法: set-proxy-timeouts(read: TIMEOUT)

设定超时时间,默认 connect, send, read 都是 60s。

参数是一个量纲,如:60 [s], 60 [ms]。

回到目录

set-proxy-recursion-depth

语法: set-proxy-recursion-depth()

语法: set-proxy-recursion-depth(DEPTH)

设置最大的代理递归深度,默认值为 -1,即不启动该功能。

注意 当该功能启用时,额外的请求头 OR-Proxy-Recursion-Depth 会发往上游

set-uri

语法: set-uri(uri)

把当前请求 URI 设置为一个新的数值。这个 URI 不应该包含任何查询串或者任何主机/端口部分。

这个动作主要用于改变那些准备通过代理转发给上层服务的 URI 请求上。

回到目录

set-uri-arg

语法: set-uri-arg(name, value)

语法: set-uri-arg(name1, value1, name2, value2, ...)

语法: set-uri-arg(%name-value-pairs)

在当前请求中设置 URI 参数。现存同名 URI 参数会被删除。如果你不想覆盖现存同名 URI 参数, 请用 add-uri-arg

下面是一个例子:

true =>
    set-uri-arg("uid", "1234");

回到目录

set-upstream-name

语法: set-upstream-name(upstream-1, weight-1?, upstream-2?, weight2?)

阶段: rewrite

设置一个或多个带权重上游,这里的 upstream-1, upstream-2 是上游名称,在运行时会根据上游名字获取上游信息。

如果应用里没有这样的上游,会从全局上游里查找。

uri('/') =>
    set-upstream-name('upstream-1', 1, 'upstream-2', 1);

回到目录

set-backup-upstream-name

语法: set-backup-upstream-name(upstream-1, upstream-2?)

阶段: rewrite

设置应用的备用上游,当主上游错误并且符合重试条件的时候,会代理到备用上游。

uri('/') =>
    set-upstream-name('upstream-2'),
    set-proxy-retry-condition('error'),
    set-proxy-retries(1),
    set-backup-upstream-name('upstream-1');

回到目录

set-upstream-addr

语法: set-upstream-addr(ip: IP, [host: DOMAIN], port: PORT , [scheme: SCHEME])

阶段: rewrite

通过地址来设置单个上游,IP 参数和 host 参数不能同时使用。scheme 参数默认是 http

uri('/ip') =>
    set-upstream-addr(ip: '127.0.0.1', port: 80);

uri('/domain') =>
    set-upstream-addr(host: 'localhost', port: 80);

uri('/scheme') =>
    set-upstream-addr(host: 'localhost', port: 443, scheme: 'https');

回到目录

upstream-has-live-nodes

语法: upstream-has-live-nodes(upstream-name)

阶段: rewrite

检查上游是否健康。

以下情况会返回 true:

  • 存在一个及以上健康的节点
  • 上游没有开启健康检查

以下情况会返回 false

  • 不存在健康节点
  • 不存在指定名称的上游
upstream-has-live-nodes('upstream-1') =>
    set-upstream-addr(ip: '127.0.0.1', port: 80);

回到目录

set-upstream-retry-uri

语法: set-upstream-retry-uri(uri)

阶段: rewrite

设置代理到上游服务器失败时的重试 URL,将会使用原 URI 重试完指定的重试次数后,再使用新的 URI 进行 1 次重试。 参数 uri 支持 edgelang 变量和字符串常量。

true =>
    set-upstream-retry-uri("/hello"),
    set-proxy-retry-condition('http_404'),
    done;

示例中如果上游服务器返回 404 状态码,将会使用 /hello 进行重试。

回到目录

set-max-body-size

语法: set-max-body-size(size)

阶段: rewrite

设置本次请求可以接受的最大 POST 体。对于那些具备有效 Content-Length 头的请求,这个方法将检查该头字段并且把超过 size 的请求立即终止,返回 413 Request Entity Too Large。对于分段的编码请求和 HTTP/2 请求,此动作将随着缓冲区处理进行检查。

如果设置为 0 则关闭此检查。

接受一个带 kBmB 这样的尺寸单位的 size 参数。

true =>
    set-max-body-size( 1 [kB]);  # 1000 bytes

回到目录

set-access-log-off

语法: set-access-log-off()

阶段: rewrite

设置本次请求不记录 access log.

true =>
    set-access-log-off();

Back to TOC

sleep

语法: sleep(time)

不阻塞地睡眠指定的秒数。我们可以声明精度为 0.001 秒(也就是 1 毫秒)的精度。

下面是例子:

true =>
    sleep(0.5);

回到目录

utc-time

语法: utc-time()

语法: utc-time(year: YEAR, month: MONTH, day: MDAY, hour: HOUR, min: MINUTE, sec: SECOND)

返回指定时间 (UTC) 的 Unix 时间戳(从协调世界时 1970 年 1 月 1 日 0 时 0 分 0 秒起至现在的总秒数), 注意返回值为有单位的数值。

如 UTC 的 2019-01-01 00:00:00 会返回 1546272000 [s]

如果调用时没有带参数,则会返回当前 Unix 时间戳(从协调世界时 1970 年 1 月 1 日 0 时 0 分 0 秒起至现在的总秒数), 返回值同样是有单位的数值。

如果带了部分参数,则剩余的部分将会被默认值填充:

  • YEAR: 0
  • MONTH: 1
  • MDAY: 1
  • HOUR: 0
  • MINUTE: 0
  • SECOND: 0

例:

true =>
    utc-time().say; # current timestamp (quantity typed value)

# 1546272000 [s]
true =>
    utc-time(year: 2019, month: 1, day: 1, hour: 0, min: 0, sec: 0).say,
    utc-time(year: 2019).say;

回到目录

waf-mark-risk

语法: waf-mark-risk(level: LEVEL, msg: MESSAGE)

这是 WAF 模块的主要动作,用于标记当前请求为恶意请求,以及具体恶意程度, levelmsg 均为选填项。

WAF 的基本逻辑是根据当前请求的匹配信息,标记不同的风险等级, 系统会自动根据来源 IP 进行汇总,当风险值累计达到拦截阈值等级(在界面上配置)时, 将执行提前设定的拦截动作(在界面上配置)。

level 表示恶意程度,level 可以是:

  1. definite 表示确定是危险请求,肯定将执行拦截动作(无论拦截阈值等级是多少)。
  2. high 表示高危险。
  3. middle 表示一般危险。
  4. low 表示低危险(默认)。
  5. debug 表示调试规则,无论命中多少次都不会触发拦截动作。

msg 为该规则的描述,当有请求命中该规则时, 它将出现在 admin 端的 WAF Logs 里。

比如:

uri contains rx:s{root\.exe}
=>
    waf-mark-risk(level: 'definite');

uri contains any('nessustest', 'appscan_fingerprint')
=>
    waf-mark-risk(msg: 'Request Indicates a Security Scanner Scanned the Site');

回到目录

waf-config

语法: waf-config(action: NAME, url: URL)

注意:在新版本里不推荐使用这个函数,推荐使用 run-waf-rule-sets

配置 WAF 拦截动作,action 为必填项,可以是这些值:

  1. log: 只是记录,
  2. reject: 403 拒绝,
  3. redirect: 302 跳转到指定地址。

actionredirect 时,url 必填,为跳转的地址。

比如:

uri-prefix("/api/")
=>
    waf-config(action: "redirect", url: "http://foo.com/bar");

uri-prefix("/static/")
=>
    waf-config(action: "reject");

回到目录

run-waf-rule-sets

语法: run-waf-rule-sets(action: ACTION, url: URL, key: KEY, threshold: THRESHOLD, observe-time: TIME, clearance-time: TIME, page-template-id: ID, name-1, name-2, ...)

运行指定的 WAF 规则集,当在观察期 observe-time 内,累计的危险值达到阈值 threshold,将执行 action 动作。

危险值是按照 key 来累加的,key 默认是 client-addr

action 动作可以是:

  1. log:只是记录,同时也是默认值。
  2. block:返回 403 状态码。
  3. redirect:302 跳转到 url 参数指定的地址。
  4. hcaptcha:返回验证码页面,使用 hCaptcha 服务:https://www.hcaptcha.com/
  5. edge-captcha:返回验证码页面,使用 Edge 内置的验证码服务。
  6. page-template:返回根据页面模板渲染后的页面,支持 ::CLIENT_IP::::HOST::::HCAPTCHA_BOX::::CAPTCHA_BOX:: 等变量。
  7. close-connection:直接关闭 HTTP 连接。
  8. redirect-validate:对请求进行 302 跳转验证,验证不通过则直接返回 403。可以用于防护 DDoS 攻击。
  9. js-challenge:对请求进行 JS 挑战。

actionredirect 的时候,url 参数必填。

actionhcaptchaedge-captchapage-template 的时候,才需要 clearance-time。通过验证之后,在 clearance-time 时间内,具有相同 key 的所有请求都会被允许通过。

示例:

true
=>
    run-waf-rule-sets(action: "hcaptcha",
                      key: client-addr,
                      threshold: 100,
                      observe-time: 60 [s],
                      clearance-time: 300 [s],
                      "14", "15"
                      );

示例中的 1415 分别对应 OpenResty Edge 的 XSS 规则集(application_attack_xss)SQL 注入规则集(application_attack_sqli)

回到目录

set-ssl-protocols

语法: set-ssl-protocols("protocol1", "protocol2", ...),

设置 HTTPS 请求的 SSL 握手协议。可选的协议类型为:SSLv2、SSLv3、TLSv1、TLSv1.1、TLSv1.2、TLSv1.3。

示例:

true =>
    set-ssl-protocols("TLSv1.1", "TLSv1.2"),
    done;

set-ssl-ciphers

语法: set-ssl-ciphers("cipher1:cipher2:...")

设置 HTTPS 请求的 SSL 算法。

示例:

true =>
    set-ssl-ciphers("DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA"),
    done;

回到目录

set-uploaded-file-args

语法: set-uploaded-file-args(max-content-len, max-file-count)

设置解析上传文件的参数。max-content-len 表示缓存的文件内容长度,0 表示不保留文件内容,默认值是 0。 文件内容通常用于 webshell 检测。 max-file-count 表示缓存的上传文件数量,0 表示不限制,默认值是 1。

示例:

true =>
    # 102400 = 100KB
    set-uploaded-file-args(max-content-len: 102400, max-file-count: 10);
    done;

回到目录

uploaded-file-extensions

语法: uploaded-file-extensions()

获取上传的文件扩展名。

示例:

any(uploaded-file-extensions) eq any("txt") =>
    say("found txt file");

回到目录

uploaded-file-contents

语法: uploaded-file-contents()

获取上传的文件内容。默认不获取文件内容,通过 set-uploaded-file-args 设置长度后方可获取。

示例:

any(uploaded-file-contents) contains any("attack data") =>
    say("found attack data");

回到目录

uploaded-file-names

语法: uploaded-file-names()

获取上传的文件名。

示例:

true =>
    say(uploaded-file-names);

回到目录

uploaded-file-combined-size

语法: uploaded-file-combined-size()

获取上传的文件大小之和。

示例:

uploaded-file-combined-size > 1024 =>
    say("file too large");

回到目录

uploaded-file-contents-matched

语法: uploaded-file-contents-matched()

检查是否所有上传的文件的扩展名和内容都匹配。匹配返回 true,否则返回 false

示例:

uploaded-file-contents-matched =>
    say("all file extensions and contents match");

回到目录

req-args-combined-size

语法: req-args-combined-size()

获取 URI 和 POST 参数的大小之和。a=1&b=2 将返回 4,分别是 a1b2

示例:

req-args-combined-size > 1024 =>
    say("args too large");

回到目录

validate-url-encoding

语法: validate-url-encoding(data)

检查 data 字符串是否是非法的 URL 编码。非法返回 true, 合法返回 false

示例:

validate-url-encoding(req-uri) == false =>
    say("valid request url");

回到目录

validate-byte-range

语法: validate-byte-range(data, "range1", "range2", ...)

检查 data 字符串中的字符是否都在所要求的范围 (range1, range2, …) 中。 如果在所要求的范围返回 false,不在则返回 true

示例:

validate-byte-range(any(uri-arg-names), "1-255") == true =>
    say("invalid uri arg names"),
    done;

回到目录

validate-csrf-token

语法: validate-csrf-token()

语法: validate-csrf-token(ttl: TTL)

该动作与 inject-csrf-token 动作搭配使用,用于 CSRF 防护。如果请求方法是 HEAD, GET, TRACE, OPTIONS 中的一种,该动作会直接返回 ok。否则会检查 URI 参数或 POST 表单参数中的 _edge_csrf_token 参数是否有效,如果有效则返回 ok,如果无效则返回相应的错误消息。

错误消息可能有:

  • missing csrf token:没有 CSRF token 参数。
  • invalid csrf token:CSRF token 非法,可能是被伪造的参数。
  • expired csrf token:CSRF token 参数已过期。

CSRF Token 的有效期可以通过 ttl 参数设置,默认过期时间为 3600 秒。如果该参数为 0,则表示 CSRF Token 永不过期。

示例:

my Str $csrf-res;

true =>
    rm-req-header("Accept-Encoding"),
    defer resp-body {
        inject-csrf-token();
    },
    $csrf-res = validate-csrf-token(3600),
    {
        $csrf-res ne "ok" =>
            waflog($csrf-res, action: "block", rule-name: "csrf_protection"),
            exit(403);
    };

回到目录

waflog

语法: waflog(msg)

语法: waflog(msg, action: ACTION, rule-name: RULENAME)

生成一条 WAF 日志,如果需要,可以通过 actionrule-name 参数来指定 WAF 日志中显示的拦截动作和规则名称。

一些例子:

true =>
    waflog("log"),
    waflog("forbidden", action: "block", rule-name: "custom-rule");

回到目录

http-version

语法: http-version()

获取 HTTP 请求的版本,取值有 0.9, 1.0, 1.1, 2.0。

一些示例:

http-version() eq "1.1" =>
    say(http-version());

回到目录

req-line

语法: req-line()

获取 HTTP 请求行。

一些示例:

true =>
    say(req-line());

输出类似:GET /test HTTP/1.1

回到目录

skip-json-values

语法: skip-json-values(uri-arg-values:true, post-arg-values:true, req-cookie-values:true)

WAF 不检查值为 JSON 字符串的 URI 参数或 Post 参数或 Cookie 参数。

一些示例:

true =>
    skip-json-values(uri-arg-values: true),
    run-waf-rule-sets(action: "block", threshold: 0, "12"),
    done;

回到目录

is-json-string

语法: is-json-string(str)

判断字符串是否是 json 格式的。

一些示例:

is-json-string('{"k":"v"}') =>
    say("is json string"),
    done;

回到目录

set-proxy-ignore-no-cache

语法: set-proxy-ignore-no-cache(enable)

设置忽略或不忽略 Cache-Control: no-cacheCache-Control: no-store

一些示例:

true =>
    enable-proxy-cache(key: uri),
    set-proxy-cache-default-ttl(1 [min]),
    set-proxy-ignore-no-cache(),
    set-upstream('my-upstream');

回到目录

set-ngx-var

语法: set-ngx-var(key, value)

此指令用于将键值对设置到 ngx.var 中。

一些示例:

true =>
    set-ngx-var("foo", 32);

返回目录

ngx-var

语法: val = ngx-var(key)

此指令用于从 ngx.var 中获取指定键的值。

一些示例:

true =>
    say(ngx-var("foo"));

返回目录

set-ctx-var

语法: set-ctx-var(key, value)

此指令用于将键值对设置到 ngx.ctx._edge_ctx 中。

一些示例:

true =>
    set-ctx-var("foo", 32);

返回目录

ctx-var

语法: val = ctx-var(key)

此指令用于从 ngx.ctx._edge_ctx 中获取指定键的值。

一些示例:

true =>
    say(ctx-var("foo"));

返回目录

run-slow-ratio-circuit-breaker

语法: run-circuit-breaker(key: KEY, window-time: WINDOWN_TIME, open-time: OPEN_TIME, hopen-time: HOPEN_TIME, failure-time: FAILURE_TIME, failure-percent: FAILURE_PERCENT, min-reqs-in-window: MIN_REQS_IN_WINDOW, open-action: OPEN_ACTION, resp-status: RESP_STATUS, resp-body: RESP_BODY)

语法: run-slow-ratio-circuit-breaker(key: KEY, window-time: WINDOWN_TIME, open-time: OPEN_TIME, hopen-time: HOPEN_TIME, failure-time: FAILURE_TIME, failure-percent: FAILURE_PERCENT, min-reqs-in-window: MIN_REQS_IN_WINDOW, open-action: OPEN_ACTION, resp-status: RESP_STATUS, resp-body: RESP_BODY)

语法: run-failure-ratio-circuit-breaker(key: KEY, window-time: WINDOWN_TIME, open-time: OPEN_TIME, hopen-time: HOPEN_TIME, failure-status: FAILURE_STATUS, failure-percent: FAILURE_PERCENT, min-reqs-in-window: MIN_REQS_IN_WINDOW, open-action: OPEN_ACTION, resp-status: RESP_STATUS, resp-body: RESP_BODY)

语法: run-failure-count-circuit-breaker(key: KEY, window-time: WINDOWN_TIME, open-time: OPEN_TIME, hopen-time: HOPEN_TIME, failure-status: FAILURE_STATUS, failure-count: FAILURE_COUNT, min-reqs-in-window: MIN_REQS_IN_WINDOW, open-action: OPEN_ACTION, resp-status: RESP_STATUS, resp-body: RESP_BODY)

这些指令用于启用熔断器。当前支持的熔断器类型有:慢请求比率熔断器、错误比率熔断器、错误计数熔断器。默认使用“慢请求比率熔断器”。

不同的熔断器使用不同的 key 进行标识。

  • window-time: 滑动窗口的时间长度,用于计算错误或慢响应比率的统计时间范围。
  • open-time: 熔断器跳闸之后,保持打开状态的时间,在这段时间内所有请求进行特定的open-action
  • hopen-time: 半开状态的时间长度,这是熔断器尝试恢复之前进行有限请求测试的阶段。
  • failure-time: 请求被视为慢请求的时间阈值。
  • failure-status: 请求被视为失败请求的状态列表,如 502,503。
  • failure-percent: 触发熔断器跳闸的失败或慢请求的百分比阈值。
  • failure-count: 触发熔断器跳闸的失败请求的数量阈值。
  • min-reqs-in-window: 在滑动窗口时间内必须达到的最小请求次数,才会计算失败百分比和失败数量并考虑跳闸。
  • open-action: 当熔断器处于打开状态时执行的动作,当前取值支持 exit 返回默认响应和 redirect 重定向到备用服务等。
  • resp-status: open-action 取值为 exit时,熔断器打开后,对请求返回的 HTTP 状态码。
  • resp-body: open-action 取值为 exit时,熔断器打开后,对请求返回的响应体内容。
  • redirect-url: open-action 取值为 redirect时,熔断器打开后,将请求重定向到指定的 URL。

一些示例:

true =>
    run-slow-ratio-circuit-breaker(key: "example", window-time: 60, failure-time: 500,
        failure-percent: 50, min-reqs-in-window: 2);
my Num @status-codes-1 = (502, 503, 504);
true =>
    run-failure-ratio-circuit-breaker(key: "example", window-time: 60, @status-codes,
        failure-percent: 50, min-reqs-in-window: 4),
my Num @status-codes-2 = (502, 503, 504);
true =>
    run-failure-count-circuit-breaker(key: "example", window-time: 60, @status-codes,
        failure-count: 2, min-reqs-in-window: 4),

返回目录

req-rejected

语法: req-rejected()

此指令在 OpenResty Edge 24.9.1-7 中首次引入,用于检查 HTTP 请求是否已经被限速动作 limit-req-rate、limit-req-count、limit-req-concurrency、block-req 标记为拒绝。

示例:

req-rejected() =>
    errlog("rejected"),
    exit(503);

示例中对被标记了的请求记录日志,并返回 503 状态码。

返回目录

req-header-has-underscore

语法: req-header-has-underscore()

此指令在 OpenResty Edge 24.9.1-7 中首次引入,用于检查 HTTP 请求头的 Key 中是否存在下划线。

示例:

req-header-has-underscore() =>
    exit(400);

示例中禁止 HTTP 请求头中含有下划线的请求。

返回目录

案例分析

案例 1. 给响应头 Content-Type 添加 charset 属性

true =>
    defer resp-header {
        {
            resp-header("Content-type") contains any("html", "javascript", "xml"),
            resp-header("Content-type") !contains "charset=utf-8" =>
                set-resp-header("Content-type", resp-header("Content-type") ~ "; charset=utf-8");
        };
    };

案例 2. 当对应的响应头不存在时添加该响应头

true =>
    defer resp-header {
        {
            ! resp-header("Access-Control-Allow-Origin") =>
                set-resp-header("Access-Control-Allow-Origin", "*");
        };
    };

案例 3. 替换响应体中的内容

true =>
    defer resp-body {
      replace-resp-filter(rx{http://example.com/}, "https://new.example.com/", g: true);
    };

注意:如果响应已经编码,可能会无法替换成功,需要设置请求头 Accept-Encoding 来避免编码。

案例 4. 向响应体插入 js 脚本

true =>
    defer resp-body {
        replace-resp-filter(rx{<head>}, "<script>the script you want to insert</script><head>");
    };

注意:如果响应已经编码,可能会无法替换成功,需要设置请求头 Accept-Encoding 来避免编码。

回到目录

作者

Yichun Zhang <yichun@openresty.com>, OpenResty Inc.

回到目录

译者

Laser He <laser@openresty.com>, OpenResty Inc.

版权与许可证

Copyright (C) 2017-2020 by OpenResty Inc. All rights reserved.

本文档为商业所有权文档,包含商业机密信息。未经版权所有者书面授权,严禁以任意形式重新分发此文档。

回到目录