[code.openresty] Openresty Nginx API for Lua -下

ngx.req.socket

语法: tcpsock, err = ngx.req.socket()

语法: tcpsock, err = ngx.req.socket(raw)

上下文: rewrite_by_lua*, access_by_lua*, content_by_lua*

返回一个包含了下游连接的只读的cosocket对象。在这个对象里面,只支持receivereceiveuntil方法。

在发生错误的情况下,将会返回一个nil值和一个描述错误的字符串。

这个方法返回的socket对象经常被用来读取请求的body,以一个流的方式。不要开启lua_need_request_body指令,并且不要将这个调用与ngx.req.read_bodyngx.req.discard_body混合使用。

如果任何请求的body数据已经被预读到Nginx内核请求header缓冲区中,结果cosocket对象要小心的处理这些来避免这种预读取中潜在的数据丢失。分块请求的body在这个API中不支持。

v0.9.0版本开始,这个方法接受一个可选的 boolean raw参数。当这个参数是true,这个方法返回一个全双工的cosocket对象包装原始的下游连接套接字,在这里你可以调用receive, receiveuntil, 和 send方法。

raw参数是true,要求这里没有从之前ngx.say, ngx.print, 或者 ngx.send_headers 调用时未解决的数据存在。所以,如果你之前有这些下游输出调用,你应该在调用ngx.req.socket(true)之前调用ngx.flush(true)来保证这里没有未解决的输出数据。如果请求body还没有被读取,那么"raw socket"也可以被用来读取请求的body。

你可以使用通过ngx.req.socket(true)返回的“raw request socket”来实现花哨的协议例如WebSocket,或者只是发出你自己的未加工的HTTP响应头或者body数据。你可以参考lua-resty-websocket library的一个真实世界的例子。

这个方法首先从v0.5.0rc1版本被介绍。

ngx.exec

语法: ngx.exec(uri,args?)

上下文: rewrite_by_lua*, access_by_lua*, content_by_lua*

做一个内部重定向到uri附带参数arg并且和echo-nginx-module中的echo_exec相似。


  ngx.exec('/some-location');
  ngx.exec('/some-location', 'a=3&b=5&c=6');
  ngx.exec('/some-location?a=3&b=5', 'c=6');

这个可选的第二个参数args可以被用来指定额外的URI查询参数,例如:


  ngx.exec("/foo", "a=3&b=hello%20world")

作为另一种选择,一个Lua table中可以被当做args参数传递,ngx_lua来展开URI转义和字符串连接。


  ngx.exec("/foo",{ a = 3, b = "hello world" })

这个结果和之前的例子是完全相同的。

通过Lua table传递args参数的形式和通过使用ngx.encode_args方法是完全一样的。

同样也支持命名的location但是第二个args参数将会被忽略掉,如果查询字符串存在并且新location会继承自以前的location(如果有的话)。

在下面的例子中,GET /foo/file.php?a=hello将会返回“hello”而不是“goodbye”。


      location /foo {
          content_by_lua_block {
              ngx.exec("@bar","a=goodbye");
          }
      }

      location @bar {
          content_by_lua_block {
              local args = ngx.req.get_uri_args()
              for key, val in pairs(args) do
                    if key == "a" then
                        ngx.say(val)
                    end
              end
          }
      }

注意ngx.exec方法和ngx.redirect并不一样,这是由于它是一个纯粹的外部重定向并且不包括额外的HTTP流量。

并且注意这个方法调用终止当前请求的处理并且它 必须ngx.send_headers或者ngx.printngx.say明确响应body输出之前调用。

建议编码时候,将这个方法调用和return语句结合,例如:return ngx.exec(...)可以在除了header_filter_by_lua*之外的上下文中使用,以强调程序已经被终止的事实。

ngx.redirect

语法: ngx.redirect(uri, status?)

上下文: rewrite_by_lua*, access_by_lua*, content_by_lua*

发出一个HTTP 301或者302重定向到uri

这个可选的status参数指定要使用的HTTP状态。现在支持下面的状态码:

  • 301
  • 302 (默认)
  • 303
  • 307

默认的是 302 (ngx.HTTP_MOVED_TEMPORARILY)

这里有一个例子,假设当前的服务器名称是localhost,并且监听的端口是 1984:


    return ngx.redirect("/foo")

等价于


 return ngx.redirect("/foo", ngx.HTTP_MOVED_TEMPORARILY)

支持重定向任意外部的URL,例如:


  return ngx.redirect("http://www.google.com")

