# vim:set ft= ts=4 sw=4 et fdm=marker:

use Test::Nginx::Socket::Lua;

repeat_each(2);

plan tests => repeat_each() * (blocks() * 3 + 9);

our $HtmlDir = html_dir;

#$ENV{TEST_NGINX_MEMCACHED_PORT} ||= 11211;

no_long_string();
#no_diff();
#log_level 'warn';
no_shuffle();

run_tests();

__DATA__

=== TEST 1: sanity
--- config
    location /t {
        content_by_lua '
            local sock, err = ngx.req.socket()
            if sock then
                ngx.say("got the request socket")
            else
                ngx.say("failed to get the request socket: ", err)
            end

            for i = 1, 3 do
                local data, err, part = sock:receive(5)
                if data then
                    ngx.say("received: ", data)
                else
                    ngx.say("failed to receive: ", err, " [", part, "]")
                end
            end
        ';
    }
--- request
POST /t
hello world
--- response_body
got the request socket
received: hello
received:  worl
failed to receive: closed [d]
--- no_error_log
[error]



=== TEST 2: multipart rfc sample (just partial streaming)
--- config
    location /t {
        content_by_lua '
            local sock, err = ngx.req.socket()
            if sock then
                ngx.say("got the request socket")
            else
                ngx.say("failed to get the request socket: ", err)
            end

            local boundary
            local header = ngx.var.http_content_type
            local m = ngx.re.match(header, [[; +boundary=(?:"(.*?)"|(\\w+))]], "jo")
            if m then
                boundary = m[1] or m[2]

            else
                ngx.say("invalid content-type header")
                return
            end

            local read_to_boundary = sock:receiveuntil("\\r\\n--" .. boundary)
            local read_line = sock:receiveuntil("\\r\\n")

            local data, err, part = read_to_boundary()
            if data then
                ngx.say("preamble: [" .. data .. "]")
            else
                ngx.say("failed to read the first boundary: ", err)
                return
            end

            local i = 1
            while true do
                local line, err = read_line()

                if not line then
                    ngx.say("failed to read post-boundary line: ", err)
                    return
                end

                m = ngx.re.match(line, "--$", "jo")
                if m then
                    ngx.say("found the end of the stream")
                    return
                end

                while true do
                    local line, err = read_line()
                    if not line then
                        ngx.say("failed to read part ", i, " header: ", err)
                        return
                    end

                    if line == "" then
                        -- the header part completes
                        break
                    end

                    ngx.say("part ", i, " header: [", line, "]")
                end

                local data, err, part = read_to_boundary()
                if data then
                    ngx.say("part ", i, " body: [" .. data .. "]")
                else
                    ngx.say("failed to read part ", i + 1, " boundary: ", err)
                    return
                end

                i = i + 1
            end
        ';
    }
--- request eval
"POST /t
This is the preamble.  It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.\r
--simple boundary\r
\r
This is implicitly typed plain ASCII text.
It does NOT end with a linebreak.\r
--simple boundary\r
Content-type: text/plain; charset=us-ascii\r
\r
This is explicitly typed plain ASCII text.
It DOES end with a linebreak.
\r
--simple boundary--\r
This is the epilogue.  It is also to be ignored.
"
--- more_headers
Content-Type: multipart/mixed; boundary="simple boundary"
--- response_body
got the request socket
preamble: [This is the preamble.  It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.]
part 1 body: [This is implicitly typed plain ASCII text.
It does NOT end with a linebreak.]
part 2 header: [Content-type: text/plain; charset=us-ascii]
part 2 body: [This is explicitly typed plain ASCII text.
It DOES end with a linebreak.
]
found the end of the stream
--- no_error_log
[error]



