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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
|
local exports = {}
exports.name = "creationix/coro-http"
exports.version = "1.2.1-1"
exports.dependencies = {
"creationix/coro-net@1.1.1",
"creationix/coro-tls@1.2.1",
"creationix/coro-wrapper@1.0.0",
"luvit/http-codec@1.0.0"
}
exports.homepage = "https://github.com/luvit/lit/blob/master/deps/coro-http.lua"
exports.description = "An coro style http(s) client and server helper."
exports.tags = {"coro", "http"}
exports.license = "MIT"
exports.author = { name = "Tim Caswell" }
local httpCodec = require('http-codec')
local net = require('coro-net')
local connect = net.connect
local createServer = net.createServer
--local tlsWrap = require('coro-tls').wrap
local wrapper = require('coro-wrapper')
function exports.createServer(options, onConnect)
createServer(options, function (rawRead, rawWrite, socket)
local read = wrapper.reader(rawRead, httpCodec.decoder())
local write = wrapper.writer(rawWrite, httpCodec.encoder())
for head in read do
local parts = {}
for part in read do
if #part > 0 then
parts[#parts + 1] = part
else
break
end
end
local body = table.concat(parts)
head, body = onConnect(head, body, socket)
write(head)
if body then write(body) end
write("")
if not head.keepAlive then break end
end
end)
end
local function parseUrl(url)
local protocol, host, hostname, port, path = url:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
if not protocol then error("Not a valid http url: " .. url) end
local tls = protocol == "https:"
port = port and tonumber(port) or (tls and 443 or 80)
if path == "" then path = "/" end
return {
tls = tls,
host = host,
hostname = hostname,
port = port,
path = path
}
end
exports.parseUrl = parseUrl
local connections = {}
local function getConnection(host, port, tls)
for i = #connections, 1, -1 do
local connection = connections[i]
if connection.host == host and connection.port == port and connection.tls == tls then
table.remove(connections, i)
-- Make sure the connection is still alive before reusing it.
if not connection.socket:is_closing() then
connection.reused = true
connection.socket:ref()
return connection
end
end
end
local read, write, socket = assert(connect({host=host,port=port}))
--if tls then
--read, write = tlsWrap(read, write)
--end
local httpRead, updateRead = wrapper.reader(read, httpCodec.decoder())
return {
socket = socket,
host = host,
port = port,
tls = tls,
read = httpRead,
write = wrapper.writer(write, httpCodec.encoder()),
reset = function ()
-- This is called after parsing the response head from a HEAD request.
-- If you forget, the codec might hang waiting for a body that doesn't exist.
updateRead(httpCodec.decoder())
end
}
end
exports.getConnection = getConnection
local function saveConnection(connection)
if connection.socket:is_closing() then return end
connections[#connections + 1] = connection
connection.socket:unref()
end
exports.saveConnection = saveConnection
function exports.request(method, url, headers, body)
local uri = parseUrl(url)
local connection = getConnection(uri.hostname, uri.port, uri.tls)
local read = connection.read
local write = connection.write
local req = {
method = method,
path = uri.path,
{"Host", uri.host}
}
local contentLength
local chunked
if headers then
for i = 1, #headers do
local key, value = unpack(headers[i])
key = key:lower()
if key == "content-length" then
contentLength = value
elseif key == "content-encoding" and value:lower() == "chunked" then
chunked = true
end
req[#req + 1] = headers[i]
end
end
if type(body) == "string" then
if not chunked and not contentLength then
req[#req + 1] = {"Content-Length", #body}
end
end
write(req)
if body then write(body) end
local res = read()
if not res then
write()
-- If we get an immediate close on a reused socket, try again with a new socket.
-- TODO: think about if this could resend requests with side effects and cause
-- them to double execute in the remote server.
if connection.reused then
return exports.request(method, url, headers, body)
end
error("Connection closed")
end
body = {}
if req.method == "HEAD" then
connection.reset()
else
while true do
local item = read()
if not item then
res.keepAlive = false
break
end
if #item == 0 then
break
end
body[#body + 1] = item
end
end
if res.keepAlive then
saveConnection(connection)
else
write()
end
-- Follow redirects
if method == "GET" and (res.code == 302 or res.code == 307) then
for i = 1, #res do
local key, location = unpack(res[i])
if key:lower() == "location" then
return exports.request(method, location, headers)
end
end
end
return res, table.concat(body)
end
return exports
|