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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
|
--[[
Copyright 2014-2015 The Luvit Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS-IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--]]
local exports = {}
exports.name = "luvit/http-codec"
exports.version = "1.0.0-1"
exports.homepage = "https://github.com/luvit/luvit/blob/master/deps/http-codec.lua"
exports.description = "A simple pair of functions for converting between hex and raw strings."
exports.tags = {"codec", "http"}
exports.license = "Apache 2"
exports.author = { name = "Tim Caswell" }
local sub = string.sub
local gsub = string.gsub
local lower = string.lower
local find = string.find
local format = string.format
local concat = table.concat
local match = string.match
local STATUS_CODES = {
[100] = 'Continue',
[101] = 'Switching Protocols',
[102] = 'Processing', -- RFC 2518, obsoleted by RFC 4918
[200] = 'OK',
[201] = 'Created',
[202] = 'Accepted',
[203] = 'Non-Authoritative Information',
[204] = 'No Content',
[205] = 'Reset Content',
[206] = 'Partial Content',
[207] = 'Multi-Status', -- RFC 4918
[300] = 'Multiple Choices',
[301] = 'Moved Permanently',
[302] = 'Moved Temporarily',
[303] = 'See Other',
[304] = 'Not Modified',
[305] = 'Use Proxy',
[307] = 'Temporary Redirect',
[400] = 'Bad Request',
[401] = 'Unauthorized',
[402] = 'Payment Required',
[403] = 'Forbidden',
[404] = 'Not Found',
[405] = 'Method Not Allowed',
[406] = 'Not Acceptable',
[407] = 'Proxy Authentication Required',
[408] = 'Request Time-out',
[409] = 'Conflict',
[410] = 'Gone',
[411] = 'Length Required',
[412] = 'Precondition Failed',
[413] = 'Request Entity Too Large',
[414] = 'Request-URI Too Large',
[415] = 'Unsupported Media Type',
[416] = 'Requested Range Not Satisfiable',
[417] = 'Expectation Failed',
[418] = "I'm a teapot", -- RFC 2324
[422] = 'Unprocessable Entity', -- RFC 4918
[423] = 'Locked', -- RFC 4918
[424] = 'Failed Dependency', -- RFC 4918
[425] = 'Unordered Collection', -- RFC 4918
[426] = 'Upgrade Required', -- RFC 2817
[500] = 'Internal Server Error',
[501] = 'Not Implemented',
[502] = 'Bad Gateway',
[503] = 'Service Unavailable',
[504] = 'Gateway Time-out',
[505] = 'HTTP Version not supported',
[506] = 'Variant Also Negotiates', -- RFC 2295
[507] = 'Insufficient Storage', -- RFC 4918
[509] = 'Bandwidth Limit Exceeded',
[510] = 'Not Extended' -- RFC 2774
}
exports.encoder = function ()
local mode
local encodeHead, encodeRaw, encodeChunked
function encodeHead(item)
if not item or item == "" then
return item
elseif not (type(item) == "table") then
error("expected a table but got a " .. type(item) .. " when encoding data")
end
local head, chunkedEncoding
local version = item.version or 1.1
if item.method then
local path = item.path
assert(path and #path > 0, "expected non-empty path")
head = { item.method .. ' ' .. item.path .. ' HTTP/' .. version .. '\r\n' }
else
local reason = item.reason or STATUS_CODES[item.code]
head = { 'HTTP/' .. version .. ' ' .. item.code .. ' ' .. reason .. '\r\n' }
end
for i = 1, #item do
local key, value = unpack(item[i])
local lowerKey = lower(key)
if lowerKey == "transfer-encoding" then
chunkedEncoding = lower(value) == "chunked"
end
value = gsub(tostring(value), "[\r\n]+", " ")
head[#head + 1] = key .. ': ' .. tostring(value) .. '\r\n'
end
head[#head + 1] = '\r\n'
mode = chunkedEncoding and encodeChunked or encodeRaw
return concat(head)
end
function encodeRaw(item)
if type(item) ~= "string" then
mode = encodeHead
return encodeHead(item)
end
return item
end
function encodeChunked(item)
if type(item) ~= "string" then
mode = encodeHead
local extra = encodeHead(item)
if extra then
return "0\r\n\r\n" .. extra
else
return "0\r\n\r\n"
end
end
if #item == 0 then
mode = encodeHead
end
return format("%x", #item) .. "\r\n" .. item .. "\r\n"
end
mode = encodeHead
return function (item)
return mode(item)
end
end
exports.decoder = function ()
-- This decoder is somewhat stateful with 5 different parsing states.
local decodeHead, decodeEmpty, decodeRaw, decodeChunked, decodeCounted
local mode -- state variable that points to various decoders
local bytesLeft -- For counted decoder
-- This state is for decoding the status line and headers.
function decodeHead(chunk)
if not chunk then return end
local _, length = find(chunk, "\r?\n\r?\n", 1)
-- First make sure we have all the head before continuing
if not length then
if #chunk < 8 * 1024 then return end
-- But protect against evil clients by refusing heads over 8K long.
error("entity too large")
end
-- Parse the status/request line
local head = {}
local _, offset
local version
_, offset, version, head.code, head.reason =
find(chunk, "^HTTP/(%d%.%d) (%d+) ([^\r\n]+)\r?\n")
if offset then
head.code = tonumber(head.code)
else
_, offset, head.method, head.path, version =
find(chunk, "^(%u+) ([^ ]+) HTTP/(%d%.%d)\r?\n")
if not offset then
error("expected HTTP data")
end
end
version = tonumber(version)
head.version = version
head.keepAlive = version > 1.0
-- We need to inspect some headers to know how to parse the body.
local contentLength
local chunkedEncoding
-- Parse the header lines
while true do
local key, value
_, offset, key, value = find(chunk, "^([^:\r\n]+): *([^\r\n]+)\r?\n", offset + 1)
if not offset then break end
local lowerKey = lower(key)
-- Inspect a few headers and remember the values
if lowerKey == "content-length" then
contentLength = tonumber(value)
elseif lowerKey == "transfer-encoding" then
chunkedEncoding = lower(value) == "chunked"
elseif lowerKey == "connection" then
head.keepAlive = lower(value) == "keep-alive"
end
head[#head + 1] = {key, value}
end
if head.keepAlive and (not (chunkedEncoding or (contentLength and contentLength > 0)))
or (head.method == "GET" or head.method == "HEAD") then
mode = decodeEmpty
elseif chunkedEncoding then
mode = decodeChunked
elseif contentLength then
bytesLeft = contentLength
mode = decodeCounted
elseif not head.keepAlive then
mode = decodeRaw
end
return head, sub(chunk, length + 1)
end
-- This is used for inserting a single empty string into the output string for known empty bodies
function decodeEmpty(chunk)
mode = decodeHead
return "", chunk or ""
end
function decodeRaw(chunk)
if not chunk then return "", "" end
if #chunk == 0 then return end
return chunk, ""
end
function decodeChunked(chunk)
local len, term
len, term = match(chunk, "^(%x+)(..)")
if not len then return end
assert(term == "\r\n")
local length = tonumber(len, 16)
if #chunk < length + 4 + #len then return end
if length == 0 then
mode = decodeHead
end
chunk = sub(chunk, #len + 3)
assert(sub(chunk, length + 1, length + 2) == "\r\n")
return sub(chunk, 1, length), sub(chunk, length + 3)
end
function decodeCounted(chunk)
if bytesLeft == 0 then
mode = decodeEmpty
return mode(chunk)
end
local length = #chunk
-- Make sure we have at least one byte to process
if length == 0 then return end
if length >= bytesLeft then
mode = decodeEmpty
end
-- If the entire chunk fits, pass it all through
if length <= bytesLeft then
bytesLeft = bytesLeft - length
return chunk, ""
end
return sub(chunk, 1, bytesLeft), sub(chunk, bytesLeft + 1)
end
-- Switch between states by changing which decoder mode points to
mode = decodeHead
return function (chunk)
return mode(chunk)
end
end
return exports
|