我们也可以直接使用数值代码作为第二个status参数:


  return ngx.redirect("/foo",301)

这个方法和标准ngx_http_rewrite_module中的附带redirect修饰符的rewrite 指令类似,例如,这个nginx.conf片段


  rewrite ^ /foo? redirect; # nginx config

等价于下面的Lua代码


  return ngx.redirect('/foo'); -- Lua code


  rewrite ^ foo? permanent; #nginx config

等价于


  return ngx.redirect('/foo',ngx.HTTP_MOVED_PERMANENTLY)  -- Lua code

也可以指定URI参数,例如:


 return ngx.redirect('/foo?a=3&b=4')

注意这个方法调用终止当前请求的运行并且它 必须ngx.send_headers调用或者通过ngx.print或者 ngx.say明确响应body输出之前。

建议的编码风格是,将这个方法调用与一个return声明结合,例如,return ngx.redirect(...)在除了header_filter_by_lua*之外的上下文中被接受,来强调一个事实就是这个请求处理已经被终止了。

ngx.send_headers

语法: ok, err = ngx.send_headers()

上下文: rewrite_by_lua*,access_by_lua*,content_by_lua*

显示的发送出响应头。

v0.8.3版本这个方法在成功的时候返回1,在其他情况下返回一个nil和一个字符串描述错误。

注意通常情况下不需要手动发送出响应头,因为ngx_lua会在内容通过 ngx.say 或者 ngx.print输出之前自动的将响应头发出,或者当content_by_lua*正常退出时。

ngx.headers_sent

语法: value = ngx.headers_sent

上下文: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*

如果响应头被发出去了(通过ngx_lua)将会返回true,其他情况返回false

这个API首先在ngx_lua v0.3.1rc6版本中被介绍。

ngx.print

语法: ok,err = ngx.print(...)

上下文: rewrite_by_lua*, access_by_lua*, content_by_lua*

发出已经连接过的参数到HTTP客户端(作为响应body)。如果响应头没有被发送,这个方法将会首先发出headers然后发出body数据。

v0.8.3,这个方法在成功后返回1,在其他情况下返回nil和一个描述错误信息的字符串。

Lua nil值将会输出"nil"字符串并且Lua boolean值将会分别输出"true""false"文字字符串。

嵌套的字符串是数组是允许的并且数组中的元素将会被一个接着一个的输出:


 local table = {
     "hello, ",
     {"world: ", true, " or ", false,
         {": ", nil}}
 }
 ngx.print(table)

将会产生输出:


 hello, world: true or false: nil

非数组的表参数将会导致抛出一个Lua异常。

这个ngx.null常量将会产生"null"字符串输出。

这是一个异步的调用并且将会立即返回而不需要等待所有的数据要写入系统发送缓存区中。要开启同步模式,在调用ngx.print之后调用ngx.flush(true)。这个对流输出特别有用。更多细节查看ngx.flush

请注意一点ngx.printngx.say会总是调用整个Nginx输出body过滤链,这个是一个昂贵的操作。所以在一个紧密循环中无论调用哪一个都要小心。你自己在Lua中缓冲数据并且保存调用。

ngx.say

语法: ok, err = ngx.say(...)

上下文: rewrite_by_lua*, access_by_lua*, content_by_lua*

ngx.print类似但是多发出一个换行符。

ngx.log

语法: ngx.log(log_level, ...)

上下文: init_by_lua*, init_worker_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

通过给定的logging级别来连接参数到error.log中。

Lua nil参数是接受的,并且结果为文字的"nil"字符串,同时Lua boolean结果为文字的"true"或者"false"字符串输出。并且ngx.null常量将会产生"null"字符串输出。

这个log_level参数可以取常数例如ngx.ERRngx.WARN.检查Nginx log level constants来获取细节。

这里有个Nginx内核硬编码的2048字节的限制在错误信息长度上。这个限制包含结尾换行开头的时间戳。如果信息大小超过了这个限制,Nginx会相应的截取信息字符串。这个限制可以通过编辑在Nginx源码树种的src/core/ngx_log.h 文件的NGX_MAX_ERROR_STR宏定义来修改。

ngx.flush

语法: ok, err = ngx.flush(wait?)

上下文: rewrite_by_lua*, access_by_lua*, content_by_lua*

Flush 响应输出到客户端。