=== TEST 3: multipart rfc sample (completely streaming)
--- config
    location /t {
        content_by_lua '
            local sock, err = ngx.req.socket()
            if sock then
                ngx.say("got the request socket")
            else
                ngx.say("failed to get the request socket: ", err)
            end

            local boundary
            local header = ngx.var.http_content_type
            local m = ngx.re.match(header, [[; +boundary=(?:"(.*?)"|(\\w+))]], "jo")
            if m then
                boundary = m[1] or m[2]

            else
                ngx.say("invalid content-type header")
                return
            end

            local read_to_boundary = sock:receiveuntil("\\r\\n--" .. boundary)
            local read_line = sock:receiveuntil("\\r\\n")

            local preamble = ""
            while true do
                local data, err, part = read_to_boundary(1)
                if data then
                    preamble = preamble .. data

                elseif not err then
                    break

                else
                    ngx.say("failed to read the first boundary: ", err)
                    return
                end
            end

            ngx.say("preamble: [" .. preamble .. "]")

            local i = 1
            while true do
                local line, err = read_line(50)

                if not line and err then
                    ngx.say("1: failed to read post-boundary line: ", err)
                    return
                end

                if line then
                    local dummy
                    dummy, err = read_line(1)
                    if err then
                        ngx.say("2: failed to read post-boundary line: ", err)
                        return
                    end

                    if dummy then
                        ngx.say("bad post-boundary line: ", dummy)
                        return
                    end

                    m = ngx.re.match(line, "--$", "jo")
                    if m then
                        ngx.say("found the end of the stream")
                        return
                    end
                end

                while true do
                    local line, err = read_line(50)
                    if not line and err then
                        ngx.say("failed to read part ", i, " header: ", err)
                        return
                    end

                    if line then
                        local line, err = read_line(1)
                        if line or err then
                            ngx.say("error")
                            return
                        end
                    end

                    if line == "" then
                        -- the header part completes
                        break
                    end

                    ngx.say("part ", i, " header: [", line, "]")
                end

                local body = ""

                while true do
                    local data, err, part = read_to_boundary(1)
                    if data then
                        body = body .. data

                    elseif err then
                        ngx.say("failed to read part ", i + 1, " boundary: ", err)
                        return

                    else
                        break
                    end
                end

                ngx.say("part ", i, " body: [" .. body .. "]")

                i = i + 1
            end
        ';
    }
--- request eval
"POST /t
This is the preamble.  It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.\r
--simple boundary\r
\r
This is implicitly typed plain ASCII text.
It does NOT end with a linebreak.\r
--simple boundary\r
Content-type: text/plain; charset=us-ascii\r
\r
This is explicitly typed plain ASCII text.
It DOES end with a linebreak.
\r
--simple boundary--\r
This is the epilogue.  It is also to be ignored.
"
--- more_headers
Content-Type: multipart/mixed; boundary="simple boundary"
--- response_body
got the request socket
preamble: [This is the preamble.  It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.]
part 1 body: [This is implicitly typed plain ASCII text.
It does NOT end with a linebreak.]
part 2 header: [Content-type: text/plain; charset=us-ascii]
part 2 body: [This is explicitly typed plain ASCII text.
It DOES end with a linebreak.
]
found the end of the stream
--- no_error_log
[error]



=== TEST 4: attempt to use the req socket across request boundary
--- http_config eval
    "lua_package_path '$::HtmlDir/?.lua;./?.lua';"
--- config
    location /t {
        content_by_lua '
            local test = require "test"
            test.go()
            ngx.say("done")
        ';
    }
--- user_files
>>> test.lua
module("test", package.seeall)

local sock, err

function go()
    if not sock then
        sock, err = ngx.req.socket()
        if sock then
            ngx.say("got the request socket")
        else
            ngx.say("failed to get the request socket: ", err)
        end
    else
        for i = 1, 3 do
            local data, err, part = sock:receive(5)
            if data then
                ngx.say("received: ", data)
            else
                ngx.say("failed to receive: ", err, " [", part, "]")
            end
        end
    end
end
--- request
POST /t
hello world
--- response_body_like
(?:got the request socket
|failed to receive: closed [d]
)?done
--- no_error_log
[alert]



=== TEST 5: receive until on request_body - receiveuntil(1) on the last byte of the body
See https://groups.google.com/group/openresty/browse_thread/thread/43cf01da3c681aba for details
--- http_config eval
    "lua_package_path '$::HtmlDir/?.lua;./?.lua';"
--- config
    location /t {
        content_by_lua '
            local test = require "test"
            test.go()
            ngx.say("done")
        ';
    }
--- user_files
>>> test.lua
module("test", package.seeall)

