Edge 语言用户手册

目录

描述

本手册是 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 有下列关键字

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

回到目录

变量

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

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

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

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

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

变量通常用 my 关键字声明,如下所示:

my $count;
my @domains;
my %city-weight;

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

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

用户程序里定义的每个标量变量,在其生命期里,只能有一个值类型。每个变量的值都必须能够在编译时判断。下面是四种标量值:

  • Str
  • Num
  • Quantity
  • Bool

用户可以选择明确声明一个标量变量的类型,如下所示:

my Num $count;
my Str $name;

在 Edgelang 中通常并不需要这样明确标注变量类型,我们并不鼓励这么做,以免导致潜在的歧义。

回到目录

请求范围内的变量

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

our $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

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

回到目录

宏变量

宏变量用语编译时的宏语句。 它们是用 macro 关键字声明的。比如:

macro @a = (1, 2, 3);
macro Num $count = 5;

回到目录

函数名称和函数调用

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" \d+ - \d+ "
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/

回到目录

通配符文本

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

wc{foo???}
wc/*.foo.com/;
wc(/country/??/);
wc"[a-z].bar.org"
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

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

回到目录

聚合

用户可以定义的动态/静态聚合规则。

回到目录

数值

主要有两种方式:

  1. 聚合数字的值,如:max/min/sum
  2. 统计规定数值出现的次数,如:hist-linear() / hist-log()

回到目录

数据过滤与属性
  • int: 将原始数值转换为整形,在这里会直接将小数部分抛弃。
  • resolution: 分辨率,即一个数字的最小单位。
    • 例如 :resolution(1) 代表着该值最小单位是 15.31 就会被转换为 5
    • 再例如 :resolution(0.1) 代表着最小单位是 0.15.31 会被转换成 5.3
  • top: 只统计条目中最大的数字,忽略较小的值。
  • label: 聚合的名称

回到目录

静态聚合

计算静态的聚合值。这里的静态指的是硬编码实现的部分,不方便动态调整。 使用语法::extractorextractor 目前支持下面的值:

  • min: 最小值

  • max: 最大值

  • avg: 平均值

  • count: 计数值

  • sum: 总和值

  • hist-linear(L, H): 统计不同的数值出现的次数

  • interval: 间隔值,这里用来指定统计的间隔

    • L: 低
    • H: 高
  • hist-log: 基数2对数直方图

例:

aggregate $req-latency-metrics :label('test 1'), :min, :max, :avg, :count, :sum,
                               :interval(1 [s]);

true =>
    $req-latency-metrics <<< req-latency;

aggregate $req-latency-ms-metrics :min, :max, :avg, :count,
                                  :resolution(1), :sum, :top(100);

true =>
    $req-latency-ms-metrics <<< req-latency * 1000;

# label: test 1
# name                  | value
# ----------------------|------
# min                   | 0.001
# max                   | 1.22
# avg                   | 0.002
# count                 | 1024
# sum                   | 1.5
# INT value
aggregate $resp-status-metrics :label('test 2'), :hist-linear(100, 599),
                               :resolution(1), :top(100);

true =>
    $resp-status-metrics <<< resp-status;

# label: test 2
# value                 | count
# ----------------------|------
# 200                   | 234
# 302                   | 11
# 404                   | 1024
# 500                   | 1
# 503                   | 7
# DOUBLE value
aggregate $req-latency-metrics :label('test 3'), :hist-log(),
                               :resolution(0.1), :top(100);

true =>
    $req-latency-metrics <<< req-latency();

# label: test 3
# value                 | count
# ----------------------|------
# 0.1                   | 1024
# 0.2                   | 1
# 0.4                   | 2
# 0.8                   | 0
# 1.6                   | 1

回到目录

任意串

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

回到目录

注释

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

# 这是一个注释

也支持块注释,比如:

#`(
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-for ~~ 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 @names = ('Tom', 'Bob', 'John');

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

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

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

my %scores = (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 $a = 3;

{
    my $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("negaive!");
    },
    done;

回到目录

As表达式

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

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

比如:

uri-preifx("/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 $a;

true =>
    $a = 3;

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

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

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

下面的赋值

$a = $a + 3

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

$a += 3

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

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

$a++

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

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

回到目录

运行时阶段

OpenResty® 在不同的运行时阶段处理每个客户端请求。 还有一些特殊的阶段是跟客户端请求无关的,比如在服务启动期间或后台轻线程中运行的阶段。每个 edgelang 代码段通常都附着有一个缺省的运行时阶段,不过有些 edgelang 的规则的执行跨越多个运行时阶段。

使用非缺省运行时阶段的 Edgelang 代码段需要用 phase 语句显式声明,比如:

phase access;

uri-prefix("/some/bad") =>
    exit(403);

在运行时阶段 access 里声明一个规则。一个 phase 语句影响所有后续的代码,直到下一个 phase 语句或者当前代码块结束为止。

目前支持下列阶段,按照他们执行的顺序排序:

  • ssl-cert

    在 SSL 握手过程中向下方 SSL 链接提供 SSL 证书的时候

  • rewrite

    请求改写和重定向的时候

  • access

    请求访问控制的时候

  • resp-header

    在响应头准备好的时候

  • resp-body

    响应体处理阶段

  • log

    收集访问日志数据阶段

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

回到目录

INIT块

INIT 块是一个特殊的代码块,用于预处理请求。 他们总会在同一个运行时阶段中最先执行,哪怕他们出现在后头也如此。 多个 INIT 块以他们在同一个运行时阶段的代码段中出现的顺序执行。

在一个靠后的阶段中的 INIT 块的确是在前面阶段的代码之后运行的。

几乎所有判断函数在一个请求的多个运行时阶段中都带着一样的数值,即使某些规则动作 改变了下面的请求数据也这样。这个特点可以极大地帮助自动规则的优化,并且最小化了不同规则之间相互错误影响的概率。 但是 INIT 块总是强制要求在每个 INIT 块的执行时重新运行那些判断函数,并且所有在 INIT 块中运行的动作的结果, 都会立即影响判断函数的值。

看看下面例子:

INIT {
    uri-prefix(rx{ / [a-z]{2} / }) =>
        # remove the first URI path component (or segment)
        rm-uri-seg(1);
}

uri("/blog.html") =>
    say("hello!");

INIT 块中的规则删除了 URI 的两个前缀字母,比如 /us//cn/。 如果客户端请求时 GET/us/blog.html,那么INIT 块外面的规则可以成功匹配。 但是,如果第一个规则在 INIT{} 块之外,那么第二个规则就永远没法匹配上, 因为 uri("/blog.html") 判断函数仍然使用原来的 URI 数据,/usr/blog.html

回到目录

连接

一个连接是一个单值等效于多值。内置的函数 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($who) =
    say("hi, $who!"),
    exit(200);

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

hi, Tom!

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

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

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

action count-down($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, $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 模块一样去调用。

在 OpenResty® Edge 平台环境下,只有管理员有权限调用外部代码。

回到目录

宏提供了一种强大的模块机制,可以帮助减少代码的重复,并且声称很多类似但是仍然有区别的规则。

比如,下面的 edgelang 规则

host("qa.foo.com") =>
    rewrite-uri-prefix("/", "/media-qa"),
    done;

host("dev.foo.com") =>
    rewrite-uri-prefix("/", "/media-dev"),
    done;

host("prod.foo.com") =>
    rewrite-uri-prefix("/", "/media-prod"),
    done;

可以简化成:

for 'dev', 'qa', 'prod' -> $env {
    host("$env.foo.com") =>
        rewrite-uri-prefix("/", "/media-$env'),
        done;
}

在这里我们用了宏构造 for 循环语句,用一个简单的规则模版生成这 3 个规则,方法是引入了 $env 宏变量。 这个 for 循环在运行时执行,会通过把 $env 替换成不同的数值, 把规则模版展开成多个具体的规则。

显然,在这个例子中,宏循环的使用大大简化了代码,并且避免了代码的重复。 如果用户想在未来增加更多子域,她只要在 for 语句中增加更多子域的前缀就行。

宏处理层海体统了 if 语句,比如,我们可以在 for 循环里用 if 语句处理特殊场景:

for 'dev', 'qa', 'prod' -> $env {
    if $env eq 'prod' {
        host("$env.foo.com") =>
            rewrite-uri-prefix("/", "/media'),
            done;

    } else {
        host("$env.foo.com") =>
            rewrite-uri-prefix("/", "/media-$env'),
            done;
    }
}

在这里我们对 prod 前缀的处理略微不同。

在规则环境之外的任何赋值都被认为是宏级别的赋值。比如:

for 'dev', 'qa', 'prod' -> $env {
    macro $host, $dir;
    $host = "$env.foo.com";
    $dir = "media-$env";
    host($host) =>
        rewrite-uri-prefix("/", "/$dir'),
        done;
}

在这个例子里,给 $host$dir 赋值都是宏层面的赋值。 因此变量 $host$dir 都是宏变量,就像变量 $env 一样。 宏层面的赋值也是编译时发生的。 用户的宏变量是用 macro 关键字声明的。 宏变量 $env 是由 for 这个宏循环语句 隐含声明的。

宏变量也可以是数组或者哈希数值。比如:@foo%bar

宏循环 while 类似 for。比如:

macro $i = 0;
while $i < 10 {
    uri-prefix("/post/$i/") =>
        say("This is post $i!");

    $i++;
}

回到目录

内置判断函数

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

回到目录

cache-status

语法: cache-status()

返回上游缓存状态。

比如:

phase resp-header;

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

回到目录

cache-creation-time

语法: cache-creation-time()

阶段: resp-header log

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

回到目录

client-addr

语法: client-addr()

返回客户端地址。

回到目录

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!");

你可以从下面连接获取所有中国省份 name-code-chart.md

回到目录

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 name-code-chart.md

回到目录

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

syntax: 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-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");

回到目录

is-empty

语法: is-empty(value)

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

回到目录

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 的长度。

回到目录

now

语法: now()

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

回到目录

now-secs

语法: now-secs()

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

回到目录

post-arg

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

语法: post-arg(*)

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

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

下面是一个是用 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!");

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

回到目录

referer-host

语法: referer-host()

语法: referer-host(pattern...)

待完成

返回 Referer 请求头数据里面的主机部分。

如果给出了参数,那么这些参数会被当作模式。 在引用者 referer 的主机可以用 eq 匹配这些模式的时候就返回真。

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

比如:

referer-host("www.facebook.com", "m.facebook.com", "facebook.com") =>
    say("hit!");

这条规则等效于下面无参数的 referer-host 调用:

referer-host eq any("www.facebook.com", "m.facebook.com", "facebook.com"
) =>
    say("hit!");

我们推荐使用前面的方式,因为它更简单。前者可以用引号字串的语法进一步简化:

referer-host(qw/www.facebook.com m.facebook.com facebook.com/) =>
    say("hit!");

可选的命名参数 opt-prefix 可以声明为任意模式参数的可选前缀。比如:

referer-host(opt-prefix: "www.", "foo.com", "bar.org")

等效于:

referer-host("www.foo.com", "foo.com", "www.bar.org", "bar.org")

回到目录

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(*)

返回 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!");

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

回到目录

req-id

语法: req-id()

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

请求 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-tld

语法: req-tld()

待完成

返回客户端请求的当前服务器主机的顶级域名(比如 .org, .com.us)。

回到目录

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-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

回到目录

server-addr

语法: server-addr()

阶段: rewrite access proxy resp-header resp-body log

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

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

下面是例子:

phase rewrite;

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()

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

回到目录

server-region

语法: server-region(region...)

待完成

如果服务器在任何参数指定的地区范围内,返回真,否则返回假。

server-region("Asia", "US West", "Pacific") =>
    resolve-origin("dc3.foo.com");

回到目录

single

语法: single(value)

待完成

如果数值是一个单一原语值(比如一个数字,一个字串,或者一个量值)的时候返回真,否则返回假。

这个函数通常用于确保某个数值的单一性。比如,为了确保 URI 参数 uid 只有一个数值,我们可以这样写:

single(uri-arg("uid") as $v) =>
    say("uid arg is single: $v");

相反的条件:

!single(uri-arg("uid") as @v) =>
    say("uid arg is not single: @v");

回到目录

substr

语法: substr(str, from[, len])

待完成

返回从角标 from (0开始的)开始,长度用 len 参数指定的子串。

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

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

下面是一些例子:

my $s = "hello world";

true =>
    say(substr($s, 0, 5)),       #  输出: hello
    say(substr($s, 6)),          #  输出: world
    say(substr($s, -5, 4)),      #  输出: worl
    say(substr($s, -5));         #  输出: world

回到目录

subst

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

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

下面是一些例子:

my $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);

回到目录

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

Back to TOC

回到目录

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

比如:

# 可以在 log 或 resp-header 阶段使用
phase resp-header;

true =>
    errlog(level: "warn", upstream-addr);

回到目录

upstream-total-response-time

语法: $total_response_time = upstream-total-response-time()

返回所有上游服务器处理当前请求所花费的时间,其类型是以为单位的 双精度浮点数(其中毫秒部分在小数位)。

比如:

phase log;

true =>
    errlog(level: "warn", upstream-total-response-time);

回到目录

upstream-total-bytes-sent

语法: $total_bytes_sent = upstream-total-bytes-sent()

比如:

返回所有上游服务器处理当前请求时接收到的字节数。

phase log;

true =>
    errlog(level: "warn", upstream-total-bytes-sent);

回到目录

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(pattern...)

语法: uri-arg(*)

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

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

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

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

待完成

下面是一个从任意 URI 参数中删除名字匹配指定正则模式的例子:

uri-arg(rx/(_[0-9]+)/) =>
    rm-uri-arg($1);

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

回到目录

query-string

语法: query-string()

返回请求中的 查询字符串

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

true =>
    say(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-ext

语法: uri-ext()

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

待完成

不带参数,这个函数返回请求 URI 声明的资源的文件扩展名。比如,请求 URI /en/company/about-us.html, 这个函数返回数值 .html。而 /static/download/foo.tar.gz, 返回 .tar.gz

如果声明了参数,uri-ext(pattern1, pattern2, ...) 等效于 uri-ext eq any(pattern1, pattern2, ...)

比如:

uri-ext(".html", ".htm") =>
    say("found an html page!");

等效于:

uri-ext eq any(".html", ".htm") =>
    say("found an html page!");

回到目录

uri-prefix

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

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

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

回到目录

uri-seg

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

语法: uri-seg(*)

这个函数把 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 中现存同名参数。如果想覆盖同名 URI 参数, 请使用 set-uri-arg

下面是一个例子:

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

回到目录

apply-std-mime-types

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

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

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

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

回到目录

foreign-call

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

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

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

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

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

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

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

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

回到目录

enable-proxy-cache

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

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

回到目录

enable-gateway-gzip

语法: enable-gateway-gzip()

语法: enable-gateway-gzip(enabled)

阶段: resp-header

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

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

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

下面是例子:

phase resp-header;

uri-prefix("/css/") =>
    enable-gateway-gzip;

回到目录

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 错误级别。

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

一些例子:

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)

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

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

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

下面是例子:

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

回到目录

limit-req-data-rate

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

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

待完成

设置接收请求(体)数据的时候的速率。 位置参数 rate 声明最大接受速度。它必须是一个量纲,接受一个速率的单位, 比如 [kB/s]

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

下面是一个例子:

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

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

回到目录

limit-req-rate

语法: limit-req-rate(key: KEY, target-rate: RATE, reject-rate: RATE)

在指定的用户✂️上限制请求速率。

有名参数 key 是可选的。如果省略之,则等小雨一个常量键字。

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

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

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

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

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

下面是例子:

true =>
    limit-req-rate(key: client-addr, target-rate: 10 [r/s], reject-rate: 20 [r/s]);

用户可以在一个请求头里头为不同的键字发起多个 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)

返回指定时间(当前时区)的时间戳,注意返回值为有量纲量.

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

如果调用时没有带参数,则会返回当前时间戳,返回值同样是有量纲

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

  • 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 access

发送 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);

回到目录

rewrite-resp-redirect-host

语法: rewrite-resp-redirect-host(pattern..., new-host)

待完成

对于 HTTP 重定向响应(301,302,303 和 307),如果响应里的 Location 响应头匹配参数列表中除了最后一个参数之外的任意一个模式, 那么这个动作会用最后一个参数里头的主机名(这个参数是用户声明的)替换掉匹配的主机名。

比如:

host("foo.com") =>
    rewrite-resp-redirect-host(wc"*.bar.com", "a.foo.com", host);

上面这条把所有重定向的响应中任何匹配模式 wc"*.bar.com"a.foo.com" 的主机名替换为当前主机名(是 foo.com)。

回到目录

语法: rewrite-resp-cookie-host(pattern..., new-host)

待完成

对哪些有 Set-Cookie 头的 HTTP 响应,如果 Domain cookie 参数值匹配任何最后一个参数之外的参数声明的模式, 那么这个动作或把所有匹配上的域名值替换为用户通过最后一个参数提供的域名。

比如:

host("foo.com") =>
    rewrite-resp-cookie-host(wc"*.bar.com", "a.foo.com", host);

这歌动作把 Domain 参数值里头所有匹配模式 wc"*.bar.com""a.foo.com" 的 cookie 的域名都 替换成当前域名(foo.com)。

回到目录

rewrite-suffix

语法: rewrite-suffix(subj, pattern, replacement)

待完成

把字串参数 subj 里头匹配模式 pattern 的后缀替换成 replacement 参数声明的数值。 如果模式不匹配目标字串的任何后缀,则啥事儿也不干。

比如:

true =>
    say(rewrite-suffix("a.b.foo.com", "b.foo.com", "blah.com"))

这条规则生成如下响应体数据:

a.blah.com

pattern 参数也可以是正则或者通配符。

回到目录

rewrite-uri-prefix

语法: rewrite-uri-prefix(pattern, replacement)

待完成

用指定的 replacement 参数值替换当前请求 URI 字串里头匹配 pattern 的前缀。 如果模式不匹配当前 URI 中的任何前缀,就啥也不干。

参数 pattern 也可以是一个正则值或者一个通配符。

比如:

uri-prefix("/wap/") =>
    rewrite-uri-prefix($prefix, "/some-new-wap/");

这条规则用 /some-new-wap/ 替换 URI 前缀 /wap/。当然,这条规则可以用 rm-rui-segadd-uri-prefix 改写成下面效率更高的方式:

uri-prefix("/wap/") =>
    rm-uri-seg(1),
    add-uri-prefix("/some-new-wap");

回到目录

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(pattern...)

语法: rm-req-cookie(*)

待完成

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

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

下面是一个例子:

true =>
    rm-req-cookie("_uid", rx/_track\d+/, wc/seed*/);

