├── .gitignore ├── Makefile ├── .travis.yml ├── lua-resty-httpipe-0.05-1.rockspec ├── t ├── 06-eof.t ├── 09-parse_uri.t ├── 07-timeout.t ├── 04-simpleinterface.t ├── 05-maxsize.t ├── 10-chunkin.t ├── 08-escape_uri.t ├── 02-filter.t ├── 11-http_version.t ├── 01-basic.t └── 03-stream.t ├── util └── lua-releng ├── README.md └── lib └── resty └── httpipe.lua /.gitignore: -------------------------------------------------------------------------------- 1 | t/servroot/ 2 | t/error.log 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPENRESTY_PREFIX=/usr/local/openresty 2 | 3 | PREFIX ?= /usr/local 4 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 5 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 6 | INSTALL ?= install 7 | 8 | .PHONY: all test install 9 | 10 | all: ; 11 | 12 | install: all 13 | $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/resty 14 | $(INSTALL) lib/resty/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/resty/ 15 | 16 | test: all 17 | PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH TEST_NGINX_NO_SHUFFLE=1 prove -I../test-nginx/lib -r t 18 | util/lua-releng 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: perl 2 | perl: 3 | - 5.10 4 | cache: 5 | - apt 6 | - ccache 7 | env: 8 | - V_OPENRESTY=1.11.2.5 9 | install: 10 | - cpanm -v --notest Test::Nginx 11 | before_script: 12 | - sudo apt-get update -q 13 | - sudo apt-get install libreadline-dev libncurses5-dev libpcre3-dev libssl-dev -y 14 | - sudo apt-get install make build-essential lua5.1 -y 15 | - wget https://openresty.org/download/openresty-$V_OPENRESTY.tar.gz 16 | - tar xzf openresty-$V_OPENRESTY.tar.gz 17 | - cd openresty-$V_OPENRESTY && ./configure && make && sudo make install && cd .. 18 | script: 19 | - make test 20 | -------------------------------------------------------------------------------- /lua-resty-httpipe-0.05-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-httpipe" 2 | version = "0.05-1" 3 | source = { 4 | url = "git://github.com/timebug/lua-resty-httpipe", 5 | tag = "v0.05", 6 | } 7 | description = { 8 | summary = "Lua HTTP client cosocket driver for OpenResty / ngx_lua, interfaces are more flexible", 9 | detailed = [[ 10 | lua-resty-httpipe - Lua HTTP client cosocket driver for OpenResty / ngx_lua, interfaces are more flexible. 11 | ]], 12 | license = "2-clause BSD", 13 | homepage = "https://github.com/timebug/lua-resty-httpipe", 14 | maintainer = "Monkey Zhang ", 15 | } 16 | dependencies = { 17 | "lua >= 5.1" 18 | } 19 | build = { 20 | type = "builtin", 21 | modules = { 22 | ["resty.httpipe"] = "lib/resty/httpipe.lua", 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /t/06-eof.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | use Test::Nginx::Socket 'no_plan'; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | }; 12 | 13 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 14 | 15 | no_long_string(); 16 | #no_diff(); 17 | 18 | run_tests(); 19 | 20 | __DATA__ 21 | === TEST 1: Check eof status. 22 | --- http_config eval: $::HttpConfig 23 | --- config 24 | location = /t { 25 | content_by_lua ' 26 | local httpipe = require "resty.httpipe" 27 | local hp = httpipe:new() 28 | 29 | ngx.say(hp:eof()) 30 | 31 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 32 | path = "/b", 33 | stream = true, 34 | }) 35 | 36 | ngx.say(hp:eof()) 37 | 38 | repeat 39 | local chunk = res.body_reader() 40 | if chunk then 41 | ngx.say(hp:eof()) 42 | end 43 | until not chunk 44 | 45 | ngx.say(hp:eof()) 46 | '; 47 | } 48 | location = /b { 49 | content_by_lua ' 50 | for j=1,3 do 51 | local t = {} 52 | for i=1,10000 do 53 | t[i] = 0 54 | end 55 | ngx.print(table.concat(t)) 56 | end 57 | 58 | local t = {} 59 | local len = 2768 60 | for i=1,len do 61 | t[i] = 0 62 | end 63 | ngx.print(table.concat(t)) 64 | '; 65 | } 66 | --- request 67 | GET /t 68 | --- response_body 69 | false 70 | false 71 | false 72 | false 73 | false 74 | false 75 | false 76 | false 77 | false 78 | true 79 | --- no_error_log 80 | [error] 81 | [warn] 82 | -------------------------------------------------------------------------------- /t/09-parse_uri.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | use Test::Nginx::Socket 'no_plan'; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | error_log logs/error.log debug; 12 | }; 13 | 14 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 15 | 16 | no_long_string(); 17 | #no_diff(); 18 | 19 | run_tests(); 20 | 21 | __DATA__ 22 | === TEST 1: http:80 23 | --- http_config eval: $::HttpConfig 24 | --- config 25 | location = /a { 26 | content_by_lua ' 27 | local httpipe = require "resty.httpipe" 28 | local hp = httpipe:new() 29 | 30 | local scheme, _, port, _, _ = unpack(hp:parse_uri("http://www.upyun.com/foo")) 31 | ngx.say(scheme .. ":" .. tostring(port)) 32 | '; 33 | } 34 | --- request 35 | GET /a 36 | --- response_body 37 | http:80 38 | --- no_error_log 39 | [error] 40 | [warn] 41 | 42 | 43 | === TEST 2: http:443 44 | --- http_config eval: $::HttpConfig 45 | --- config 46 | location = /a { 47 | content_by_lua ' 48 | local httpipe = require "resty.httpipe" 49 | local hp = httpipe:new() 50 | 51 | local function parse_uri(uri) 52 | local res, err = hp:parse_uri(uri) 53 | if res then 54 | local scheme, _, port, _, _ = unpack(res) 55 | ngx.say(scheme .. ":" .. tostring(port)) 56 | end 57 | if err then 58 | ngx.say(err) 59 | end 60 | end 61 | 62 | parse_uri("http://www.upyun.com/foo") 63 | parse_uri("https://www.upyun.com/foo") 64 | parse_uri("httpss://www.upyun.com/foo") 65 | '; 66 | } 67 | --- request 68 | GET /a 69 | --- response_body 70 | http:80 71 | https:443 72 | bad uri 73 | --- no_error_log 74 | [error] 75 | [warn] 76 | -------------------------------------------------------------------------------- /t/07-timeout.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | use Test::Nginx::Socket 'no_plan'; 6 | 7 | repeat_each(2); 8 | 9 | my $pwd = cwd(); 10 | 11 | our $HttpConfig = qq{ 12 | lua_package_path "$pwd/lib/?.lua;;"; 13 | }; 14 | 15 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 16 | 17 | no_long_string(); 18 | #no_diff(); 19 | 20 | run_tests(); 21 | 22 | __DATA__ 23 | === TEST 1: Read timeout. 24 | --- http_config eval: $::HttpConfig 25 | --- config 26 | location = /t { 27 | content_by_lua ' 28 | local httpipe = require "resty.httpipe" 29 | local hp = httpipe:new() 30 | 31 | hp:set_timeout(3 * 1000) 32 | 33 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 34 | path = "/b", 35 | read_timeout = 2 * 1000 36 | }) 37 | 38 | ngx.say(err) 39 | '; 40 | } 41 | location = /b { 42 | content_by_lua ' 43 | ngx.sleep(3) 44 | return ngx.exit(ngx.HTTP_OK) 45 | '; 46 | } 47 | --- request 48 | GET /t 49 | --- response_body 50 | timeout 51 | --- error_log 52 | lua tcp socket read timed out 53 | --- timeout: 4 54 | 55 | 56 | === TEST 2: Read timeout when discard line. 57 | --- http_config eval: $::HttpConfig 58 | --- config 59 | location = /t { 60 | content_by_lua ' 61 | local httpipe = require "resty.httpipe" 62 | local hp = httpipe:new() 63 | 64 | hp:set_timeout(3 * 1000) 65 | 66 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 67 | path = "/b", 68 | read_timeout = 2 * 1000 69 | }) 70 | 71 | ngx.say(err) 72 | '; 73 | } 74 | location = /b { 75 | content_by_lua ' 76 | local sock, _ = ngx.req.socket(true) 77 | sock:send("HTTP/1.1 100 Continue\\r\\n") 78 | ngx.sleep(3) 79 | sock:send("\\r\\n") 80 | '; 81 | } 82 | --- request 83 | GET /t 84 | --- response_body 85 | timeout 86 | --- error_log 87 | lua tcp socket read timed out 88 | --- timeout: 4 89 | -------------------------------------------------------------------------------- /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 | 23 | } elsif (/(?x) (?:_VERSION) \s* = \s* ([a-zA-Z_]\S*)/) { 24 | warn "$file: $1\n"; 25 | $found_ver = 1; 26 | last; 27 | } 28 | 29 | if ($ver and $version and !$skipping) { 30 | if ($version ne $ver) { 31 | # die "$file: $ver != $version\n"; 32 | } 33 | } elsif ($ver and !$version) { 34 | $version = $ver; 35 | } 36 | } 37 | if (!$found_ver) { 38 | warn "WARNING: No \"_VERSION\" or \"version\" field found in `$file`.\n"; 39 | } 40 | close $in; 41 | 42 | print "Checking use of Lua global variables in file $file ...\n"; 43 | system("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'"); 44 | #file_contains($file, "attempt to write to undeclared variable"); 45 | system("grep -H -n -E --color '.{120}' $file"); 46 | } 47 | 48 | sub file_contains ($$) { 49 | my ($file, $regex) = @_; 50 | open my $in, $file 51 | or die "Cannot open $file fo reading: $!\n"; 52 | my $content = do { local $/; <$in> }; 53 | close $in; 54 | #print "$content"; 55 | return scalar ($content =~ /$regex/); 56 | } 57 | 58 | if (-d 't') { 59 | for my $file (map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }) { 60 | system(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $file}); 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /t/04-simpleinterface.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | use Test::Nginx::Socket 'no_plan'; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | error_log logs/error.log debug; 12 | }; 13 | 14 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 15 | 16 | no_long_string(); 17 | #no_diff(); 18 | 19 | run_tests(); 20 | 21 | __DATA__ 22 | === TEST 1: Simple URI interface 23 | --- http_config eval: $::HttpConfig 24 | --- config 25 | location = /a { 26 | content_by_lua ' 27 | local httpipe = require "resty.httpipe" 28 | local hp = httpipe:new() 29 | 30 | local res, err = hp:request_uri("http://127.0.0.1:"..ngx.var.server_port.."/b?a=1&b=2") 31 | 32 | if not res then 33 | ngx.log(ngx.ERR, err) 34 | end 35 | ngx.status = res.status 36 | 37 | ngx.header["X-Header-A"] = res.headers["X-Header-A"] 38 | ngx.header["X-Header-B"] = res.headers["X-Header-B"] 39 | 40 | ngx.print(res.body) 41 | '; 42 | } 43 | location = /b { 44 | content_by_lua ' 45 | for k,v in pairs(ngx.req.get_uri_args()) do 46 | ngx.header["X-Header-" .. string.upper(k)] = v 47 | end 48 | ngx.say("OK") 49 | '; 50 | } 51 | --- request 52 | GET /a 53 | --- response_headers 54 | X-Header-A: 1 55 | X-Header-B: 2 56 | --- response_body 57 | OK 58 | --- no_error_log 59 | [error] 60 | [warn] 61 | 62 | 63 | === TEST 2: Simple URI interface HTTP 1.0 64 | --- http_config eval: $::HttpConfig 65 | --- config 66 | location = /a { 67 | content_by_lua ' 68 | local httpipe = require "resty.httpipe" 69 | local hp = httpipe:new() 70 | 71 | local res, err = hp:request_uri( 72 | "http://127.0.0.1:"..ngx.var.server_port.."/b?a=1&b=2", { 73 | } 74 | ) 75 | 76 | ngx.status = res.status 77 | 78 | ngx.header["X-Header-A"] = res.headers["X-Header-A"] 79 | ngx.header["X-Header-B"] = res.headers["X-Header-B"] 80 | 81 | ngx.print(res.body) 82 | '; 83 | } 84 | location = /b { 85 | content_by_lua ' 86 | for k,v in pairs(ngx.req.get_uri_args()) do 87 | ngx.header["X-Header-" .. string.upper(k)] = v 88 | end 89 | ngx.say("OK") 90 | '; 91 | } 92 | --- request 93 | GET /a 94 | --- response_headers 95 | X-Header-A: 1 96 | X-Header-B: 2 97 | --- response_body 98 | OK 99 | --- no_error_log 100 | [error] 101 | [warn] 102 | 103 | 104 | === TEST 3 Simple URI interface, params override 105 | --- http_config eval: $::HttpConfig 106 | --- config 107 | location = /a { 108 | content_by_lua ' 109 | local httpipe = require "resty.httpipe" 110 | local hp = httpipe:new() 111 | 112 | local res, err = hp:request_uri( 113 | "http://127.0.0.1:"..ngx.var.server_port.."/b?a=1&b=2", { 114 | path = "/c", 115 | query = { 116 | a = 2, 117 | b = 3, 118 | }, 119 | } 120 | ) 121 | 122 | ngx.status = res.status 123 | 124 | ngx.header["X-Header-A"] = res.headers["X-Header-A"] 125 | ngx.header["X-Header-B"] = res.headers["X-Header-B"] 126 | 127 | ngx.print(res.body) 128 | '; 129 | } 130 | location = /c { 131 | content_by_lua ' 132 | for k,v in pairs(ngx.req.get_uri_args()) do 133 | ngx.header["X-Header-" .. string.upper(k)] = v 134 | end 135 | ngx.say("OK") 136 | '; 137 | } 138 | --- request 139 | GET /a 140 | --- response_headers 141 | X-Header-A: 2 142 | X-Header-B: 3 143 | --- response_body 144 | OK 145 | --- no_error_log 146 | [error] 147 | [warn] 148 | -------------------------------------------------------------------------------- /t/05-maxsize.t: -------------------------------------------------------------------------------- 1 | use lib 'lib'; 2 | use Test::Nginx::Socket; 3 | use Cwd qw(cwd); 4 | use Test::Nginx::Socket 'no_plan'; 5 | 6 | repeat_each(2); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | }; 13 | 14 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 15 | 16 | no_long_string(); 17 | 18 | run_tests(); 19 | 20 | __DATA__ 21 | 22 | === TEST 1: Content Length greater than maximum size 23 | --- http_config eval: $::HttpConfig 24 | --- config 25 | resolver $TEST_NGINX_RESOLVER; 26 | location /foo { 27 | content_by_lua ' 28 | local len = 1024 * 1024 29 | ngx.header.content_length = len 30 | local t = {} 31 | for i=1,len do 32 | t[i] = 0 33 | end 34 | ngx.print(table.concat(t)) 35 | '; 36 | } 37 | 38 | location /t { 39 | content_by_lua ' 40 | local httpipe = require "resty.httpipe" 41 | local hp = httpipe:new() 42 | 43 | hp:set_timeout(5000) 44 | 45 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 46 | path = "/foo", 47 | maxsize = 1024 48 | }) 49 | 50 | ngx.say(err) 51 | '; 52 | } 53 | --- request 54 | GET /t 55 | --- response_body 56 | exceeds maxsize 57 | --- no_error_log 58 | [error] 59 | 60 | 61 | 62 | === TEST 2: Chunked with length greater than maximum size 63 | --- http_config eval: $::HttpConfig 64 | --- config 65 | resolver $TEST_NGINX_RESOLVER; 66 | location /foo { 67 | content_by_lua ' 68 | local len = 1024 * 1024 69 | local t = {} 70 | for i=1,len do 71 | t[i] = 0 72 | end 73 | ngx.print(table.concat(t)) 74 | '; 75 | } 76 | 77 | location /t { 78 | content_by_lua ' 79 | local httpipe = require "resty.httpipe" 80 | local hp = httpipe:new() 81 | 82 | hp:set_timeout(5000) 83 | 84 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 85 | path = "/foo", 86 | maxsize = 1024 87 | }) 88 | 89 | ngx.say(err) 90 | '; 91 | } 92 | --- request 93 | GET /t 94 | --- response_body 95 | exceeds maxsize 96 | --- no_error_log 97 | [error] 98 | 99 | 100 | 101 | === TEST 3: HTTP/1.0 with length greater than maximum size 102 | --- http_config eval: $::HttpConfig 103 | --- config 104 | lua_http10_buffering on; 105 | resolver $TEST_NGINX_RESOLVER; 106 | location /foo { 107 | content_by_lua ' 108 | local len = 1024 * 1024 109 | local t = {} 110 | for i=1,len do 111 | t[i] = 0 112 | end 113 | ngx.print(table.concat(t)) 114 | '; 115 | } 116 | 117 | location /t { 118 | content_by_lua ' 119 | local httpipe = require "resty.httpipe" 120 | local hp = httpipe:new() 121 | 122 | hp:set_timeout(5000) 123 | 124 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 125 | path = "/foo", 126 | maxsize = 1024, 127 | version = 10 128 | }) 129 | 130 | ngx.say(err) 131 | '; 132 | } 133 | --- request 134 | GET /t 135 | --- response_body 136 | exceeds maxsize 137 | --- no_error_log 138 | [error] 139 | 140 | 141 | === TEST 4: HTTP/1.0 with length less than maximum size 142 | --- http_config eval: $::HttpConfig 143 | --- config 144 | lua_http10_buffering on; 145 | resolver $TEST_NGINX_RESOLVER; 146 | location /foo { 147 | content_by_lua ' 148 | local len = 1023 149 | local t = {} 150 | for i=1,len do 151 | t[i] = "a" 152 | end 153 | ngx.print(table.concat(t)) 154 | '; 155 | } 156 | 157 | location /t { 158 | content_by_lua ' 159 | local httpipe = require "resty.httpipe" 160 | local hp = httpipe:new() 161 | 162 | hp:set_timeout(5000) 163 | 164 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 165 | path = "/foo", 166 | maxsize = 1024, 167 | version = 10 168 | }) 169 | 170 | ngx.say(err) 171 | ngx.say(#res.body) 172 | '; 173 | } 174 | --- request 175 | GET /t 176 | --- response_body 177 | nil 178 | 1023 179 | --- no_error_log 180 | [error] 181 | -------------------------------------------------------------------------------- /t/10-chunkin.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | use Test::Nginx::Socket 'no_plan'; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | }; 12 | 13 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 14 | 15 | no_long_string(); 16 | #no_diff(); 17 | 18 | run_tests(); 19 | 20 | __DATA__ 21 | === TEST 1: Chunked-Encoding request body support 22 | --- http_config eval: $::HttpConfig 23 | --- config 24 | location = /a { 25 | content_by_lua ' 26 | local httpipe = require "resty.httpipe" 27 | local hp0 = httpipe:new() 28 | 29 | local r0, err = hp0:request("127.0.0.1", ngx.var.server_port, { 30 | path = "/b", 31 | stream = true, 32 | }) 33 | ngx.say(r0.status) 34 | ngx.say(r0.headers["Transfer-Encoding"]) 35 | 36 | local hp1 = httpipe:new() 37 | local r1, err = hp1:request("127.0.0.1", ngx.var.server_port, { 38 | method = "POST", path = "/c", 39 | body = r0.body_reader, 40 | }) 41 | 42 | ngx.say(r1.status) 43 | ngx.say(r1.headers["Transfer-Encoding"]) 44 | ngx.say(#r1.body) 45 | '; 46 | } 47 | location = /b { 48 | content_by_lua ' 49 | for j=1,6 do 50 | local t = {} 51 | for i=1, math.pow(3, j) do 52 | t[i] = "a" 53 | end 54 | ngx.print(table.concat(t)) 55 | end 56 | '; 57 | } 58 | location = /c { 59 | content_by_lua ' 60 | ngx.req.read_body() 61 | local body, err = ngx.req.get_body_data() 62 | ngx.print(body) 63 | '; 64 | } 65 | --- request 66 | GET /a 67 | --- response_body 68 | 200 69 | chunked 70 | 200 71 | chunked 72 | 1092 73 | --- no_error_log 74 | [error] 75 | [warn] 76 | 77 | 78 | === TEST 2: Chunked-Encoding request body support with zero size 79 | --- http_config eval: $::HttpConfig 80 | --- config 81 | location = /a { 82 | content_by_lua ' 83 | local httpipe = require "resty.httpipe" 84 | local hp0 = httpipe:new() 85 | 86 | local r0, err = hp0:request("127.0.0.1", ngx.var.server_port, { 87 | path = "/b", 88 | stream = true, 89 | }) 90 | ngx.say(r0.status) 91 | ngx.say(r0.headers["Transfer-Encoding"]) 92 | 93 | local hp1 = httpipe:new() 94 | local r1, err = hp1:request("127.0.0.1", ngx.var.server_port, { 95 | method = "POST", path = "/c", 96 | body = r0.body_reader, 97 | }) 98 | 99 | ngx.say(r1.status) 100 | ngx.say(r1.headers["Transfer-Encoding"]) 101 | ngx.say(#r1.body) 102 | '; 103 | } 104 | location = /b { 105 | content_by_lua ' 106 | ngx.print() 107 | '; 108 | } 109 | location = /c { 110 | content_by_lua ' 111 | ngx.req.read_body() 112 | local body, err = ngx.req.get_body_data() 113 | if not body and not err then 114 | ngx.print() 115 | end 116 | '; 117 | } 118 | --- request 119 | GET /a 120 | --- response_body 121 | 200 122 | chunked 123 | 200 124 | chunked 125 | 0 126 | --- no_error_log 127 | [error] 128 | [warn] 129 | 130 | 131 | === TEST 3: Chunked-Encoding request body support with pipe 132 | --- http_config eval: $::HttpConfig 133 | --- config 134 | location = /a { 135 | content_by_lua ' 136 | local httpipe = require "resty.httpipe" 137 | local hp0 = httpipe:new() 138 | 139 | local r0, err = hp0:request("127.0.0.1", ngx.var.server_port, { 140 | path = "/b", 141 | stream = true, 142 | }) 143 | ngx.say(r0.status) 144 | ngx.say(r0.headers["Transfer-Encoding"]) 145 | 146 | local pipe = r0.pipe 147 | local r1, err = pipe:request("127.0.0.1", ngx.var.server_port, { 148 | method = "POST", path = "/c", 149 | }) 150 | 151 | ngx.say(r1.status) 152 | ngx.say(r1.headers["Transfer-Encoding"]) 153 | ngx.say(#r1.body) 154 | '; 155 | } 156 | location = /b { 157 | content_by_lua ' 158 | for j=1,6 do 159 | local t = {} 160 | for i=1, math.pow(3, j) do 161 | t[i] = "a" 162 | end 163 | ngx.print(table.concat(t)) 164 | end 165 | '; 166 | } 167 | location = /c { 168 | content_by_lua ' 169 | ngx.req.read_body() 170 | local body, err = ngx.req.get_body_data() 171 | ngx.print(body) 172 | '; 173 | } 174 | --- request 175 | GET /a 176 | --- response_body 177 | 200 178 | chunked 179 | 200 180 | chunked 181 | 1092 182 | --- no_error_log 183 | [error] 184 | [warn] 185 | -------------------------------------------------------------------------------- /t/08-escape_uri.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | use Test::Nginx::Socket 'no_plan'; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | error_log logs/error.log debug; 12 | }; 13 | 14 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 15 | 16 | no_long_string(); 17 | #no_diff(); 18 | 19 | run_tests(); 20 | 21 | __DATA__ 22 | === TEST 1: ngx.var.request_uri. 23 | --- http_config eval: $::HttpConfig 24 | --- config 25 | location /abc { 26 | content_by_lua ' 27 | local httpipe = require "resty.httpipe" 28 | local hp = httpipe:new() 29 | 30 | hp:set_timeout(5000) 31 | 32 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 33 | method = "GET", 34 | path = "/def" .. ngx.var.request_uri, 35 | }) 36 | 37 | ngx.print(res.body) 38 | '; 39 | } 40 | location /def { 41 | content_by_lua ' 42 | ngx.say(ngx.var.request_uri) 43 | '; 44 | } 45 | --- request 46 | GET /abc/中文/测试.txt 47 | --- response_body 48 | /def/abc/中文/测试.txt 49 | --- no_error_log 50 | [error] 51 | [warn] 52 | 53 | 54 | === TEST 2: ngx.var.request_uri + escape. 55 | --- http_config eval: $::HttpConfig 56 | --- config 57 | location /abc { 58 | content_by_lua ' 59 | local httpipe = require "resty.httpipe" 60 | local hp = httpipe:new() 61 | 62 | hp:set_timeout(5000) 63 | 64 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 65 | method = "GET", 66 | path = "/def" .. ngx.var.request_uri, 67 | }) 68 | 69 | ngx.print(res.body) 70 | '; 71 | } 72 | location /def { 73 | content_by_lua ' 74 | ngx.say(ngx.var.request_uri) 75 | '; 76 | } 77 | --- request 78 | GET /abc/%E4%B8%AD%E6%96%87/%E6%B5%8B%E8%AF%95.txt 79 | --- response_body 80 | /def/abc/%E4%B8%AD%E6%96%87/%E6%B5%8B%E8%AF%95.txt 81 | --- no_error_log 82 | [error] 83 | [warn] 84 | 85 | 86 | === TEST 3: ngx.var.uri + ngx.escape_uri. 87 | --- http_config eval: $::HttpConfig 88 | --- config 89 | location /abc { 90 | content_by_lua ' 91 | local httpipe = require "resty.httpipe" 92 | local hp = httpipe:new() 93 | 94 | hp:set_timeout(5000) 95 | 96 | local escape_path = function (path) 97 | local s = string.gsub(path, "([^/]+)", function (s) 98 | return ngx.escape_uri(s) 99 | end) 100 | return s 101 | end 102 | 103 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 104 | method = "GET", 105 | path = "/def" .. escape_path(ngx.var.uri), 106 | }) 107 | 108 | local ver = ngx.config.nginx_version 109 | if ver >= 1007004 then 110 | if res.body == "/def/abc/%E4%B8%AD%E6%96%87/%E6%B5%8B%E8%AF%95.txt" then 111 | ngx.say("ok") 112 | else 113 | ngx.say("err") 114 | end 115 | else 116 | if res.body == "/def/abc/%e4%b8%ad%e6%96%87/%e6%b5%8b%e8%af%95.txt" then 117 | ngx.say("ok") 118 | else 119 | ngx.say("err") 120 | end 121 | end 122 | '; 123 | } 124 | location /def { 125 | content_by_lua ' 126 | ngx.print(ngx.var.request_uri) 127 | '; 128 | } 129 | --- request 130 | GET /abc/中文/测试.txt 131 | --- response_body 132 | ok 133 | --- no_error_log 134 | [error] 135 | [warn] 136 | 137 | 138 | === TEST 4: ngx.var.uri + ngx.escape_uri + escape. 139 | --- http_config eval: $::HttpConfig 140 | --- config 141 | location /abc { 142 | content_by_lua ' 143 | local httpipe = require "resty.httpipe" 144 | local hp = httpipe:new() 145 | 146 | hp:set_timeout(5000) 147 | 148 | local escape_path = function (path) 149 | local s = string.gsub(path, "([^/]+)", function (s) 150 | return ngx.escape_uri(s) 151 | end) 152 | return s 153 | end 154 | 155 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 156 | method = "GET", 157 | path = "/def" .. escape_path(ngx.var.uri), 158 | }) 159 | 160 | local ver = ngx.config.nginx_version 161 | if ver >= 1007004 then 162 | if res.body == "/def/abc/%E4%B8%AD%E6%96%87/%E6%B5%8B%E8%AF%95.txt" then 163 | ngx.say("ok") 164 | else 165 | ngx.say("err") 166 | end 167 | else 168 | if res.body == "/def/abc/%e4%b8%ad%e6%96%87/%e6%b5%8b%e8%af%95.txt" then 169 | ngx.say("ok") 170 | else 171 | ngx.say("err") 172 | end 173 | end 174 | '; 175 | } 176 | location /def { 177 | content_by_lua ' 178 | ngx.print(ngx.var.request_uri) 179 | '; 180 | } 181 | --- request 182 | GET /abc/%E4%B8%AD%E6%96%87/%E6%B5%8B%E8%AF%95.txt 183 | --- response_body 184 | ok 185 | --- no_error_log 186 | [error] 187 | [warn] 188 | -------------------------------------------------------------------------------- /t/02-filter.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | use Test::Nginx::Socket 'no_plan'; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | error_log logs/error.log debug; 12 | }; 13 | 14 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 15 | 16 | no_long_string(); 17 | #no_diff(); 18 | 19 | run_tests(); 20 | 21 | __DATA__ 22 | === TEST 1: Header filter. 23 | --- http_config eval: $::HttpConfig 24 | --- config 25 | location = /a { 26 | content_by_lua ' 27 | local httpipe = require "resty.httpipe" 28 | local hp = httpipe:new() 29 | 30 | hp:set_timeout(5000) 31 | 32 | local ok, err = hp:connect("127.0.0.1", ngx.var.server_port) 33 | 34 | local ok, err = hp:send_request{ 35 | method = "GET", 36 | path = "/b", 37 | } 38 | 39 | local res, err = hp:read_response{ 40 | header_filter = function (status, headers) 41 | headers["X-Test-A"] = nil 42 | end 43 | } 44 | 45 | ngx.status = res.status 46 | ngx.say(res.headers["X-Test-A"]) 47 | ngx.say(res.headers["X-Test-B"]) 48 | ngx.say(res.body) 49 | '; 50 | } 51 | location = /b { 52 | content_by_lua ' 53 | ngx.header["X-Test-A"] = "x-value-a" 54 | ngx.header["X-Test-B"] = "x-value-b" 55 | ngx.print("OK") 56 | '; 57 | } 58 | --- request 59 | GET /a 60 | --- response_body 61 | nil 62 | x-value-b 63 | OK 64 | --- no_error_log 65 | [error] 66 | [warn] 67 | 68 | 69 | === TEST 2: Body filter. 70 | --- http_config eval: $::HttpConfig 71 | --- config 72 | location = /a { 73 | content_by_lua ' 74 | local httpipe = require "resty.httpipe" 75 | local h0 = httpipe:new(5) 76 | 77 | h0:set_timeout(5000) 78 | 79 | local r0, err = h0:request("127.0.0.1", ngx.var.server_port, { 80 | method = "GET", 81 | path = "/b", 82 | stream = true 83 | }) 84 | 85 | local h1 = httpipe:new() 86 | 87 | h1:set_timeout(5000) 88 | 89 | local headers = { 90 | ["Content-Length"] = r0.headers["Content-Length"] 91 | } 92 | 93 | local r1, err = h1:request("127.0.0.1", ngx.var.server_port, { 94 | method = "POST", 95 | path = "/c", 96 | headers = headers, 97 | body = r0.body_reader 98 | }) 99 | 100 | ngx.status = r1.status 101 | ngx.say(#r1.body) 102 | '; 103 | } 104 | location = /b { 105 | content_by_lua ' 106 | local t = {} 107 | local chunksize = 1024 108 | for i=1, chunksize do 109 | t[i] = 1 110 | end 111 | ngx.header.content_length = chunksize 112 | ngx.print(table.concat(t)) 113 | '; 114 | } 115 | location = /c { 116 | content_by_lua ' 117 | ngx.req.read_body() 118 | local body, err = ngx.req.get_body_data() 119 | if #body == 1024 then 120 | local t = {} 121 | for i=1, 32768 do 122 | t[i] = 0 123 | end 124 | ngx.print(table.concat(t)) 125 | else 126 | ngx.log(ngx.ERR, "failed") 127 | end 128 | '; 129 | } 130 | --- request 131 | GET /a 132 | --- response_body 133 | 32768 134 | --- no_error_log 135 | [error] 136 | [warn] 137 | 138 | 139 | === TEST 3: HTTP Pipe. 140 | --- http_config eval: $::HttpConfig 141 | --- config 142 | location = /a { 143 | content_by_lua ' 144 | local httpipe = require "resty.httpipe" 145 | local hp = httpipe:new(5) 146 | 147 | hp:set_timeout(5000) 148 | 149 | local r0, err = hp:request("127.0.0.1", ngx.var.server_port, { 150 | method = "GET", 151 | path = "/b", 152 | stream = true 153 | }) 154 | 155 | local pipe = r0.pipe 156 | 157 | pipe:set_timeout(5000) 158 | 159 | local r1, err = pipe:request("127.0.0.1", ngx.var.server_port, { 160 | method = "POST", 161 | path = "/c" 162 | }) 163 | 164 | ngx.status = r1.status 165 | ngx.say(#r1.body) 166 | '; 167 | } 168 | location = /b { 169 | content_by_lua ' 170 | local t = {} 171 | local chunksize = 1024 172 | for i=1, chunksize do 173 | t[i] = 1 174 | end 175 | ngx.header.content_length = chunksize 176 | ngx.print(table.concat(t)) 177 | '; 178 | } 179 | location = /c { 180 | content_by_lua ' 181 | ngx.req.read_body() 182 | local body, err = ngx.req.get_body_data() 183 | if #body == 1024 then 184 | local t = {} 185 | for i=1, 32768 do 186 | t[i] = 0 187 | end 188 | ngx.print(table.concat(t)) 189 | else 190 | ngx.log(ngx.ERR, "failed") 191 | end 192 | '; 193 | } 194 | --- request 195 | GET /a 196 | --- response_body 197 | 32768 198 | --- no_error_log 199 | [error] 200 | [warn] 201 | -------------------------------------------------------------------------------- /t/11-http_version.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | use Test::Nginx::Socket 'no_plan'; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | }; 12 | 13 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 14 | 15 | no_long_string(); 16 | #no_diff(); 17 | 18 | run_tests(); 19 | 20 | __DATA__ 21 | === TEST 1: HTTP 1.1. 22 | --- http_config eval: $::HttpConfig 23 | --- config 24 | location = /a { 25 | content_by_lua ' 26 | local httpipe = require "resty.httpipe" 27 | local hp = httpipe:new() 28 | 29 | hp:set_timeout(5000) 30 | 31 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 32 | method = "GET", 33 | path = "/b" 34 | }) 35 | 36 | ngx.say(res.headers["X-Foo"]) 37 | ngx.say(res.headers["Connection"]) 38 | ngx.say(res.body) 39 | '; 40 | } 41 | location = /b { 42 | content_by_lua ' 43 | ngx.header["X-Foo"] = ngx.req.get_headers()["Connection"] 44 | ngx.print(ngx.req.http_version() * 10) 45 | '; 46 | } 47 | --- request 48 | GET /a 49 | --- response_body 50 | nil 51 | keep-alive 52 | 11 53 | --- no_error_log 54 | [error] 55 | [warn] 56 | 57 | 58 | === TEST 2: HTTP 1.0. 59 | --- http_config eval: $::HttpConfig 60 | --- config 61 | location = /a { 62 | content_by_lua ' 63 | local httpipe = require "resty.httpipe" 64 | local hp = httpipe:new() 65 | 66 | hp:set_timeout(5000) 67 | 68 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 69 | version = 10, 70 | method = "GET", 71 | path = "/b" 72 | }) 73 | 74 | ngx.say(res.headers["X-Foo"]) 75 | ngx.say(res.headers["Connection"]) 76 | ngx.say(res.body) 77 | '; 78 | } 79 | location = /b { 80 | content_by_lua ' 81 | ngx.header["X-Foo"] = ngx.req.get_headers()["Connection"] 82 | ngx.print(ngx.req.http_version() * 10) 83 | '; 84 | } 85 | --- request 86 | GET /a 87 | --- response_body 88 | keep-alive 89 | keep-alive 90 | 10 91 | --- no_error_log 92 | [error] 93 | [warn] 94 | 95 | 96 | === TEST 3: HTTP 0.9. 97 | --- http_config eval: $::HttpConfig 98 | --- config 99 | location = /a { 100 | content_by_lua ' 101 | local httpipe = require "resty.httpipe" 102 | local hp = httpipe:new() 103 | 104 | hp:set_timeout(5000) 105 | 106 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 107 | version = 9, 108 | method = "GET", 109 | path = "/b" 110 | }) 111 | 112 | ngx.say(err) 113 | '; 114 | } 115 | location = /b { 116 | content_by_lua ' 117 | ngx.print(ngx.req.http_version() * 10) 118 | '; 119 | } 120 | --- request 121 | GET /a 122 | --- response_body 123 | unknown HTTP version 124 | --- no_error_log 125 | [error] 126 | [warn] 127 | 128 | 129 | === TEST 4: HTTP 1.1 + close. 130 | --- http_config eval: $::HttpConfig 131 | --- config 132 | location = /a { 133 | content_by_lua ' 134 | local httpipe = require "resty.httpipe" 135 | local hp = httpipe:new() 136 | 137 | hp:set_timeout(5000) 138 | 139 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 140 | method = "GET", 141 | path = "/b", 142 | headers = { ["Connection"] = "close" }, 143 | }) 144 | 145 | ngx.say(res.headers["X-Foo"]) 146 | ngx.say(res.headers["Connection"]) 147 | ngx.say(res.body) 148 | '; 149 | } 150 | location = /b { 151 | content_by_lua ' 152 | ngx.header["X-Foo"] = ngx.req.get_headers()["Connection"] 153 | ngx.print(ngx.req.http_version() * 10) 154 | '; 155 | } 156 | --- request 157 | GET /a 158 | --- response_body 159 | close 160 | close 161 | 11 162 | --- no_error_log 163 | [error] 164 | [warn] 165 | 166 | 167 | === TEST 5: HTTP 1.0 + close. 168 | --- http_config eval: $::HttpConfig 169 | --- config 170 | location = /a { 171 | content_by_lua ' 172 | local httpipe = require "resty.httpipe" 173 | local hp = httpipe:new() 174 | 175 | hp:set_timeout(5000) 176 | 177 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 178 | version = 10, 179 | method = "GET", 180 | path = "/b", 181 | headers = { ["Connection"] = "close" }, 182 | }) 183 | 184 | ngx.say(res.headers["X-Foo"]) 185 | ngx.say(res.headers["Connection"]) 186 | ngx.say(res.body) 187 | '; 188 | } 189 | location = /b { 190 | content_by_lua ' 191 | ngx.header["X-Foo"] = ngx.req.get_headers()["Connection"] 192 | ngx.print(ngx.req.http_version() * 10) 193 | '; 194 | } 195 | --- request 196 | GET /a 197 | --- response_body 198 | close 199 | close 200 | 10 201 | --- no_error_log 202 | [error] 203 | [warn] 204 | 205 | 206 | === TEST 6: HTTP 1.1 + default. 207 | --- http_config eval: $::HttpConfig 208 | --- config 209 | location = /a { 210 | content_by_lua ' 211 | local httpipe = require "resty.httpipe" 212 | local hp = httpipe:new() 213 | 214 | hp:set_timeout(5000) 215 | 216 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 217 | method = "GET", 218 | path = "/b", 219 | headers = { ["Connection"] = "default" }, 220 | }) 221 | 222 | ngx.say(res.headers["X-Foo"]) 223 | ngx.say(res.headers["Connection"]) 224 | ngx.say(res.body) 225 | '; 226 | } 227 | location = /b { 228 | content_by_lua ' 229 | ngx.header["X-Foo"] = ngx.req.get_headers()["Connection"] 230 | ngx.print(ngx.req.http_version() * 10) 231 | '; 232 | } 233 | --- request 234 | GET /a 235 | --- response_body 236 | default 237 | keep-alive 238 | 11 239 | --- no_error_log 240 | [error] 241 | [warn] 242 | 243 | 244 | === TEST 7: HTTP 1.0 + default. 245 | --- http_config eval: $::HttpConfig 246 | --- config 247 | location = /a { 248 | content_by_lua ' 249 | local httpipe = require "resty.httpipe" 250 | local hp = httpipe:new() 251 | 252 | hp:set_timeout(5000) 253 | 254 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 255 | version = 10, 256 | method = "GET", 257 | path = "/b", 258 | headers = { ["Connection"] = "default" }, 259 | }) 260 | 261 | ngx.say(res.headers["X-Foo"]) 262 | ngx.say(res.headers["Connection"]) 263 | ngx.say(res.body) 264 | '; 265 | } 266 | location = /b { 267 | content_by_lua ' 268 | ngx.header["X-Foo"] = ngx.req.get_headers()["Connection"] 269 | ngx.print(ngx.req.http_version() * 10) 270 | '; 271 | } 272 | --- request 273 | GET /a 274 | --- response_body 275 | default 276 | close 277 | 10 278 | --- no_error_log 279 | [error] 280 | [warn] 281 | -------------------------------------------------------------------------------- /t/01-basic.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | use Test::Nginx::Socket 'no_plan'; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | error_log logs/error.log debug; 12 | }; 13 | 14 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 15 | $ENV{TEST_NGINX_HTML_DIR} ||= html_dir(); 16 | $ENV{TEST_NGINX_PWD} ||= $pwd; 17 | 18 | no_long_string(); 19 | #no_diff(); 20 | 21 | run_tests(); 22 | 23 | __DATA__ 24 | === TEST 1: Simple get. 25 | --- http_config eval: $::HttpConfig 26 | --- config 27 | location = /a { 28 | content_by_lua ' 29 | local httpipe = require "resty.httpipe" 30 | local hp = httpipe:new() 31 | 32 | hp:set_timeout(5000) 33 | 34 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 35 | method = "GET", 36 | path = "/b" 37 | }) 38 | 39 | ngx.print(res.body) 40 | '; 41 | } 42 | location = /b { 43 | echo "OK"; 44 | } 45 | --- request 46 | GET /a 47 | --- response_body 48 | OK 49 | --- no_error_log 50 | [error] 51 | [warn] 52 | 53 | 54 | === TEST 2: Status code 55 | --- http_config eval: $::HttpConfig 56 | --- config 57 | location = /a { 58 | content_by_lua ' 59 | local httpipe = require "resty.httpipe" 60 | local hp = httpipe:new() 61 | 62 | hp:set_timeout(5000) 63 | 64 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 65 | method = "GET", 66 | path = "/b" 67 | }) 68 | 69 | ngx.status = res.status 70 | ngx.print(res.body) 71 | '; 72 | } 73 | location = /b { 74 | content_by_lua ' 75 | ngx.status = 404 76 | ngx.say("OK") 77 | '; 78 | } 79 | --- request 80 | GET /a 81 | --- response_body 82 | OK 83 | --- error_code: 404 84 | --- no_error_log 85 | [error] 86 | [warn] 87 | 88 | 89 | === TEST 3: Response headers 90 | --- http_config eval: $::HttpConfig 91 | --- config 92 | location = /a { 93 | content_by_lua ' 94 | local httpipe = require "resty.httpipe" 95 | local hp = httpipe:new() 96 | 97 | hp:set_timeout(5000) 98 | 99 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 100 | method = "GET", 101 | path = "/b" 102 | }) 103 | 104 | ngx.status = res.status 105 | ngx.say(res.headers["X-Test"]) 106 | 107 | if type(res.headers["Set-Cookie"]) == "table" then 108 | ngx.say(table.concat(res.headers["Set-Cookie"], ", ")) 109 | else 110 | ngx.say(res.headers["Set-Cookie"]) 111 | end 112 | 113 | '; 114 | } 115 | location = /b { 116 | content_by_lua ' 117 | ngx.header["X-Test"] = "x-value" 118 | ngx.header["Set-Cookie"] = {"a=32; path=/", "b=4; path=/", "c"} 119 | ngx.say("OK") 120 | '; 121 | } 122 | --- request 123 | GET /a 124 | --- response_body 125 | x-value 126 | a=32; path=/, b=4; path=/, c 127 | --- no_error_log 128 | [error] 129 | [warn] 130 | 131 | 132 | === TEST 4: Query 133 | --- http_config eval: $::HttpConfig 134 | --- config 135 | location = /a { 136 | content_by_lua ' 137 | local httpipe = require "resty.httpipe" 138 | local hp = httpipe:new() 139 | 140 | hp:set_timeout(5000) 141 | 142 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 143 | method = "GET", 144 | path = "/b", 145 | query = { 146 | a = 1, 147 | b = 2, 148 | }, 149 | }) 150 | 151 | ngx.status = res.status 152 | for k,v in pairs(res.headers) do 153 | ngx.header[k] = v 154 | end 155 | ngx.say(res.body) 156 | '; 157 | } 158 | location = /b { 159 | content_by_lua ' 160 | for k,v in pairs(ngx.req.get_uri_args()) do 161 | ngx.header["X-Header-" .. string.upper(k)] = v 162 | end 163 | '; 164 | } 165 | --- request 166 | GET /a 167 | --- response_headers 168 | X-Header-A: 1 169 | X-Header-B: 2 170 | --- no_error_log 171 | [error] 172 | [warn] 173 | 174 | 175 | === TEST 5: HEAD has no body. 176 | --- http_config eval: $::HttpConfig 177 | --- config 178 | location = /a { 179 | content_by_lua ' 180 | local httpipe = require "resty.httpipe" 181 | local hp = httpipe:new() 182 | 183 | hp:set_timeout(5000) 184 | 185 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 186 | method = "HEAD", 187 | path = "/b" 188 | }) 189 | 190 | ngx.status = res.status 191 | if res.body then 192 | ngx.print(res.body) 193 | end 194 | '; 195 | } 196 | location = /b { 197 | echo "OK"; 198 | } 199 | --- request 200 | GET /a 201 | --- response_body 202 | --- no_error_log 203 | [error] 204 | [warn] 205 | 206 | 207 | === TEST 6: Request without connect. 208 | --- http_config eval: $::HttpConfig 209 | --- config 210 | location = /a { 211 | content_by_lua ' 212 | local httpipe = require "resty.httpipe" 213 | local hp = httpipe:new() 214 | 215 | hp:set_timeout(5000) 216 | 217 | local ok, err = hp:connect("127.0.0.1", ngx.var.server_port) 218 | 219 | local res, err = hp:request{ 220 | method = "GET", 221 | path = "/b" 222 | } 223 | 224 | ngx.print(res.body) 225 | '; 226 | } 227 | location = /b { 228 | echo "OK"; 229 | } 230 | --- request 231 | GET /a 232 | --- response_body 233 | OK 234 | --- no_error_log 235 | [error] 236 | [warn] 237 | 238 | 239 | === TEST 7: 304 without Content-Length. 240 | --- http_config eval: $::HttpConfig 241 | --- config 242 | location = /a { 243 | content_by_lua ' 244 | local httpipe = require "resty.httpipe" 245 | local hp = httpipe:new() 246 | 247 | hp:set_timeout(2000) 248 | 249 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 250 | method = "GET", 251 | path = "/b" 252 | }) 253 | 254 | if not res then 255 | ngx.header["X-Foo"] = err 256 | ngx.header["X-Eof"] = tostring(hp:eof()) 257 | return ngx.exit(503) 258 | end 259 | 260 | ngx.status = res.status 261 | for k,v in pairs(res.headers) do 262 | ngx.header[k] = v 263 | end 264 | ngx.header["X-Eof"] = tostring(hp:eof()) 265 | return ngx.exit(res.status) 266 | '; 267 | } 268 | location = /b { 269 | content_by_lua ' 270 | ngx.status = 304 271 | ngx.header["Content-Length"] = nil 272 | ngx.header["X-Foo"] = "bar" 273 | return ngx.exit(ngx.OK) 274 | '; 275 | } 276 | --- request 277 | GET /a 278 | --- response_headers 279 | X-Foo: bar 280 | X-Eof: true 281 | --- error_code: 304 282 | --- no_error_log 283 | [error] 284 | [warn] 285 | 286 | 287 | === TEST 8: Simple get + Host. 288 | --- http_config eval: $::HttpConfig 289 | --- config 290 | location = /a { 291 | content_by_lua ' 292 | local httpipe = require "resty.httpipe" 293 | local hp = httpipe:new() 294 | 295 | hp:set_timeout(5000) 296 | 297 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 298 | method = "GET", 299 | path = "/b" 300 | }) 301 | 302 | ngx.print(res.body) 303 | '; 304 | } 305 | location = /b { 306 | echo $http_host; 307 | } 308 | --- request 309 | GET /a 310 | --- response_body 311 | 127.0.0.1:1984 312 | --- no_error_log 313 | [error] 314 | [warn] 315 | 316 | 317 | === TEST 9: Simple get + Unix Socket Host. 318 | --- http_config 319 | lua_package_path "$TEST_NGINX_PWD/lib/?.lua;;"; 320 | error_log logs/error.log debug; 321 | server { 322 | listen unix:/tmp/nginx.sock; 323 | default_type 'text/plain'; 324 | server_tokens off; 325 | 326 | location = /b { 327 | echo $http_host; 328 | } 329 | } 330 | --- config 331 | location = /a { 332 | content_by_lua ' 333 | local httpipe = require "resty.httpipe" 334 | local hp = httpipe:new() 335 | 336 | hp:set_timeout(5000) 337 | 338 | local res, err = hp:request("unix:/tmp/nginx.sock", { 339 | method = "GET", 340 | path = "/b" 341 | }) 342 | 343 | ngx.status = res.status 344 | ngx.print(res.body) 345 | '; 346 | } 347 | --- request 348 | GET /a 349 | --- error_code: 400 350 | --- no_error_log 351 | [error] 352 | [warn] -------------------------------------------------------------------------------- /t/03-stream.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | use Test::Nginx::Socket 'no_plan'; 6 | 7 | my $pwd = cwd(); 8 | 9 | our $HttpConfig = qq{ 10 | lua_package_path "$pwd/lib/?.lua;;"; 11 | }; 12 | 13 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 14 | 15 | no_long_string(); 16 | #no_diff(); 17 | 18 | run_tests(); 19 | 20 | __DATA__ 21 | === TEST 1: Chunked streaming body reader returns the right content length. 22 | --- http_config eval: $::HttpConfig 23 | --- config 24 | location = /a { 25 | content_by_lua ' 26 | local httpipe = require "resty.httpipe" 27 | local hp = httpipe:new() 28 | 29 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 30 | path = "/b", 31 | stream = true, 32 | }) 33 | 34 | local chunks = {} 35 | repeat 36 | local chunk = res.body_reader() 37 | if chunk then 38 | table.insert(chunks, chunk) 39 | end 40 | until not chunk 41 | 42 | local body = table.concat(chunks) 43 | ngx.say(#body) 44 | ngx.say(res.headers["Transfer-Encoding"]) 45 | ngx.say(res.headers["Content-Length"]) 46 | ngx.say(res.headers["Connection"]) 47 | '; 48 | } 49 | location = /b { 50 | content_by_lua ' 51 | for j=1,3 do 52 | local t = {} 53 | for i=1,10000 do 54 | t[i] = 0 55 | end 56 | ngx.print(table.concat(t)) 57 | end 58 | 59 | local t = {} 60 | local len = 2768 61 | for i=1,len do 62 | t[i] = 0 63 | end 64 | ngx.print(table.concat(t)) 65 | '; 66 | } 67 | --- request 68 | GET /a 69 | --- response_body 70 | 32768 71 | chunked 72 | nil 73 | keep-alive 74 | --- no_error_log 75 | [error] 76 | [warn] 77 | 78 | 79 | === TEST 2: Non-Chunked streaming body reader returns the right content length. 80 | --- http_config eval: $::HttpConfig 81 | --- config 82 | location = /a { 83 | content_by_lua ' 84 | local httpipe = require "resty.httpipe" 85 | local hp = httpipe:new() 86 | 87 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 88 | path = "/b", 89 | stream = true, 90 | }) 91 | 92 | local chunks = {} 93 | local size = 8192 94 | repeat 95 | local chunk = res.body_reader(size) 96 | if chunk then 97 | table.insert(chunks, chunk) 98 | end 99 | size = size + size 100 | until not chunk 101 | 102 | local body = table.concat(chunks) 103 | ngx.say(#body) 104 | ngx.say(res.headers["Transfer-Encoding"]) 105 | ngx.say(res.headers["Content-Length"]) 106 | ngx.say(res.headers["Connection"]) 107 | ngx.say(#chunks) 108 | '; 109 | } 110 | location = /b { 111 | chunked_transfer_encoding off; 112 | content_by_lua ' 113 | local len = 32768 114 | local t = {} 115 | for i=1,len do 116 | t[i] = 0 117 | end 118 | ngx.print(table.concat(t)) 119 | '; 120 | } 121 | --- request 122 | GET /a 123 | --- response_body 124 | 32768 125 | nil 126 | nil 127 | close 128 | 3 129 | --- no_error_log 130 | [error] 131 | [warn] 132 | 133 | 134 | === TEST 3: Request reader correctly reads body 135 | --- http_config eval: $::HttpConfig 136 | --- config 137 | location = /a { 138 | lua_need_request_body off; 139 | content_by_lua ' 140 | local httpipe = require "resty.httpipe" 141 | local hp = httpipe:new() 142 | 143 | local reader, err = hp:get_client_body_reader() 144 | 145 | repeat 146 | local chunk, err = reader() 147 | if chunk then 148 | ngx.print(chunk) 149 | end 150 | until chunk == nil 151 | 152 | '; 153 | } 154 | 155 | --- request 156 | POST /a 157 | foobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbaz 158 | --- response_body: foobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbaz 159 | --- no_error_log 160 | [error] 161 | [warn] 162 | 163 | 164 | === TEST 4: Request reader correctly reads body in chunks 165 | --- http_config eval: $::HttpConfig 166 | --- config 167 | location = /a { 168 | lua_need_request_body off; 169 | content_by_lua ' 170 | local httpipe = require "resty.httpipe" 171 | local hp = httpipe:new() 172 | 173 | local reader, err = hp:get_client_body_reader(64) 174 | 175 | local chunks = 0 176 | repeat 177 | chunks = chunks +1 178 | local chunk, err = reader() 179 | if chunk then 180 | ngx.print(chunk) 181 | end 182 | until chunk == nil 183 | ngx.say("\\n"..chunks) 184 | '; 185 | } 186 | 187 | --- request 188 | POST /a 189 | foobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbaz 190 | --- response_body 191 | foobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbaz 192 | 3 193 | --- no_error_log 194 | [error] 195 | [warn] 196 | 197 | 198 | === TEST 5: Request reader passes into client 199 | --- http_config eval: $::HttpConfig 200 | --- config 201 | location = /a { 202 | lua_need_request_body off; 203 | content_by_lua ' 204 | local httpipe = require "resty.httpipe" 205 | local hp = httpipe:new() 206 | 207 | local reader, err = hp:get_client_body_reader(64) 208 | 209 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 210 | method = POST, 211 | path = "/b", 212 | body = reader, 213 | headers = ngx.req.get_headers(100, true), 214 | }) 215 | 216 | ngx.say(res.body) 217 | '; 218 | } 219 | 220 | location = /b { 221 | content_by_lua ' 222 | ngx.req.read_body() 223 | local body, err = ngx.req.get_body_data() 224 | ngx.print(body) 225 | '; 226 | } 227 | 228 | --- request 229 | POST /a 230 | foobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbaz 231 | --- response_body 232 | foobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbaz 233 | --- no_error_log 234 | [error] 235 | 236 | 237 | === TEST 6: Body reader is a function returning nil when no body is present. 238 | --- http_config eval: $::HttpConfig 239 | --- config 240 | location = /a { 241 | content_by_lua ' 242 | local httpipe = require "resty.httpipe" 243 | local hp = httpipe:new() 244 | 245 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 246 | path = "/b", 247 | method = "HEAD", 248 | stream = true, 249 | }) 250 | 251 | repeat 252 | local chunk = res.body_reader() 253 | until not chunk 254 | '; 255 | } 256 | location = /b { 257 | content_by_lua ' 258 | ngx.exit(200) 259 | '; 260 | } 261 | --- request 262 | GET /a 263 | --- no_error_log 264 | [error] 265 | [warn] 266 | 267 | 268 | === TEST 7: Non-Chunked Streaming body reader with Content-Length response header. 269 | --- http_config eval: $::HttpConfig 270 | --- config 271 | location = /a { 272 | content_by_lua ' 273 | local httpipe = require "resty.httpipe" 274 | local hp = httpipe:new() 275 | 276 | local res, err = hp:request("127.0.0.1", ngx.var.server_port, { 277 | path = "/b", 278 | stream = true, 279 | }) 280 | 281 | local chunks = {} 282 | repeat 283 | local chunk = res.body_reader() 284 | if chunk then 285 | table.insert(chunks, chunk) 286 | end 287 | until not chunk 288 | 289 | local body = table.concat(chunks) 290 | ngx.say(#body) 291 | ngx.say(res.headers["Transfer-Encoding"]) 292 | ngx.say(res.headers["Content-Length"]) 293 | ngx.say(res.headers["Connection"]) 294 | '; 295 | } 296 | location = /b { 297 | content_by_lua ' 298 | ngx.header["Content-Length"] = 32768 299 | for j=1,3 do 300 | local t = {} 301 | for i=1,10000 do 302 | t[i] = 0 303 | end 304 | ngx.print(table.concat(t)) 305 | end 306 | 307 | local t = {} 308 | local len = 2768 309 | for i=1,len do 310 | t[i] = 0 311 | end 312 | ngx.print(table.concat(t)) 313 | '; 314 | } 315 | --- request 316 | GET /a 317 | --- response_body 318 | 32768 319 | nil 320 | 32768 321 | keep-alive 322 | --- no_error_log 323 | [error] 324 | [warn] 325 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | [![Build Status](https://travis-ci.org/timebug/lua-resty-httpipe.svg)](https://travis-ci.org/timebug/lua-resty-httpipe) 4 | 5 | lua-resty-httpipe - Lua HTTP client cosocket driver for [OpenResty](http://openresty.org/) / [ngx_lua](https://github.com/chaoslawful/lua-nginx-module), interfaces are more flexible. 6 | 7 | # Table of Contents 8 | 9 | * [Status](#status) 10 | * [Features](#features) 11 | * [Synopsis](#synopsis) 12 | * [Methods](#methods) 13 | * [Connection](#connection) 14 | * [new](#new) 15 | * [connect](#connect) 16 | * [set_timeout](#set_timeout) 17 | * [ssl_handshake](#ssl_handshake) 18 | * [set_keepalive](#set_keepalive) 19 | * [get_reused_times](#get_reused_times) 20 | * [close](#close) 21 | * [Requesting](#requesting) 22 | * [request](#request) 23 | * [request_uri](#request_uri) 24 | * [res.body_reader](#res.body_reader) 25 | * [send_request](#send_request) 26 | * [read_response](#read_response) 27 | * [read](#read) 28 | * [eof](#eof) 29 | * [Utility](#utility) 30 | * [parse_uri](#parse_uri) 31 | * [get_client_body_reader](#get_client_body_reader) 32 | * [Author](#author) 33 | * [Copyright and License](#copyright-and-license) 34 | * [See Also](#see-also) 35 | 36 | 37 | # Status 38 | 39 | Ready for testing. Probably production ready in most cases, though not yet proven in the wild. Please check the issues list and let me know if you have any problems / questions. 40 | 41 | # Features 42 | 43 | * HTTP 1.0/1.1 and HTTPS 44 | * Flexible interface design 45 | * Streaming reader and uploads 46 | * Chunked-encoding request / response body 47 | * Sets the timeout for read and send operations 48 | * Limit the maximum response body size 49 | * Keepalive 50 | 51 | # Synopsis 52 | 53 | ````lua 54 | lua_package_path "/path/to/lua-resty-httpipe/lib/?.lua;;"; 55 | 56 | server { 57 | 58 | listen 9090; 59 | 60 | location /echo { 61 | content_by_lua_block { 62 | local raw_header = ngx.req.raw_header() 63 | 64 | if ngx.req.get_method() == "GET" then 65 | ngx.header["Content-Length"] = #raw_header 66 | end 67 | 68 | ngx.req.read_body() 69 | local body, err = ngx.req.get_body_data() 70 | 71 | ngx.print(raw_header) 72 | ngx.print(body) 73 | } 74 | } 75 | 76 | location /simple { 77 | content_by_lua_block { 78 | local httpipe = require "resty.httpipe" 79 | 80 | local hp, err = httpipe:new() 81 | if not hp then 82 | ngx.log(ngx.ERR, "failed to new httpipe: ", err) 83 | return ngx.exit(503) 84 | end 85 | 86 | hp:set_timeout(5 * 1000) -- 5 sec 87 | 88 | local res, err = hp:request("127.0.0.1", 9090, { 89 | method = "GET", path = "/echo" }) 90 | if not res then 91 | ngx.log(ngx.ERR, "failed to request: ", err) 92 | return ngx.exit(503) 93 | end 94 | 95 | ngx.status = res.status 96 | 97 | for k, v in pairs(res.headers) do 98 | ngx.header[k] = v 99 | end 100 | 101 | ngx.say(res.body) 102 | } 103 | } 104 | 105 | location /generic { 106 | content_by_lua_block { 107 | local cjson = require "cjson" 108 | local httpipe = require "resty.httpipe" 109 | 110 | local hp, err = httpipe:new(10) -- chunk_size = 10 111 | if not hp then 112 | ngx.log(ngx.ERR, "failed to new httpipe: ", err) 113 | return ngx.exit(503) 114 | end 115 | 116 | hp:set_timeout(5 * 1000) -- 5 sec 117 | 118 | local ok, err = hp:connect("127.0.0.1", 9090) 119 | if not ok then 120 | ngx.log(ngx.ERR, "failed to connect: ", err) 121 | return ngx.exit(503) 122 | end 123 | 124 | local ok, err = hp:send_request{ method = "GET", path = "/echo" } 125 | if not ok then 126 | ngx.log(ngx.ERR, "failed to send request: ", err) 127 | return ngx.exit(503) 128 | end 129 | 130 | -- full streaming parser 131 | 132 | while true do 133 | local typ, res, err = hp:read() 134 | if not typ then 135 | ngx.say("failed to read: ", err) 136 | return 137 | end 138 | 139 | ngx.say("read: ", cjson.encode({typ, res})) 140 | 141 | if typ == 'eof' then 142 | break 143 | end 144 | end 145 | } 146 | } 147 | 148 | location /advanced { 149 | content_by_lua_block { 150 | local httpipe = require "resty.httpipe" 151 | 152 | local hp, err = httpipe:new() 153 | 154 | hp:set_timeout(5 * 1000) -- 5 sec 155 | 156 | local r0, err = hp:request("127.0.0.1", 9090, { 157 | method = "GET", path = "/echo", 158 | stream = true }) 159 | 160 | -- from one http stream to another, just like a unix pipe 161 | 162 | local pipe = r0.pipe 163 | 164 | pipe:set_timeout(5 * 1000) -- 5 sec 165 | 166 | --[[ 167 | local headers = {["Content-Length"] = r0.headers["Content-Length"]} 168 | local r1, err = pipe:request("127.0.0.1", 9090, { 169 | method = "POST", path = "/echo", 170 | headers = headers, 171 | body = r0.body_reader }) 172 | --]] 173 | local r1, err = pipe:request("127.0.0.1", 9090, { 174 | method = "POST", path = "/echo" }) 175 | 176 | ngx.status = r1.status 177 | 178 | for k, v in pairs(r1.headers) do 179 | ngx.header[k] = v 180 | end 181 | 182 | ngx.say(r1.body) 183 | } 184 | } 185 | 186 | } 187 | ```` 188 | 189 | A typical output of the `/simple` location defined above is: 190 | 191 | ``` 192 | GET /echo HTTP/1.1 193 | Host: 127.0.0.1 194 | User-Agent: Resty/HTTPipe-1.00 195 | Accept: */* 196 | 197 | ``` 198 | 199 | A typical output of the `/generic` location defined above is: 200 | 201 | ``` 202 | read: ["statusline","200"] 203 | read: ["header",["Server","openresty\/1.5.12.1","Server: openresty\/1.5.12.1"]] 204 | read: ["header",["Date","Tue, 10 Jun 2014 07:29:57 GMT","Date: Tue, 10 Jun 2014 07:29:57 GMT"]] 205 | read: ["header",["Content-Type","text\/plain","Content-Type: text\/plain"]] 206 | read: ["header",["Connection","keep-alive","Connection: keep-alive"]] 207 | read: ["header",["Content-Length","84","Content-Length: 84"]] 208 | read: ["header_end"] 209 | read: ["body","GET \/echo "] 210 | read: ["body","HTTP\/1.1\r\n"] 211 | read: ["body","Host: 127."] 212 | read: ["body","0.0.1\r\nUse"] 213 | read: ["body","r-Agent: R"] 214 | read: ["body","esty\/HTTPi"] 215 | read: ["body","pe-1.00\r\nA"] 216 | read: ["body","ccept: *\/*"] 217 | read: ["body","\r\n\r\n"] 218 | read: ["body_end"] 219 | read: ["eof"] 220 | ``` 221 | 222 | A typical output of the `/advanced` location defined above is: 223 | 224 | ``` 225 | POST /echo HTTP/1.1 226 | Content-Length: 84 227 | User-Agent: Resty/HTTPipe-1.00 228 | Accept: */* 229 | Host: 127.0.0.1 230 | 231 | GET /echo HTTP/1.1 232 | Host: 127.0.0.1 233 | User-Agent: Resty/HTTPipe-1.00 234 | Accept: */* 235 | 236 | 237 | ``` 238 | 239 | # Methods 240 | 241 | [Back to TOC](#table-of-contents) 242 | 243 | ## Connection 244 | 245 | ### new 246 | 247 | **syntax:** `hp, err = httpipe:new(chunk_size?, sock?)` 248 | 249 | Creates the httpipe object. In case of failures, returns `nil` and a string describing the error. 250 | 251 | The argument, `chunk_size`, specifies the buffer size used by cosocket reading operations. Defaults to `8192`. 252 | 253 | [Back to TOC](#table-of-contents) 254 | 255 | ### connect 256 | 257 | `syntax: ok, err = hp:connect(host, port, options_table?)` 258 | 259 | `syntax: ok, err = hp:connect("unix:/path/to/unix.sock", options_table?)` 260 | 261 | Attempts to connect to the web server. 262 | 263 | Before actually resolving the host name and connecting to the remote backend, this method will always look up the connection pool for matched idle connections created by previous calls of this method. 264 | 265 | An optional Lua table can be specified as the last argument to this method to specify various connect options: 266 | 267 | * `pool` 268 | : Specifies a custom name for the connection pool being used. If omitted, then the connection pool name will be generated from the string template `:` or ``. 269 | 270 | [Back to TOC](#table-of-contents) 271 | 272 | ### set_timeout 273 | 274 | **syntax:** `hp:set_timeout(time)` 275 | 276 | Sets the timeout (in ms) protection for subsequent operations, including the `connect` method. 277 | 278 | [Back to TOC](#table-of-contents) 279 | 280 | ### ssl_handshake 281 | 282 | **syntax:** `hp:ssl_handshake(reused_session?, server_name?, ssl_verify?)` 283 | 284 | Does SSL/TLS handshake on the currently established connection. 285 | 286 | See more: 287 | 288 | [Back to TOC](#table-of-contents) 289 | 290 | ### set_keepalive 291 | 292 | **syntax:** `ok, err = hp:set_keepalive(max_idle_timeout, pool_size)` 293 | 294 | Attempts to puts the current connection into the ngx_lua cosocket connection pool. 295 | 296 | **Note** Normally, it will be called automatically after processing the request. In other words, we cannot release the connection back to the pool unless you consume all the data. 297 | 298 | You can specify the max idle timeout (in ms) when the connection is in the pool and the maximal size of the pool every nginx worker process. 299 | 300 | In case of success, returns 1. In case of errors, returns nil with a string describing the error. 301 | 302 | [Back to TOC](#table-of-contents) 303 | 304 | ### get_reused_times 305 | 306 | **syntax:** `times, err = hp:get_reused_times()` 307 | 308 | This method returns the (successfully) reused times for the current connection. In case of error, it returns `nil` and a string describing the error. 309 | 310 | If the current connection does not come from the built-in connection pool, then this method always returns `0`, that is, the connection has never been reused (yet). If the connection comes from the connection pool, then the return value is always non-zero. So this method can also be used to determine if the current connection comes from the pool. 311 | 312 | [Back to TOC](#table-of-contents) 313 | 314 | ### close 315 | 316 | **syntax:** `ok, err = hp:close()` 317 | 318 | Closes the current connection and returns the status. 319 | 320 | In case of success, returns `1`. In case of errors, returns `nil` with a string describing the error. 321 | 322 | [Back to TOC](#table-of-contents) 323 | 324 | 325 | ## Requesting 326 | 327 | ### request 328 | 329 | **syntax:** `res, err = hp:request(opts?)` 330 | 331 | **syntax:** `res, err = hp:request(host, port, opts?)` 332 | 333 | **syntax:** `res, err = hp:request("unix:/path/to/unix-domain.socket", opts?)` 334 | 335 | The `opts` table accepts the following fields: 336 | 337 | * `version`: Sets the HTTP version. Use `10` for HTTP/1.0 and `11` for HTTP/1.1. Defaults to `11`. 338 | * `method`: The HTTP method string. Defaults to `GET`. 339 | * `path`: The path string. Default to `/`. 340 | * `query`: Specifies query parameters. Accepts either a string or a Lua table. 341 | * `headers`: A table of request headers. Accepts a Lua table. 342 | * `body`: The request body as a string, or an iterator function. 343 | * `read_timeout`: Sets the timeout in milliseconds for network read operations specially. 344 | * `send_timeout`: Sets the timeout in milliseconds for network send operations specially. 345 | * `stream`: If set to `true`, return an iterable `res.body_reader` object instead of `res.body`. 346 | * `maxsize`: Sets the maximum size in bytes to fetch. A response body larger than this will cause the fucntion to return a `exceeds maxsize` error. Defaults to nil which means no limit. 347 | * `ssl_verify`: A Lua boolean value to control whether to perform SSL verification. 348 | 349 | When the request is successful, `res` will contain the following fields: 350 | 351 | * `res.status` (number): The resonse status, e.g. 200 352 | * `res.headers` (table): A Lua table with response headers. 353 | * `res.body` (string): The plain response body. 354 | * `res.body_reader` (function): An iterator function for reading the body in a streaming fashion. 355 | * `res.pipe` (httpipe): A new http pipe which use the current `body_reader` as input body by default. 356 | 357 | **Note** All headers (request and response) are noramlized for capitalization - e.g., Accept-Encoding, ETag, Foo-Bar, Baz - in the normal HTTP "standard." 358 | 359 | In case of errors, returns nil with a string describing the error. 360 | 361 | [Back to TOC](#table-of-contents) 362 | 363 | ### request_uri 364 | 365 | **syntax:** `res, err = hp:request_uri(uri, opts?)` 366 | 367 | The simple interface. Options supplied in the `opts` table are the same as in the generic interface, and will override components found in the uri itself. 368 | 369 | Returns a res object as same as `hp:request` method. 370 | 371 | In case of errors, returns nil with a string describing the error. 372 | 373 | [Back to TOC](#table-of-contents) 374 | 375 | ### res.body_reader 376 | 377 | The `body_reader` iterator can be used to stream the response body in chunk sizes of your choosing, as follows: 378 | 379 | ````lua 380 | local reader = res.body_reader 381 | 382 | repeat 383 | local chunk, err = reader(8192) 384 | if err then 385 | ngx.log(ngx.ERR, err) 386 | break 387 | end 388 | 389 | if chunk then 390 | -- process 391 | end 392 | until not chunk 393 | ```` 394 | 395 | [Back to TOC](#table-of-contents) 396 | 397 | ### send_request 398 | 399 | **syntax:** `ok, err = hp:send_request(opts?)` 400 | 401 | In case of errors, returns nil with a string describing the error. 402 | 403 | [Back to TOC](#table-of-contents) 404 | 405 | ### read_response 406 | 407 | **syntax:** `local res, err = hp:read_response(callback?)` 408 | 409 | The `callback` table accepts the following fields: 410 | 411 | * `header_filter`: A callback function for response headers filter 412 | 413 | ````lua 414 | local res, err = hp:read_response{ 415 | header_filter = function (status, headers) 416 | if status == 200 then 417 | return 1 418 | end 419 | end } 420 | ```` 421 | 422 | * `body_filter`: A callback function for response body filter 423 | 424 | ````lua 425 | local res, err = hp:read_response{ 426 | body_filter = function (chunk) 427 | ngx.print(chunk) 428 | end 429 | } 430 | ```` 431 | 432 | Additionally there is no ability to stream the response body in this method. If the response is successful, res will contain the following fields: `res.status`, `res.headers`, `res.body`. 433 | 434 | **Note** When return true in callback function,filter process will be interrupted. 435 | 436 | In case of errors, returns nil with a string describing the error. 437 | 438 | [Back to TOC](#table-of-contents) 439 | 440 | ### read 441 | 442 | **syntax:** `local typ, res, err = hp:read()` 443 | 444 | Streaming parser for the full response. 445 | 446 | The user just needs to call the read method repeatedly until a nil token type is returned. For each token returned from the read method, just check the first return value for the current token type. The token type can be `statusline`, `header`, `header_end`, `body`, `body_end` and `eof`. About the format of `res` value, please refer to the above example. For example, several body tokens holding each body data chunk, so `res` value is equal to the body data chunk. 447 | 448 | In case of errors, returns nil with a string describing the error. 449 | 450 | [Back to TOC](#table-of-contents) 451 | 452 | ### eof 453 | 454 | **syntax:** `local eof = hp:eof()` 455 | 456 | If return `true` indicating already consume all the data; Otherwise, the request there is still no end, you need call `hp:close` to close the connection forcibly. 457 | 458 | [Back to TOC](#table-of-contents) 459 | 460 | ## Utility 461 | 462 | ### parse_uri 463 | 464 | **syntax:** `local scheme, host, port, path, args = unpack(hp:parse_uri(uri))` 465 | 466 | This is a convenience function allowing one to more easily use the generic interface, when the input data is a URI. 467 | 468 | [Back to TOC](#table-of-contents) 469 | 470 | ### get_client_body_reader 471 | 472 | **syntax:** `reader, err = hp:get_client_body_reader(chunk_size?)` 473 | 474 | Returns an iterator function which can be used to read the downstream client request body in a streaming fashion. For example: 475 | 476 | ```lua 477 | local req_reader = hp:get_client_body_reader() 478 | 479 | repeat 480 | local chunk, err = req_reader(8192) 481 | if err then 482 | ngx.log(ngx.ERR, err) 483 | break 484 | end 485 | 486 | if chunk then 487 | -- process 488 | end 489 | until not chunk 490 | ``` 491 | 492 | This iterator can also be used as the value for the body field in request params, allowing one to stream the request body into a proxied upstream request. 493 | 494 | ```lua 495 | local client_body_reader, err = hp:get_client_body_reader() 496 | 497 | local res, err = hp:request{ 498 | path = "/helloworld", 499 | body = client_body_reader, 500 | } 501 | ``` 502 | 503 | [Back to TOC](#table-of-contents) 504 | 505 | # Author 506 | 507 | Monkey Zhang , UPYUN Inc. 508 | 509 | Originally started life based on . 510 | 511 | The part of the interface design inspired from . 512 | 513 | Cosocket docs and implementation borrowed from the other lua-resty-* cosocket modules. 514 | 515 | [Back to TOC](#table-of-contents) 516 | 517 | # Copyright and License 518 | 519 | This module is licensed under the 2-clause BSD license. 520 | 521 | Copyright (c) 2015 - 2017, Monkey Zhang , UPYUN Inc. 522 | 523 | All rights reserved. 524 | 525 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 526 | 527 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 528 | 529 | * 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. 530 | 531 | 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. 532 | 533 | [Back to TOC](#table-of-contents) 534 | 535 | # See Also 536 | 537 | * the ngx_lua module: https://github.com/openresty/lua-nginx-module 538 | * OpenResty: https://openresty.org/ 539 | 540 | [Back to TOC](#table-of-contents) 541 | -------------------------------------------------------------------------------- /lib/resty/httpipe.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Monkey Zhang (timebug), UPYUN Inc. 2 | 3 | 4 | local type = type 5 | local error = error 6 | local pairs = pairs 7 | local ipairs = ipairs 8 | local rawset = rawset 9 | local rawget = rawget 10 | local sub = string.sub 11 | local gsub = string.gsub 12 | local find = string.find 13 | local tostring = tostring 14 | local tonumber = tonumber 15 | local tcp = ngx.socket.tcp 16 | local match = string.match 17 | local upper = string.upper 18 | local lower = string.lower 19 | local concat = table.concat 20 | local insert = table.insert 21 | local format = string.format 22 | local setmetatable = setmetatable 23 | local ngx_re_match = ngx.re.match 24 | local encode_args = ngx.encode_args 25 | local ngx_req_socket = ngx.req.socket 26 | local ngx_req_get_headers = ngx.req.get_headers 27 | 28 | 29 | local _M = { _VERSION = "0.05" } 30 | 31 | -------------------------------------- 32 | -- LOCAL CONSTANTS -- 33 | -------------------------------------- 34 | 35 | local mt = { __index = _M } 36 | 37 | local HTTP = { 38 | [11] = " HTTP/1.1\r\n", 39 | [10] = " HTTP/1.0\r\n" 40 | } 41 | 42 | local PORT = { 43 | http = 80, 44 | https = 443 45 | } 46 | 47 | local USER_AGENT = "Resty/HTTPipe-" .. _M._VERSION 48 | 49 | local STATE_NOT_READY = 0 50 | local STATE_BEGIN = 1 51 | local STATE_READING_HEADER = 2 52 | local STATE_READING_BODY = 3 53 | local STATE_EOF = 4 54 | 55 | local common_headers = { 56 | "Cache-Control", 57 | "Content-Length", 58 | "Content-Type", 59 | "Date", 60 | "ETag", 61 | "Expires", 62 | "Host", 63 | "Location", 64 | "User-Agent" 65 | } 66 | 67 | for _, key in ipairs(common_headers) do 68 | rawset(common_headers, key, key) 69 | rawset(common_headers, lower(key), key) 70 | end 71 | 72 | local state_handlers 73 | 74 | -------------------------------------- 75 | -- HTTP BASE FUNCTIONS -- 76 | -------------------------------------- 77 | 78 | local function normalize_header(key) 79 | local val = common_headers[key] 80 | if val then 81 | return val 82 | end 83 | key = lower(key) 84 | val = common_headers[lower(key)] 85 | if val then 86 | return val 87 | end 88 | 89 | key = gsub(key, "^%l", upper) 90 | key = gsub(key, "-%l", upper) 91 | return key 92 | end 93 | 94 | 95 | local function req_header(self, opts) 96 | self.method = upper(opts.method or "GET") 97 | 98 | local req = { 99 | self.method, 100 | " " 101 | } 102 | 103 | local path = opts.path 104 | if type(path) ~= "string" then 105 | path = "/" 106 | elseif sub(path, 1, 1) ~= "/" then 107 | path = "/" .. path 108 | end 109 | insert(req, path) 110 | 111 | if type(opts.query) == "table" then 112 | opts.query = encode_args(opts.query) 113 | end 114 | 115 | if type(opts.query) == "string" then 116 | insert(req, "?" .. opts.query) 117 | end 118 | 119 | insert(req, HTTP[opts.version]) 120 | 121 | opts.headers = opts.headers or {} 122 | 123 | local headers = {} 124 | 125 | for k, v in pairs(opts.headers) do 126 | headers[normalize_header(k)] = v 127 | end 128 | 129 | if type(opts.body) == "string" then 130 | headers["Content-Length"] = #opts.body 131 | elseif self.previous.content_length and 132 | self.previous.content_length >= 0 then 133 | headers["Content-Length"] = self.previous.content_length 134 | end 135 | 136 | if type(opts.body) == "function" and 137 | not headers["Content-Length"] and not headers["Transfer-Encoding"] then 138 | headers["Transfer-Encoding"] = "chunked" 139 | end 140 | 141 | if not headers["Host"] then 142 | headers["Host"] = self.host 143 | end 144 | 145 | if not headers["User-Agent"] then 146 | headers["User-Agent"] = USER_AGENT 147 | end 148 | 149 | if not headers["Accept"] then 150 | headers["Accept"] = "*/*" 151 | end 152 | 153 | if opts.version == 10 and not headers["Connection"] then 154 | headers["Connection"] = "keep-alive" 155 | end 156 | 157 | for key, values in pairs(headers) do 158 | if type(values) ~= "table" then 159 | values = { values } 160 | end 161 | 162 | key = tostring(key) 163 | for _, value in pairs(values) do 164 | insert(req, key .. ": " .. tostring(value) .. "\r\n") 165 | end 166 | end 167 | 168 | insert(req, "\r\n") 169 | 170 | return concat(req), headers 171 | end 172 | 173 | 174 | -- local scheme, host, port, path, args = unpack(_M:parse_uri(uri)) 175 | function _M.parse_uri(self, uri) 176 | local r = [[^(https?)://([^:/]+)(?::(\d+))?(.*)]] 177 | local m, err = ngx_re_match(uri, r, "jo") 178 | if not m then 179 | return nil, err or "bad uri" 180 | end 181 | 182 | if not m[3] then m[3] = PORT[m[1]] end 183 | if not m[4] then 184 | m[4] = "/" 185 | else 186 | local raw = m[4] 187 | local from = find(raw, "?") 188 | if from then 189 | m[4] = raw:sub(1, from - 1) -- path 190 | m[5] = raw:sub(from + 1) -- args 191 | end 192 | end 193 | 194 | return m 195 | end 196 | 197 | 198 | -------------------------------------- 199 | -- HTTP PIPE FUNCTIONS -- 200 | -------------------------------------- 201 | 202 | -- local hp, err = _M:new(chunk_size?, sock?) 203 | function _M.new(self, chunk_size, sock) 204 | local state = STATE_NOT_READY 205 | 206 | if not sock then 207 | local s, err = tcp() 208 | if not s then 209 | return nil, err 210 | end 211 | sock = s 212 | end 213 | 214 | return setmetatable({ 215 | sock = sock, 216 | chunk_size = chunk_size or 8192, 217 | total_size = 0, 218 | state = state, 219 | chunked = false, 220 | keepalive = true, 221 | _eof = false, 222 | previous = {}, 223 | }, mt) 224 | end 225 | 226 | 227 | -- local ok, err = _M:set_timeout(time) 228 | function _M.set_timeout(self, time) 229 | local sock = self.sock 230 | if not sock then 231 | return nil, "not initialized" 232 | end 233 | 234 | return sock:settimeout(time) 235 | end 236 | 237 | 238 | -- local session, err = _M:ssl_handshake(self, ...) 239 | function _M.ssl_handshake(self, ...) 240 | local sock = self.sock 241 | if not sock then 242 | return nil, "not initialized" 243 | end 244 | 245 | if not ngx.config 246 | or not ngx.config.ngx_lua_version 247 | or ngx.config.ngx_lua_version < 9011 248 | then 249 | error("ngx_lua 0.9.11+ required") 250 | end 251 | 252 | return sock:sslhandshake(...) 253 | end 254 | 255 | 256 | -- local ok, err = _M:connect(self, host, port, opts?) 257 | -- local ok, err = _M:connect(self, "unix:/path/to/unix-domain.socket", opts?) 258 | function _M.connect(self, ...) 259 | local sock = self.sock 260 | if not sock then 261 | return nil, "not initialized" 262 | end 263 | 264 | self.host = select(1, ...) 265 | if sub(self.host, 1, 5) == "unix:" then 266 | -- https://tools.ietf.org/html/rfc2616#section-14.23 267 | -- A client MUST include a Host header field in all HTTP/1.1 request 268 | -- messages . If the requested URI does not include an Internet host 269 | -- name for the service being requested, then the Host header field MUST 270 | -- be given with an empty value. 271 | self.host = "" 272 | end 273 | 274 | self.port = select(2, ...) 275 | if type(self.port) == "string" then 276 | self.port = tonumber(self.port) 277 | elseif type(self.port) ~= "number" then 278 | self.port = nil 279 | end 280 | 281 | return sock:connect(...) 282 | end 283 | 284 | 285 | local function discard_line(self) 286 | local read_line = self.read_line 287 | 288 | local line, err = read_line() 289 | if not line then 290 | return nil, err 291 | end 292 | 293 | return 1 294 | end 295 | 296 | 297 | local function should_receive_body(method, code) 298 | if method == "HEAD" then return nil end 299 | if code == 204 or code == 304 then return nil end 300 | if code >= 100 and code < 200 then return nil end 301 | return true 302 | end 303 | 304 | 305 | local function read_body_part(self) 306 | if not self.is_req_socket and 307 | not should_receive_body(self.method, self.status_code) then 308 | self.state = STATE_EOF 309 | return 'body_end', nil 310 | end 311 | 312 | local sock = self.sock 313 | local remaining = self.remaining 314 | local chunk_size = self.chunk_size 315 | 316 | if self.maxsize and remaining and remaining > self.maxsize then 317 | return nil, nil, "exceeds maxsize" 318 | end 319 | 320 | if self.chunked == true and 321 | (remaining == nil or remaining == 0) then 322 | local read_line = self.read_line 323 | local data, err = read_line() 324 | 325 | if err then 326 | return nil, nil, err 327 | end 328 | 329 | if data == "" then 330 | data, err = read_line() 331 | if err then 332 | return nil, nil, err 333 | end 334 | end 335 | 336 | if data then 337 | if data == "0" then 338 | local ok, err = discard_line(self) 339 | if not ok then 340 | return nil, nil, err 341 | end 342 | 343 | self.state = STATE_EOF 344 | return 'body_end', nil 345 | else 346 | local length = tonumber(data, 16) 347 | remaining = length 348 | end 349 | end 350 | end 351 | 352 | if remaining == 0 then 353 | self.state = STATE_EOF 354 | return 'body_end', nil 355 | end 356 | 357 | if remaining ~= nil and remaining < chunk_size then 358 | chunk_size = remaining 359 | end 360 | 361 | local chunk, err, partial = sock:receive(chunk_size) 362 | 363 | local data = "" 364 | if not err then 365 | if chunk then 366 | data = chunk 367 | end 368 | elseif err == "closed" then 369 | self.state = STATE_EOF 370 | if partial then 371 | chunk_size = #partial 372 | if remaining and remaining - chunk_size ~= 0 then 373 | return nil, partial, err 374 | end 375 | 376 | data = partial 377 | else 378 | return 'body_end', nil 379 | end 380 | else 381 | return nil, nil, err 382 | end 383 | 384 | if remaining ~= nil then 385 | self.remaining = remaining - chunk_size 386 | self.total_size = self.total_size + chunk_size 387 | 388 | if self.maxsize and self.total_size > self.maxsize then 389 | return nil, nil, "exceeds maxsize" 390 | end 391 | end 392 | 393 | return 'body', data 394 | end 395 | 396 | 397 | local function read_header_part(self) 398 | local read_line = self.read_line 399 | 400 | local line, err = read_line() 401 | if not line then 402 | return nil, nil, err 403 | end 404 | 405 | if line == "" then 406 | if self.chunked then 407 | self.remaining = nil 408 | end 409 | 410 | self.state = STATE_READING_BODY 411 | return 'header_end', nil 412 | end 413 | 414 | local m, err = ngx_re_match(line, [[^(.+?):\s*(.+)]], "jo") 415 | if not m then 416 | return 'header', line 417 | end 418 | 419 | local name, value = m[1], m[2] 420 | 421 | local vname = lower(name) 422 | if vname == "content-length" then 423 | self.remaining = tonumber(value) 424 | end 425 | 426 | if vname == "transfer-encoding" and value ~= "identity" then 427 | self.chunked = true 428 | end 429 | 430 | if vname == "connection" and value == "close" then 431 | self.keepalive = value ~= "close" 432 | end 433 | 434 | return 'header', { normalize_header(name), value, line } 435 | end 436 | 437 | 438 | local function read_statusline(self) 439 | local sock = self.sock 440 | if self.read_line == nil then 441 | local rl, err = sock:receiveuntil("\n") 442 | if not rl then 443 | return nil, nil, err 444 | end 445 | self.read_line = function() 446 | --[[ 447 | The line terminator for message-header fields is the sequence CRLF. 448 | However, we recommend that applications, when parsing such headers, 449 | recognize a single LF as a line terminator and ignore the leading 450 | CR. 451 | REF: http://stackoverflow.com/questions/5757290/http-header-line-break-style 452 | --]] 453 | local data, err = rl() 454 | if data and data:sub(-1) == "\r" then 455 | data = data:sub(1, -2) 456 | end 457 | 458 | return data, err 459 | end 460 | end 461 | 462 | local read_line = self.read_line 463 | 464 | local line, err = read_line() 465 | if not line then 466 | return nil, nil, err 467 | end 468 | 469 | local version, status = match(line, "HTTP/(%d*%.%d*) (%d%d%d)") 470 | if not version or not status then 471 | -- return nil, nil, "not match statusline" 472 | return nil, nil, line 473 | end 474 | 475 | version = tonumber(version) * 10 476 | if version < 11 then 477 | self.keepalive = false 478 | end 479 | 480 | self.status_code = tonumber(status) 481 | 482 | if self.status_code == 100 then 483 | local ok, err = discard_line(self) 484 | if not ok then 485 | return nil, nil, err 486 | end 487 | 488 | self.state = STATE_BEGIN 489 | else 490 | self.state = STATE_READING_HEADER 491 | end 492 | 493 | return 'statusline', status 494 | end 495 | 496 | 497 | -- local ok, err = _M:set_keepalive(...) 498 | function _M.set_keepalive(self, ...) 499 | local sock = self.sock 500 | if not sock then 501 | return nil, "not initialized" 502 | end 503 | 504 | self._eof = true 505 | 506 | if self.keepalive then 507 | return sock:setkeepalive(...) 508 | end 509 | 510 | return sock:close() 511 | end 512 | 513 | 514 | -- local times, err = _M:get_reused_times() 515 | function _M.get_reused_times(self) 516 | local sock = self.sock 517 | if not sock then 518 | return nil, "not initialized" 519 | end 520 | 521 | return sock:getreusedtimes() 522 | end 523 | 524 | 525 | -- local ok, err = _M:close() 526 | function _M.close(self) 527 | local sock = self.sock 528 | if not sock then 529 | return nil, "not initialized" 530 | end 531 | 532 | self._eof = true 533 | 534 | return sock:close() 535 | end 536 | 537 | 538 | local function read_eof(self) 539 | self:set_keepalive() 540 | return 'eof', nil 541 | end 542 | 543 | 544 | -- local typ, res, err = _M:read(chunk_size?) 545 | function _M.read(self, chunk_size) 546 | local sock = self.sock 547 | if not sock then 548 | return nil, nil, "not initialized" 549 | end 550 | 551 | if chunk_size then 552 | self.chunk_size = chunk_size 553 | end 554 | 555 | if self.state == STATE_NOT_READY then 556 | return nil, nil, "not ready" 557 | end 558 | 559 | if self.read_timeout then 560 | sock:settimeout(self.read_timeout) 561 | end 562 | 563 | local handler = state_handlers[self.state] 564 | if handler then 565 | return handler(self) 566 | end 567 | 568 | return nil, nil, "bad state: " .. self.state 569 | end 570 | 571 | 572 | -- local eof = _M:eof() 573 | function _M.eof(self) 574 | return self._eof 575 | end 576 | 577 | 578 | state_handlers = { 579 | read_statusline, 580 | read_header_part, 581 | read_body_part, 582 | read_eof 583 | } 584 | 585 | 586 | -- local res, err = _M:read_response(callback?) 587 | function _M.read_response(self, ...) 588 | local sock = self.sock 589 | if not sock then 590 | return nil, "not initialized" 591 | end 592 | 593 | local callback = ... 594 | if type(callback) ~= "table" then 595 | callback = {} 596 | end 597 | 598 | local status 599 | local headers = {} 600 | local chunks = {} 601 | 602 | while not self._eof do 603 | local typ, res, err = self:read() 604 | if not typ then 605 | return nil, err 606 | end 607 | 608 | if typ == 'statusline' then 609 | status = tonumber(res) 610 | end 611 | 612 | if typ == 'header' then 613 | if type(res) == "table" then 614 | local key = res[1] 615 | if headers[key] then 616 | if type(headers[key]) ~= "table" then 617 | headers[key] = { headers[key] } 618 | end 619 | insert(headers[key], tostring(res[2])) 620 | else 621 | headers[key] = tostring(res[2]) 622 | end 623 | end 624 | end 625 | 626 | if typ == 'header_end' then 627 | if callback.header_filter then 628 | local rc = callback.header_filter(status, headers) 629 | if rc then break end 630 | end 631 | end 632 | 633 | if typ == 'body' then 634 | if callback.body_filter then 635 | local rc = callback.body_filter(res) 636 | if rc then break end 637 | else 638 | insert(chunks, res) 639 | end 640 | end 641 | 642 | if typ == 'eof' then 643 | break 644 | end 645 | end 646 | 647 | return { status = status, headers = headers, body = concat(chunks) } 648 | end 649 | 650 | 651 | -- local ok, err = _M:send_request(opts?) 652 | function _M.send_request(self, opts) 653 | local sock = self.sock 654 | if not sock then 655 | return nil, "not initialized" 656 | end 657 | 658 | if opts.send_timeout then 659 | sock:settimeout(opts.send_timeout) 660 | end 661 | 662 | if opts.read_timeout then 663 | self.read_timeout = opts.read_timeout 664 | end 665 | 666 | if opts.version and not HTTP[opts.version] then 667 | return nil, "unknown HTTP version" 668 | else 669 | opts.version = opts.version or 11 670 | end 671 | 672 | local req, headers = req_header(self, opts) 673 | local bytes, err = sock:send(req) 674 | if not bytes then 675 | return nil, err 676 | end 677 | 678 | if type(opts.body) == "string" then 679 | local bytes, err = sock:send(opts.body) 680 | if not bytes then 681 | return nil, err 682 | end 683 | elseif type(opts.body) == "function" then 684 | local chunked = headers["Transfer-Encoding"] == "chunked" 685 | repeat 686 | local chunk, err = opts.body() 687 | if chunk then 688 | local size = #chunk 689 | if chunked and size > 0 then 690 | chunk = concat({ 691 | format("%X", size), "\r\n", 692 | chunk, "\r\n", 693 | }) 694 | end 695 | local bytes, err = sock:send(chunk) 696 | if not bytes then 697 | return nil, err 698 | end 699 | elseif err then 700 | return nil, err 701 | end 702 | until not chunk 703 | if chunked then 704 | local bytes, err = sock:send("0\r\n\r\n") 705 | if not bytes then 706 | return nil ,err 707 | end 708 | end 709 | end 710 | 711 | self.maxsize = opts.maxsize 712 | self.state = STATE_BEGIN 713 | 714 | return 1 715 | end 716 | 717 | 718 | local function get_body_reader(self) 719 | return function (chunk_size) 720 | local typ, res, err = self:read(chunk_size) 721 | if not typ then 722 | return nil, err 723 | end 724 | 725 | if typ == 'body' then 726 | return res 727 | end 728 | 729 | if typ == 'body_end' then 730 | if not self.is_req_socket then 731 | read_eof(self) 732 | end 733 | end 734 | 735 | return 736 | end 737 | end 738 | 739 | 740 | -- local res, err = _M:request(opts?) 741 | -- local res, err = _M:request(host, port, opts?) 742 | -- local res, err = _M:request("unix:/path/to/unix-domain.socket", opts?) 743 | function _M.request(self, ...) 744 | local sock = self.sock 745 | if not sock then 746 | return nil, "not initialized" 747 | end 748 | 749 | local arguments = {...} 750 | 751 | local n = #arguments 752 | if n > 3 then 753 | return nil, "expecting 0, 1, 2, or 3 arguments, but seen " .. 754 | tostring(n) 755 | end 756 | 757 | local opts = {} 758 | if type(arguments[n]) == "table" then 759 | opts = arguments[n] 760 | arguments[n] = nil 761 | end 762 | 763 | if not opts.headers then 764 | opts.headers = {} 765 | end 766 | 767 | if n > 0 and arguments[1] then 768 | local rc, err = self:connect(unpack(arguments)) 769 | if not rc then 770 | return nil, err 771 | end 772 | 773 | if self.port then 774 | if opts.ssl_enable and self.port ~= 443 then 775 | self.host = self.host .. ":" .. tostring(self.port) 776 | elseif not opts.ssl_enable and self.port ~= 80 then 777 | self.host = self.host .. ":" .. tostring(self.port) 778 | end 779 | end 780 | 781 | if opts.ssl_enable then 782 | local server_name = opts.headers["Host"] or self.host 783 | if opts.ssl_server_name then 784 | server_name = opts.ssl_server_name 785 | end 786 | 787 | local ssl_verify = true 788 | if opts.ssl_verify == false then 789 | ssl_verify = false 790 | end 791 | 792 | local ok, err = self:ssl_handshake(nil, server_name, ssl_verify) 793 | if not ok then 794 | return nil, err 795 | end 796 | end 797 | end 798 | 799 | local prev = self.previous.pipe 800 | if self.previous.body_reader then 801 | opts.body = self.previous.body_reader 802 | end 803 | 804 | local ok, err = self:send_request(opts) 805 | if not ok then 806 | if prev then prev:close() end 807 | return nil, err 808 | end 809 | 810 | local res, err = self:read_response{ 811 | header_filter = function (status, headers) 812 | return opts.stream 813 | end 814 | } 815 | 816 | if res and opts.stream then 817 | res.body_reader = get_body_reader(self) 818 | 819 | local size = tonumber(res.headers["Content-Length"]) or -1 -- -1:chunked 820 | local pipe, err = self:new(self.chunk_size) 821 | if not pipe then 822 | return nil, err 823 | end 824 | 825 | pipe.previous.pipe = self 826 | pipe.previous.content_length = size 827 | pipe.previous.body_reader = res.body_reader 828 | 829 | res.pipe = pipe 830 | end 831 | 832 | return res, err 833 | end 834 | 835 | 836 | -- local res, err = _M:request_uri(uri, opts?) 837 | function _M.request_uri(self, uri, opts) 838 | if not opts then 839 | opts = {} 840 | end 841 | 842 | local parsed_uri, err = self:parse_uri(uri) 843 | if not parsed_uri then 844 | return nil, err 845 | end 846 | 847 | local scheme, host, port, path, args = unpack(parsed_uri) 848 | if not opts.path then 849 | opts.path = path 850 | end 851 | 852 | if not opts.query then 853 | opts.query = args 854 | end 855 | 856 | if scheme == "https" then 857 | opts.ssl_enable = true 858 | end 859 | 860 | return self:request(host, port, opts) 861 | end 862 | 863 | 864 | -- local reader, err = _M:get_client_body_reader(chunk_size?) 865 | function _M.get_client_body_reader(self, chunk_size) 866 | local sock, err = ngx_req_socket() 867 | 868 | if not sock then 869 | return nil, err 870 | end 871 | 872 | local hp = self:new(chunk_size, sock) 873 | local headers = ngx_req_get_headers() 874 | 875 | hp.chunked = headers["Transfer-Encoding"] == "chunked" 876 | if not hp.chunked then 877 | hp.remaining = tonumber(headers["Content-Length"]) 878 | end 879 | 880 | hp.state = STATE_READING_BODY 881 | hp.is_req_socket = 1 882 | 883 | return get_body_reader(hp) 884 | end 885 | 886 | 887 | return _M 888 | --------------------------------------------------------------------------------