├── .ci └── setup.sh ├── .gitattributes ├── .gitignore ├── .luacheckrc ├── .travis.yml ├── CHANGELOG.markdown ├── LICENSE ├── Makefile ├── README.markdown ├── dist.ini ├── lib └── resty │ ├── requests.lua │ └── requests │ ├── adapter.lua │ ├── request.lua │ ├── response.lua │ ├── session.lua │ └── util.lua ├── lua-resty-requests-0.7.3-1.rockspec ├── t ├── 00-sanity.t ├── 01-head.t ├── 02-shortpath.t ├── 03-redirect.t ├── 04-error_filter.t ├── 05-session.t ├── 06-http2.t ├── 07-proxy.t ├── 08-shortcut.t └── ssl │ ├── tokers.crt │ └── tokers.key └── util └── lua-releng /.ci/setup.sh: -------------------------------------------------------------------------------- 1 | OPENRESTY_DOWNLOAD_DIR=$DOWNLOAD_CACHE/openresty-$V_OPENRESTY 2 | LUAROCKS_DOWNLOAD_DIR=$DOWNLOAD_CACHE/luarocks-$V_LUAROCKS 3 | 4 | mkdir -p $OPENRESTY_DOWNLOAD_DIR $LUAROCKS_DOWNLOAD_DIR 5 | 6 | if [ ! $(ls -A $OPENRESTY_DOWNLOAD_DIR) ]; then 7 | pushd $DOWNLOAD_CACHE 8 | wget https://openresty.org/download/openresty-$V_OPENRESTY.tar.gz 9 | tar xzf openresty-$V_OPENRESTY.tar.gz 10 | popd 11 | fi 12 | 13 | if [ ! $(ls -A $LUAROCKS_DOWNLOAD_DIR) ]; then 14 | pushd $DOWNLOAD_CACHE 15 | wget http://luarocks.github.io/luarocks/releases/luarocks-$V_LUAROCKS.tar.gz 16 | tar xzf luarocks-$V_LUAROCKS.tar.gz 17 | popd 18 | fi 19 | 20 | OPENRESTY_INSTALL_DIR=$INSTALL_CACHE/openresty-$V_OPENRESTY 21 | LUAROCKS_INSTALL_DIR=$INSTALL_CACHE/luarocks-$V_LUAROCKS 22 | 23 | mkdir -p $OPENRESTY_INSTALL_DIR $LUAROCKS_INSTALL_DIR 24 | 25 | if [ ! "$(ls -A $OPENRESTY_INSTALL_DIR)" ]; then 26 | pushd $OPENRESTY_DOWNLOAD_DIR 27 | ./configure \ 28 | --prefix=$OPENRESTY_INSTALL_DIR \ 29 | --with-http_v2_module \ 30 | &> build.log || (cat build.log && exit 1) 31 | 32 | make &> build.log || (cat build.log && exit 1) 33 | make install &> build.log || (cat build.log && exit 1) 34 | popd 35 | 36 | git clone https://github.com/tokers/lua-resty-http2 37 | cp -r lua-resty-http2/lib/resty/http2 $OPENRESTY_INSTALL_DIR/lualib/resty 38 | cp lua-resty-http2/lib/resty/http2.lua $OPENRESTY_INSTALL_DIR/lualib/resty 39 | rm -rf lua-resty-http2 40 | fi 41 | 42 | if [ ! "$(ls -A $LUAROCKS_INSTALL_DIR)" ]; then 43 | pushd $LUAROCKS_DOWNLOAD_DIR 44 | ./configure \ 45 | --prefix=$LUAROCKS_INSTALL_DIR \ 46 | --lua-suffix=jit \ 47 | --with-lua=$OPENRESTY_INSTALL_DIR/luajit \ 48 | --with-lua-include=$OPENRESTY_INSTALL_DIR/luajit/include/luajit-2.1 \ 49 | &> build.log || (cat build.log && exit 1) 50 | 51 | make build &> build.log || (cat build.log && exit 1) 52 | make install &> build.log || (cat build.log && exit 1) 53 | popd 54 | fi 55 | 56 | export PATH=$PATH:$OPENRESTY_INSTALL_DIR/nginx/sbin:$OPENRESTY_INSTALL_DIR/bin:$LUAROCKS_INSTALL_DIR/bin 57 | 58 | eval `luarocks path` 59 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.t linguist-language=Text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | t/servroot/* 43 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | unused_args = false 3 | redefined = false 4 | max_line_length = 80 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: perl 2 | perl: 3 | - 5.10 4 | cache: 5 | - apt: true 6 | - ccache: true 7 | env: 8 | global: 9 | - V_OPENRESTY=1.13.6.1 10 | - V_LUAROCKS=3.0.1 11 | - DOWNLOAD_CACHE=$HOME/download_cache 12 | - INSTALL_CACHE=$HOME/install_cache 13 | install: 14 | - cpanm -v --notest --sudo Test::Nginx 15 | before_script: 16 | - sudo apt-get update -q 17 | - sudo apt-get install libreadline-dev libncurses5-dev libpcre3-dev libssl-dev -y 18 | - sudo apt-get install make build-essential lua5.1 -y 19 | - source .ci/setup.sh 20 | - luarocks install luacheck 21 | - luarocks install lua-resty-socket 22 | script: 23 | - OPENRESTY_INSTALL_DIR=$INSTALL_CACHE/openresty-$V_OPENRESTY make test 24 | -------------------------------------------------------------------------------- /CHANGELOG.markdown: -------------------------------------------------------------------------------- 1 | Table of Contents 2 | ================= 3 | 4 | * [v0.7.3](#v0.7.3) 5 | * [v0.7.2](#v0.7.2) 6 | * [v0.7.2](#v0.7.2) 7 | * [v0.7.1](#v0.7.1) 8 | * [v0.7](#v0.7) 9 | * [v0.6](#v0.6) 10 | * [v0.5](#v0.5) 11 | * [v0.4](#v0.4) 12 | * [v0.3](#v0.3) 13 | 14 | 15 | v0.7.3 16 | ====== 17 | 18 | > Date: 2019.07.18 19 | 20 | * bugfix: treat Expect header value case-insensitive. 21 | * test: download Test::Nginx with the super user permission. 22 | * feature: shortcut calling for requests APIs. 23 | 24 | v0.7.2 25 | ====== 26 | 27 | > Date: 2019.03.08 28 | 29 | * doc: added the annotations about the compatibilty for the blocking phases. 30 | * change: updated the lua-resty-socket dependency. 31 | * bugfix: fixed the bootless keep-alive feature, now it works when the 32 | condition is conformed. 33 | 34 | v0.7.1 35 | ====== 36 | 37 | The old version is not so semantic, now we tweaked it. 38 | 39 | > Date: 2019.02.21 40 | 41 | This version just contains some minor improvements. 42 | 43 | * doc: fix timeout misrepresent, seconds => milliseconds. Thanks jetz for the 44 | patch. 45 | * doc: README fix typo ("Authorization") 7 hours. Thanks Chris Kuehl for the 46 | patch. 47 | * doc: fixed some documentary typos. 48 | * feature: used lua-resty-socket when the phase is not yieldable. 49 | * feature: used cosocket pool backlog if the ngx_lua is new enough. 50 | 51 | v0.7 52 | ==== 53 | 54 | > Date: 2018.12.03 55 | 56 | This version refactored the logic about headers indexing, also fixed the relevant bug. 57 | 58 | * refactored the request/response headers table logic. 59 | 60 | v0.6 61 | ==== 62 | 63 | > Date: 2018.11.15 64 | 65 | This version added some new features like r:json(), HTTPS proxy and some bugfixs. 66 | 67 | * feature: supported HTTPS proxy based on HTTP CONNECT method. 68 | * feature: r:json(), now one can get a Lua table which deserializes the response body from calling this method. 69 | * feature: added a new option "use_default_type" to control whether adds a default content-type request header when request body exists. 70 | * bugfix: the ttfb metric always records the "time to first header". 71 | * bugfix: add "charset; utf-8" check to json response object "content-type" header, thanks Happy Totem for the report and pull request. 72 | 73 | v0.5 74 | ==== 75 | 76 | > Date: 2018.10.25 77 | 78 | This version has minor modifications but with a compatibilty broken change. 79 | 80 | * change: Content-Length header will not be deleted even when users are using the function request body fashion. 81 | * improve: now we don't launch ssl handshake if a reused connection is using. 82 | * bugfix: http2.lua cannot be copied to the correct openresty lualib dir. 83 | 84 | v0.4 85 | ==== 86 | 87 | > Date: 2018.10.09 88 | 89 | This version supplemented more test cases and introduced the following 90 | features. 91 | 92 | * feature: supported the test of Expect request header. 93 | * feature: supported installation from LuaRocks. 94 | * feature: intergrated with lua-resty-http2, can use the HTTP/2 in the plain connection, this is still experimental. 95 | 96 | v0.3 97 | ==== 98 | 99 | > Date: 2018.06.18 100 | 101 | The first release. 102 | 103 | 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2019, Alex Zhang 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPENRESTY_INSTALL_DIR ?= /usr/local/openresty 2 | 3 | .PHONY: all luacheck test install 4 | 5 | all: ; 6 | 7 | luacheck: 8 | luacheck lib/resty/requests.lua lib/resty/requests/*.lua 9 | @echo "" 10 | 11 | luareleng: 12 | util/lua-releng 13 | @echo "" 14 | 15 | test: luareleng luacheck 16 | prove -I../test-nginx/lib -r -s t/ 17 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-requests - Yet Another HTTP Library for OpenResty. 5 | 6 | ![Build Status](https://travis-ci.org/tokers/lua-resty-requests.svg?branch=master) 7 | 8 | ```bash 9 | resty -e 'print(require "resty.requests".get{ url = "https://github.com", stream = false }.content)' 10 | ``` 11 | 12 | Table of Contents 13 | ================= 14 | 15 | * [Name](#name) 16 | * [Status](#status) 17 | * [Features](#features) 18 | * [Synopsis](#synopsis) 19 | * [Installation](#installation) 20 | * [Methods](#methods) 21 | * [request](#request) 22 | * [state](#state) 23 | * [get](#get) 24 | * [head](#head) 25 | * [post](#post) 26 | * [put](#put) 27 | * [delete](#delete) 28 | * [options](#options) 29 | * [patch](#patch) 30 | * [Response Object](#response-object) 31 | * [Session](#session) 32 | * [TODO](#todo) 33 | * [Author](#author) 34 | * [Copyright and License](#copyright-and-license) 35 | * [See Also](#see-also) 36 | 37 | Status 38 | ====== 39 | 40 | This Lua module now can be considered as production ready. 41 | 42 | Note since the `v0.7.1` release, this module started using lua-resty-socket, 43 | for working in the non-yieldable phases, but still more efforts are needed, 44 | so **DONOT** use it in the `init` or `init_worker` phases (or other 45 | non-yieldable phases). 46 | 47 | Features 48 | ======== 49 | 50 | * HTTP/1.0, HTTP/1.1 and HTTP/2 (WIP). 51 | * SSL/TLS support. 52 | * Chunked data support. 53 | * Convenient interfaces to support features like json, authorization and etc. 54 | * Stream interfaces to read body. 55 | * HTTP/HTTPS proxy. 56 | * Latency metrics. 57 | * Session support. 58 | 59 | Synopsis 60 | ======== 61 | 62 | ```lua 63 | local requests = require "resty.requests" 64 | 65 | -- example url 66 | local url = "http://example.com/index.html" 67 | 68 | local r, err = requests.get(url) 69 | if not r then 70 | ngx.log(ngx.ERR, err) 71 | return 72 | end 73 | 74 | -- read all body 75 | local body = r:body() 76 | ngx.print(body) 77 | 78 | -- or you can iterate the response body 79 | -- while true do 80 | -- local chunk, err = r:iter_content(4096) 81 | -- if not chunk then 82 | -- ngx.log(ngx.ERR, err) 83 | -- return 84 | -- end 85 | -- 86 | -- if chunk == "" then 87 | -- break 88 | -- end 89 | -- 90 | -- ngx.print(chunk) 91 | -- end 92 | 93 | -- you can also use the non-stream mode 94 | -- local opts = { 95 | -- stream = false 96 | -- } 97 | -- 98 | -- local r, err = requests.get(url, opts) 99 | -- if not r then 100 | -- ngx.log(ngx.ERR, err) 101 | -- end 102 | -- 103 | -- ngx.print(r.content) 104 | 105 | -- or you can use the shortcut way to make the code cleaner. 106 | local r, err = requests.get { url = url, stream = false } 107 | ``` 108 | 109 | Installation 110 | ============ 111 | 112 | * [LuaRocks](https://luarocks.org): 113 | 114 | ```bash 115 | $ luarocks install lua-resty-requests 116 | ``` 117 | 118 | * [OPM](https://github.com/openresty/opm): 119 | 120 | ```bash 121 | $ opm get tokers/lua-resty-requests 122 | ``` 123 | 124 | * Manually: 125 | 126 | Just tweeks the `lua_package_path` or the `LUA_PATH` environment variable, to add the installation path for this Lua module: 127 | 128 | ``` 129 | /path/to/lua-resty-requests/lib/resty/?.lua; 130 | ``` 131 | 132 | Methods 133 | ======= 134 | 135 | ### request 136 | 137 | **syntax**: *local r, err = requests.request(method, url, opts?)* 138 | **syntax**: *local r, err = requests.request { method = method, url = url, ... } 139 | 140 | This is the pivotal method in `lua-resty-requests`, it will return a [response object](#response-object) `r`. In the case of failure, `nil`, and a Lua string which describles the corresponding error will be given. 141 | 142 | The first parameter `method`, is the HTTP method that you want to use(same as 143 | HTTP's semantic), which takes a Lua string and the value can be: 144 | 145 | * `GET` 146 | * `HEAD` 147 | * `POST` 148 | * `PUT` 149 | * `DELETE` 150 | * `OPTIONS` 151 | * `PATCH` 152 | 153 | The second parameter `url`, just takes the literal meaning(i.e. Uniform Resource Location), 154 | for instance, `http://foo.com/blah?a=b`, you can omit the scheme prefix and as the default scheme, 155 | `http` will be selected. 156 | 157 | The third param, an optional Lua table, which contains a number of options: 158 | 159 | * `headers` holds the custom request headers. 160 | 161 | * `allow_redirects` specifies whether redirecting to the target url(specified by `Location` header) or not when the status code is `301`, `302`, `303`, `307` or `308`. 162 | 163 | * `redirect_max_times` specifies the redirect limits, default is `10`. 164 | 165 | * `body`, the request body, can be: 166 | * a Lua string, or 167 | * a Lua function, without parameter and returns a piece of data (string) or an empty Lua string to represent EOF, or 168 | * a Lua table, each key-value pair will be concatenated with the "&", and Content-Type header will `"application/x-www-form-urlencoded"` 169 | 170 | * `error_filter`, holds a Lua function which takes two parameters, `state` and `err`. 171 | the parameter `err` describes the error and `state` is always one of these values(represents the current stage): 172 | * `requests.CONNECT` 173 | * `requests.HANDSHAKE` 174 | * `requests.SEND_HEADER` 175 | * `requests.SEND_BODY` 176 | * `requests.RECV_HEADER` 177 | * `requests.RECV_BODY` 178 | * `requests.CLOSE` 179 | 180 | You can use the method [requests.state](#state) to get the textual meaning of these values. 181 | 182 | 183 | * `timeouts`, an array-like table, `timeouts[1]`, `timeouts[2]` and `timeouts[3]` represents `connect timeout`, `send timeout` and `read timeout` respectively (in milliseconds). 184 | 185 | * `http10` specify whether the `HTTP/1.0` should be used, default verion is `HTTP/1.1`. 186 | * `http20` specify whether the `HTTP/2` should be used, default verion is `HTTP/1.1`. 187 | 188 | Note this is still unstable, caution should be exercised. Also, there are some 189 | limitations, see [lua-resty-http2](https://github.com/tokers/lua-resty-http2) for the details. 190 | 191 | * `ssl` holds a Lua table, with three fields: 192 | * `verify`, controls whether to perform SSL verification 193 | * `server_name`, is used to specify the server name for the new TLS extension Server Name Indication (SNI) 194 | 195 | * `proxies` specify proxy servers, the form is like 196 | 197 | ```lua 198 | { 199 | http = { host = "127.0.0.1", port = 80 }, 200 | https = { host = "192.168.1.3", port = 443 }, 201 | } 202 | ``` 203 | 204 | When using HTTPS proxy, a preceding CONNECT request will be sent to proxy server. 205 | 206 | * `hooks`, also a Lua table, represents the hook system that you can use to 207 | manipulate portions of the request process. Available hooks are: 208 | * `response`, will be triggered immediately after receiving the response headers 209 | 210 | you can assign Lua functions to hooks, these functions accept the [response object](#response-object) as the unique param. 211 | 212 | ```lua 213 | local hooks = { 214 | response = function(r) 215 | ngx.log(ngx.WARN, "during requests process") 216 | end 217 | } 218 | ``` 219 | 220 | Considering the convenience, there are also some "short path" options: 221 | 222 | * `auth`, to do the Basic HTTP Authorization, takes a Lua table contains `user` and `pass`, e.g. when `auth` is: 223 | 224 | ```lua 225 | { 226 | user = "alex", 227 | pass = "123456" 228 | } 229 | ``` 230 | 231 | Request header `Authorization` will be added, and the value is `Basic YWxleDoxMjM0NTY=`. 232 | 233 | * `json`, takes a Lua table, it will be serialized by `cjson`, the serialized data will be sent as the request body, and it takes the priority when both `json` and `body` are specified. 234 | 235 | * `cookie`, takes a Lua table, the key-value pairs will be organized according to the `Cookie` header's rule, e.g. `cookie` is: 236 | 237 | ```lua 238 | { 239 | ["PHPSESSID"] = "298zf09hf012fh2", 240 | ["csrftoken"] = "u32t4o3tb3gg43" 241 | } 242 | ``` 243 | 244 | The `Cookie` header will be `PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43 `. 245 | 246 | * `stream`, takes a boolean value, specifies whether reading the body in the stream mode, and it will be true by default. 247 | 248 | ### state 249 | 250 | **syntax**: *local state_name = requests.state(state)* 251 | 252 | The method is used for getting the textual meaning of these values: 253 | 254 | * `requests.CONNECT` 255 | * `requests.HANDSHAKE` 256 | * `requests.SEND_HEADER` 257 | * `requests.SEND_BODY` 258 | * `requests.RECV_HEADER` 259 | * `requests.RECV_BODY` 260 | * `requests.CLOSE` 261 | 262 | a Lua string `"unknown"` will be returned if `state` isn't one of the above values. 263 | 264 | ### get 265 | 266 | **syntax**: *local r, err = requests.get(url, opts?)* 267 | **syntax**: *local r, err = requests.get { url = url, ... }* 268 | 269 | Sends a HTTP GET request. This is identical with 270 | 271 | ```lua 272 | requests.request("GET", url, opts) 273 | ``` 274 | 275 | ### head 276 | **syntax**: *local r, err = requests.head(url, opts?)* 277 | **syntax**: *local r, err = requests.head { url = url, ... }* 278 | 279 | Sends a HTTP HEAD request. This is identical with 280 | 281 | ```lua 282 | requests.request("HEAD", url, opts) 283 | ``` 284 | 285 | ### post 286 | **syntax**: *local r, err = requests.post(url, opts?)* 287 | **syntax**: *local r, err = requests.post { url = url, ... }* 288 | 289 | Sends a HTTP POST request. This is identical with 290 | 291 | ```lua 292 | requests.request("POST", url, opts) 293 | ``` 294 | 295 | ### put 296 | **syntax**: *local r, err = requests.put(url, opts?)* 297 | **syntax**: *local r, err = requests.put { url = url, ... }* 298 | 299 | Sends a HTTP PUT request. This is identical with 300 | 301 | ```lua 302 | requests.request("PUT", url, opts) 303 | ``` 304 | 305 | ### delete 306 | **syntax**: *local r, err = requests.delete(url, opts?)* 307 | **syntax**: *local r, err = requests.delete { url = url, ... }* 308 | 309 | Sends a HTTP DELETE request. This is identical with 310 | 311 | ```lua 312 | requests.request("DELETE", url, opts) 313 | ``` 314 | 315 | ### options 316 | **syntax**: *local r, err = requests.options(url, opts?)* 317 | **syntax**: *local r, err = requests.options { url = url, ... }* 318 | 319 | Sends a HTTP OPTIONS request. This is identical with 320 | 321 | ```lua 322 | requests.request("OPTIONS", url, opts) 323 | ``` 324 | 325 | ### patch 326 | **syntax**: *local r, err = requests.patch(url, opts?)* 327 | **syntax**: *local r, err = requests.patch { url = url, ... }* 328 | 329 | Sends a HTTP PATCH request. This is identical with 330 | 331 | ```lua 332 | requests.request("PATCH", url, opts) 333 | ``` 334 | 335 | Response Object 336 | =============== 337 | 338 | Methods like `requests.get` and others will return a response object `r`, which can be manipulated by the following methods and variables: 339 | 340 | * `url`, the url passed from caller 341 | * `method`, the request method, e.g. `POST` 342 | * `status_line`, the raw status line(received from the remote) 343 | * `status_code`, the HTTP status code 344 | * `http_version`, the HTTP version of response, e.g. `HTTP/1.1` 345 | * `headers`, a Lua table represents the HTTP response headers(case-insensitive) 346 | * `close`, holds a Lua function, used to close(keepalive) the underlying TCP connection 347 | * `drop`, is a Lua function, used for dropping the unread HTTP response body, will be invoked automatically when closing (if any unread data remains) 348 | * `iter_content`, which is also a Lua function, emits a part of response body(decoded from chunked format) each time called. 349 | 350 | This function accepts an optional param `size` to specify the size of body that the caller wants, when absent, `iter_content` returns `8192` bytes when the response body is plain or returns a piece of chunked data if the resposne body is chunked. 351 | 352 | In case of failure, `nil` and a Lua string described the error will be returned. 353 | 354 | * `body`, also holds a Lua function that returns the whole response body. 355 | 356 | In case of failure, `nil` and a Lua string described the error will be returned. 357 | 358 | * `json`, holds a Lua function, serializes the body to a Lua table, note the `Content-Type` should be `application/json`. In case of failure, `nil` and an error string will be given. 359 | 360 | * `content`, the response body, only valid in the non-stream mode. 361 | 362 | * `elapsed`, a hash-like Lua table which represents the cost time (in seconds) for each stage. 363 | * `elapsed.connect`, cost time for the TCP 3-Way Handshake; 364 | * `elapsed.handshake`, cost time for the SSL/TLS handshake (if any); 365 | * `elapsed.send_header`, cost time for sending the HTTP request headers; 366 | * `elapsed.send_body`, cost time for sending the HTTP request body (if any); 367 | * `elapsed.read_header`, cost time for receiving the HTTP response headers; 368 | * `elapsed.ttfb`, the time to first byte. 369 | 370 | Note When HTTP/2 protocol is applied, the `elapsed.send_body` (if any) will be same as `elapsed.send_header`. 371 | 372 | Session 373 | ======= 374 | 375 | A session persists some data across multiple requests, like cookies data, authorization data and etc. 376 | 377 | This mechanism now is still experimental. 378 | 379 | A simple example: 380 | 381 | ```lua 382 | s = requests.session() 383 | local r, err = s:get("https://www.example.com") 384 | ngx.say(r:body()) 385 | ``` 386 | 387 | A session object has same interfaces with `requests`, i.e. those http methods. 388 | 389 | TODO 390 | ==== 391 | 392 | * other interesting features... 393 | 394 | 395 | Author 396 | ====== 397 | 398 | Alex Zhang (张超) zchao1995@gmail.com, UPYUN Inc. 399 | 400 | Copyright and License 401 | ===================== 402 | 403 | The bundle itself is licensed under the 2-clause BSD license. 404 | 405 | Copyright (c) 2017-2019, Alex Zhang. 406 | 407 | This module is licensed under the terms of the BSD license. 408 | 409 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 410 | 411 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 412 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 413 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 414 | 415 | See Also 416 | ========= 417 | 418 | * upyun-resty: https://github.com/upyun/upyun-resty 419 | * httpipe: https://github.com/timebug/lua-resty-httpipe 420 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = lua-resty-requests 2 | abstract = Yet Another HTTP library for OpenResty 3 | version = 0.7.2 4 | author = Alex Zhang(张超) zchao1995@gmail.com, UPYUN Inc. 5 | is_original = yes 6 | license = 2bsd 7 | lib_dir = lib 8 | repo_link = https://github.com/tokers/lua-resty-requests 9 | -------------------------------------------------------------------------------- /lib/resty/requests.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Alex Zhang 2 | 3 | local util = require "resty.requests.util" 4 | local session = require "resty.requests.session" 5 | 6 | 7 | local is_tab = util.is_tab 8 | local error = error 9 | 10 | local _M = { _VERSION = "0.7.3" } 11 | 12 | 13 | local function request_shortcut(method, opts) 14 | method = method or opts.method 15 | if not method then 16 | error("no specified HTTP method") 17 | end 18 | 19 | local url = opts.url 20 | local s = session.new() 21 | return s:request(method, url, opts) 22 | end 23 | 24 | 25 | local function request(method, url, opts) 26 | if not url and is_tab(method) then 27 | -- shortcut type 28 | return request_shortcut(nil, method) 29 | end 30 | 31 | local s = session.new() 32 | return s:request(method, url, opts) 33 | end 34 | 35 | 36 | local function get(url, opts) 37 | if not opts and is_tab(url) then 38 | -- shortcut type 39 | return request_shortcut("GET", url) 40 | end 41 | 42 | return request("GET", url, opts) 43 | end 44 | 45 | 46 | local function head(url, opts) 47 | if not opts and is_tab(url) then 48 | -- shortcut type 49 | return request_shortcut("HEAD", url) 50 | end 51 | 52 | return request("HEAD", url, opts) 53 | end 54 | 55 | 56 | local function post(url, opts) 57 | if not opts and is_tab(url) then 58 | -- shortcut type 59 | return request_shortcut("POST", url) 60 | end 61 | 62 | return request("POST", url, opts) 63 | end 64 | 65 | 66 | local function put(url, opts) 67 | if not opts and is_tab(url) then 68 | -- shortcut type 69 | return request_shortcut("PUT", url) 70 | end 71 | 72 | return request("PUT", url, opts) 73 | end 74 | 75 | 76 | local function delete(url, opts) 77 | if not opts and is_tab(url) then 78 | -- shortcut type 79 | return request_shortcut("DELETE", url) 80 | end 81 | 82 | return request("DELETE", url, opts) 83 | end 84 | 85 | 86 | local function options(url, opts) 87 | if not opts and is_tab(url) then 88 | -- shortcut type 89 | return request_shortcut("OPTIONS", url) 90 | end 91 | 92 | return request("OPTIONS", url, opts) 93 | end 94 | 95 | 96 | local function patch(url, opts) 97 | if not opts and is_tab(url) then 98 | -- shortcut type 99 | return request_shortcut("PATCH", url) 100 | end 101 | 102 | return request("PATCH", url, opts) 103 | end 104 | 105 | 106 | local function state(s) 107 | return util.STATE_NAME[s] or "unknown" 108 | end 109 | 110 | 111 | _M.request = request 112 | _M.get = get 113 | _M.head = head 114 | _M.post = post 115 | _M.put = put 116 | _M.delete = delete 117 | _M.options = options 118 | _M.patch = patch 119 | _M.state = state 120 | _M.session = session.new 121 | 122 | local STATE = util.STATE 123 | for k, v in pairs(STATE) do 124 | _M[k] = v 125 | end 126 | 127 | return _M 128 | -------------------------------------------------------------------------------- /lib/resty/requests/adapter.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Alex Zhang 2 | 3 | local util = require "resty.requests.util" 4 | local resty_socket = require "resty.socket" 5 | local response = require "resty.requests.response" 6 | local check_http2, http2 = pcall(require, "resty.http2") 7 | 8 | local pairs = pairs 9 | local tonumber = tonumber 10 | local tostring = tostring 11 | local lower = string.lower 12 | local format = string.format 13 | local insert = table.insert 14 | local concat = table.concat 15 | local tcp_socket = ngx.socket.tcp 16 | local ngx_match = ngx.re.match 17 | local ngx_now = ngx.now 18 | local get_phase = ngx.get_phase 19 | local dict = util.dict 20 | local new_tab = util.new_tab 21 | local is_tab = util.is_tab 22 | local is_func = util.is_func 23 | local ngx_lua_version = ngx.config.ngx_lua_version 24 | 25 | local _M = { _VERSION = "0.4" } 26 | local mt = { __index = _M } 27 | 28 | local DEFUALT_POOL_BACKLOG = 10 29 | local DEFAULT_POOL_SIZE = 30 30 | local DEFAULT_IDLE_TIMEOUT = 60 * 1000 31 | local DEFAULT_CONN_TIMEOUT = 2 * 1000 32 | local DEFAULT_SEND_TIMEOUT = 10 * 1000 33 | local DEFAULT_READ_TIMEOUT = 30 * 1000 34 | local STATE = util.STATE 35 | local HTTP2_MEMO 36 | local HTTP2_MEMO_LAST_ENTRY 37 | local LAST_HTTP2_KEY 38 | 39 | if check_http2 then 40 | -- the single linked list for caching the HTTP/2 session key 41 | HTTP2_MEMO = new_tab(0, 4) 42 | -- the last entry in the single linked list 43 | HTTP2_MEMO_LAST_ENTRY = new_tab(0, 4) 44 | LAST_HTTP2_KEY = new_tab(0, 4) 45 | end 46 | 47 | 48 | local function socket() 49 | local phase = get_phase() 50 | 51 | -- ignore the other non-yiedable phases, since these phases are 52 | -- requests-specific and we shouldn't use the blocking APIs, it will hurt 53 | -- the event loop, so just let the Cosocket throws "API disabled ..." 54 | -- error. 55 | if phase == "init" or phase == "init_worker" then 56 | return resty_socket() 57 | end 58 | 59 | return tcp_socket() 60 | end 61 | 62 | 63 | local function parse_status_line(status_line) 64 | local m, err = ngx_match(status_line, "(HTTP/.+?)\\s.*?(\\d+).*", "jo") 65 | if not m then 66 | return nil, err 67 | end 68 | 69 | return { 70 | status_code = tonumber(m[2]), 71 | http_version = m[1], 72 | } 73 | end 74 | 75 | 76 | local function parse_header_line(line) 77 | if not line or #line < 3 then 78 | return 79 | end 80 | 81 | local m, err = ngx_match(line, "^(.+?):\\s*(.+)$", "jo") 82 | if not m then 83 | return nil, nil, err 84 | end 85 | 86 | return m[1], m[2] 87 | end 88 | 89 | 90 | local function parse_headers(self) 91 | local read_timeout = self.read_timeout 92 | local sock = self.sock 93 | 94 | sock:settimeout(read_timeout) 95 | 96 | local reader = sock:receiveuntil("\r\n") 97 | 98 | local status_line, err = reader() 99 | if not status_line then 100 | return nil, err 101 | end 102 | 103 | self.elapsed.ttfb = ngx_now() - self.start 104 | 105 | local part, err = parse_status_line(status_line) 106 | if not part then 107 | return nil, err or "bad status line" 108 | end 109 | 110 | local headers = dict(nil, 0, 9) 111 | 112 | while true do 113 | local line, err = reader() 114 | if not line then 115 | return nil, err 116 | end 117 | 118 | if line == "" then 119 | break 120 | end 121 | 122 | local name, value, err = parse_header_line(line) 123 | if err then 124 | return nil, err 125 | end 126 | 127 | if name and value then 128 | -- FIXME transform underscore to hyphen 129 | name = lower(name) 130 | 131 | local ovalue = headers[name] 132 | if not ovalue then 133 | headers[name] = value 134 | 135 | elseif is_tab(ovalue) then 136 | insert(headers[name], value) 137 | 138 | else 139 | headers[name] = new_tab(2, 0) 140 | headers[name][1] = ovalue 141 | headers[name][2] = value 142 | end 143 | end 144 | end 145 | 146 | return status_line, part, headers 147 | end 148 | 149 | 150 | local function drop_data(sock, len) 151 | while true do 152 | if len == 0 then 153 | return true 154 | end 155 | 156 | local size = len < 8192 and len or 8192 157 | local _, err = sock:receive(size) 158 | if err then 159 | return nil, err 160 | end 161 | 162 | len = len - size 163 | end 164 | end 165 | 166 | 167 | local function proxy(self, request) 168 | if not self.https_proxy then 169 | return true 170 | end 171 | 172 | local sock = self.sock 173 | local host = self.https_proxy 174 | 175 | local message = new_tab(4, 0) 176 | message[1] = format("CONNECT %s HTTP/1.1\r\n", host) 177 | message[2] = format("Host: %s\r\n", host) 178 | message[3] = format("User-Agent: resty-requests\r\n") 179 | message[4] = format("Proxy-Connection: keep-alive\r\n\r\n") 180 | 181 | sock:settimeout(self.send_timeout) 182 | 183 | local _, err = sock:send(message) 184 | if err then 185 | return nil, err 186 | end 187 | 188 | local status_line, part, headers = parse_headers(self) 189 | if not status_line then 190 | return nil, err 191 | end 192 | 193 | -- drop the body (if any) 194 | if headers["Transfer-Encoding"] then 195 | local reader = sock:receiveuntil("\r\n") 196 | while true do 197 | local size, err = reader() 198 | if not size then 199 | return nil, err 200 | end 201 | 202 | -- just ignore the chunk-extensions 203 | local ext = size:find(";", nil, true) 204 | if ext then 205 | size = size:sub(1, ext - 1) 206 | end 207 | 208 | size = tonumber(size, 16) 209 | if not size then 210 | return nil, "invalid chunk header" 211 | end 212 | 213 | if size > 0 then 214 | local ok, err = drop_data(sock, size) 215 | if not ok then 216 | return nil, err 217 | end 218 | end 219 | 220 | -- read the last "\r\n" 221 | local dummy, err = reader() 222 | if dummy ~= "" then 223 | return nil, err or "invalid chunked data" 224 | end 225 | 226 | if size == 0 then 227 | break 228 | end 229 | end 230 | else 231 | local len = tonumber(headers["Content-Length"]) 232 | if len > 0 then 233 | local ok, err = drop_data(sock, len) 234 | if not ok then 235 | return nil, err 236 | end 237 | end 238 | end 239 | 240 | if part.status_code ~= 200 then 241 | return nil, format("invalid status code: %d (https proxy)", 242 | part.status_code) 243 | end 244 | 245 | return true 246 | end 247 | 248 | 249 | local function connect(self, request) 250 | self.state = STATE.CONNECT 251 | 252 | local conn_timeout = self.conn_timeout 253 | local read_timeout = self.read_timeout 254 | local send_timeout = self.send_timeout 255 | local proxies = request.proxies 256 | local scheme = request.scheme 257 | 258 | local host, port 259 | 260 | if proxies and proxies[scheme] then 261 | host = proxies[scheme].host 262 | port = proxies[scheme].port 263 | if scheme == "https" then 264 | self.https_proxy = format("%s:%d", request.host, request.port) 265 | end 266 | else 267 | host = request.host 268 | port = request.port 269 | end 270 | 271 | local sock = socket() 272 | self.sock = sock 273 | sock:settimeouts(conn_timeout, send_timeout, read_timeout) 274 | 275 | if check_http2 and request.http_version == "HTTP/2" then 276 | local key = host .. ":" .. port 277 | local pool_key 278 | local reuse 279 | 280 | if HTTP2_MEMO[key] then 281 | local entry = HTTP2_MEMO[key] 282 | HTTP2_MEMO[key] = entry.next 283 | pool_key = entry.key 284 | reuse = true 285 | 286 | else 287 | local last_key = LAST_HTTP2_KEY[key] or 0 288 | pool_key = key .. ":" .. last_key 289 | LAST_HTTP2_KEY[key] = last_key + 1 290 | end 291 | 292 | self.h2_session_key = pool_key 293 | self.h2_key = key 294 | self.pool_size = 1 295 | 296 | local ok, err = sock:connect(host, port, { pool = pool_key }) 297 | if not ok then 298 | return nil, err 299 | end 300 | 301 | if reuse and sock:getreusedtimes() == 0 then 302 | -- the new connection 303 | local last_key = LAST_HTTP2_KEY[key] 304 | LAST_HTTP2_KEY[key] = last_key + 1 305 | self.h2_session_key = key .. ":" .. last_key 306 | end 307 | 308 | return true 309 | end 310 | 311 | if ngx_lua_version < 10014 or self.h2_session_key then 312 | return sock:connect(host, port) 313 | end 314 | 315 | local opts = { 316 | pool_size = self.pool_size, 317 | backlog = DEFUALT_POOL_BACKLOG, 318 | } 319 | 320 | return sock:connect(host, port, opts) 321 | end 322 | 323 | 324 | local function handshake(self, request) 325 | local scheme = request.scheme 326 | if scheme ~= "https" then 327 | -- carefully use HTTP/2 with plain connection 328 | if request.http_version == "HTTP/2" then 329 | self.h2 = true 330 | end 331 | 332 | return true 333 | end 334 | 335 | local verify = self.verify 336 | local reused_session = self.reused_session 337 | local server_name = self.server_name 338 | local sock = self.sock 339 | sock:settimeout(self.send_timeout) 340 | local times, err = sock:getreusedtimes() 341 | if err then 342 | return nil, err 343 | end 344 | 345 | if times ~= 0 then 346 | return true 347 | end 348 | 349 | self.state = STATE.HANDSHAKE 350 | 351 | if http2 and request.http_version == "HTTP/2" then 352 | local ok, proto = sock:sslhandshake(reused_session, server_name, verify, 353 | nil, "h2") 354 | if ok and proto == "h2" then 355 | self.h2 = true 356 | end 357 | 358 | return ok, proto 359 | end 360 | 361 | return sock:sslhandshake(reused_session, server_name, verify) 362 | end 363 | 364 | 365 | local function send_header(self, request) 366 | local uri = request.uri 367 | local args = request.args 368 | 369 | if args then 370 | uri = uri .. "?" .. args 371 | end 372 | 373 | local t = new_tab(4, 0) 374 | 375 | for k, v in pairs(request.headers) do 376 | t[#t + 1] = format("%s: %s", k, v) 377 | end 378 | 379 | t[#t + 1] = "\r\n" 380 | 381 | local content = { 382 | request.method, " ", uri, " ", 383 | request.http_version, "\r\n", concat(t, "\r\n") 384 | } 385 | 386 | self.state = STATE.SEND_HEADER 387 | 388 | local sock = self.sock 389 | local send_timeout = self.send_timeout 390 | 391 | sock:settimeout(send_timeout) 392 | 393 | local _, err = sock:send(content) 394 | if err then 395 | return nil, err 396 | end 397 | 398 | return true 399 | end 400 | 401 | 402 | local function send_body(self, request) 403 | local body = request.body 404 | if not body then 405 | return true 406 | end 407 | 408 | self.state = STATE.SEND_BODY 409 | 410 | -- firstly we need to wait the 100-Continue response 411 | if request.expect and request.http_version ~= util.HTTP10 then 412 | local reader = self.sock:receiveuntil("\r\n\r\n") 413 | local resp, err = reader() 414 | if not resp then 415 | return nil, err 416 | end 417 | 418 | if lower(resp) ~= "http/1.1 100 continue" then 419 | return nil, "invalid 100-continue response" 420 | end 421 | end 422 | 423 | local sock = self.sock 424 | 425 | if not is_func(body) then 426 | return sock:send(body) 427 | end 428 | 429 | local chunked = request.headers["Transfer-Encoding"] ~= nil 430 | 431 | repeat 432 | local chunk, err = body() 433 | if not chunk then 434 | return nil, err 435 | end 436 | 437 | local data = chunk 438 | 439 | if chunked then 440 | if chunk == "" then 441 | data = "0\r\n\r\n" 442 | else 443 | data = format("%x\r\n%s\r\n", #chunk, chunk) 444 | end 445 | end 446 | 447 | local _, err = sock:send(data) 448 | if err then 449 | return nil, err 450 | end 451 | 452 | until chunk == "" 453 | 454 | return true 455 | end 456 | 457 | 458 | local function read_header(self, request) 459 | self.state = STATE.RECV_HEADER 460 | 461 | local status_line, part, headers = parse_headers(self) 462 | if not status_line then 463 | -- part holds the error 464 | return nil, part 465 | end 466 | 467 | self.response = response.new { 468 | url = request.url, 469 | method = request.method, 470 | status_line = status_line, 471 | status_code = part.status_code, 472 | http_version = part.http_version, 473 | headers = headers, 474 | adapter = self, 475 | elapsed = self.elapsed, 476 | } 477 | 478 | return true 479 | end 480 | 481 | 482 | local function new(opts) 483 | opts = opts or {} 484 | 485 | local self = { 486 | sock = nil, 487 | response = nil, 488 | 489 | state = STATE.UNREADY, 490 | stream = opts.stream, 491 | 492 | verify = opts.verify, 493 | reused_session = opts.reused_session, 494 | server_name = opts.server_name, 495 | 496 | pool_size = opts.pool_size or DEFAULT_POOL_SIZE, 497 | 498 | idle_timeout = opts.idle_timeout or DEFAULT_IDLE_TIMEOUT, 499 | conn_timeout = opts.conn_timeout or DEFAULT_CONN_TIMEOUT, 500 | read_timeout = opts.read_timeout or DEFAULT_READ_TIMEOUT, 501 | send_timeout = opts.send_timeout or DEFAULT_SEND_TIMEOUT, 502 | 503 | elapsed = { 504 | connect = nil, 505 | handshake = nil, 506 | send_header= nil, 507 | send_body = nil, 508 | read_header = nil, 509 | read_body = nil, 510 | ttfb = nil, 511 | }, 512 | 513 | start = ngx_now(), 514 | 515 | h2 = false, 516 | h2_key = nil, 517 | h2_session_key = nil, 518 | h2_session = nil, 519 | h2_stream = nil, 520 | 521 | https_proxy = nil, -- https proxy 522 | 523 | error_filter = opts.error_filter, 524 | } 525 | 526 | return setmetatable(self, mt) 527 | end 528 | 529 | 530 | local function close(self, keepalive) 531 | local sock = self.sock 532 | if not sock or self.state == STATE.CLOSE then 533 | return true 534 | end 535 | 536 | self.state = STATE.CLOSE 537 | self.sock = nil 538 | 539 | if keepalive then 540 | if self.h2 then 541 | local key = self.h2_key 542 | local session_key = self.h2_session_key 543 | local entry = { key = session_key, next = nil } 544 | 545 | if not HTTP2_MEMO[key] then 546 | HTTP2_MEMO[key] = entry 547 | else 548 | HTTP2_MEMO_LAST_ENTRY[key].next = entry 549 | end 550 | 551 | HTTP2_MEMO_LAST_ENTRY[key] = entry 552 | 553 | self.h2_session:keepalive(session_key) 554 | end 555 | 556 | local idle_timeout = self.conn_idle_timeout 557 | 558 | if self.h2 or ngx_lua_version < 10014 then 559 | return sock:setkeepalive(idle_timeout, self.pool_size) 560 | end 561 | 562 | return sock:setkeepalive(idle_timeout) 563 | end 564 | 565 | if self.h2 then 566 | local ok, err = self.h2_session:close() 567 | if not ok then 568 | return nil, err 569 | end 570 | end 571 | 572 | return sock:close() 573 | end 574 | 575 | 576 | local function handle_http2(self, request) 577 | local client, err = http2.new { 578 | ctx = self.sock, 579 | recv = self.sock.receive, 580 | send = self.sock.send, 581 | key = self.h2_session_key, 582 | } 583 | 584 | local error_filter = self.error_filter 585 | self.state = STATE.SEND_HEADER 586 | 587 | if not client then 588 | return nil, err 589 | end 590 | 591 | local ok, err = client:acknowledge_settings() 592 | if not ok then 593 | return nil, err 594 | end 595 | 596 | self.h2_session = client 597 | 598 | local headers = new_tab(8, 0) 599 | local req_headers = request.headers 600 | 601 | local uri = request.uri 602 | local args = request.args 603 | 604 | if args then 605 | headers[#headers + 1] = { name = ":path", value = uri .. "?" .. args } 606 | else 607 | headers[#headers + 1] = { name = ":path", value = uri } 608 | end 609 | 610 | headers[#headers + 1] = { name = ":method", value = request.method } 611 | headers[#headers + 1] = { name = ":authority", value = req_headers["Host"] } 612 | headers[#headers + 1] = { name = ":scheme", value = request.scheme } 613 | 614 | for k, v in pairs(req_headers) do 615 | headers[#headers + 1] = { name = k, value = tostring(v) } 616 | end 617 | 618 | local tm1 = ngx_now() 619 | local stream, err = client:send_request(headers, request.body) 620 | if not stream then 621 | if error_filter then 622 | error_filter(self.state, err) 623 | end 624 | 625 | return nil, err 626 | end 627 | 628 | local tm2 = ngx_now() 629 | 630 | self.elapsed.send_header = tm2 - tm1 631 | self.elapsed.send_body = request.body and tm2 - tm1 or 0 632 | 633 | self.h2_stream = stream 634 | 635 | self.state = STATE.RECV_HEADER 636 | 637 | headers, err = client:read_headers(stream) 638 | if not headers then 639 | if error_filter then 640 | error_filter(self.state, err) 641 | end 642 | 643 | return nil, err 644 | end 645 | 646 | local tm3 = ngx_now() 647 | self.elapsed.ttfb = tm3 - self.start 648 | self.elapsed.read_header = tm3 - tm2 649 | 650 | local status_code = tonumber(headers[":status"]) 651 | headers[":status"] = nil 652 | 653 | self.response = response.new { 654 | url = request.url, 655 | method = request.method, 656 | status_line = "HTTP/2 " .. status_code, 657 | status_code = status_code, 658 | http_version = "HTTP/2", 659 | headers = headers, 660 | adapter = self, 661 | elapsed = self.elapsed, 662 | } 663 | 664 | return self.response 665 | end 666 | 667 | 668 | local function send(self, request) 669 | local stages = { 670 | connect, 671 | proxy, 672 | handshake, 673 | send_header, 674 | send_body, 675 | read_header, 676 | } 677 | 678 | local distr = { 679 | "connect", 680 | "proxy", 681 | "handshake", 682 | "send_header", 683 | "send_body", 684 | "read_header" 685 | } 686 | 687 | local error_filter = self.error_filter 688 | 689 | for i = 1, #stages do 690 | local now = ngx_now() 691 | local ok, err = stages[i](self, request) 692 | 693 | -- calculate each stage's cost time 694 | self.elapsed[distr[i]] = ngx_now() - now 695 | 696 | if not ok then 697 | if error_filter then 698 | error_filter(self.state, err) 699 | end 700 | 701 | return nil, err 702 | end 703 | 704 | if self.h2 then 705 | return handle_http2(self, request) 706 | end 707 | end 708 | 709 | return self.response 710 | end 711 | 712 | 713 | local function read(self, size) 714 | local sock = self.sock 715 | if not self.h2 then 716 | return sock:receive(size) 717 | end 718 | 719 | -- size will be ignored in the HTTP/2 case 720 | local session = self.h2_session 721 | local stream = self.h2_stream 722 | 723 | return session:read_body(stream) 724 | end 725 | 726 | 727 | local function reader(self, till) 728 | return self.sock:receiveuntil(till) 729 | end 730 | 731 | 732 | _M.new = new 733 | _M.send = send 734 | _M.close = close 735 | _M.read = read 736 | _M.reader = reader 737 | 738 | return _M 739 | -------------------------------------------------------------------------------- /lib/resty/requests/request.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Alex Zhang 2 | 3 | local cjson = require "cjson.safe" 4 | local util = require "resty.requests.util" 5 | 6 | local setmetatable = setmetatable 7 | local pairs = pairs 8 | local tostring = tostring 9 | local new_tab = util.new_tab 10 | local is_func = util.is_func 11 | local is_str = util.is_str 12 | local is_tab = util.is_tab 13 | local insert = table.insert 14 | local concat = table.concat 15 | local find = string.find 16 | local sub = string.sub 17 | local lower = string.lower 18 | local format = string.format 19 | local ngx_match = ngx.re.match 20 | local _M = { _VERSION = "0.2" } 21 | local mt = { __index = _M } 22 | 23 | local url_pattern = [[(?:(https?)://)?([^:/]+)(?::(\d+))?(.*)]] 24 | 25 | local DEFAULT_PORT = { 26 | http = 80, 27 | https = 443, 28 | } 29 | 30 | 31 | local function parse_url(url) 32 | local m, err = ngx_match(url, url_pattern, "jo") 33 | if not m then 34 | return nil, err 35 | end 36 | 37 | local parts = new_tab(0, 5) 38 | 39 | parts.scheme = m[1] or "http" 40 | parts.host = m[2] 41 | parts.port = m[3] or DEFAULT_PORT[parts.scheme] 42 | 43 | if not m[4] or m[4] == "" then 44 | parts.path = "/" 45 | parts.args = nil 46 | 47 | else 48 | local query = find(m[4], "?", nil, true) 49 | if query then 50 | parts.path = sub(m[4], 1, query - 1) 51 | parts.args = sub(m[4], query + 1) 52 | else 53 | parts.path = m[4] 54 | parts.args = nil 55 | end 56 | 57 | if parts.path == "" then 58 | parts.path = "/" 59 | end 60 | end 61 | 62 | return parts 63 | end 64 | 65 | 66 | local function prepare(url_parts, session, config) 67 | local headers = session.headers 68 | 69 | local content 70 | local json = config.json 71 | local body = config.body 72 | 73 | if json then 74 | content = cjson.encode(json) 75 | headers["content-length"] = #content 76 | headers["content-type"] = "application/json" 77 | else 78 | content = body 79 | if is_func(body) then 80 | -- users may know their request body size 81 | if not headers["content-length"] then 82 | headers["transfer-encoding"] = "chunked" 83 | end 84 | 85 | if not headers["content-type"] and config.use_default_type then 86 | headers["content-type"] = "application/octet-stream" 87 | end 88 | 89 | elseif is_str(body) then 90 | headers["content-length"] = #body 91 | headers["transfer-encoding"] = nil 92 | 93 | if not headers["content-type"] and config.use_default_type then 94 | headers["content-type"] = "text/plain" 95 | end 96 | 97 | elseif is_tab(body) then 98 | if not headers["content-type"] and config.use_default_type then 99 | headers["content-type"] = "application/x-www-form-urlencoded" 100 | end 101 | 102 | local param = new_tab(4, 0) 103 | for k, v in pairs(body) do 104 | param[#param + 1] = format("%s=%s", tostring(k), tostring(v)) 105 | end 106 | 107 | content = concat(param, "&") 108 | headers["content-length"] = #content 109 | headers["transfer-encoding"] = nil 110 | end 111 | end 112 | 113 | if not headers["host"] then 114 | headers["host"] = url_parts.host 115 | end 116 | 117 | if headers["transfer-encoding"] then 118 | headers["content-length"] = nil 119 | end 120 | 121 | headers["connection"] = "keep-alive" 122 | 123 | local auth = session.auth 124 | if auth then 125 | headers["authorization"] = auth 126 | end 127 | 128 | local cookie = session.cookie 129 | if cookie then 130 | local plain = new_tab(4, 0) 131 | for k, v in pairs(cookie) do 132 | insert(plain, ("%s=%s"):format(k, v)) 133 | end 134 | 135 | headers["cookie"] = concat(plain, "; ") 136 | end 137 | 138 | return content 139 | end 140 | 141 | 142 | local function new(method, url, session, config) 143 | local url_parts, err = parse_url(url) 144 | if not url_parts then 145 | return nil, err or "malformed url" 146 | end 147 | 148 | local body = prepare(url_parts, session, config) 149 | local expect = false 150 | 151 | do 152 | local expect_field = session.headers["expect"] 153 | if expect_field then 154 | expect = lower(expect_field) == "100-continue" 155 | end 156 | end 157 | 158 | local r = { 159 | method = method, 160 | scheme = url_parts.scheme, 161 | host = url_parts.host, 162 | port = url_parts.port, 163 | uri = url_parts.path, 164 | args = url_parts.args, 165 | headers = session.headers, 166 | http_version = session.version, 167 | proxies = session.proxies, 168 | body = body, 169 | expect = expect, 170 | } 171 | 172 | return setmetatable(r, mt) 173 | end 174 | 175 | 176 | _M.new = new 177 | 178 | return _M 179 | -------------------------------------------------------------------------------- /lib/resty/requests/response.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Alex Zhang 2 | 3 | local cjson = require "cjson.safe" 4 | local util = require "resty.requests.util" 5 | 6 | local is_tab = util.is_tab 7 | local new_tab = util.new_tab 8 | local find = string.find 9 | local lower = string.lower 10 | local insert = table.insert 11 | local concat = table.concat 12 | local tonumber = tonumber 13 | local setmetatable = setmetatable 14 | 15 | local _M = { _VERSION = "0.2" } 16 | local mt = { __index = _M } 17 | 18 | local DEFAULT_ITER_SIZE = 8192 19 | local STATE = util.STATE 20 | local HTTP10 = 10 21 | local HTTP11 = 11 22 | local HTTP20 = 20 23 | 24 | 25 | local function no_body(r) 26 | local status_code = r.status_code 27 | 28 | -- 1xx, 204 and 304 29 | if status_code < 200 30 | or status_code == 204 31 | or status_code == 304 then 32 | return true 33 | end 34 | 35 | -- HEAD 36 | if r.method == "HEAD" then 37 | return true 38 | end 39 | end 40 | 41 | 42 | local function process_headers(headers) 43 | for k, v in pairs(headers) do 44 | if is_tab(v) then 45 | headers[k] = concat(v, ",") 46 | end 47 | end 48 | 49 | return headers 50 | end 51 | 52 | 53 | local function iter_chunked(r, size) 54 | local chunk = r._chunk 55 | 56 | if chunk.leave then 57 | r._read_eof = true 58 | chunk.leave = false 59 | return "" 60 | end 61 | 62 | local adapter = r._adapter 63 | 64 | size = size or DEFAULT_ITER_SIZE 65 | 66 | adapter.state = STATE.RECV_BODY 67 | 68 | local t = new_tab(0, 4) 69 | local reader = chunk.reader 70 | 71 | while true do 72 | if chunk.rest == 0 then 73 | local size, err = reader() 74 | if not size then 75 | return nil, err 76 | end 77 | 78 | -- just ignore the chunk-extensions 79 | local ext = size:find(";", nil, true) 80 | if ext then 81 | size = size:sub(1, ext - 1) 82 | end 83 | 84 | size = tonumber(size, 16) 85 | if not size then 86 | return nil, "invalid chunk header" 87 | end 88 | 89 | chunk.size = size 90 | chunk.rest = size 91 | end 92 | 93 | -- end 94 | if chunk.size == 0 then 95 | chunk.leave = true 96 | 97 | -- read the last "\r\n" 98 | local dummy, err = reader() 99 | if dummy ~= "" then 100 | return nil, err or "invalid chunked data" 101 | end 102 | 103 | break 104 | end 105 | 106 | local read_size = size 107 | if read_size > chunk.rest then 108 | read_size = chunk.rest 109 | end 110 | 111 | local data, err = adapter:read(read_size) 112 | if err then 113 | return data, err 114 | end 115 | 116 | size = size - #data 117 | chunk.rest = chunk.rest - #data 118 | 119 | if chunk.rest == 0 then 120 | local dummy, err = reader() 121 | if dummy ~= "" then 122 | return nil, err or "invalid chunked data" 123 | end 124 | end 125 | 126 | insert(t, data) 127 | if size == 0 then 128 | break 129 | end 130 | end 131 | 132 | return concat(t, "") 133 | end 134 | 135 | 136 | local function iter_plain(r, size) 137 | local rest = r._rest 138 | local adapter = r._adapter 139 | 140 | adapter.state = STATE.RECV_BODY 141 | 142 | if rest == 0 then 143 | r._read_eof = true 144 | return "" 145 | end 146 | 147 | size = size or DEFAULT_ITER_SIZE 148 | 149 | if rest and rest < size then 150 | size = rest 151 | end 152 | 153 | local data, err = adapter:read(size) 154 | if err then 155 | return data, err 156 | end 157 | 158 | r._rest = rest - #data 159 | 160 | return data 161 | end 162 | 163 | 164 | local function iter_http2(r, size) 165 | local adapter = r._adapter 166 | adapter.state = STATE.RECV_BODY 167 | 168 | -- just a flag in the HTTP/2 case 169 | local rest = r._rest 170 | if rest == 0 then 171 | r._read_eof = true 172 | return "" 173 | end 174 | 175 | return adapter:read(size) 176 | end 177 | 178 | 179 | local function new(opts) 180 | local r = { 181 | url = opts.url, 182 | method = opts.method, 183 | status_line = opts.status_line, 184 | status_code = opts.status_code, 185 | http_version = opts.http_version, 186 | headers = opts.headers, 187 | request = opts.request, 188 | elapsed = opts.elapsed, 189 | content = nil, 190 | 191 | -- internal members 192 | _adapter = opts.adapter, 193 | _consumed = false, 194 | _chunk = nil, 195 | _rest = -1, 196 | _read_eof = false, 197 | _keepalive = false, 198 | _http_ver = HTTP10, 199 | } 200 | 201 | if r.http_version == "HTTP/2" then 202 | r._http_ver = HTTP20 203 | elseif r.http_version == "HTTP/1.1" then 204 | r._http_ver = HTTP11 205 | end 206 | 207 | if r._http_ver ~= HTTP20 then 208 | local chunk = r.headers["transfer-encoding"] 209 | if chunk and find(chunk, "chunked", nil, true) then 210 | r._chunk = { 211 | size = 0, -- current chunked header size 212 | rest = 0, -- rest part size in current chunked header 213 | leave = false, 214 | reader = r._adapter:reader("\r\n"), 215 | } 216 | else 217 | r._rest = tonumber(r.headers["content-length"]) 218 | if r._rest == 0 or no_body(r) then 219 | r._read_eof = true 220 | end 221 | end 222 | end 223 | 224 | local http_ver = r._http_ver 225 | local connection = r.headers["connection"] 226 | 227 | if connection == "keep-alive" 228 | or (not connection and http_ver == HTTP11) -- HTTP/1.1 default behavior 229 | or http_ver == HTTP20 230 | then 231 | r._keepalive = true 232 | end 233 | 234 | r.headers = process_headers(r.headers) 235 | 236 | return setmetatable(r, mt) 237 | end 238 | 239 | 240 | local function iter_content(r, size) 241 | if r._read_eof then 242 | return nil, "eof" 243 | end 244 | 245 | local adapter = r._adapter 246 | if adapter.state == STATE.CLOSE then 247 | return nil, "closed" 248 | end 249 | 250 | local data, err 251 | 252 | if r.http_version == "HTTP/2" then 253 | data, err = iter_http2(r, size) 254 | elseif r._chunk then 255 | data, err = iter_chunked(r, size) 256 | else 257 | data, err = iter_plain(r) 258 | end 259 | 260 | local error_filter = adapter.error_filter 261 | 262 | if err then 263 | if error_filter then 264 | error_filter(adapter.state, err) 265 | end 266 | 267 | adapter.state = STATE.CLOSE 268 | 269 | adapter:close(r._keepalive) 270 | 271 | return nil, err 272 | end 273 | 274 | return data 275 | end 276 | 277 | 278 | local function body(r) 279 | if r.consumed then 280 | return nil, "is consumed" 281 | end 282 | 283 | r.consumed = true 284 | 285 | local t = new_tab(8, 0) 286 | while true do 287 | local data, err = r:iter_content() 288 | if err then 289 | return nil, err 290 | end 291 | 292 | if data == "" then 293 | break 294 | end 295 | 296 | insert(t, data) 297 | end 298 | 299 | return concat(t, "") 300 | end 301 | 302 | 303 | local function json(r) 304 | local data, err = r:body() 305 | if not data then 306 | return nil, err 307 | end 308 | 309 | local content_type = r.headers["content-type"] 310 | if not content_type then 311 | return nil, "not json" 312 | end 313 | 314 | content_type = lower(content_type) 315 | if content_type ~= "application/json" 316 | and content_type ~= "application/json; charset=utf-8" 317 | then 318 | return nil, "not json" 319 | end 320 | 321 | return cjson.decode(data) 322 | end 323 | 324 | 325 | local function drop(r) 326 | while true do 327 | local chunk, err = r:iter_content(4096) 328 | if not chunk then 329 | return nil, err 330 | end 331 | 332 | if chunk == "" then 333 | return true 334 | end 335 | end 336 | end 337 | 338 | 339 | local function close(r) 340 | if r._keepalive then 341 | if not r._read_eof then 342 | local ok, err = r:drop() 343 | if not ok then 344 | return nil, err 345 | end 346 | end 347 | end 348 | 349 | local adapter = r._adapter 350 | return adapter:close(r._keepalive) 351 | end 352 | 353 | 354 | _M.new = new 355 | _M.close = close 356 | _M.iter_content = iter_content 357 | _M.body = body 358 | _M.drop = drop 359 | _M.json = json 360 | 361 | return _M 362 | -------------------------------------------------------------------------------- /lib/resty/requests/session.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Alex Zhang 2 | 3 | local util = require "resty.requests.util" 4 | local request = require "resty.requests.request" 5 | local adapter = require "resty.requests.adapter" 6 | 7 | local setmetatable = setmetatable 8 | local format = string.format 9 | local pairs = pairs 10 | local ngx_now = ngx.now 11 | 12 | local _M = { _VERSION = "0.2" } 13 | local mt = { __index = _M } 14 | local DEFAULT_TIMEOUTS = util.DEFAULT_TIMEOUTS 15 | local BUILTIN_HEADERS = util.BUILTIN_HEADERS 16 | local send_request 17 | 18 | local function new() 19 | local headers = util.dict(nil, 0, 8) 20 | 21 | for k, v in pairs(BUILTIN_HEADERS) do 22 | headers[k] = v 23 | end 24 | 25 | local self = { 26 | verify = false, 27 | reused_session = nil, 28 | server_name = nil, 29 | 30 | conn_timeout = DEFAULT_TIMEOUTS[1], 31 | read_timeout = DEFAULT_TIMEOUTS[2], 32 | send_timeout = DEFAULT_TIMEOUTS[3], 33 | 34 | version = "HTTP/1.1", 35 | headers = headers, 36 | stream = true, 37 | proxies = nil, 38 | allow_redirects = false, 39 | redirect_max_times = 10, 40 | error_filter = nil, 41 | auth = nil, 42 | hooks = nil, 43 | 44 | adapters = nil, 45 | 46 | redirects = 0, 47 | } 48 | 49 | return setmetatable(self, mt) 50 | end 51 | 52 | 53 | local function mount(self, scheme, ap) 54 | if not self.adapters then 55 | self.adapters = { 56 | [scheme] = ap 57 | } 58 | 59 | return 60 | end 61 | 62 | -- now we only support one adapter for a specific scheme 63 | self.adapters[scheme] = ap 64 | end 65 | 66 | 67 | local function rebuild_method(status_code, method) 68 | if status_code == 303 and method ~= "HEAD" then 69 | return "GET" 70 | end 71 | 72 | -- respects Python Requests 73 | if status_code == 302 and method ~= "HEAD" then 74 | return "GET" 75 | end 76 | 77 | if status_code == 301 and method == "POST" then 78 | return "GET" 79 | end 80 | 81 | return method 82 | end 83 | 84 | 85 | local function resolve_redirects(self, old_req, old_resp) 86 | if not self.allow_redirects then 87 | return old_resp 88 | end 89 | 90 | local status_code = old_resp.status_code 91 | if status_code ~= 301 92 | and status_code ~= 302 93 | and status_code ~= 303 94 | and status_code ~= 307 95 | and status_code ~= 308 96 | then 97 | return old_resp 98 | end 99 | 100 | if self.redirct_max_times <= self.redirects then 101 | self.redirects = 0 102 | return old_resp 103 | end 104 | 105 | local url = old_resp.headers["Location"] 106 | if not url then 107 | return old_resp 108 | end 109 | 110 | self.redirects = self.redirects + 1 111 | 112 | -- process relative location 113 | if url:byte(1, 1) == ("/"):byte(1, 1) then 114 | url = format("%s://%s:%s%s", old_req.scheme, old_req.host, 115 | old_req.port, url) 116 | end 117 | 118 | local new_method = rebuild_method(status_code, old_resp.method) 119 | -- we don't read the body (non-stream) by ourselves since caller may use it 120 | return send_request(self, new_method, url, self.opts) 121 | end 122 | 123 | 124 | local function merge_settings(self, config) 125 | if config.ssl then 126 | if config.ssl.verify then 127 | self.verify = true 128 | end 129 | 130 | if config.ssl.server_name then 131 | self.server_name = config.ssl.server_name 132 | end 133 | end 134 | 135 | if config.version then 136 | self.version = config.version 137 | end 138 | 139 | if config.allow_redirects then 140 | self.allow_redirects = true 141 | self.redirct_max_times = config.redirect_max_times 142 | end 143 | 144 | if config.error_filter then 145 | self.error_filter = config.error_filter 146 | end 147 | 148 | if config.proxies then 149 | self.proxies = config.proxies 150 | end 151 | 152 | if config.hooks then 153 | self.hooks = config.hooks 154 | end 155 | 156 | if config.auth then 157 | self.auth = config.auth 158 | end 159 | 160 | if config.cookie then 161 | self.cookie = config.cookie 162 | end 163 | 164 | if config.headers then 165 | for k, v in pairs(config.headers) do 166 | self.headers[k] = v 167 | end 168 | end 169 | 170 | if config.hooks then 171 | self.hooks = config.hooks 172 | end 173 | 174 | local timeouts = config.timeouts 175 | if timeouts then 176 | self.conn_timeout = timeouts[1] 177 | self.send_timeout = timeouts[2] 178 | self.read_timeout = timeouts[3] 179 | end 180 | 181 | local stream = config.stream 182 | if stream ~= nil then 183 | self.stream = stream 184 | else 185 | self.stream = true 186 | end 187 | end 188 | 189 | 190 | send_request = function(self, method, url, opts) 191 | local config = util.set_config(opts) 192 | merge_settings(self, config) 193 | 194 | local req, err = request.new(method, url, self, config) 195 | if not req then 196 | return nil, err 197 | end 198 | 199 | local scheme = req.scheme 200 | local ap 201 | 202 | if self.adapters and self.adapters[scheme] then 203 | ap = self.adapters[scheme] 204 | else 205 | ap = adapter.new(self) 206 | 207 | self.adapters = { 208 | [scheme] = ap 209 | } 210 | end 211 | 212 | local r, err = ap:send(req) 213 | if not r then 214 | return nil, err 215 | end 216 | 217 | if self.hooks then 218 | self.hooks.response(r) 219 | end 220 | 221 | if not self.stream then 222 | local now = ngx_now() 223 | r.content = r:body() 224 | r.elapsed.read_body = ngx_now() - now 225 | end 226 | 227 | local new_resp, err = resolve_redirects(self, req, r) 228 | if not new_resp then 229 | return nil, err 230 | end 231 | 232 | -- TODO add the historic requests to r.history 233 | 234 | return new_resp 235 | end 236 | 237 | 238 | local function get(self, url, opts) 239 | return self:request("GET", url, opts) 240 | end 241 | 242 | 243 | local function head(self, url, opts) 244 | return self:request("HEAD", url, opts) 245 | end 246 | 247 | 248 | local function post(self, url, opts) 249 | return self:request("POST", url, opts) 250 | end 251 | 252 | 253 | local function put(self, url, opts) 254 | return self:request("PUT", url, opts) 255 | end 256 | 257 | 258 | local function delete(self, url, opts) 259 | return self:request("DELETE", url, opts) 260 | end 261 | 262 | 263 | local function options(self, url, opts) 264 | return self:request("OPTIONS", url, opts) 265 | end 266 | 267 | 268 | local function patch(self, url, opts) 269 | return self:request("PATCH", url, opts) 270 | end 271 | 272 | 273 | _M.new = new 274 | _M.request = send_request 275 | _M.get = get 276 | _M.head = head 277 | _M.post = post 278 | _M.put = put 279 | _M.delete = delete 280 | _M.options = options 281 | _M.patch = patch 282 | _M.mount = mount 283 | 284 | return _M 285 | -------------------------------------------------------------------------------- /lib/resty/requests/util.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Alex Zhang 2 | 3 | local type = type 4 | local pcall = pcall 5 | local pairs = pairs 6 | local error = error 7 | local rawget = rawget 8 | local setmetatable = setmetatable 9 | local lower = string.lower 10 | local ngx_gsub = ngx.re.gsub 11 | local base64 = ngx.encode_base64 12 | 13 | local _M = { _VERSION = '0.1' } 14 | 15 | local header_mt = { 16 | __index = function(t, k) 17 | local name, _, err = ngx_gsub(lower(k), "_", "-", "jo") 18 | if err then 19 | error(err) 20 | end 21 | 22 | return rawget(t, name) 23 | end 24 | } 25 | 26 | local ok, new_tab = pcall(require, "table.new") 27 | if not ok then 28 | new_tab = function(narr, nrec) 29 | return {} 30 | end 31 | end 32 | 33 | local BUILTIN_HEADERS = { 34 | ["accept"] = "*/*", 35 | ["user-agent"] = "resty-requests", 36 | } 37 | 38 | local STATE = { 39 | UNREADY = -1, 40 | READY = 0, 41 | CONNECT = 1, 42 | PROXY = 2, 43 | HANDSHAKE = 3, 44 | SEND_HEADER = 4, 45 | SEND_BODY = 5, 46 | RECV_HEADER = 6, 47 | RECV_BODY = 7, 48 | CLOSE = 8, 49 | } 50 | 51 | local STATE_NAME = { 52 | [STATE.UNREADY] = "unready", 53 | [STATE.READY] = "ready", 54 | [STATE.CONNECT] = "connect", 55 | [STATE.HANDSHAKE] = "handshake", 56 | [STATE.SEND_HEADER] = "send_header", 57 | [STATE.SEND_BODY] = "send_body", 58 | [STATE.RECV_HEADER] = "recv_header", 59 | [STATE.RECV_BODY] = "recv_body", 60 | [STATE.CLOSE] = "close", 61 | } 62 | 63 | local HTTP10 = "HTTP/1.0" 64 | local HTTP11 = "HTTP/1.1" 65 | local HTTP2 = "HTTP/2" 66 | 67 | local DEFAULT_TIMEOUTS = { 10 * 1000, 30 * 1000, 60 * 1000 } 68 | 69 | local function is_str(obj) return type(obj) == "string" end 70 | local function is_num(obj) return type(obj) == "number" end 71 | local function is_tab(obj) return type(obj) == "table" end 72 | local function is_func(obj) return type(obj) == "function" end 73 | 74 | 75 | local function dict(d, narr, nrec) 76 | if not d then 77 | d = new_tab(narr, nrec) 78 | end 79 | 80 | return setmetatable(d, header_mt) 81 | end 82 | 83 | 84 | local function basic_auth(user, pass) 85 | local token = base64(("%s:%s"):format(user, pass)) 86 | return ("Basic %s"):format(token) 87 | end 88 | 89 | 90 | local function set_config(opts) 91 | opts = opts or {} 92 | local config = new_tab(0, 14) 93 | 94 | -- 1) timeouts 95 | local timeouts = opts.timeouts 96 | if not is_tab(timeouts) then 97 | config.timeouts = DEFAULT_TIMEOUTS 98 | else 99 | config.timeouts = timeouts 100 | end 101 | 102 | -- 2) http version 103 | if opts.http10 then 104 | config.version = HTTP10 105 | elseif opts.http2 then 106 | config.version = HTTP2 107 | else 108 | config.version = HTTP11 109 | end 110 | 111 | -- 3) request headers 112 | config.headers = dict(nil, 0, 5) 113 | 114 | if opts.headers then 115 | for k, v in pairs(opts.headers) do 116 | local name, _, err = ngx_gsub(lower(k), "_", "-", "jo") 117 | if err then 118 | error(err) 119 | end 120 | 121 | config.headers[name] = v 122 | end 123 | end 124 | 125 | for k, v in pairs(BUILTIN_HEADERS) do 126 | if not config.headers[k] then 127 | config.headers[k] = v 128 | end 129 | end 130 | 131 | -- 4) body 132 | config.body = opts.body 133 | 134 | -- 5) ssl verify 135 | config.ssl = opts.ssl 136 | 137 | -- 6) redirect 138 | config.allow_redirects = opts.allow_redirects 139 | if config.allow_redirects then 140 | config.redirect_max_times = opts.redirect_max_times or 10 141 | if config.redirect_max_times < 1 then 142 | config.redirect_max_times = 1 143 | end 144 | end 145 | 146 | -- 7) erorr filter 147 | config.error_filter = opts.error_filter 148 | 149 | -- 8) proxies 150 | config.proxies = opts.proxies 151 | 152 | -- 9) auth 153 | local auth = opts.auth 154 | if auth then 155 | if is_str(auth) then 156 | config.auth = auth 157 | else 158 | config.auth = basic_auth(auth.user, auth.pass) 159 | end 160 | end 161 | 162 | -- 10) cookie 163 | local cookie = opts.cookie 164 | if cookie then 165 | config.cookie = cookie 166 | end 167 | 168 | -- 11) json 169 | local json = opts.json 170 | if json then 171 | config.json = json 172 | end 173 | 174 | -- 12) event hooks 175 | local hooks = opts.hooks 176 | if hooks then 177 | config.hooks = hooks 178 | end 179 | 180 | -- 13) stream 181 | local stream = opts.stream 182 | if stream ~= nil then 183 | config.stream = stream and true or false 184 | else 185 | config.stream = true 186 | end 187 | 188 | -- 14) use_default_type 189 | config.use_default_type = opts.use_default_type ~= false 190 | 191 | return config 192 | end 193 | 194 | 195 | _M.new_tab = new_tab 196 | _M.is_str = is_str 197 | _M.is_num = is_num 198 | _M.is_tab = is_tab 199 | _M.is_func = is_func 200 | _M.set_config = set_config 201 | _M.dict = dict 202 | _M.basic_auth = basic_auth 203 | _M.DEFAULT_TIMEOUTS = DEFAULT_TIMEOUTS 204 | _M.BUILTIN_HEADERS = BUILTIN_HEADERS 205 | _M.STATE = STATE 206 | _M.STATE_NAME = STATE_NAME 207 | _M.HTTP10 = HTTP10 208 | _M.HTTP11 = HTTP11 209 | 210 | return _M 211 | -------------------------------------------------------------------------------- /lua-resty-requests-0.7.3-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-requests" 2 | version = "0.7.3-1" 3 | 4 | source = { 5 | url = "git://github.com/tokers/lua-resty-requests", 6 | tag = "v0.7.3", 7 | } 8 | 9 | description = { 10 | summary = "Yet Another HTTP library for OpenResty", 11 | detailed = [[ 12 | HTTP library for Humans. 13 | ]], 14 | license = "2-clause BSD", 15 | homepage = "https://github.com/tokers/lua-resty-requests", 16 | maintainer = "Alex Zhang ", 17 | } 18 | 19 | dependencies = { 20 | "lua >= 5.1", 21 | "lua-resty-socket == 1.0.0", 22 | } 23 | 24 | build = { 25 | type = "builtin", 26 | modules = { 27 | ["resty.requests"] = "lib/resty/requests.lua", 28 | ["resty.requests.adapter"] = "lib/resty/requests/adapter.lua", 29 | ["resty.requests.request"] = "lib/resty/requests/request.lua", 30 | ["resty.requests.response"] = "lib/resty/requests/response.lua", 31 | ["resty.requests.session"] = "lib/resty/requests/session.lua", 32 | ["resty.requests.util"] = "lib/resty/requests/util.lua", 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /t/00-sanity.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | my $pwd = cwd(); 5 | 6 | sub read_file { 7 | my $infile = shift; 8 | open my $in, $infile 9 | or die "cannot open $infile for reading: $!"; 10 | my $cert = do { local $/; <$in> }; 11 | close $in; 12 | $cert; 13 | } 14 | 15 | $ENV{TEST_NGINX_PWD} ||= $pwd; 16 | our $TestCertificate = read_file("t/ssl/tokers.crt"); 17 | our $TestCertificateKey = read_file("t/ssl/tokers.key"); 18 | repeat_each(3); 19 | plan tests => repeat_each() * (blocks() * 3 + 1); 20 | 21 | our $http_config = << 'EOC'; 22 | lua_package_path "lib/?.lua;;"; 23 | 24 | server { 25 | listen 10088; 26 | 27 | location = /t1 { 28 | content_by_lua_block { 29 | ngx.print(ngx.req.raw_header()) 30 | } 31 | } 32 | 33 | location = /t2 { 34 | chunked_transfer_encoding off; 35 | content_by_lua_block { 36 | local data = ngx.req.raw_header() 37 | ngx.header["Content-Length"] = #data 38 | ngx.print(data) 39 | } 40 | } 41 | 42 | location = /t3 { 43 | content_by_lua_block { 44 | ngx.req.read_body() 45 | ngx.print(ngx.req.raw_header()) 46 | ngx.print(ngx.req.get_body_data()) 47 | } 48 | } 49 | 50 | location = /t4 { 51 | client_body_in_single_buffer on; 52 | client_body_buffer_size 16k; 53 | lua_need_request_body on; 54 | content_by_lua_block { 55 | ngx.header["X-Request-Content-Length"] = ngx.var.http_content_length 56 | local data = ngx.req.get_body_data() 57 | local data_len = #data 58 | local now = 1 59 | local rest = data_len 60 | local send_count = math.random(1, data_len) 61 | for i = 1, send_count do 62 | local len = math.random(1, rest - (send_count - i)) 63 | rest = rest - len 64 | ngx.print(data:sub(now, now + len - 1)) 65 | now = now + len 66 | end 67 | } 68 | } 69 | 70 | location = /t5 { 71 | content_by_lua_block { 72 | ngx.status = 200 73 | ngx.header["X-AAA"] = {"a", "b", "c"} 74 | ngx.exit(200) 75 | } 76 | } 77 | 78 | location = /t6 { 79 | lua_need_request_body on; 80 | content_by_lua_block { 81 | ngx.status = 200 82 | local arg = ngx.req.get_post_args() 83 | ngx.say(arg.name, " ", arg.pass, " ", arg.token) 84 | ngx.say(ngx.var.http_content_type) 85 | } 86 | } 87 | 88 | location = /t7 { 89 | return 200 "Hello World"; 90 | } 91 | 92 | location = /t8 { 93 | content_by_lua_block { 94 | for i = 1, 256 do 95 | local str = "" 96 | for j = 33, 100 do 97 | str = str .. string.char(j) 98 | ngx.print(str) 99 | end 100 | end 101 | } 102 | } 103 | 104 | location = /t9 { 105 | content_by_lua_block { 106 | ngx.sleep(1) 107 | ngx.status = 200 108 | ngx.say("hello world") 109 | ngx.say("after 1s") 110 | } 111 | } 112 | 113 | location = /t10 { 114 | client_body_in_single_buffer on; 115 | client_body_buffer_size 1m; 116 | client_max_body_size 1m; 117 | content_by_lua_block { 118 | local ok = ngx.var.arg_ok 119 | if ok == "true" then 120 | ngx.req.read_body() 121 | ngx.print(ngx.req.get_body_data()) 122 | return ngx.exit(ngx.status) 123 | else 124 | ngx.print("no no no") 125 | ngx.flush(true) 126 | end 127 | } 128 | } 129 | 130 | location = /t11 { 131 | content_by_lua_block { 132 | local t = {} 133 | for i = 1, 1024 do 134 | t[i] = "abbbbasdj" 135 | end 136 | 137 | ngx.print(t) 138 | } 139 | } 140 | 141 | location = /t12 { 142 | content_by_lua_block { 143 | ngx.header["Content-Type"] = ngx.var.arg_content_type 144 | ngx.print("{\"a\": 1, \"b\": 2}") 145 | } 146 | } 147 | } 148 | EOC 149 | 150 | no_long_string(); 151 | run_tests(); 152 | 153 | __DATA__ 154 | 155 | 156 | === TEST 1: normal GET request 157 | 158 | --- http_config eval: $::http_config 159 | 160 | --- config 161 | location = /t { 162 | content_by_lua_block { 163 | local requests = require "resty.requests" 164 | local url = "http://127.0.0.1:10088/t1?foo=bar&c=" 165 | 166 | local r, err = requests.get(url) 167 | if not r then 168 | ngx.log(ngx.ERR, err) 169 | end 170 | 171 | local body, err = r:body() 172 | if err then 173 | ngx.log(ngx.ERR, err) 174 | end 175 | 176 | ngx.print(body) 177 | } 178 | } 179 | 180 | --- request 181 | GET /t 182 | 183 | --- response_body eval 184 | qq{GET /t1?foo=bar&c= HTTP/1.1\r 185 | host: 127.0.0.1\r 186 | user-agent: resty-requests\r 187 | accept: */*\r 188 | connection: keep-alive\r 189 | \r 190 | }; 191 | 192 | --- no_error_log 193 | [error] 194 | 195 | 196 | === TEST 2: normal GET request with iterating body 197 | 198 | --- http_config eval: $::http_config 199 | 200 | --- config 201 | location = /t { 202 | content_by_lua_block { 203 | local requests = require "resty.requests" 204 | local url = "http://127.0.0.1:10088/t1" 205 | local headers = { 206 | ["cache-control"] = "max-age=0" 207 | } 208 | 209 | local opts = { 210 | headers = headers 211 | } 212 | 213 | local r, err = requests.get(url, opts) 214 | if not r then 215 | ngx.log(ngx.ERR, "error") 216 | end 217 | 218 | while true do 219 | local data, err = r:iter_content(1) 220 | if err then 221 | ngx.log(ngx.ERR, err) 222 | return 223 | end 224 | 225 | ngx.print(data) 226 | if data == "" then 227 | break 228 | end 229 | end 230 | } 231 | } 232 | 233 | --- request 234 | GET /t 235 | 236 | --- response_body eval 237 | qq{GET /t1 HTTP/1.1\r 238 | cache-control: max-age=0\r 239 | user-agent: resty-requests\r 240 | accept: */*\r 241 | connection: keep-alive\r 242 | host: 127.0.0.1\r 243 | \r 244 | } 245 | 246 | --- no_error_log 247 | [error] 248 | 249 | 250 | === TEST 3: normal GET request with body 251 | 252 | --- http_config eval: $::http_config 253 | --- config 254 | location /t1 { 255 | content_by_lua_block { 256 | local req_data = "你好吗?Hello?" 257 | local requests = require "resty.requests" 258 | local url = "http://127.0.0.1:10088/t3?usebody=true&af=b" 259 | local headers = { 260 | ["content-length"] = #req_data 261 | } 262 | 263 | local opts = { 264 | headers = headers, 265 | body = req_data 266 | } 267 | 268 | local r, err = requests.get(url, opts) 269 | if not r then 270 | ngx.log(ngx.ERR, err) 271 | end 272 | 273 | ngx.print(r:body()) 274 | } 275 | } 276 | 277 | --- request 278 | GET /t1 279 | 280 | --- status_code: 200 281 | --- response_body eval 282 | qq{GET /t3?usebody=true&af=b HTTP/1.1\r 283 | host: 127.0.0.1\r 284 | content-length: 18\r 285 | user-agent: resty-requests\r 286 | accept: */*\r 287 | connection: keep-alive\r 288 | content-type: text/plain\r 289 | \r 290 | 你好吗?Hello?} 291 | 292 | --- no_error_log 293 | [error] 294 | 295 | 296 | === TEST 4: the normal GET request with body(function) 297 | 298 | --- http_config eval: $::http_config 299 | 300 | --- config 301 | location /t1 { 302 | content_by_lua_block { 303 | local requests = require "resty.requests" 304 | local url = "http://127.0.0.1:10088/t3" 305 | local size = 20 306 | local opts = { 307 | body = function() 308 | if size == 0 then 309 | return "" 310 | end 311 | 312 | local r = "hello" 313 | size = size - 5 314 | return r 315 | end 316 | } 317 | 318 | local r, err = requests.get(url, opts) 319 | if not r then 320 | ngx.log(ngx.ERR, err) 321 | end 322 | 323 | ngx.print(r:body()) 324 | } 325 | } 326 | 327 | --- request 328 | GET /t1 329 | 330 | --- response_body eval 331 | qq{GET /t3 HTTP/1.1\r 332 | host: 127.0.0.1\r 333 | content-type: application/octet-stream\r 334 | user-agent: resty-requests\r 335 | accept: */*\r 336 | connection: keep-alive\r 337 | transfer-encoding: chunked\r 338 | \r 339 | hellohellohellohello} 340 | 341 | --- no_error_log 342 | [error] 343 | 344 | 345 | 346 | === TEST 5: normal GET request with bulk body 347 | 348 | --- http_config eval: $::http_config 349 | --- config 350 | location /t1 { 351 | content_by_lua_block { 352 | local req_data_len = math.random(1024, 8192) 353 | local tab = {} 354 | for i = 1, req_data_len do 355 | local c = string.char(math.random(32, 127)) 356 | table.insert(tab, c) 357 | end 358 | 359 | local req_body = table.concat(tab) 360 | 361 | local requests = require "resty.requests" 362 | local url = "http://127.0.0.1:10088/t4?" 363 | local headers = { 364 | ["content-length"] = req_data_len 365 | } 366 | 367 | local opts = { 368 | headers = headers, 369 | body = req_body 370 | } 371 | 372 | local r, err = requests.get(url, opts) 373 | if not r then 374 | ngx.log(ngx.ERR, err) 375 | end 376 | 377 | local body = r:body() 378 | 379 | if body == req_body then 380 | ngx.say("OK") 381 | else 382 | ngx.say("FAILURE") 383 | end 384 | } 385 | } 386 | 387 | --- request 388 | GET /t1 389 | 390 | --- status_code: 200 391 | --- response_body 392 | OK 393 | 394 | --- no_error_log 395 | [error] 396 | 397 | 398 | === TEST 6: the normal GET request with bulk body(function) 399 | 400 | --- http_config eval: $::http_config 401 | 402 | --- config 403 | location /t1 { 404 | content_by_lua_block { 405 | local req_data_len = math.random(1024, 8192) 406 | 407 | local gt = {} 408 | 409 | local get_body = function() 410 | if req_data_len == 0 then 411 | return "" 412 | end 413 | 414 | local len = math.random(1, req_data_len) 415 | 416 | local tab = {} 417 | for i = 1, len do 418 | local c = string.char(math.random(32, 127)) 419 | table.insert(tab, c) 420 | table.insert(gt, c) 421 | end 422 | 423 | req_data_len = req_data_len - len 424 | 425 | return table.concat(tab) 426 | end 427 | 428 | local requests = require "resty.requests" 429 | local url = "http://127.0.0.1:10088/t4?" 430 | 431 | local opts = { 432 | body = get_body, 433 | } 434 | 435 | local r, err = requests.get(url, opts) 436 | if not r then 437 | ngx.log(ngx.ERR, err) 438 | return 439 | end 440 | 441 | local body = r:body() 442 | local req_length = r.headers["x-req-Content-Length"] 443 | 444 | if not req_length and body == table.concat(gt) then 445 | ngx.say("OK") 446 | else 447 | ngx.say("FAILURE") 448 | end 449 | } 450 | } 451 | 452 | --- request 453 | GET /t1 454 | 455 | --- response_body 456 | OK 457 | 458 | --- no_error_log 459 | [error] 460 | 461 | 462 | === TEST 7: GET request with duplicate response headers 463 | 464 | --- http_config eval: $::http_config 465 | 466 | --- config 467 | location /t1 { 468 | content_by_lua_block { 469 | local requests = require "resty.requests" 470 | local url = "http://127.0.0.1:10088/t5" 471 | local r, err = requests.get(url) 472 | if not r then 473 | ngx.log(ngx.ERR, err) 474 | return 475 | end 476 | 477 | ngx.print(r.headers["x-aaa"]) 478 | 479 | r:close() 480 | } 481 | } 482 | 483 | --- request 484 | GET /t1 485 | 486 | --- response_body: a,b,c 487 | 488 | --- no_error_log 489 | [error] 490 | 491 | 492 | === TEST 8: event hooks 493 | 494 | --- http_config eval: $::http_config 495 | 496 | --- config 497 | location /t1 { 498 | content_by_lua_block { 499 | local requests = require "resty.requests" 500 | local url = "http://127.0.0.1:10088/t1?test=event_hook" 501 | local hook = function(r) 502 | ngx.print(r:body()) 503 | ngx.log(ngx.WARN, "event hook") 504 | end 505 | 506 | local opts = { 507 | hooks = { 508 | response = hook 509 | } 510 | } 511 | 512 | local r, err = requests.get(url, opts) 513 | if not r then 514 | ngx.log(ngx.ERR, err) 515 | return 516 | end 517 | 518 | r:close() 519 | } 520 | } 521 | 522 | --- request 523 | GET /t1 524 | 525 | --- response_body eval 526 | qq{GET /t1?test=event_hook HTTP/1.1\r 527 | host: 127.0.0.1\r 528 | user-agent: resty-requests\r 529 | accept: */*\r 530 | connection: keep-alive\r 531 | \r 532 | }; 533 | 534 | --- no_error_log 535 | [error] 536 | 537 | --- grep_error_log: event hook 538 | --- grep_error_log_out 539 | event hook 540 | 541 | 542 | === TEST 9: POST args 543 | 544 | --- http_config eval: $::http_config 545 | 546 | --- config 547 | location /t1 { 548 | content_by_lua_block { 549 | local requests = require "resty.requests" 550 | local url = "http://127.0.0.1:10088/t6" 551 | 552 | local body = { 553 | name = "alex", 554 | pass = "123456", 555 | token = "@3~j09PcXa398-", 556 | } 557 | 558 | local opts = { 559 | body = body 560 | } 561 | 562 | local r, err = requests.get(url, opts) 563 | if not r then 564 | ngx.log(ngx.ERR, err) 565 | return 566 | end 567 | 568 | local body = r:body() 569 | ngx.print(body) 570 | 571 | r:close() 572 | } 573 | } 574 | 575 | --- request 576 | GET /t1 577 | 578 | --- response_body 579 | alex 123456 @3~j09PcXa398- 580 | application/x-www-form-urlencoded 581 | 582 | --- no_error_log 583 | [error] 584 | 585 | 586 | === TEST 10: duplicate response body reading 587 | 588 | --- http_config eval: $::http_config 589 | 590 | --- config 591 | location /t1 { 592 | content_by_lua_block { 593 | local requests = require "resty.requests" 594 | local url = "http://127.0.0.1:10088/t7" 595 | 596 | local r, err = requests.get(url) 597 | if not r then 598 | ngx.log(ngx.ERR, err) 599 | return 600 | end 601 | 602 | local body = r:body() 603 | ngx.say(body) 604 | 605 | local body, err = r:body() 606 | ngx.say(err) 607 | 608 | r:close() 609 | } 610 | } 611 | 612 | --- request 613 | GET /t1 614 | 615 | --- response_body 616 | Hello World 617 | is consumed 618 | 619 | --- no_error_log 620 | [error] 621 | 622 | 623 | === TEST 11: request elapsed time 624 | 625 | --- http_config eval: $::http_config 626 | 627 | --- config 628 | location /t1 { 629 | lua_socket_send_timeout 10s; 630 | lua_socket_read_timeout 10s; 631 | content_by_lua_block { 632 | local requests = require "resty.requests" 633 | local url = "http://127.0.0.1:10088/t9" 634 | local opts = { 635 | timeouts = { 636 | 10 * 1000, 637 | 30 * 1000, 638 | 60 * 1000, 639 | } 640 | } 641 | 642 | local r, err = requests.get(url, opts) 643 | if not r then 644 | ngx.log(ngx.ERR, err) 645 | return 646 | end 647 | 648 | local body = r:body() 649 | ngx.print(body) 650 | 651 | if r.elapsed.connect > 1 then 652 | ngx.log(ngx.ERR, "connect time considerably large") 653 | end 654 | 655 | if r.elapsed.handshake ~= 0 then 656 | ngx.log(ngx.ERR, "what's up, we don't do the SSL/TLS handshake") 657 | end 658 | 659 | if r.elapsed.send_header > 1 then 660 | ngx.log(ngx.ERR, "send header time considerably large") 661 | end 662 | 663 | if r.elapsed.send_body ~= 0 then 664 | ngx.log(ngx.ERR, "what's up, we don't send the HTTP request body") 665 | end 666 | 667 | if r.elapsed.read_header < 1 then 668 | ngx.log(ngx.ERR, "we really postpone the header sending for 3s") 669 | end 670 | 671 | if r.elapsed.read_header > 2 then 672 | ngx.log(ngx.ERR, "read header time considerably large") 673 | end 674 | 675 | if r.elapsed.read_body ~= nil then 676 | ngx.log(ngx.ERR, "r.elapsed.read_body makes sense only ", 677 | "under non-stream mode") 678 | end 679 | 680 | local ttfb = r.elapsed.ttfb 681 | if ttfb < 1 or ttfb > 2 then 682 | ngx.log(ngx.ERR, "weird time to first byte") 683 | end 684 | 685 | local ok, err = r:close() 686 | if not ok then 687 | ngx.log(ngx.ERR, "failed to close r: ", err) 688 | end 689 | } 690 | } 691 | 692 | --- request 693 | GET /t1 694 | 695 | --- response_body 696 | hello world 697 | after 1s 698 | 699 | --- wait: 1 700 | 701 | --- no_error_log 702 | [error] 703 | 704 | 705 | === TEST 12: non-stream mode 706 | 707 | --- http_config eval: $::http_config 708 | 709 | --- config 710 | location /t1 { 711 | content_by_lua_block { 712 | local requests = require "resty.requests" 713 | local url = "http://127.0.0.1:10088/t7" 714 | local opts = { 715 | stream = false, 716 | } 717 | 718 | local r, err = requests.get(url, opts) 719 | if not r then 720 | ngx.log(ngx.ERR, err) 721 | return 722 | end 723 | 724 | ngx.say(r.content) 725 | local data, err = r:body() 726 | if err == nil then 727 | ngx.log(ngx.ERR, "cannot read data continuously") 728 | end 729 | 730 | if r.elapsed.read_body == nil then 731 | ngx.log(ngx.ERR, "unknown read body time") 732 | end 733 | 734 | local ok, err = r:close() 735 | if not ok then 736 | ngx.log(ngx.ERR, err) 737 | end 738 | } 739 | } 740 | 741 | --- request 742 | GET /t1 743 | 744 | --- response_body 745 | Hello World 746 | 747 | --- no_error_log 748 | [error] 749 | 750 | 751 | === TEST 13: send request body with Expect header 752 | 753 | --- http_config eval: $::http_config 754 | 755 | --- config 756 | location = /t1 { 757 | content_by_lua_block { 758 | local requests = require "resty.requests" 759 | local url = "http://127.0.0.1:10088/t10?ok=true" 760 | local opts = { 761 | stream = false, 762 | headers = { 763 | Expect = "100-Continue", 764 | }, 765 | 766 | body = "你好世界你好世界", 767 | } 768 | 769 | local r, err = requests.post(url, opts) 770 | if not r then 771 | ngx.log(ngx.ERR, err) 772 | return 773 | end 774 | 775 | ngx.print(r.content) 776 | 777 | opts.headers["Expect"] = "100-continue" 778 | local r, err = requests.post(url, opts) 779 | if not r then 780 | ngx.log(ngx.ERR, err) 781 | return 782 | end 783 | 784 | ngx.print(r.content) 785 | 786 | local r, err = requests.post("http://127.0.0.1:10088/t10", opts) 787 | assert(r == nil) 788 | ngx.log(ngx.ERR, err) 789 | } 790 | } 791 | 792 | --- request 793 | GET /t1 794 | 795 | --- response_body: 你好世界你好世界你好世界你好世界 796 | 797 | --- grep_error_log: invalid 100-continue response 798 | --- grep_error_log_out 799 | invalid 100-continue response 800 | 801 | 802 | === TEST 14: read chunked body 803 | 804 | --- http_config eval: $::http_config 805 | 806 | --- config 807 | location = /t1 { 808 | content_by_lua_block { 809 | local requests = require "resty.requests" 810 | local url = "http://127.0.0.1:10088/t8" 811 | local r, err = requests.post(url) 812 | if not r then 813 | ngx.log(ngx.ERR, err) 814 | return 815 | end 816 | 817 | local t = {} 818 | local size = 1 819 | 820 | while true do 821 | local data, err = r:iter_content(size) 822 | if not data then 823 | ngx.log(ngx.ERR, err) 824 | return 825 | end 826 | 827 | size = (size + 1) % 20 + 1 828 | 829 | if data == "" then 830 | break 831 | end 832 | 833 | t[#t + 1] = data 834 | end 835 | 836 | local single = {} 837 | local part = "" 838 | for i = 33, 100 do 839 | single[#single + 1] = string.char(i) 840 | part = part .. table.concat(single) 841 | end 842 | 843 | local raw = table.concat(t) 844 | 845 | if raw == string.rep(part, 256) then 846 | ngx.print("OK") 847 | else 848 | ngx.print("FAILURE") 849 | end 850 | } 851 | } 852 | --- request 853 | GET /t1 854 | 855 | --- response_body: OK 856 | 857 | --- no_error_log 858 | [error] 859 | 860 | 861 | === TEST 15: ssl handshake 862 | 863 | --- http_config 864 | lua_package_path "lib/?.lua;;"; 865 | server { 866 | listen 10089 ssl; 867 | server_name tokers.com; 868 | ssl_certificate ../html/tokers.crt; 869 | ssl_certificate_key ../html/tokers.key; 870 | 871 | return 200 yes; 872 | } 873 | 874 | --- config 875 | location = /t1 { 876 | content_by_lua_block { 877 | local requests = require "resty.requests" 878 | local url = "https://127.0.0.1:10089/t8" 879 | local r, err = requests.get(url) 880 | if not r then 881 | ngx.log(ngx.ERR, err) 882 | return 883 | end 884 | 885 | ngx.print(r:body()) 886 | } 887 | } 888 | 889 | --- user_files eval 890 | ">>> tokers.key 891 | $::TestCertificateKey 892 | >>> tokers.crt 893 | $::TestCertificate" 894 | 895 | --- request 896 | GET /t1 897 | 898 | --- response_body: yes 899 | --- no_error_log 900 | [error] 901 | 902 | 903 | === TEST 16: ssl handshake failed 904 | 905 | --- http_config eval: $::http_config 906 | 907 | --- config 908 | location = /t1 { 909 | content_by_lua_block { 910 | local requests = require "resty.requests" 911 | local url = "https://127.0.0.1:10088/t8" 912 | 913 | local filter = function(state, err) 914 | ngx.print(requests.state(state), ":", err) 915 | end 916 | 917 | local r, err = requests.get(url, { error_filter = filter }) 918 | 919 | if r then 920 | ngx.print("incorrect result") 921 | return 922 | end 923 | 924 | ngx.log(ngx.ERR, err) 925 | } 926 | } 927 | 928 | --- request 929 | GET /t1 930 | 931 | --- response_body: handshake:handshake failed 932 | --- grep_error_log: handshake failed 933 | --- grep_error_log_out 934 | handshake failed 935 | 936 | 937 | === TEST 17: ssl handshake failed since the certificate verify 938 | 939 | --- http_config 940 | lua_package_path "lib/?.lua;;"; 941 | server { 942 | listen 10089 ssl; 943 | server_name tokers.com; 944 | ssl_certificate ../html/tokers.crt; 945 | ssl_certificate_key ../html/tokers.key; 946 | 947 | return 200 yes; 948 | } 949 | 950 | --- config 951 | location = /t1 { 952 | content_by_lua_block { 953 | local requests = require "resty.requests" 954 | local url = "https://127.0.0.1:10089/t8a?asd&c=d" 955 | 956 | local ssl = { 957 | verify = true, 958 | server_name = "abc.com", 959 | } 960 | 961 | local r, err = requests.get(url, { ssl = ssl }) 962 | if r then 963 | ngx.print("incorrect result") 964 | return 965 | end 966 | 967 | ngx.log(ngx.ERR, err) 968 | ngx.print("good") 969 | } 970 | } 971 | 972 | --- user_files eval 973 | ">>> tokers.key 974 | $::TestCertificateKey 975 | >>> tokers.crt 976 | $::TestCertificate" 977 | 978 | --- request 979 | GET /t1 980 | 981 | --- response_body: good 982 | --- grep_error_log: lua ssl certificate verify error 983 | --- grep_error_log_out 984 | lua ssl certificate verify error 985 | 986 | 987 | === TEST 18: r:drop 988 | 989 | --- http_config eval: $::http_config 990 | --- config 991 | location = /t { 992 | content_by_lua_block { 993 | local requests = require "resty.requests" 994 | local url = "http://127.0.0.1:10088/t11" 995 | local r, err = requests.get(url) 996 | if not r then 997 | ngx.log(ngx.ERR, "incorrect result: ", err) 998 | return 999 | end 1000 | 1001 | local ok, err = r:drop() 1002 | if not ok then 1003 | ngx.log(ngx.ERR, "incorrect result: ", err) 1004 | end 1005 | 1006 | ngx.print("OK") 1007 | } 1008 | } 1009 | 1010 | --- request 1011 | GET /t 1012 | 1013 | --- response_body: OK 1014 | --- no_error_log 1015 | [error] 1016 | 1017 | 1018 | === TEST 19: r:json 1019 | --- http_config eval: $::http_config 1020 | --- config 1021 | location = /t { 1022 | content_by_lua_block { 1023 | local requests = require "resty.requests" 1024 | local url = "http://127.0.0.1:10088/t12?content_type=application/json" 1025 | local r, err = requests.get(url, { stream = true }) 1026 | if not r then 1027 | ngx.log(ngx.ERR, "incorrect result: ", err) 1028 | return 1029 | end 1030 | 1031 | local t, err = r:json() 1032 | if not t then 1033 | ngx.log(ngx.ERR, err) 1034 | return 1035 | end 1036 | 1037 | ngx.print(t["a"], t["b"]) 1038 | 1039 | local url = "http://127.0.0.1:10088/t12?content_type=application/json; charset=UTF-8" 1040 | local r, err = requests.get(url, { stream = true }) 1041 | if not r then 1042 | ngx.log(ngx.ERR, "incorrect result: ", err) 1043 | return 1044 | end 1045 | 1046 | local t, err = r:json() 1047 | if not t then 1048 | ngx.log(ngx.ERR, err) 1049 | return 1050 | end 1051 | 1052 | ngx.print(t["a"], t["b"]) 1053 | 1054 | local url = "http://127.0.0.1:10088/t12" 1055 | local r, err = requests.get(url, { stream = true }) 1056 | if not r then 1057 | ngx.log(ngx.ERR, "incorrect result: ", err) 1058 | return 1059 | end 1060 | 1061 | local t, err = r:json() 1062 | if t then 1063 | ngx.log(ngx.ERR, "unexpected json") 1064 | return 1065 | end 1066 | 1067 | ngx.print("nil") 1068 | } 1069 | } 1070 | 1071 | --- request 1072 | GET /t 1073 | 1074 | --- response_body: 1212nil 1075 | --- no_error_log 1076 | [error] 1077 | 1078 | 1079 | === TEST 20: use_default_type 1080 | --- http_config eval: $::http_config 1081 | --- config 1082 | location = /t { 1083 | content_by_lua_block { 1084 | local requests = require "resty.requests" 1085 | local url = "http://127.0.0.1:10088/t1" 1086 | local opts = { 1087 | use_default_type = false, 1088 | body = "abc", 1089 | stream = false, 1090 | } 1091 | local r, err = requests.get(url, opts) 1092 | if not r then 1093 | ngx.log(ngx.ERR, "incorrect result: ", err) 1094 | return 1095 | end 1096 | 1097 | ngx.print(r.content) 1098 | } 1099 | } 1100 | --- request 1101 | GET /t 1102 | 1103 | --- response_body eval 1104 | qq{GET /t1 HTTP/1.1\r 1105 | host: 127.0.0.1\r 1106 | content-length: 3\r 1107 | user-agent: resty-requests\r 1108 | accept: */*\r 1109 | connection: keep-alive\r 1110 | \r 1111 | }; 1112 | --- no_error_log 1113 | [error] 1114 | -------------------------------------------------------------------------------- /t/01-head.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | 3 | repeat_each(3); 4 | plan tests => repeat_each() * (blocks() * 3); 5 | 6 | our $http_config = << 'EOC'; 7 | lua_package_path "lib/?.lua;;"; 8 | 9 | server { 10 | listen 10086; 11 | 12 | location = /t1 { 13 | content_by_lua_block { 14 | ngx.status = 200 15 | } 16 | } 17 | 18 | location = /t2 { 19 | content_by_lua_block { 20 | ngx.status = 200 21 | ngx.print("dummy body data") 22 | } 23 | } 24 | } 25 | EOC 26 | 27 | no_long_string(); 28 | run_tests(); 29 | 30 | __DATA__ 31 | 32 | === TEST 1: the normal HEAD request. 33 | 34 | --- http_config eval: $::http_config 35 | 36 | --- config 37 | 38 | location /t1 { 39 | content_by_lua_block { 40 | local requests = require "resty.requests" 41 | local url = "http://127.0.0.1:10086/t1" 42 | local r, err = requests.head(url) 43 | if not r then 44 | ngx.log(ngx.ERR, err) 45 | end 46 | 47 | ngx.say(r.status_code) 48 | local data, err = r:body() 49 | -- data is "" 50 | ngx.say(data, err) 51 | 52 | local data, err = r:iter_content() 53 | ngx.say(data, err) 54 | } 55 | } 56 | 57 | 58 | --- request 59 | GET /t1 60 | 61 | --- status_code: 200 62 | --- response_body 63 | 200 64 | nileof 65 | nileof 66 | 67 | --- no_error_log 68 | [error] 69 | 70 | 71 | === TEST 2: HEAD request with request body. 72 | 73 | --- http_config eval: $::http_config 74 | 75 | --- config 76 | 77 | location /t1 { 78 | content_by_lua_block { 79 | local requests = require "resty.requests" 80 | local url = "http://127.0.0.1:10086/t1" 81 | local body = "dummy body data" 82 | local r, err = requests.head(url, { body = body }) 83 | if not r then 84 | ngx.log(ngx.ERR, err) 85 | end 86 | 87 | ngx.say(r.status_code) 88 | local data, err = r:body() 89 | -- data is "" 90 | ngx.say(data, err) 91 | 92 | local data, err = r:iter_content() 93 | ngx.say(data, err) 94 | } 95 | } 96 | 97 | 98 | --- request 99 | GET /t1 100 | 101 | --- status_code: 200 102 | --- response_body 103 | 200 104 | nileof 105 | nileof 106 | 107 | --- no_error_log 108 | [error] 109 | -------------------------------------------------------------------------------- /t/02-shortpath.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | repeat_each(3); 3 | 4 | plan tests => repeat_each() * (blocks() * 3); 5 | 6 | our $http_config = << 'EOC'; 7 | lua_package_path "lib/?.lua;;"; 8 | lua_package_cpath "?.so;;"; 9 | 10 | server { 11 | listen 10088; 12 | 13 | location = /t1 { 14 | content_by_lua_block { 15 | ngx.print(ngx.req.raw_header()) 16 | } 17 | } 18 | 19 | location = / { 20 | lua_need_request_body on; 21 | content_by_lua_block { 22 | local data = ngx.req.get_body_data() 23 | ngx.print(data) 24 | } 25 | } 26 | } 27 | EOC 28 | 29 | no_long_string(); 30 | run_tests(); 31 | 32 | __DATA__ 33 | 34 | 35 | === TEST 1: cookie 36 | 37 | --- http_config eval: $::http_config 38 | 39 | --- config 40 | location = /t { 41 | content_by_lua_block { 42 | local requests = require "resty.requests" 43 | local url = "http://127.0.0.1:10088/t1" 44 | 45 | local opts = { 46 | cookie = { 47 | name1 = "value1", 48 | name2 = "value2", 49 | name3 = "value3", 50 | } 51 | } 52 | 53 | local r, err = requests.get(url, opts) 54 | if not r then 55 | ngx.log(ngx.ERR, err) 56 | end 57 | 58 | local body, err = r:body() 59 | if err then 60 | ngx.log(ngx.ERR, err) 61 | end 62 | 63 | local auth = ngx.var.http_authorization 64 | if auth then 65 | local data = ngx.decode_base64(auth) 66 | if data ~= "alex:123456" then 67 | ngx.log(ngx.ERR, "bad authorization") 68 | end 69 | end 70 | 71 | ngx.print(body) 72 | } 73 | } 74 | 75 | --- request 76 | GET /t 77 | 78 | --- response_body eval 79 | qq{GET /t1 HTTP/1.1\r 80 | host: 127.0.0.1\r 81 | cookie: name3=value3; name1=value1; name2=value2\r 82 | user-agent: resty-requests\r 83 | accept: */*\r 84 | connection: keep-alive\r 85 | \r 86 | }; 87 | 88 | --- no_error_log 89 | [error] 90 | 91 | 92 | === TEST 2: basic auth 93 | 94 | --- http_config eval: $::http_config 95 | 96 | --- config 97 | location = /t { 98 | content_by_lua_block { 99 | local requests = require "resty.requests" 100 | local url = "http://127.0.0.1:10088/t1" 101 | 102 | local opts = { 103 | auth = { 104 | user = "alex", 105 | pass = "123456" 106 | } 107 | } 108 | 109 | local r, err = requests.get(url, opts) 110 | if not r then 111 | ngx.log(ngx.ERR, err) 112 | end 113 | 114 | local body, err = r:body() 115 | if err then 116 | ngx.log(ngx.ERR, err) 117 | end 118 | 119 | ngx.print(body) 120 | } 121 | } 122 | 123 | --- request 124 | GET /t 125 | 126 | --- response_body eval 127 | qq{GET /t1 HTTP/1.1\r 128 | host: 127.0.0.1\r 129 | authorization: Basic YWxleDoxMjM0NTY=\r 130 | user-agent: resty-requests\r 131 | accept: */*\r 132 | connection: keep-alive\r 133 | \r 134 | }; 135 | 136 | --- no_error_log 137 | [error] 138 | 139 | 140 | === TEST 3: json 141 | 142 | --- http_config eval: $::http_config 143 | 144 | --- config 145 | location = /t { 146 | content_by_lua_block { 147 | local requests = require "resty.requests" 148 | local cjson = require "cjson.safe" 149 | local url = "http://127.0.0.1:10088?ac=a" 150 | 151 | local opts = { 152 | json = { 153 | name = "alex", 154 | pass = "123456", 155 | num = { 1, 2, 3, 4, 5, 6}, 156 | } 157 | } 158 | 159 | local r, err = requests.post(url, opts) 160 | if not r then 161 | ngx.log(ngx.ERR, err) 162 | end 163 | 164 | local body, err = r:body() 165 | if err then 166 | ngx.log(ngx.ERR, err) 167 | end 168 | 169 | local after = cjson.decode(body) 170 | ngx.say(after.name) 171 | ngx.say(after.pass) 172 | ngx.say(cjson.encode(after.num)) 173 | } 174 | } 175 | 176 | --- request 177 | GET /t 178 | 179 | --- response_body 180 | alex 181 | 123456 182 | [1,2,3,4,5,6] 183 | 184 | --- no_error_log 185 | [error] 186 | -------------------------------------------------------------------------------- /t/03-redirect.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | repeat_each(3); 3 | 4 | plan tests => repeat_each() * (blocks() * 3); 5 | 6 | our $http_config = << 'EOC'; 7 | lua_package_path "lib/?.lua;;"; 8 | 9 | server { 10 | listen 10088; 11 | 12 | location / { 13 | content_by_lua_block { 14 | local code = tonumber(ngx.var.arg_code) 15 | local count = tonumber(ngx.var.arg_count) 16 | if count == 0 then 17 | ngx.status = 200 18 | return ngx.say("stop redirects") 19 | end 20 | 21 | count = count - 1 22 | 23 | local url = "http://127.0.0.1:10088/%s?count=%d&code=%d" 24 | url = string.format(url, "/a/b", count, code) 25 | 26 | ngx.redirect(url, code) 27 | } 28 | } 29 | } 30 | EOC 31 | 32 | run_tests(); 33 | 34 | __DATA__ 35 | 36 | 37 | === TEST 1: 301 redirects unfollow 38 | 39 | --- http_config eval: $::http_config 40 | 41 | --- config 42 | location /t1 { 43 | content_by_lua_block { 44 | local requests = require "resty.requests" 45 | local url = "http://127.0.0.1:10088/t1?count=3&code=301" 46 | 47 | local r, err = requests.get(url) 48 | if not r or r.status_code ~= 301 then 49 | ngx.log(ngx.ERR, err) 50 | end 51 | 52 | while true do 53 | local data, err = r:iter_content(2) 54 | if err then 55 | ngx.log(ngx.ERR, err) 56 | return 57 | end 58 | 59 | if data == "" then 60 | break 61 | end 62 | 63 | ngx.print(data) 64 | end 65 | } 66 | } 67 | 68 | --- request 69 | GET /t1 70 | 71 | --- status_code: 200 72 | --- response_body_like: 301 Moved Permanently 73 | 74 | --- no_error_log 75 | [error] 76 | 77 | 78 | === TEST 2: 302 redirects unfollow 79 | 80 | --- http_config eval: $::http_config 81 | 82 | --- config 83 | location /t1 { 84 | content_by_lua_block { 85 | local requests = require "resty.requests" 86 | local url = "http://127.0.0.1:10088/t1?count=3&code=302" 87 | 88 | local r, err = requests.get(url) 89 | if not r or r.status_code ~= 302 then 90 | ngx.log(ngx.ERR, err) 91 | end 92 | 93 | while true do 94 | local data, err = r:iter_content(2) 95 | if err then 96 | ngx.log(ngx.ERR, err) 97 | return 98 | end 99 | 100 | if data == "" then 101 | break 102 | end 103 | 104 | ngx.print(data) 105 | end 106 | } 107 | } 108 | 109 | --- request 110 | GET /t1 111 | 112 | --- status_code: 200 113 | --- response_body_like: 302 Found 114 | 115 | --- no_error_log 116 | [error] 117 | 118 | 119 | === TEST 3: 301 redirects follow(one time) 120 | 121 | --- http_config eval: $::http_config 122 | 123 | --- config 124 | location /t1 { 125 | content_by_lua_block { 126 | local requests = require "resty.requests" 127 | local url = "http://127.0.0.1:10088/t1?count=3&code=301" 128 | local opts = { 129 | allow_redirects = true, 130 | redirect_max_times = 1, 131 | } 132 | 133 | local r, err = requests.get(url, opts) 134 | if not r or r.status_code ~= 301 then 135 | ngx.log(ngx.ERR, err) 136 | end 137 | 138 | while true do 139 | local data, err = r:iter_content(2) 140 | if err then 141 | ngx.log(ngx.ERR, err) 142 | return 143 | end 144 | 145 | if data == "" then 146 | break 147 | end 148 | 149 | ngx.print(data) 150 | end 151 | } 152 | } 153 | 154 | --- request 155 | GET /t1 156 | 157 | --- status_code: 200 158 | --- response_body_like: 301 Moved Permanently 159 | 160 | --- no_error_log 161 | [error] 162 | 163 | 164 | === TEST 4: 302 redirects follow(one time) 165 | 166 | --- http_config eval: $::http_config 167 | 168 | --- config 169 | location /t1 { 170 | content_by_lua_block { 171 | local requests = require "resty.requests" 172 | local url = "http://127.0.0.1:10088/t1?count=3&code=302" 173 | local opts = { 174 | allow_redirects = true, 175 | redirect_max_times = 1, 176 | } 177 | 178 | local r, err = requests.get(url, opts) 179 | if not r or r.status_code ~= 302 then 180 | ngx.log(ngx.ERR, err) 181 | end 182 | 183 | while true do 184 | local data, err = r:iter_content(2) 185 | if err then 186 | ngx.log(ngx.ERR, err) 187 | return 188 | end 189 | 190 | if data == "" then 191 | break 192 | end 193 | 194 | ngx.print(data) 195 | end 196 | } 197 | } 198 | 199 | --- request 200 | GET /t1 201 | 202 | --- status_code: 200 203 | --- response_body_like: 302 Found 204 | 205 | --- no_error_log 206 | [error] 207 | 208 | 209 | === TEST 5: 301 redirects follow(multi times) 210 | 211 | --- http_config eval: $::http_config 212 | 213 | --- config 214 | location /t1 { 215 | content_by_lua_block { 216 | local requests = require "resty.requests" 217 | local url = "http://127.0.0.1:10088/t1?count=3&code=301" 218 | local opts = { 219 | allow_redirects = true, 220 | redirect_max_times = 4, 221 | } 222 | 223 | local r, err = requests.get(url, opts) 224 | if not r or r.status_code ~= 200 then 225 | ngx.log(ngx.ERR, err) 226 | end 227 | 228 | while true do 229 | local data, err = r:iter_content(2) 230 | if err then 231 | ngx.log(ngx.ERR, err) 232 | return 233 | end 234 | 235 | if data == "" then 236 | break 237 | end 238 | 239 | ngx.print(data) 240 | end 241 | } 242 | } 243 | 244 | --- request 245 | GET /t1 246 | 247 | --- status_code: 200 248 | --- response_body 249 | stop redirects 250 | 251 | --- no_error_log 252 | [error] 253 | 254 | 255 | === TEST 6: 302 redirects follow(multi times) 256 | 257 | --- http_config eval: $::http_config 258 | 259 | --- config 260 | location /t1 { 261 | content_by_lua_block { 262 | local requests = require "resty.requests" 263 | local url = "http://127.0.0.1:10088/t1?count=7&code=302" 264 | local opts = { 265 | allow_redirects = true, 266 | redirect_max_times = 10, 267 | } 268 | 269 | local r, err = requests.get(url, opts) 270 | if not r or r.status_code ~= 200 then 271 | ngx.log(ngx.ERR, err) 272 | end 273 | 274 | while true do 275 | local data, err = r:iter_content(3) 276 | if err then 277 | ngx.log(ngx.ERR, err) 278 | return 279 | end 280 | 281 | if data == "" then 282 | break 283 | end 284 | 285 | ngx.print(data) 286 | end 287 | } 288 | } 289 | 290 | --- request 291 | GET /t1 292 | 293 | --- status_code: 200 294 | --- response_body 295 | stop redirects 296 | 297 | --- no_error_log 298 | [error] 299 | 300 | 301 | === TEST 7: 303 redirects follow(multi times) 302 | 303 | --- http_config eval: $::http_config 304 | 305 | --- config 306 | location /t1 { 307 | content_by_lua_block { 308 | local requests = require "resty.requests" 309 | local url = "http://127.0.0.1:10088/t1?count=7&code=303" 310 | local opts = { 311 | allow_redirects = true, 312 | redirect_max_times = 10, 313 | } 314 | 315 | local r, err = requests.get(url, opts) 316 | if not r or r.status_code ~= 200 then 317 | ngx.log(ngx.ERR, err) 318 | end 319 | 320 | while true do 321 | local data, err = r:iter_content(3) 322 | if err then 323 | ngx.log(ngx.ERR, err) 324 | return 325 | end 326 | 327 | if data == "" then 328 | break 329 | end 330 | 331 | ngx.print(data) 332 | end 333 | } 334 | } 335 | 336 | --- request 337 | GET /t1 338 | 339 | --- status_code: 200 340 | --- response_body 341 | stop redirects 342 | 343 | --- no_error_log 344 | [error] 345 | 346 | 347 | === TEST 8: 307 redirects follow(multi times) 348 | 349 | --- http_config eval: $::http_config 350 | 351 | --- config 352 | location /t1 { 353 | content_by_lua_block { 354 | local requests = require "resty.requests" 355 | local url = "http://127.0.0.1:10088/t1?count=7&code=303" 356 | local opts = { 357 | allow_redirects = true, 358 | redirect_max_times = 10, 359 | } 360 | 361 | local r, err = requests.get(url, opts) 362 | if not r or r.status_code ~= 200 then 363 | ngx.log(ngx.ERR, err) 364 | end 365 | 366 | while true do 367 | local data, err = r:iter_content(3) 368 | if err then 369 | ngx.log(ngx.ERR, err) 370 | return 371 | end 372 | 373 | if data == "" then 374 | break 375 | end 376 | 377 | ngx.print(data) 378 | end 379 | } 380 | } 381 | 382 | --- request 383 | GET /t1 384 | 385 | --- status_code: 200 386 | --- response_body 387 | stop redirects 388 | 389 | --- no_error_log 390 | [error] 391 | 392 | 393 | === TEST 8: 307 redirects follow(multi times) 394 | 395 | --- http_config eval: $::http_config 396 | 397 | --- config 398 | location /t1 { 399 | content_by_lua_block { 400 | local requests = require "resty.requests" 401 | local url = "http://127.0.0.1:10088/t1?count=7&code=303" 402 | local opts = { 403 | allow_redirects = true, 404 | redirect_max_times = 10, 405 | } 406 | 407 | local r, err = requests.post(url, opts) 408 | if not r or r.status_code ~= 200 then 409 | ngx.log(ngx.ERR, err) 410 | end 411 | 412 | while true do 413 | local data, err = r:iter_content(3) 414 | if err then 415 | ngx.log(ngx.ERR, err) 416 | return 417 | end 418 | 419 | if data == "" then 420 | break 421 | end 422 | 423 | ngx.print(data) 424 | end 425 | } 426 | } 427 | 428 | --- request 429 | GET /t1 430 | 431 | --- status_code: 200 432 | --- response_body 433 | stop redirects 434 | 435 | --- no_error_log 436 | [error] 437 | -------------------------------------------------------------------------------- /t/04-error_filter.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | repeat_each(3); 3 | 4 | plan tests => repeat_each() * (blocks() * 3); 5 | 6 | our $http_config = << 'EOC'; 7 | lua_package_path "lib/?.lua;;"; 8 | 9 | server { 10 | listen 10088; 11 | 12 | server_name _; 13 | } 14 | 15 | server { 16 | listen 10089; 17 | server_name _; 18 | 19 | location / { 20 | limit_rate 1; 21 | return 200; 22 | } 23 | 24 | location /t1 { 25 | limit_rate 1; 26 | limit_rate_after 200; 27 | 28 | content_by_lua_block { 29 | ngx.print("abcccccc") 30 | ngx.print("abcccccc") 31 | ngx.print("abcccccc") 32 | ngx.print("abcccccc") 33 | ngx.print("abcccccc") 34 | ngx.print("abcccccc") 35 | ngx.print("abcccccc") 36 | } 37 | } 38 | } 39 | EOC 40 | 41 | no_long_string(); 42 | run_tests(); 43 | 44 | __DATA__ 45 | 46 | === TEST 1: error_filter(connecting) 47 | 48 | --- http_config eval: $::http_config 49 | 50 | --- config 51 | location /t1 { 52 | content_by_lua_block { 53 | local requests = require "resty.requests" 54 | local url = "http://127.0.0.1:10087/backend1" 55 | local error_filter = function(state, err) 56 | ngx.print("state: ", requests.state(state), " err: ", err) 57 | end 58 | 59 | local opts = { 60 | error_filter = error_filter, 61 | timeouts = { 1000 } 62 | } 63 | 64 | requests.get(url, opts) 65 | } 66 | } 67 | 68 | --- request 69 | GET /t1 70 | 71 | --- status_code: 200 72 | --- response_body: state: connect err: connection refused 73 | 74 | --- grep_error_log eval: qr/connect.*failed.*Connection refused\)/ 75 | --- grep_error_log_out 76 | connect() failed (111: Connection refused) 77 | 78 | 79 | === TEST 2: error_filter(handshake) 80 | 81 | --- http_config eval: $::http_config 82 | 83 | --- config 84 | location /t1 { 85 | content_by_lua_block { 86 | local requests = require "resty.requests" 87 | local url = "https://127.0.0.1:10088" 88 | local error_filter = function(state, err) 89 | ngx.print("state: ", requests.state(state), " err: ", err) 90 | end 91 | 92 | local opts = { 93 | error_filter = error_filter, 94 | timeouts = { 1000 }, 95 | ssl = { 96 | reused_session = true, 97 | server_name = "alex.com", 98 | verify = false, 99 | }, 100 | } 101 | 102 | requests.get(url, opts) 103 | } 104 | } 105 | 106 | --- request 107 | GET /t1 108 | 109 | --- status_code: 200 110 | --- response_body: state: handshake err: handshake failed 111 | 112 | --- grep_error_log eval: qr/SSL_do_handshake\(\).*failed/ 113 | --- grep_error_log_out 114 | SSL_do_handshake() failed 115 | 116 | 117 | === TEST 3: error_filter(recv header) 118 | 119 | --- http_config eval: $::http_config 120 | 121 | --- config 122 | location /t1 { 123 | content_by_lua_block { 124 | local requests = require "resty.requests" 125 | local url = "http://127.0.0.1:10089/t" 126 | local error_filter = function(state, err) 127 | ngx.say("state: ", requests.state(state), " err: ", err) 128 | end 129 | 130 | local opts = { 131 | error_filter = error_filter, 132 | timeouts = { 10, 10, 10 }, 133 | } 134 | 135 | requests.get(url, opts) 136 | } 137 | } 138 | 139 | --- request 140 | GET /t1 141 | 142 | --- status_code: 200 143 | --- response_body 144 | state: recv_header err: timeout 145 | 146 | --- grep_error_log eval: qr/read.*timed.*out/ 147 | --- grep_error_log_out 148 | read timed out 149 | 150 | 151 | === TEST 4: error_filter(recv body) 152 | 153 | --- http_config eval: $::http_config 154 | 155 | --- config 156 | location /t1 { 157 | content_by_lua_block { 158 | local requests = require "resty.requests" 159 | local url = "http://127.0.0.1:10089/t1" 160 | local error_filter = function(state, err) 161 | ngx.say("state: ", requests.state(state), " err: ", err) 162 | end 163 | 164 | local opts = { 165 | error_filter = error_filter, 166 | timeouts = { 10, 10, 10 }, 167 | } 168 | 169 | local r, err = requests.get(url, opts) 170 | local _, err = r:body() 171 | } 172 | } 173 | 174 | --- request 175 | GET /t1 176 | 177 | --- status_code: 200 178 | --- response_body 179 | state: recv_body err: timeout 180 | 181 | --- grep_error_log eval: qr/read.*timed.*out/ 182 | --- grep_error_log_out 183 | read timed out 184 | -------------------------------------------------------------------------------- /t/05-session.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | repeat_each(3); 3 | 4 | plan tests => repeat_each() * (blocks() * 3); 5 | 6 | our $http_config = << 'EOC'; 7 | lua_package_path "lib/?.lua;;"; 8 | 9 | server { 10 | listen 10088; 11 | server_name _; 12 | 13 | location = /t1 { 14 | content_by_lua_block { 15 | for i = 1, 5 do 16 | ngx.say("i = ", i) 17 | ngx.say("Hello World") 18 | end 19 | } 20 | } 21 | 22 | location = /t2 { 23 | add_header Req-Cookie $http_cookie; 24 | return 200; 25 | } 26 | } 27 | EOC 28 | 29 | no_long_string(); 30 | run_tests(); 31 | 32 | __DATA__ 33 | 34 | === TEST 1: normal session get 35 | --- http_config eval: $::http_config 36 | --- config 37 | location = /t { 38 | content_by_lua_block { 39 | local requests = require "resty.requests" 40 | local url = "http://127.0.0.1:10088/t1" 41 | 42 | local s = requests.session() 43 | local r, err = s:get(url) 44 | if not r then 45 | ngx.print(err) 46 | return ngx.exit(200) 47 | end 48 | 49 | while true do 50 | local data, err = r:iter_content(1) 51 | if not data then 52 | ngx.log(ngx.ERR, err) 53 | return ngx.exit(200) 54 | end 55 | 56 | if data == "" then 57 | return ngx.exit(200) 58 | end 59 | 60 | ngx.print(data) 61 | end 62 | } 63 | } 64 | 65 | --- request 66 | GET /t 67 | 68 | --- response_body 69 | i = 1 70 | Hello World 71 | i = 2 72 | Hello World 73 | i = 3 74 | Hello World 75 | i = 4 76 | Hello World 77 | i = 5 78 | Hello World 79 | 80 | --- no_error_log 81 | [error] 82 | -------------------------------------------------------------------------------- /t/06-http2.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | 3 | repeat_each(1); 4 | plan tests => repeat_each() * (blocks() * 3); 5 | 6 | our $http_config = << 'EOC'; 7 | lua_package_path "lib/?.lua;;"; 8 | server { 9 | listen 10088 http2; 10 | 11 | location = /t1 { 12 | return 200 "hello world"; 13 | } 14 | 15 | location = /t2 { 16 | lua_need_request_body on; 17 | client_body_buffer_size 20m; 18 | content_by_lua_block { 19 | local data = ngx.req.get_body_data() 20 | ngx.print(data) 21 | } 22 | } 23 | 24 | location = /t3 { 25 | lua_need_request_body on; 26 | client_body_in_file_only on; 27 | content_by_lua_block { 28 | local file = ngx.req.get_body_file() 29 | local f = io.open(file, "r") 30 | ngx.print(f:read("*a")) 31 | f:close() 32 | } 33 | } 34 | } 35 | EOC 36 | 37 | no_long_string(); 38 | run_tests(); 39 | 40 | __DATA__ 41 | 42 | 43 | === TEST 1: normal GET request 44 | 45 | --- http_config eval: $::http_config 46 | --- config 47 | location = /t { 48 | content_by_lua_block { 49 | local requests = require "resty.requests" 50 | local url = "http://127.0.0.1:10088/t1?foo=bar&c=" 51 | 52 | local r, err = requests.get(url, { http2 = true }) 53 | if not r then 54 | ngx.log(ngx.ERR, err) 55 | end 56 | 57 | local body, err = r:body() 58 | if err then 59 | ngx.log(ngx.ERR, err) 60 | end 61 | 62 | ngx.print(body) 63 | } 64 | } 65 | 66 | --- request 67 | GET /t 68 | --- response_body: hello world 69 | --- no_error_log 70 | [error] 71 | 72 | 73 | 74 | === TEST 2: normal GET request with body 75 | 76 | --- http_config eval: $::http_config 77 | --- config 78 | location /t1 { 79 | content_by_lua_block { 80 | local req_data = "你好吗?Hello?" 81 | local requests = require "resty.requests" 82 | local url = "http://127.0.0.1:10088/t2?usebody=true&af=b" 83 | local headers = { 84 | ["content-length"] = #req_data 85 | } 86 | 87 | local opts = { 88 | headers = headers, 89 | body = req_data, 90 | http2 = true, 91 | } 92 | 93 | local r, err = requests.get(url, opts) 94 | if not r then 95 | ngx.log(ngx.ERR, err) 96 | end 97 | 98 | ngx.print(r:body()) 99 | } 100 | } 101 | 102 | --- request 103 | GET /t1 104 | 105 | --- status_code: 200 106 | --- response_body: 你好吗?Hello? 107 | --- no_error_log 108 | [error] 109 | 110 | 111 | 112 | === TEST 3: the normal GET request with body(function) 113 | 114 | --- http_config eval: $::http_config 115 | 116 | --- config 117 | location /t1 { 118 | content_by_lua_block { 119 | local requests = require "resty.requests" 120 | local url = "http://127.0.0.1:10088/t3" 121 | local size = 20 122 | local opts = { 123 | body = function() 124 | if size == 0 then 125 | return "" 126 | end 127 | 128 | local r = "hello" 129 | size = size - 5 130 | return r 131 | end, 132 | 133 | http2 = true, 134 | } 135 | 136 | local r, err = requests.get(url, opts) 137 | if not r then 138 | ngx.log(ngx.ERR, err) 139 | end 140 | 141 | ngx.print(r:body()) 142 | } 143 | } 144 | 145 | --- request 146 | GET /t1 147 | 148 | --- response_body: hellohellohellohello 149 | --- no_error_log 150 | [error] 151 | 152 | 153 | 154 | === TEST 4: normal GET request with bulk body 155 | 156 | --- http_config eval: $::http_config 157 | --- config 158 | location /t1 { 159 | content_by_lua_block { 160 | local req_data_len = math.random(1024, 8192) 161 | local tab = {} 162 | for i = 1, req_data_len do 163 | local c = string.char(math.random(32, 127)) 164 | table.insert(tab, c) 165 | end 166 | 167 | local req_body = table.concat(tab) 168 | 169 | local requests = require "resty.requests" 170 | local url = "http://127.0.0.1:10088/t2?" 171 | local headers = { 172 | ["content-length"] = req_data_len 173 | } 174 | 175 | local opts = { 176 | headers = headers, 177 | body = req_body, 178 | http2 = true, 179 | } 180 | 181 | local r, err = requests.get(url, opts) 182 | if not r then 183 | ngx.log(ngx.ERR, err) 184 | end 185 | 186 | local body = r:body() 187 | 188 | if body == req_body then 189 | ngx.say("OK") 190 | else 191 | ngx.say("FAILURE") 192 | end 193 | } 194 | } 195 | 196 | --- request 197 | GET /t1 198 | 199 | --- status_code: 200 200 | --- response_body 201 | OK 202 | 203 | --- no_error_log 204 | [error] 205 | 206 | 207 | 208 | === TEST 6: the normal GET request with bulk body(function) 209 | 210 | --- http_config eval: $::http_config 211 | 212 | --- config 213 | location /t1 { 214 | content_by_lua_block { 215 | local req_data_len = math.random(1024, 8192) 216 | 217 | local gt = {} 218 | 219 | local get_body = function() 220 | if req_data_len == 0 then 221 | return "" 222 | end 223 | 224 | local len = math.random(1, req_data_len) 225 | 226 | local tab = {} 227 | for i = 1, len do 228 | local c = string.char(math.random(32, 127)) 229 | table.insert(tab, c) 230 | table.insert(gt, c) 231 | end 232 | 233 | req_data_len = req_data_len - len 234 | 235 | return table.concat(tab) 236 | end 237 | 238 | local requests = require "resty.requests" 239 | local url = "http://127.0.0.1:10088/t3?" 240 | local headers = { 241 | ["content-length"] = req_data_len 242 | } 243 | 244 | local opts = { 245 | headers = headers, 246 | body = get_body, 247 | http2 = true, 248 | } 249 | 250 | local r, err = requests.get(url, opts) 251 | if not r then 252 | ngx.log(ngx.ERR, err) 253 | return 254 | end 255 | 256 | local body = r:body() 257 | local req_length = r.headers["x-req-Content-Length"] 258 | 259 | if not req_length and body == table.concat(gt) then 260 | ngx.say("OK") 261 | else 262 | ngx.say("FAILURE") 263 | end 264 | } 265 | } 266 | 267 | --- request 268 | GET /t1 269 | 270 | --- response_body 271 | OK 272 | 273 | --- no_error_log 274 | [error] 275 | 276 | 277 | === TEST 7: keepalive 278 | 279 | --- http_config eval: $::http_config 280 | 281 | --- config 282 | location /t1 { 283 | content_by_lua_block { 284 | local requests = require "resty.requests" 285 | local url = "http://127.0.0.1:10088/t1" 286 | 287 | local r, err = requests.get(url, { http2 = true }) 288 | if not r then 289 | ngx.log(ngx.ERR, err) 290 | return 291 | end 292 | 293 | local body, err = r:body() 294 | if err then 295 | ngx.log(ngx.ERR, err) 296 | return 297 | end 298 | 299 | local stream = r._adapter.h2_stream 300 | 301 | assert(stream.sid == 3) 302 | 303 | local ok, err = r:close() 304 | if not ok then 305 | ngx.log(ngx.ERR, err) 306 | return 307 | end 308 | 309 | local r, err = requests.get(url, { http2 = true }) 310 | if not r then 311 | ngx.log(ngx.ERR, err) 312 | end 313 | 314 | local body, err = r:body() 315 | if err then 316 | ngx.log(ngx.ERR, err) 317 | end 318 | 319 | local stream = r._adapter.h2_stream 320 | 321 | assert(stream.sid == 5) 322 | 323 | local ok, err = r:close() 324 | if not ok then 325 | ngx.log(ngx.ERR, err) 326 | return 327 | end 328 | 329 | local r, err = requests.get(url, { http2 = true }) 330 | if not r then 331 | ngx.log(ngx.ERR, err) 332 | end 333 | 334 | local body, err = r:body() 335 | if err then 336 | ngx.log(ngx.ERR, err) 337 | end 338 | 339 | local stream = r._adapter.h2_stream 340 | 341 | assert(stream.sid == 7) 342 | 343 | r._keepalive = false 344 | local ok, err = r:close() 345 | if not ok then 346 | ngx.log(ngx.ERR, err) 347 | return 348 | end 349 | 350 | local r, err = requests.get(url, { http2 = true }) 351 | if not r then 352 | ngx.log(ngx.ERR, err) 353 | end 354 | 355 | local body, err = r:body() 356 | if err then 357 | ngx.log(ngx.ERR, err) 358 | end 359 | 360 | local stream = r._adapter.h2_stream 361 | 362 | assert(stream.sid == 3) 363 | 364 | ngx.print(body) 365 | } 366 | } 367 | 368 | --- request 369 | GET /t1 370 | --- response_body: hello world 371 | --- no_error_log 372 | [error] 373 | -------------------------------------------------------------------------------- /t/07-proxy.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Test::Nginx::Socket::Lua::Stream; 3 | use Cwd qw(cwd); 4 | 5 | my $pwd = cwd(); 6 | 7 | sub read_file { 8 | my $infile = shift; 9 | open my $in, $infile 10 | or die "cannot open $infile for reading: $!"; 11 | my $cert = do { local $/; <$in> }; 12 | close $in; 13 | $cert; 14 | } 15 | 16 | $ENV{TEST_NGINX_PWD} ||= $pwd; 17 | our $TestCertificate = read_file("t/ssl/tokers.crt"); 18 | our $TestCertificateKey = read_file("t/ssl/tokers.key"); 19 | repeat_each(3); 20 | plan tests => repeat_each() * (blocks() * 3); 21 | 22 | our $stream_config = << 'EOC'; 23 | listen 10088; 24 | lua_socket_read_timeout 200s; 25 | lua_socket_send_timeout 200s; 26 | content_by_lua_block { 27 | local req_sock, err = ngx.req.socket(true) 28 | if not req_sock then 29 | ngx.log(ngx.ERR, err) 30 | return 31 | end 32 | 33 | local reader = req_sock:receiveuntil("\r\n") 34 | local request_line, err = reader() 35 | if err then 36 | ngx.log(ngx.ERR, err) 37 | return 38 | end 39 | 40 | local m, err = ngx.re.match(request_line, [[^CONNECT ([^:]+):(\d+) HTTP/1.1$]], "jio") 41 | if not m then 42 | ngx.log(ngx.ERR, err or "invalid request line") 43 | return 44 | end 45 | 46 | while true do 47 | local data, err = reader() 48 | if err then 49 | ngx.log(ngx.ERR, err) 50 | return 51 | end 52 | 53 | if data == "" then 54 | break 55 | end 56 | end 57 | 58 | ngx.print("HTTP/1.1 200 OK\r\n") 59 | ngx.print("Transfer-Encoding: chunked\r\n") 60 | ngx.print("\r\n") 61 | ngx.print("5\r\n12345\r\n") 62 | ngx.print("5\r\n12345\r\n") 63 | ngx.print("5\r\n12345\r\n") 64 | ngx.print("1\r\n1\r\n") 65 | ngx.print("0\r\n\r\n") 66 | 67 | local host = m[1] 68 | local port = m[2] 69 | local sock = ngx.socket.tcp() 70 | local ok, err = sock:connect(host, port) 71 | if not ok then 72 | ngx.log(ngx.ERR, err) 73 | return ngx.exit(200) 74 | end 75 | 76 | local f1 = function() 77 | while true do 78 | local data, err = sock:receive(1) 79 | if err then 80 | return 81 | end 82 | 83 | local ok, err = req_sock:send(data) 84 | if err then 85 | return 86 | end 87 | end 88 | end 89 | 90 | local co = ngx.thread.spawn(f1, sock) 91 | while true do 92 | local data, err = req_sock:receive(1) 93 | if err then 94 | break 95 | end 96 | 97 | local ok, err = sock:send(data) 98 | if err then 99 | break 100 | end 101 | end 102 | 103 | ngx.thread.wait(co) 104 | return ngx.exit(200) 105 | } 106 | EOC 107 | 108 | our $http_config = << 'EOC'; 109 | lua_package_path "lib/?.lua;;"; 110 | server { 111 | listen 10089 ssl; 112 | server_name tokers.com; 113 | ssl_certificate ../html/tokers.crt; 114 | ssl_certificate_key ../html/tokers.key; 115 | 116 | return 200 "Yes, I'm the 10089 ssl server\n"; 117 | } 118 | 119 | server { 120 | listen 10090 ssl; 121 | server_name tokers.com; 122 | ssl_certificate ../html/tokers.crt; 123 | ssl_certificate_key ../html/tokers.key; 124 | 125 | location / { 126 | content_by_lua_block { 127 | ngx.status = 200 128 | ngx.say("hello world") 129 | ngx.say("hello world") 130 | ngx.say("hello world") 131 | ngx.say("hello world") 132 | ngx.say("12345") 133 | } 134 | } 135 | } 136 | 137 | server { 138 | listen 10091; 139 | return 200 "fake http proxy\n"; 140 | } 141 | EOC 142 | 143 | no_long_string(); 144 | run_tests(); 145 | 146 | __DATA__ 147 | 148 | === TEST 1: https proxy 149 | --- http_config eval: $::http_config 150 | --- stream_server_config eval: $::stream_config 151 | 152 | --- config 153 | location /t1 { 154 | content_by_lua_block { 155 | local requests = require "resty.requests" 156 | local url = "https://127.0.0.1:10089/" 157 | local opts = { 158 | proxies = { 159 | https = { host = "127.0.0.1", port = 10088 }, 160 | }, 161 | headers = { 162 | ["Connection"] = "keep-alive" 163 | }, 164 | stream = false, 165 | } 166 | local r, err = requests.get(url, opts) 167 | if not r then 168 | ngx.log(ngx.ERR, err) 169 | return ngx.exit(400) 170 | end 171 | 172 | ngx.print(r.content) 173 | } 174 | } 175 | 176 | --- user_files eval 177 | ">>> tokers.key 178 | $::TestCertificateKey 179 | >>> tokers.crt 180 | $::TestCertificate" 181 | 182 | --- request 183 | GET /t1 184 | --- response_body 185 | Yes, I'm the 10089 ssl server 186 | --- no_error_log 187 | [error] 188 | 189 | 190 | 191 | === TEST 2: https proxy with chunked body 192 | --- http_config eval: $::http_config 193 | --- stream_server_config eval: $::stream_config 194 | 195 | --- config 196 | location /t1 { 197 | content_by_lua_block { 198 | local requests = require "resty.requests" 199 | local url = "https://127.0.0.1:10090/" 200 | local opts = { 201 | proxies = { 202 | https = { host = "127.0.0.1", port = 10088 }, 203 | }, 204 | headers = { 205 | ["Connection"] = "keep-alive" 206 | }, 207 | stream = false, 208 | } 209 | local r, err = requests.get(url, opts) 210 | if not r then 211 | ngx.log(ngx.ERR, err) 212 | return ngx.exit(400) 213 | end 214 | 215 | ngx.print(r.content) 216 | } 217 | } 218 | 219 | --- user_files eval 220 | ">>> tokers.key 221 | $::TestCertificateKey 222 | >>> tokers.crt 223 | $::TestCertificate" 224 | 225 | --- request 226 | GET /t1 227 | --- response_body 228 | hello world 229 | hello world 230 | hello world 231 | hello world 232 | 12345 233 | --- no_error_log 234 | [error] 235 | 236 | 237 | === TEST 3: http proxy 238 | --- http_config eval: $::http_config 239 | 240 | --- config 241 | location /t1 { 242 | content_by_lua_block { 243 | local requests = require "resty.requests" 244 | local url = "http://127.0.0.1:10090/" 245 | local opts = { 246 | proxies = { 247 | http = { host = "127.0.0.1", port = 10091 }, 248 | }, 249 | stream = false, 250 | } 251 | local r, err = requests.get(url, opts) 252 | if not r then 253 | ngx.log(ngx.ERR, err) 254 | return ngx.exit(400) 255 | end 256 | 257 | ngx.print(r.content) 258 | } 259 | } 260 | 261 | --- user_files eval 262 | ">>> tokers.key 263 | $::TestCertificateKey 264 | >>> tokers.crt 265 | $::TestCertificate" 266 | 267 | --- request 268 | GET /t1 269 | --- response_body 270 | fake http proxy 271 | --- no_error_log 272 | [error] 273 | -------------------------------------------------------------------------------- /t/08-shortcut.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | my $pwd = cwd(); 5 | 6 | repeat_each(1); 7 | plan tests => repeat_each() * (blocks() * 3); 8 | 9 | our $http_config = << 'EOC'; 10 | lua_package_path "lib/?.lua;;"; 11 | 12 | server { 13 | listen 10088; 14 | location = /t1 { 15 | content_by_lua_block { 16 | ngx.print("10088 virtual server") 17 | } 18 | } 19 | location = /t2 { 20 | content_by_lua_block { 21 | ngx.req.read_body() 22 | ngx.print(ngx.req.get_body_data()) 23 | } 24 | } 25 | } 26 | EOC 27 | 28 | no_long_string(); 29 | run_tests(); 30 | 31 | __DATA__ 32 | 33 | 34 | === TEST 1: shortcut GET request 35 | 36 | --- http_config eval: $::http_config 37 | 38 | --- config 39 | location = /t { 40 | content_by_lua_block { 41 | local requests = require "resty.requests" 42 | local url = "http://127.0.0.1:10088/t1" 43 | local r, err = requests.get { url = url, stream = false } 44 | if not r then 45 | ngx.log(ngx.ERR, err) 46 | end 47 | 48 | ngx.say(r.content) 49 | } 50 | } 51 | 52 | --- request 53 | GET /t 54 | 55 | --- response_body 56 | 10088 virtual server 57 | --- no_error_log 58 | [error] 59 | 60 | 61 | 62 | === TEST 2: shortcut request without specified HTTP method 63 | 64 | --- http_config eval: $::http_config 65 | 66 | --- config 67 | location = /t { 68 | content_by_lua_block { 69 | local requests = require "resty.requests" 70 | local url = "http://127.0.0.1:10088/t1" 71 | local ok, err = pcall(requests.request, { url = url, stream = false }) 72 | ngx.say(err) 73 | } 74 | } 75 | 76 | --- request 77 | GET /t 78 | 79 | --- response_body_like 80 | no specified HTTP method 81 | --- no_error_log 82 | [error] 83 | 84 | 85 | 86 | === TEST 3: shortcut HEAD request 87 | 88 | --- http_config eval: $::http_config 89 | 90 | --- config 91 | location = /t { 92 | content_by_lua_block { 93 | local requests = require "resty.requests" 94 | local url = "http://127.0.0.1:10088/t1" 95 | local r, err = requests.head { url = url, stream = false } 96 | if not r then 97 | ngx.log(ngx.ERR, err) 98 | end 99 | 100 | ngx.say("ok") 101 | } 102 | } 103 | 104 | --- request 105 | GET /t 106 | 107 | --- response_body 108 | ok 109 | --- no_error_log 110 | [error] 111 | 112 | 113 | 114 | === TEST 4: shortcut POST request 115 | 116 | --- http_config eval: $::http_config 117 | 118 | --- config 119 | location = /t { 120 | content_by_lua_block { 121 | local requests = require "resty.requests" 122 | local url = "http://127.0.0.1:10088/t2" 123 | local r, err = requests.post { url = url, stream = false, body = "hello world" } 124 | if not r then 125 | ngx.log(ngx.ERR, err) 126 | end 127 | 128 | ngx.say(r.content) 129 | } 130 | } 131 | 132 | --- request 133 | GET /t 134 | 135 | --- response_body 136 | hello world 137 | --- no_error_log 138 | [error] 139 | -------------------------------------------------------------------------------- /t/ssl/tokers.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEBjCCAu6gAwIBAgIJALlP1EbUjDl5MA0GCSqGSIb3DQEBCwUAMIGXMQswCQYD 3 | VQQGEwJDTjESMBAGA1UECAwJWmhlIEppYW5nMRIwEAYDVQQHDAlIYW5nIFpob3Ux 4 | EjAQBgNVBAoMCVVQWVVOIEluYzETMBEGA1UECwwKQWxleCBaaGFuZzETMBEGA1UE 5 | AwwKdG9rZXJzLmNvbTEiMCAGCSqGSIb3DQEJARYTemNoYW8xOTk1QGdtYWlsLmNv 6 | bTAeFw0xODA4MzAwMzIxMDRaFw0xOTA4MzAwMzIxMDRaMIGXMQswCQYDVQQGEwJD 7 | TjESMBAGA1UECAwJWmhlIEppYW5nMRIwEAYDVQQHDAlIYW5nIFpob3UxEjAQBgNV 8 | BAoMCVVQWVVOIEluYzETMBEGA1UECwwKQWxleCBaaGFuZzETMBEGA1UEAwwKdG9r 9 | ZXJzLmNvbTEiMCAGCSqGSIb3DQEJARYTemNoYW8xOTk1QGdtYWlsLmNvbTCCASIw 10 | DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKXHsl9qHLtqIdmPKyapLWMO+kSV 11 | GoC5qicDnligOt/PBbhU6+LnYrAcyIyd14X6DdK0DzO9OzzwNs3lyxGhBgINVbSi 12 | gFifaEIwl5XlpbFXnOKR6A61ZWpp461Et2vAXvsYwagdA+14qM9Gq0GLc8/DeVWz 13 | Lw0+5f8OvIls6gTV/o6KHyHvNy90KX7WGLivut+6vrf+gOUuYQPGEvoNZOvHRb3b 14 | Ven+xzcxDZ0p4e4koTDx/AzT8L74AOfgfOwKkG+nWkVHfQeZ0fAohWnHlBtJSTIr 15 | A9QXKTYfcMEm0kmCwH7G1Kk64PKO3sK1RdK6QkoZnSUdMYLO2EUQ4BNoQ1cCAwEA 16 | AaNTMFEwHQYDVR0OBBYEFInoFYlHbnGSKVEIhCkNaPzQLNvxMB8GA1UdIwQYMBaA 17 | FInoFYlHbnGSKVEIhCkNaPzQLNvxMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN 18 | AQELBQADggEBAJNnQ6JNd8Y+LERVkrtF61A/Sunc/0nvW3D40NpMJ1JgLSqEAEQm 19 | lJBK5BQL+NMvDPZ5j/u3TZb32Q660JTZxC4nZANEa/r6DrgxD1LbaTRaRPUm5qqb 20 | LwMfAKbl3IRrcsH0EW9s5gzf34mvBNP6oqjAlyKCfF5ZqG4bakFdi7OF6qNnZCJo 21 | C13+pt3Me7O5DnYdWlEm6PQ/VQhwXefaNj3gSkZ12urWFyVLqAhukKcCbkYlzio7 22 | 4WHw2drx2HCcJg6cceJXOUFmgRcO+DrGdCmuRepzrP5gyT20zkknwERJX12bg7ZQ 23 | ZOMJWrSubA2DnQZbNN1EjSRH20LlWsgMIh0= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /t/ssl/tokers.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQClx7Jfahy7aiHZ 3 | jysmqS1jDvpElRqAuaonA55YoDrfzwW4VOvi52KwHMiMndeF+g3StA8zvTs88DbN 4 | 5csRoQYCDVW0ooBYn2hCMJeV5aWxV5zikegOtWVqaeOtRLdrwF77GMGoHQPteKjP 5 | RqtBi3PPw3lVsy8NPuX/DryJbOoE1f6Oih8h7zcvdCl+1hi4r7rfur63/oDlLmED 6 | xhL6DWTrx0W921Xp/sc3MQ2dKeHuJKEw8fwM0/C++ADn4HzsCpBvp1pFR30HmdHw 7 | KIVpx5QbSUkyKwPUFyk2H3DBJtJJgsB+xtSpOuDyjt7CtUXSukJKGZ0lHTGCzthF 8 | EOATaENXAgMBAAECggEAeqQQRlv244LxhcRPdOtlA0paRRBVKfOwd+pKY2AuDdJW 9 | dfaFBziMHqM8jiJbBedmAIFqWaldQIio0ot0Phd0nWIGDHrHHkjXVVgjXCYm/NpG 10 | 2qp8FR2G6EFryIv0cYc//Nxkr5y/ghdoMuch3Hxw8bu3cHn9jfwBJD5fpf7n5SjF 11 | t7o6bLsGWgoMNh/UDYVM/lYAQEKP0rEidoZHBlxm70fAFrRtyQLnenVlS1fnfbqI 12 | QNYbsTxS9botY5mZmvV9ab2RPqaNezL5pxsMxsAQOGy9XxPKYKEQKVQTlMgqBgtN 13 | CpW6tk4seKU4hGkxfrcvPcn1eYFRBaXGSJY4r5D9sQKBgQDPRMBBNvxyHlv94q+a 14 | C0GQ6Jh1Q1MaAmcmb4vib3ixd+99/Py4lGd/HxNQYEfR+3DGj4wctUJ/+rrnEkkg 15 | ERxhs00lLbUVu+5bTKOchZx2AWt2H4364agtVPJqMzm1vzk0OfpxVjRtMJIEMnmk 16 | 9EKxBfUUYrN9W0ckBFcY+HqLTwKBgQDMwc1yL6AiW2aMX7/77AfQqHAWbS7mS7eZ 17 | PofwC6tBpmcJFTqHnVL5buGP4zawX4GaOdSFg3JOsXIaW7a1zsvGWakFDpNTq56D 18 | CqCSxbJ91XN+LbibEwlUoIzcmpxQm24ud3EntJPYugrqATGcwvFslbMb26yrc+zU 19 | tVS8AIYleQKBgQCfDFDDOmCJaYaJ0hOSD6Umf3XONfVk9gcVVW9wOL3S4QfkE+gR 20 | 879fqwb1HIzAM0Qc+jY4KVd4QXx+qGd4teijiCVgrCxTGz0SzkKWb2janMhLgnc+ 21 | 1z5oWtEScXCNaMmKrBrkMZRVXXoOwnTaAZ3TnZCtLfgx74Mtvi7fuHzAvQKBgFI8 22 | 3JkqC7UA5NUod+Fk0Va68BnxzE5uxtTjypFc/nGltehuAE4LoBHSuQGjjomUMmeR 23 | JoxhWP3GQz/W6jFnV2zZAgun6QBIA6g6EnggsynbG0HodybBpJO60BiieHnfk4dS 24 | Lb52xpLbVhraU+TUX0bvcTqVJmrOaqacFeZUh4xxAoGAGBDrCuNKRHZJiokREKJ4 25 | +lTY9r/5b6si0YcR/CncRfB9XcIMmUUOMzlZ8wPehBpXcdu+n8U9jfahVqfXE7Wi 26 | TVZjKerAPrOj5GrkENqzwm1QyzRLgE7XmQcWXcZmBgM+CwtpX9ZltioRXD80DcWY 27 | /H+KhnxoPB82JDxH72qGgrg= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /util/lua-releng: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | sub file_contains ($$); 7 | 8 | my $version; 9 | for my $file (map glob, qw{ *.lua lib/*.lua lib/*/*.lua lib/*/*/*.lua lib/*/*/*/*.lua lib/*/*/*/*/*.lua }) { 10 | # Check the sanity of each .lua file 11 | open my $in, $file or 12 | die "ERROR: Can't open $file for reading: $!\n"; 13 | my $found_ver; 14 | while (<$in>) { 15 | my ($ver, $skipping); 16 | if (/(?x) (?:_VERSION) \s* = .*? ([\d\.]*\d+) (.*? SKIP)?/) { 17 | my $orig_ver = $ver = $1; 18 | $found_ver = 1; 19 | # $skipping = $2; 20 | $ver =~ s{^(\d+)\.(\d{3})(\d{3})$}{join '.', int($1), int($2), int($3)}e; 21 | warn "$file: $orig_ver ($ver)\n"; 22 | } elsif (/(?x) (?:_VERSION) \s* = \s* ([a-zA-Z_]\S*)/) { 23 | warn "$file: $1\n"; 24 | $found_ver = 1; 25 | last; 26 | } 27 | if ($ver and $version and !$skipping) { 28 | if ($version ne $ver) { 29 | # die "$file: $ver != $version\n"; 30 | } 31 | } elsif ($ver and !$version) { 32 | $version = $ver; 33 | } 34 | } 35 | if (!$found_ver) { 36 | warn "WARNING: No \"_VERSION\" or \"version\" field found in `$file`.\n"; 37 | } 38 | close $in; 39 | print "Checking use of Lua global variables in file $file ...\n"; 40 | my $output = `luac -p -l $file | grep ETGLOBAL | grep -vE 'require|type|tostring|error|ngx\$|ndk|jit|setmetatable|getmetatable|string|table|io|os|print|tonumber|math|pcall|xpcall|unpack|pairs|ipairs|assert|module|package|coroutine|[gs]etfenv|next|select|rawset|rawget|debug'`; 41 | if ($output) { 42 | print $output; 43 | exit 1; 44 | } 45 | #file_contains($file, "attempt to write to undeclared variable"); 46 | system("grep -H -n -E --color '.{120}' $file"); 47 | } 48 | 49 | sub file_contains ($$) { 50 | my ($file, $regex) = @_; 51 | open my $in, $file 52 | or die "Cannot open $file fo reading: $!\n"; 53 | my $content = do { local $/; <$in> }; 54 | close $in; 55 | #print "$content"; 56 | return scalar ($content =~ /$regex/); 57 | } 58 | 59 | if (-d 't') { 60 | for my $file (map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }) { 61 | system(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $file}); 62 | } 63 | } 64 | --------------------------------------------------------------------------------