上面这条规则无条件删除任何名字里头有 _uid 或者名字由 _track 和几个数字组成或者名字以 seed 开头的cookie。

如果是任意值,那么下面这条规则删除所有当前请求带来的 cookie:

uri-prefix("/public/") =>
    rm-req-cookie(*);

回到目录

rm-req-header

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

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

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

下面是一个例子:

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

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

回到目录

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

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

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

这里是一个例子:

true =>
    rm-resp-cookie("_uid", rx/_track\d+/, wc/seed*/);

上面这条规则无条件删除响应里头名字是 _uid 的 cookie 或者名字包含 _track 和几个十进制数值位的cookie, 以及还会删除以 seed 开头的 cookie。

如果是任意值,这个动作删除所有当前响应中携带的 cookie。比如:

uri-prefix("/public/") =>
    rm-resp-cookie(*);

rm-resp-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-resp-header-param

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

待完成

从指定响应头里头删除指定参数名的头参数。

比如:

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

这条规则从当前响应的 Cache-Control 头数值中删除任何 s-maxage=xxx 参数。

回到目录

rm-uri-arg

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

语法: rm-uri-arg(*)

删除指定名字的 URI 参数。

如果声明了一个任意值(*),那么它删除所有URI参数。

回到目录

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");

回到目录

