1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
-- Test fixture for exercising socket.http.request against a fully
-- controlled, scripted HTTP response -- no Apache, no Docker, no
-- third-party hosts. Built on top of the existing testsrvr.lua remote-
-- execution server (see test/testclnt.lua for the `remote()` protocol this
-- reuses): a control connection sends Lua snippets for testsrvr.lua to
-- `load()` and run, which is how it's told to accept the connection that
-- socket.http.request makes and write raw response bytes back to it.
local socket = require("socket")
local M = {}
M.host = "localhost"
M.port = "8383"
-- Connects to a running testsrvr.lua as a control channel and returns a
-- `remote(fmt, ...)` function that scripts a Lua snippet to run on the
-- server. Mirrors test/testclnt.lua's own remote(): the snippet is
-- squashed onto a single line (embedded newlines become ';', runs of
-- whitespace collapse to one space) since the control channel is
-- line-oriented, and the ack sent back is only proof the server received
-- the command, not that it finished running it -- so a snippet that blocks
-- (e.g. server:accept()) doesn't stall the caller.
function M.connect(host, port)
local control = assert(socket.connect(host or M.host, port or M.port))
control:setoption("tcp-nodelay", true)
local function remote(fmt, ...)
local s = string.format(fmt, ...)
s = string.gsub(s, "\n", ";")
s = string.gsub(s, "%s+", " ")
s = string.gsub(s, "^%s*", "")
assert(control:send(s .. "\n"))
control:receive()
end
return control, remote
end
-- Turns a raw Lua string into a single-line, double-quoted Lua string
-- literal safe to embed in a remote()-scripted snippet: escapes backslash,
-- quote, and every whitespace control character so no byte in the
-- *encoded* text is itself whitespace. remote() collapses whitespace runs
-- and turns newlines into ';', which would otherwise corrupt raw response
-- bytes (status lines/headers/chunked framing all rely on literal CRLF).
local function quote(s)
s = string.gsub(s, "\\", "\\\\")
s = string.gsub(s, "\"", "\\\"")
s = string.gsub(s, "\r", "\\r")
s = string.gsub(s, "\n", "\\n")
s = string.gsub(s, "\t", "\\t")
return "\"" .. s .. "\""
end
-- Appends the script parts for one accept-send-close cycle (see
-- accept_and_send_sequence) onto `parts`.
local function append_connection(parts, chunks)
parts[#parts + 1] = "if data then data:close() data = nil end"
parts[#parts + 1] = "data = server:accept()"
parts[#parts + 1] = "data:setoption(\"tcp-nodelay\", true)"
for _, chunk in ipairs(chunks) do
local body, delay
if type(chunk) == "table" then
body, delay = chunk[1], chunk.delay
else
body = chunk
end
if delay then
parts[#parts + 1] = string.format("socket.sleep(%f)", delay)
end
parts[#parts + 1] = "data:send(" .. quote(body) .. ")"
end
parts[#parts + 1] = "data:close() data = nil"
end
-- Scripts the server to accept one connection (into the shared `data`
-- global, mirroring test/testclnt.lua's reconnect()), write a sequence of
-- raw byte chunks to it, then close it. `chunks` is an array of either
-- plain strings, or {body, delay = seconds} tables -- the delay (via
-- socket.sleep) is applied before sending that chunk, so callers can prove
-- incremental/partial delivery instead of one atomic send.
function M.accept_and_send(remote, chunks)
local parts = {}
append_connection(parts, chunks)
remote(table.concat(parts, "\n"))
end
-- Like accept_and_send, but scripts several accept-send-close cycles as one
-- server-side script sent over a single remote() round trip. Needed for a
-- client call that opens more than one connection in sequence within a
-- single call of its own (e.g. socket.http.request following a redirect):
-- queuing a second accept_and_send for that connection ahead of time would
-- deadlock, since its remote() call can't get an ack until the server
-- finishes the first accept_and_send's blocking accept() -- which itself
-- can't complete until the client makes the very call that's stuck waiting
-- on that ack. `connections` is an array of `chunks` arrays, one per
-- connection, handled in order.
function M.accept_and_send_sequence(remote, connections)
local parts = {}
for _, chunks in ipairs(connections) do
append_connection(parts, chunks)
end
remote(table.concat(parts, "\n"))
end
-- Builds a raw HTTP/1.1 response (status line + headers + body) as a
-- single CRLF-framed string, ready to hand to accept_and_send.
function M.response(status, headers, body)
local lines = { "HTTP/1.1 " .. status }
for _, h in ipairs(headers or {}) do
lines[#lines + 1] = h
end
lines[#lines + 1] = ""
lines[#lines + 1] = body or ""
return table.concat(lines, "\r\n")
end
-- Encodes a list of body pieces as HTTP chunked-transfer-encoding, ending
-- with the terminating zero-size chunk. Saves callers from hand-rolling
-- the "<hex-size>\r\n<data>\r\n" framing for chunked test bodies.
function M.chunked(pieces)
local out = {}
for _, piece in ipairs(pieces) do
out[#out + 1] = string.format("%x\r\n%s\r\n", #piece, piece)
end
out[#out + 1] = "0\r\n\r\n"
return table.concat(out)
end
return M
|