function go()
   local sock, err = ngx.req.socket()
   if sock then
      ngx.say("got the request socket")
   else
      ngx.say("failed to get the request socket: ", err)
      return
   end

   local data, err, part = sock:receive(56)
   if data then
      ngx.say("received: ", data)
   else
      ngx.say("failed to receive: ", err, " [", part, "]")
   end

   local discard_line = sock:receiveuntil('\r\n')

   local data, err, part = discard_line(8192)
   if data then
      ngx.say("received len: ", #data)
   else
      ngx.say("failed to receive: ", err, " [", part, "]")
   end

   local data, err, part = discard_line(1)
   if data then
      ngx.say("received: ", data)
   else
      ngx.say("failed to receive: ", err, " [", part, "]")
   end
end
--- request
POST /t
-----------------------------820127721219505131303151179################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################$
--- response_body
got the request socket
received: -----------------------------820127721219505131303151179
received len: 8192
received: $
done
--- no_error_log
[error]
--- timeout: 10



=== TEST 6: pipelined POST requests
--- http_config eval
    "lua_package_path '$::HtmlDir/?.lua;./?.lua';"
--- config
    location /t {
        content_by_lua '
            local test = require "test"
            test.go()
            ngx.say("done")
        ';
    }
--- user_files
>>> test.lua
module("test", package.seeall)

function go()
   local sock, err = ngx.req.socket()
   if sock then
      ngx.say("got the request socket")
   else
      ngx.say("failed to get the request socket: ", err)
      return
   end

   while true do
       local data, err, part = sock:receive(4)
       if data then
          ngx.say("received: ", data)
       else
          ngx.say("failed to receive: ", err, " [", part, "]")
          return
       end
   end
end
--- pipelined_requests eval
["POST /t
hello, world",
"POST /t
hiya, world"]
--- response_body eval
["got the request socket
received: hell
received: o, w
received: orld
failed to receive: closed []
done
",
"got the request socket
received: hiya
received: , wo
failed to receive: closed [rld]
done
"]
--- no_error_log
[error]



=== TEST 7: Expect & 100 Continue
--- config
    location /t {
        content_by_lua '
            local sock, err = ngx.req.socket()
            if sock then
                ngx.say("got the request socket")
            else
                ngx.say("failed to get the request socket: ", err)
                return
            end

            for i = 1, 3 do
                local data, err, part = sock:receive(5)
                if data then
                    ngx.say("received: ", data)
                else
                    ngx.say("failed to receive: ", err, " [", part, "]")
                end
            end
        ';
    }
--- request
POST /t
hello world
--- more_headers
Expect: 100-Continue
--- error_code: 100
--- response_body_like chomp
\breceived: hello\b.*?\breceived:  worl\b
--- no_error_log
[error]



=== TEST 8: pipelined requests, big buffer, small steps
--- config
    location /t {
        lua_socket_buffer_size 5;
        content_by_lua '
            local sock, err = ngx.req.socket()
            if sock then
                ngx.say("got the request socket")
            else
                ngx.say("failed to get the request socket: ", err)
            end

            for i = 1, 6 do
                local data, err, part = sock:receive(2)
                if data then
                    ngx.say("received: ", data)
                else
                    ngx.say("failed to receive: ", err, " [", part, "]")
                end
            end
        ';
    }
--- stap2
M(http-lua-req-socket-consume-preread) {
    println("preread: ", user_string_n($arg2, $arg3))
}

--- pipelined_requests eval
["POST /t
hello world","POST /t
hiya globe"]
--- response_body eval
["got the request socket
received: he
received: ll
received: o 
received: wo
received: rl
failed to receive: closed [d]
","got the request socket
received: hi
received: ya
received:  g
received: lo
received: be
failed to receive: closed []
"]
--- no_error_log
[error]



=== TEST 9: chunked support is still a TODO
--- config
    location /t {
        content_by_lua '
            local sock, err = ngx.req.socket()
            if sock then
                ngx.say("got the request socket")
            else
                ngx.req.read_body()
                ngx.say("failed to get the request socket: ", err)
                return
            end

            for i = 1, 3 do
                local data, err, part = sock:receive(5)
                if data then
                    ngx.say("received: ", data)
                else
                    ngx.say("failed to receive: ", err, " [", part, "]")
                end
            end
        ';
    }
--- raw_request eval
"POST /t HTTP/1.1\r
Host: localhost\r
Transfer-Encoding: chunked\r
Connection: close\r
\r
b\r
hello world\r
0\r
\r
"
--- stap2
/*
F(ngx_http_finalize_request) {
    if ($r->main->count == 2) {
        print_ubacktrace()
    }
}
F(ngx_http_free_request) {
    print_ubacktrace()
}
*/
--- response_body
failed to get the request socket: chunked request bodies not supported yet
--- no_error_log
[error]
[alert]
--- skip_nginx: 4: <1.3.9



=== TEST 10: chunked support in ngx.req.read_body
--- config
    location /t {
        content_by_lua '
            ngx.req.read_body()
            ngx.say(ngx.req.get_body_data())
        ';
    }
--- raw_request eval
"POST /t HTTP/1.1\r
Host: localhost\r
Transfer-Encoding: chunked\r
Connection: close\r
\r
b\r
hello world\r
0\r
\r
"
--- stap2
/*
F(ngx_http_finalize_request) {
    if ($r->main->count == 2) {
        print_ubacktrace()
    }
}
F(ngx_http_free_request) {
    print_ubacktrace()
}
*/
--- response_body
hello world
--- no_error_log
[error]
[alert]
--- skip_nginx: 4: <1.3.9



=== TEST 11: downstream cosocket for GET requests (w/o request bodies)
--- config
    #resolver 8.8.8.8;
    location = /t {
        content_by_lua '
           local sock, err = ngx.req.socket()

           if not sock then
              ngx.say("failed to get socket: ", err)
              return nil
           end

           while true do
              local data, err, partial = sock:receive(4096)

              ngx.log(ngx.INFO, "Received data")

              if err then
                 ngx.say("err: ", err)
                 if partial then
                    ngx.print(partial)
                 end

                 break
              end

              if data then
                 ngx.print(data)
              end
           end
        ';
    }

--- request
GET /t
--- response_body
failed to get socket: no body
--- no_error_log
[error]



=== TEST 12: downstream cosocket for POST requests with 0 size bodies
--- config
    #resolver 8.8.8.8;
    location = /t {
        content_by_lua '
           local sock, err = ngx.req.socket()

           if not sock then
              ngx.say("failed to get socket: ", err)
              return nil
           end

           while true do
              local data, err, partial = sock:receive(4096)

              ngx.log(ngx.INFO, "Received data")

              if err then
                 ngx.say("err: ", err)
                 if partial then
                    ngx.print(partial)
                 end

                 break
              end

              if data then
                 ngx.print(data)
              end
           end
        ';
    }

--- request
POST /t
--- more_headers
Content-Length: 0
--- response_body
failed to get socket: no body
--- no_error_log
[error]



=== TEST 13: failing reread after reading timeout happens
--- config
    location = /t {
        content_by_lua '
            local sock, err = ngx.req.socket()

            if not sock then
               ngx.say("failed to get socket: ", err)
               return nil
            end

            sock:settimeout(100);

            local data, err, partial = sock:receive(4096)
            if err then
               ngx.say("err: ", err, ", partial: ", partial)
            end

            local data, err, partial = sock:receive(4096)
            if err then
               ngx.say("err: ", err, ", partial: ", partial)
               return
            end
        ';
    }

--- raw_request eval
"POST /t HTTP/1.0\r
Host: localhost\r
Content-Length: 10245\r
\r
hello"
--- response_body
err: timeout, partial: hello
err: timeout, partial: 

--- error_log
lua tcp socket read timed out



=== TEST 14: successful reread after reading timeout happens (receive -> receive)
--- config
    location = /t {
        content_by_lua '
            local sock = ngx.socket.tcp()
            local ok, err = sock:connect("127.0.0.1", ngx.var.server_port)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local bytes, err = sock:send("POST /back HTTP/1.0\\r\\nHost: localhost\\r\\nContent-Length: 1024\\r\\n\\r\\nabc")
            if not bytes then
                ngx.say("failed to send: ", err)
            else
                ngx.say("sent: ", bytes)
            end

            ngx.sleep(0.2)

            local bytes, err = sock:send("hello world")
            if not bytes then
                ngx.say("failed to send: ", err)
            else
                ngx.say("sent: ", bytes)
            end

            local reader = sock:receiveuntil("\\r\\n\\r\\n")
            local header, err = reader()
            if not header then
                ngx.say("failed to receive header: ", err)
                return
            end

            for i = 1, 2 do
                local line, err = sock:receive()
                if not line then
                    ngx.say("failed to receive line: ", err)
                    return
                end
                ngx.say("received: ", line)
            end
        ';
    }

    location = /back {
        content_by_lua '
            ngx.send_headers()
            ngx.flush(true)

            local sock, err = ngx.req.socket()

            if not sock then
               ngx.say("failed to get socket: ", err)
               return nil
            end

            sock:settimeout(100);

            local data, err, partial = sock:receive(4096)
            if err then
               ngx.say("err: ", err, ", partial: ", partial)
            else
                ngx.say("received: ", data)
            end

            ngx.sleep(0.1)

            local data, err, partial = sock:receive(11)
            if err then
               ngx.say("err: ", err, ", partial: ", partial)
            else
                ngx.say("received: ", data)
            end
        ';
    }

--- request
GET /t
--- response_body
sent: 65
sent: 11
received: err: timeout, partial: abc
received: received: hello world

--- error_log
lua tcp socket read timed out



=== TEST 15: successful reread after reading timeout happens (receive -> receiveuntil)
--- config
    location = /t {
        content_by_lua '
            local sock = ngx.socket.tcp()
            local ok, err = sock:connect("127.0.0.1", ngx.var.server_port)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local bytes, err = sock:send("POST /back HTTP/1.0\\r\\nHost: localhost\\r\\nContent-Length: 1024\\r\\n\\r\\nabc")
            if not bytes then
                ngx.say("failed to send: ", err)
            else
                ngx.say("sent: ", bytes)
            end

            ngx.sleep(0.2)

            local bytes, err = sock:send("hello world\\n")
            if not bytes then
                ngx.say("failed to send: ", err)
            else
                ngx.say("sent: ", bytes)
            end

            local reader = sock:receiveuntil("\\r\\n\\r\\n")
            local header, err = reader()
            if not header then
                ngx.say("failed to receive header: ", err)
                return
            end

            for i = 1, 2 do
                local line, err = sock:receive()
                if not line then
                    ngx.say("failed to receive line: ", err)
                    return
                end
                ngx.say("received: ", line)
            end
        ';
    }

    location = /back {
        content_by_lua '
            ngx.send_headers()
            ngx.flush(true)

            local sock, err = ngx.req.socket()

            if not sock then
               ngx.say("failed to get socket: ", err)
               return nil
            end

            sock:settimeout(100);

            local data, err, partial = sock:receive(4096)
            if err then
               ngx.say("err: ", err, ", partial: ", partial)
            else
                ngx.say("received: ", data)
            end

            ngx.sleep(0.1)

            local reader = sock:receiveuntil("\\n")
            local data, err, partial = reader()
            if err then
               ngx.say("err: ", err, ", partial: ", partial)
            else
                ngx.say("received: ", data)
            end
        ';
    }

--- request
GET /t
--- response_body
sent: 65
sent: 12
received: err: timeout, partial: abc
received: received: hello world

--- error_log
lua tcp socket read timed out



=== TEST 16: successful reread after reading timeout happens (receiveuntil -> receive)
--- config
    location = /t {
        content_by_lua '
            local sock = ngx.socket.tcp()
            local ok, err = sock:connect("127.0.0.1", ngx.var.server_port)
            if not ok then
                ngx.say("failed to connect: ", err)
                return
            end

            local bytes, err = sock:send("POST /back HTTP/1.0\\r\\nHost: localhost\\r\\nContent-Length: 1024\\r\\n\\r\\nabc")
            if not bytes then
                ngx.say("failed to send: ", err)
            else
                ngx.say("sent: ", bytes)
            end

            ngx.sleep(0.2)

            local bytes, err = sock:send("hello world\\n")
            if not bytes then
                ngx.say("failed to send: ", err)
            else
                ngx.say("sent: ", bytes)
            end

            local reader = sock:receiveuntil("\\r\\n\\r\\n")
            local header, err = reader()
            if not header then
                ngx.say("failed to receive header: ", err)
                return
            end

            for i = 1, 2 do
                local line, err = sock:receive()
                if not line then
                    ngx.say("failed to receive line: ", err)
                    return
                end
                ngx.say("received: ", line)
            end
        ';
    }

    location = /back {
        content_by_lua '
            ngx.send_headers()
            ngx.flush(true)

            local sock, err = ngx.req.socket()

            if not sock then
               ngx.say("failed to get socket: ", err)
               return nil
            end

            sock:settimeout(100);

            local reader = sock:receiveuntil("no-such-terminator")
            local data, err, partial = reader()
            if not data then
               ngx.say("err: ", err, ", partial: ", partial)
            else
                ngx.say("received: ", data)
            end

            ngx.sleep(0.1)

            local data, err, partial = sock:receive()
            if err then
               ngx.say("err: ", err, ", partial: ", partial)
            else
                ngx.say("received: ", data)
            end
        ';
    }

--- request
GET /t
--- response_body
sent: 65
sent: 12
received: err: timeout, partial: abc
received: received: hello world

--- error_log
lua tcp socket read timed out



=== TEST 17: req socket GC'd
--- config
    location /t {
        content_by_lua '
            do
                local sock, err = ngx.req.socket()
                if sock then
                    ngx.say("got the request socket")
                else
                    ngx.say("failed to get the request socket: ", err)
                end
            end
            collectgarbage()
            ngx.log(ngx.WARN, "GC cycle done")

            ngx.say("done")
        ';
    }
--- request
POST /t
hello world
--- response_body
got the request socket
done
--- no_error_log
[error]
--- grep_error_log eval: qr/lua finalize socket|GC cycle done/
--- grep_error_log_out
lua finalize socket
GC cycle done