scan-req-body

语法: scan-req-body { rule... }

待完成

回到目录

scan-resp-body

语法: scan-resp-body { rule... }

待完成

回到目录

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...)

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

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

  1. resp-body: HTML 内容
  2. refetch-url: 静态资源 URL

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

支持以下的错误响应码:

  • 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-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-mime-type

语法: set-mime-type(type)

待完成

为响应体数据设置 MIME 类型。比如,text/plaintext/html 等等。

比如:

uri-suffix(".mp4") =>
    set-mime-type("video/mp4");

用户不应该在参数值里声明任何 charset 参数。应该去参考 set-resp-charset

这个动作影响当前响应发送给客户端的 Content-Type 响应头。

回到目录

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

回到目录

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)

将上游代理服务器收到的请求 URI 设置为一个新数值,不改变当前 URI。 这里的 URI 不应该包含任何查询串或者任何主机/端口部分。

回到目录

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)

根据指定的关键字来限制请求的频率。

其中, 参数 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 页面。

例:

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-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-status

语法: set-resp-status(code)

把当前响应的状态码设置为参数 code 的值。

不要在响应头已经发出之后调用这个动作(比如响应头已经被 一个 print 或者 say 调用触发)。

回到目录

set-proxy-retry-condition

语法: set-proxy-retry-condition(CONDITION)