ngx.flush接受一个可选的boolean wait参数(默认:false),首先在v0.3.1rc34版本中介绍。当通过默认的参数调用的时候,它缠身给一个异步的调用(立即返回而不等待输出数据被写入到系统的发送缓冲中)。通过wait参数设置为true调用这个方法会转变为同步模式。

在同步模式,这个方法将会不会返回直到所有的输出数据已经被写入到系统输出缓冲中或者直到send_timeout设置已经过期了。注意使用Lua协同程序机制意味着这个方法并不阻塞Nginx事件循环及时在同步模式。

ngx.flush(true)ngx.print或者ngx.say之后被立即调用,这回导致后者的方法在同步模式下运行。这个在流输出中特别有用。

注意ngx.flush在HTTP 1.0输出缓冲模式中并不是一个功能。查看HTTP 1.0 support

v0.8.3开始这个方法在成功之后返回1,在其他情况下返回nil和一个字符串来描述错误。

ngx.exit

语法: ngx.exit(status)

上下文: rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

status >= 200 (例如,ngx.HTTP_OK和以上),它将会中断当前的请求并且返回状态码给nginx。

status == 0 (例如,ngx.OK),它将会退出当前的阶段处理器(或者内容处理器如果 content_by_lua*指令被使用了)并且为当前请求继续运行后面的阶段(如果有的话)。

这个status参数可以使ngx.OK, ngx.ERRORngx.HTTP_NOT_FOUNDngx.HTTP_MOVED_TEMPORARILY,或者其他 HTTP status constants

要返回一个包含当前内容的错误页,可以使用像这样的代码片段:


 ngx.status = ngx.HTTP_GONE
 ngx.say("This is our own content")
 -- to cause quit the whole request rather than the current phase handler
 ngx.exit(ngx.HTTP_OK)

执行的效果:


 $ curl -i http://localhost/test
 HTTP/1.1 410 Gone
 Server: nginx/1.0.6
 Date: Thu, 15 Sep 2011 00:51:48 GMT
 Content-Type: text/plain
 Transfer-Encoding: chunked
 Connection: keep-alive

 This is our own content

数值文本可以直接被用在参数上,例如,


 ngx.exit(501)

注意尽管这个方法接受所有的 HTTP status constants作为输入,它只接受core constants中的NGX_OKNGX_ERROR

还要注意,这个方法调用欧冠终止当前请求的处理并且建议的编码风格是将这个方法调用欧冠与一个return声明连接,例如, return ngx.exit(...)被用来强调一个事实是请求处理已经被终止了。

当用在header_filter_by_lua*, balancer_by_lua*, 和
ssl_session_store_by_lua*的上下中时,ngx.exit()作为一个异步的操作将会立即返回。这个行为在未来可能会改变并且推荐用户使用上面推荐的return来结合使用。

ngx.eof

语法: ok, err = ngx.eof()

上下文: rewrite_by_lua*, access_by_lua*, content_by_lua*

显示的指定响应输出流的结束。在HTTP 1.1分块编码输出的情况下,它将只是会触发Nginx内核发送出"last chunk"。

当你在你下游连接中禁用了HTTP 1.1的keep-alive特性时,你可以调用这个方法来写到HTTP client来主动的关闭连接。这个技巧可以用在做一些后台工作而不让HTTP客户端等待连接,例如下面的例子:


 location = /async {
     keepalive_timeout 0;
     content_by_lua_block {
         ngx.say("got the task!")
         ngx.eof()  -- well written HTTP clients will close the connection at this point
         -- access MySQL, PostgreSQL, Redis, Memcached, and etc here...
     }
 }

但是如果你创建子请求来访问其他通过nginx upstream模块配置的location,那么你应该配置这些upstream模块来忽略客户端连接中断,如果他们不是默认情况的话。例如,在默认情况下标准的ngx_http_proxy_module将会同时中断子请求和主请求,当客户端重点连接时,所以在你的location块ngx_http_proxy_module配置中开启proxy_ignore_client_abort](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_client_abort) 指令是非常重要的。


 proxy_ignore_client_abort on;

一个更好的来做后台工作的方式是使用ngx.timer.atAPI接口。

v0.8.3开始这个方法在成功时候返回1,在其他错误情况下返回nil和一个字符串描述。

ngx.sleep

语法: ngx.sleep(seconds)

上下文: rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*

睡眠指定的描述而不阻塞。可以指定一个时间分辨率达到0.001秒(例如:一毫秒)。

在后面的场景,这个方法使用的是Nginx timers。