设定重试条件,默认: error, timeout

CONDITION 可以是如下的一项或多项:

  • 'error'
  • 'timeout'
  • 'invalid_header'
  • 'http_500'
  • 'http_502'
  • 'http_503'
  • 'http_504'
  • 'http_403'
  • 'http_404'
  • 'http_429'
  • 'non_idempotent'
  • 'off'

例如:

set-proxy-retry-condition('error', 'timeout', 'invalid_header')

回到目录

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。

回到目录

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-max-body-size

语法: set-max-body-size(size)

阶段: rewrite access

设置本次请求可以接受的最大 POST 体。对于那些具备有效 Content-Length 头的请求,这个方法将检查该头字段并且把超过 size 的请求立即终止,返回 413 Request Entity Too Large。对于分段的编码请求和 HTTP/2 请求,此动作将随着缓冲区处理进行检查。

如果设置为 0 则关闭此检查。

接受一个带 kBmB 这样的尺寸单位的 size 参数。

true =>
    set-max-body-size( 1 [kB]);  # 1000 bytes

回到目录

sleep

语法: sleep(time)

不阻塞地睡眠指定的秒数。我们可以声明精度为 0.001 秒(也就是 1 毫秒)的精度。

下面是例子:

true =>
    sleep(0.5);

回到目录

subst-resp-body-matched

待完成

回到目录

test-bit

语法: test-bit(num, bit-pos)

待完成

测试 num 参数里头某个特定的二进制位是否为1。二进制位的位置从1开始。第一个二进制位是最低的那个位(最右边)。 如果对应二进制位已置(为1),那么返回 true;否则返回 false

比如:

true =>
    say(test-bit(2, 1)),   # false
    say(test-bit(2, 2)),   # true
    say(test-bit(2, 3));   # false

这个例子可以用面向对象的方法语法重写:

true =>
    test-bit(2, 1).say,   # false
    test-bit(2, 2).say,   # true
    test-bit(2, 3).say;   # false

回到目录

utc-time

语法: utc-time()

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

返回指定时间 (UTC) 的时间戳,注意返回值为有量纲量.

如 UTC 的 2019-01-01 00:00:00 会返回 1546272000 [s].

如果调用时没有带参数,则会返回当前时间戳,返回值同样是有量纲

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

  • 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)

配置 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");

回到目录

案例分析

待完成

回到目录

作者

Yichun Zhang <yichun@openresty.com>, OpenResty Inc.

回到目录

译者

Laser He <laser@openresty.com>, OpenResty Inc.

版权与许可证

Copyright (C) 2017 by OpenResty Inc. All rights reserved.

本文档为商业所有权文档,包含商业机密信息。未经版权所有者书面授权,严禁以任意形式重新分发此文档。

回到目录