0.7.20版本开始,这个0时间参数也可以被指定。

这个方法首先在0.5.0rc30版本中被介绍。

ngx.escape_uri

语法: newstr = ngx.escape_uri(str)

上下文: init_by_lua*, init_worker_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

编码str来作为URI组件。

ngx.unescape_uri

语法: newstr = ngx.unescape_uri(str)

上下文: init_by_lua*, init_worker_by_lua*, set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*

解码str,str是一个编码过的URI组件。

例如:


 ngx.say(ngx.unescape_uri("b%20r56+7"))

得到输出


    b r56 7

ngx.encode_args

语法: str = ngx.encode_args(table)

上下文: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*

根据URI的编码规则编码Lua table为一个查询参数字符串。

例如,


 ngx.encode_args({foo = 3, ["b r"] = "hello world"})

产生


foo=3&b%20r=hello%20world

这个table的keys必须是Lua字符串。

同时也支持多值查询参数。使用一个lua table来作为参数值,例如:


 ngx.encode_args({baz = {32, "hello"}})

得到


    baz=32&baz=hello

如果这个value table为空那么影响等价于nil值。

同时也支持boolean参数值,例如,


 ngx.encode_args({a = true, b = 1})

产生


    a&b=1

如果这个参数值为false,那么影响等价于nil值。

这个方法首先在 v0.3.1rc27版本中被介绍。

ngx.decode_args

语法: table = ngx.decode_args(str, max_args?)

上下文: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

将一个编码过的查询字符串解码为一个Lua table。这个是ngx.encode_args的相反的方法。

这个可选的max_args参数可以被用来指定通过str参数解析的参数数量的最大值。默认情况下,默认解析100个请求参数(包括这些名字相同的)并且额外的URI参数会被默默的丢弃来防止潜在的拒绝服务攻击。

这个参数可以被设置为0来移除对接受请求参数处理的限制:


 local args = ngx.decode_args(str, 0)

强烈不建议移除max_args

这个方法首先在v0.5.0rc29版本中被介绍。

encode_base64

语法: newstr = ngx.encode_base64(str, no_padding?)

上下文: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

编码str成为一个base64摘要。

0.9.16版本开始,一个可选的boolean类型no_padding参数可以被指定来控制是否base64摘要应该被追加到结果摘要中(默认是false,例如,使用启用追加)。

ngx.decode_base64

语法: newstr = ngx.decode_base64(str)

上下文: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

解码str参数将base64摘要成为一个原始的形式。如果str不是正常形式返回nil

ngx.crc32_short

语法: intval = ngx.crc32_short(str)

上下文: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

计算str字符串的CRC-32(循环冗余码)摘要。

这个方法对较短的str输入(例如,小于30~60bytes)表现较好,和ngx.crc32_long相比。这个结果和ngx.crc32_long完全一样。

幕后的实现,这个只是一个薄薄的对Nginx核心中的ngx_crc32_short的封装。

这个API首先在v0.3.1rc8版本中被介绍。

ngx.crc32_long

语法: intval = ngx.crc32_long(str)

上下文: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

计算参数strHMAC-SHA1摘要,并且返回使用密钥<secret_key>的结果。

原始字节形式的HMAC-SHA1证书将会生成,使用ngx.encode_base64,例如,如果需要的话将结果以结果的文本表示。

例如,


 local key = "thisisverysecretstuff"
 local src = "some string we want to sign"
 local digest = ngx.hmac_sha1(key, src)
 ngx.say(ngx.encode_base64(digest))

将会导致输出:


    R/pvxzHC4NLtj7S+kXFg/NePTmk=

这个API需要在Nginx编译的时候开启OpenSSL库(通常在./configure脚本里面传入--with-http_ssl_module参数)

这个方法首先在v0.3.1rc29版本中被介绍。

ngx.md5

语法: ngx.md5(str)

上下文: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

返回str参数的16进制的MD5摘要。

例如,


 location = /md5 {
     content_by_lua_block { ngx.say(ngx.md5("hello")) }
 }

产生输出


    5d41402abc4b2a76b9719d911017c592

查看ngx.md5_bin,如果需要原始的MD5摘要。

ngx.sha1_bin

语法: digest = ngx.sha1_bin(str)

上下文: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

返回str参数的二级制形式的SHA-1摘要。

这个方法需要在Nginx编译时的SHA-1支持。(这个经常意味着在构建Nginx时需要安装OpenSSL)

这个方法首先在v0.5.0rc6中被介绍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容