moonbridge

view moonbridge_http.lua @ 154:831f2d4b2d73

Initial work on reimplemented HTTP layer (utilizing non-blocking I/O with coroutines and a cleaner object-oriented structure)
author jbe
date Thu May 21 02:40:39 2015 +0200 (2015-05-21)
parents 7014436d88ea
children 2c22b0f222c7
line source
1 #!/usr/bin/env lua
3 -- module preamble
4 local _G, _M = _ENV, {}
5 _ENV = setmetatable({}, {
6 __index = function(self, key)
7 local value = _M[key]; if value ~= nil then return value end
8 return _G[key]
9 end,
10 __newindex = _M
11 })
13 -- function that encodes certain HTML entities:
14 -- (not used by the library itself)
15 function encode_html(text)
16 return (
17 string.gsub(
18 text, '[<>&"]',
19 function(char)
20 if char == '<' then
21 return "&lt;"
22 elseif char == '>' then
23 return "&gt;"
24 elseif char == '&' then
25 return "&amp;"
26 elseif char == '"' then
27 return "&quot;"
28 end
29 end
30 )
31 )
33 end
35 -- function that encodes special characters for URIs:
36 -- (not used by the library itself)
37 function encode_uri(text)
38 return (
39 string.gsub(text, "[^0-9A-Za-z_%.~-]",
40 function (char)
41 return string.format("%%%02x", string.byte(char))
42 end
43 )
44 )
45 end
47 -- function undoing URL encoding:
48 do
49 local b0 = string.byte("0")
50 local b9 = string.byte("9")
51 local bA = string.byte("A")
52 local bF = string.byte("F")
53 local ba = string.byte("a")
54 local bf = string.byte("f")
55 function decode_uri(str)
56 return (
57 string.gsub(
58 string.gsub(str, "%+", " "),
59 "%%([0-9A-Fa-f][0-9A-Fa-f])",
60 function(hex)
61 local n1, n2 = string.byte(hex, 1, 2)
62 if n1 >= b0 and n1 <= b9 then n1 = n1 - b0
63 elseif n1 >= bA and n1 <= bF then n1 = n1 - bA + 10
64 elseif n1 >= ba and n1 <= bf then n1 = n1 - ba + 10
65 else error("Assertion failed") end
66 if n2 >= b0 and n2 <= b9 then n2 = n2 - b0
67 elseif n2 >= bA and n2 <= bF then n2 = n2 - bA + 10
68 elseif n2 >= ba and n2 <= bf then n2 = n2 - ba + 10
69 else error("Assertion failed") end
70 return string.char(n1 * 16 + n2)
71 end
72 )
73 )
74 end
75 end
77 -- status codes that carry no response body (in addition to 1xx):
78 -- (set to "zero_content_length" if Content-Length header is required)
79 status_without_response_body = {
80 ["101"] = true, -- list 101 to allow protocol switch
81 ["204"] = true,
82 ["205"] = "zero_content_length",
83 ["304"] = true
84 }
86 -- parses URL encoded form data:
87 local function read_urlencoded_form(data)
88 local tbl = {}
89 for rawkey, rawvalue in string.gmatch(data, "([^?=&]*)=([^?=&]*)") do
90 local key = decode_uri(rawkey)
91 local value = decode_uri(rawvalue)
92 local subtbl = tbl[key]
93 if subtbl then
94 subtbl[#subtbl+1] = value
95 else
96 tbl[key] = {value}
97 end
98 end
99 return tbl
100 end
102 -- extracts first value from each subtable:
103 local function get_first_values(tbl)
104 local newtbl = {}
105 for key, subtbl in pairs(tbl) do
106 newtbl[key] = subtbl[1]
107 end
108 return newtbl
109 end
111 request_pt = {}
112 request_mt = { __index = request_pt }
114 function request_pt:_init(handler, options)
115 -- process options:
116 options = options or {}
117 do
118 -- named arg "static_headers" is used to create the preamble:
119 local s = options.static_headers
120 local t = {}
121 if s then
122 if type(s) == "string" then
123 for line in string.gmatch(s, "[^\r\n]+") do
124 t[#t+1] = line
125 end
126 else
127 for i, kv in ipairs(options.static_headers) do
128 if type(kv) == "string" then
129 t[#t+1] = kv
130 else
131 t[#t+1] = kv[1] .. ": " .. kv[2]
132 end
133 end
134 end
135 end
136 t[#t+1] = ""
137 self._preamble = table.concat(t, "\r\n") -- preamble sent with every(!) HTTP response
138 end
139 self._input_chunk_size = options.maximum_input_chunk_size or options.chunk_size or 16384
140 self._output_chunk_size = options.minimum_output_chunk_size or options.chunk_size or 1024
141 self._header_size_limit = options.header_size_limit or 1024*1024
142 local function init_timeout(name, default)
143 local value = options[name]
144 if value == nil then
145 self["_"..name] = default
146 else
147 self["_"..name] = value or 0
148 end
149 end
150 init_timeout("request_idle_timeout", 330)
151 init_timeout("request_header_timeout", 30)
152 init_timeout("request_body_timeout", 1800)
153 init_timeout("response_timeout", 1830)
154 self._poll = options.poll_function or moonbridge_io.poll
155 self:_create_closure("_write_yield")
156 self:_create_closure("_handler")
157 -- table mapping header field names to value-lists:
158 self._headers_mt = {
159 __index = function(tbl, key)
160 local lowerkey = string.lower(key)
161 local result = self._headers[lowerkey]
162 if result == nil then
163 result = {}
164 end
165 tbl[lowerkey] = result
166 tbl[key] = result
167 return result
168 end
169 }
170 -- table mapping header field names to value-lists
171 -- (for headers with comma separated values):
172 self._headers_csv_table_mt = {
173 __index = function(tbl, key)
174 local result = {}
175 for i, line in ipairs(self.headers[key]) do
176 for entry in string.gmatch(line, "[^,]+") do
177 local value = string.match(entry, "^[ \t]*(..-)[ \t]*$")
178 if value then
179 result[#result+1] = value
180 end
181 end
182 end
183 tbl[key] = result
184 return result
185 end
186 }
187 -- table mapping header field names to a comma separated string
188 -- (for headers with comma separated values):
189 self._headers_csv_string_mt = {
190 __index = function(tbl, key)
191 local result = {}
192 for i, line in ipairs(self.headers[key]) do
193 result[#result+1] = line
194 end
195 result = string.concat(result, ", ")
196 tbl[key] = result
197 return result
198 end
199 }
200 -- table mapping header field names to a single string value
201 -- (or false if header has been sent multiple times):
202 self._headers_value_mt = {
203 __index = function(tbl, key)
204 if self._headers_value_nil[key] then
205 return nil
206 end
207 local result = nil
208 local values = self.headers_csv_table[key]
209 if #values == 0 then
210 self._headers_value_nil[key] = true
211 elseif #values == 1 then
212 result = values[1]
213 else
214 result = false
215 end
216 tbl[key] = result
217 return result
218 end
219 }
220 -- table mapping header field names to a flag table,
221 -- indicating if the comma separated value contains certain entries:
222 self._headers_flags_mt = {
223 __index = function(tbl, key)
224 local result = setmetatable({}, {
225 __index = function(tbl, key)
226 local lowerkey = string.lower(key)
227 local result = rawget(tbl, lowerkey) or false
228 tbl[lowerkey] = result
229 tbl[key] = result
230 return result
231 end
232 })
233 for i, value in ipairs(self.headers_csv_table[key]) do
234 result[string.lower(value)] = true
235 end
236 tbl[key] = result
237 return result
238 end
239 }
240 end
242 function request_pt:_create_closure(name)
243 self[name.."_closure"] = function(...)
244 return self[name](self, ...)
245 end
246 end
248 function request_pt:_create_magictable(name)
249 self[name] = setmetatable({}, self["_"..name.."_mt"])
250 end
252 function request_pt:_handler(socket)
253 self._socket = socket
254 self._survive = true
255 self._socket_set = {[socket] = true}
256 self._faulty = false
257 self._consume_input = self._drain_input
258 self._headers = {}
259 self._headers_value_nil = {}
260 self:_create_magictable("headers")
261 self:_create_magictable("headers_csv_table")
262 self:_create_magictable("headers_csv_string")
263 self:_create_magictable("headers_value")
264 self:_create_magictable("headers_flags")
265 repeat
266 -- wait for input:
267 if not moonbridge_io.poll(self._socket_set, nil, self._request_idle_timeout) then
268 self:_error("408 Request Timeout", "Idle connection timed out")
269 return self._survive
270 end
271 -- read headers (with timeout):
272 do
273 local coro = coroutine.wrap(self._read_headers)
274 local timeout = self._request_header_timeout
275 local starttime = timeout and moonbridge_io.timeref()
276 while true do
277 local status = coro(self)
278 if status == nil then
279 local remaining
280 if timeout then
281 remaining = timeout - moonbridge_io.timeref(starttime)
282 end
283 if not self._poll(self._socket_set, nil, remaining) then
284 self:_error("408 Request Timeout", "Timeout while receiving headers")
285 return self._survive
286 end
287 elseif status == false then
288 return self._survive
289 elseif status == true then
290 break
291 else
292 error("Unexpected yield value")
293 end
294 end
295 end
296 until true
297 end
299 function request_pt:_error(status, explanation)
300 end
302 function request_pt:_read(...)
303 local line, status = self._socket:read_yield(...)
304 if line == nil then
305 self._faulty = true
306 error(status)
307 else
308 return line, status
309 end
310 end
312 function request_pt:_read_headers()
313 local remaining = self._header_size_limit
314 -- read and parse request line:
315 local target, proto
316 do
317 local line, status = self:_read(remaining-2, "\n")
318 if status == "maxlen" then
319 self:_error("414 Request-URI Too Long")
320 return false
321 elseif status == "eof" then
322 if line ~= "" then
323 self:_error("400 Bad Request", "Unexpected EOF in request-URI line")
324 end
325 return false
326 end
327 remaining = remaining - #line
328 self.method, target, proto =
329 line:match("^([^ \t\r]+)[ \t]+([^ \t\r]+)[ \t]*([^ \t\r]*)[ \t]*\r?\n$")
330 if not request.method then
331 self:_error("400 Bad Request", "Invalid request-URI line")
332 return false
333 elseif proto ~= "HTTP/1.1" then
334 self:_error("505 HTTP Version Not Supported")
335 return false
336 end
337 end
338 -- read and parse headers:
339 while true do
340 local line, status = self:_read(remaining, "\n");
341 if status == "maxlen" then
342 self:_error("431 Request Header Fields Too Large")
343 return false
344 elseif status == "eof" then
345 self:_error("400 Bad Request", "Unexpected EOF in request headers")
346 return false
347 end
348 remaining = remaining - #line
349 if line == "\r\n" or line == "\n" then
350 break
351 end
352 local key, value = string.match(line, "^([^ \t\r]+):[ \t]*(.-)[ \t]*\r?\n$")
353 if not key then
354 self:_error("400 Bad Request", "Invalid header line")
355 return false
356 end
357 local lowerkey = key:lower()
358 local values = self._headers[lowerkey]
359 if values then
360 values[#values+1] = value
361 else
362 self._headers[lowerkey] = {value}
363 end
364 end
365 -- process "Connection: close" header if existent:
366 self._connection_close_requested = self.headers_flags["Connection"]["close"]
367 -- process "Content-Length" header if existent:
368 do
369 local values = self.headers_csv_table["Content-Length"]
370 if #values > 0 then
371 self._request_body_content_length = tonumber(values[1])
372 local proper_value = tostring(request_body_content_length)
373 for i, value in ipairs(values) do
374 value = string.match(value, "^0*(.*)")
375 if value ~= proper_value then
376 self:_error("400 Bad Request", "Content-Length header(s) invalid")
377 return false
378 end
379 end
380 if request_body_content_length > self._body_size_limit then
381 self:_error("413 Request Entity Too Large", "Announced request body size is too big")
382 return false
383 end
384 end
385 end
386 -- process "Transfer-Encoding" header if existent:
387 do
388 local flag = self.headers_flags["Transfer-Encoding"]["chunked"]
389 local list = self.headers_csv_table["Transfer-Encoding"]
390 if (flag and #list ~= 1) or (not flag and #list ~= 0) then
391 self:_error("400 Bad Request", "Unexpected Transfer-Encoding")
392 return false
393 end
394 end
395 -- process "Expect" header if existent:
396 for i, value in ipairs(self.headers_csv_table["Expect"]) do
397 if string.lower(value) ~= "100-continue" then
398 self:_error("417 Expectation Failed", "Unexpected Expect header")
399 return false
400 end
401 end
402 -- get mandatory Host header according to RFC 7230:
403 self.host = self.headers_value["Host"]
404 if not self.host then
405 self:_error("400 Bad Request", "No valid host header")
406 return false
407 end
408 -- parse request target:
409 self.path, self.query = string.match(target, "^/([^?]*)(.*)$")
410 if not self.path then
411 local host2
412 host2, self.path, self.query = string.match(target, "^[Hh][Tt][Tt][Pp]://([^/?]+)/?([^?]*)(.*)$")
413 if host2 then
414 if self.host ~= host2 then
415 self:_error("400 Bad Request", "No valid host header")
416 return false
417 end
418 elseif not (target == "*" and self.method == "OPTIONS") then
419 self:_error("400 Bad Request", "Invalid request target")
420 end
421 end
422 -- parse GET params:
423 if self.query then
424 self.get_params_list = read_urlencoded_form(request.query)
425 self.get_params = get_first_values(self.get_params_list)
426 end
427 -- parse cookies:
428 for i, line in ipairs(self.headers["Cookie"]) do
429 for rawkey, rawvalue in
430 string.gmatch(line, "([^=; ]*)=([^=; ]*)")
431 do
432 self.cookies[decode_uri(rawkey)] = decode_uri(rawvalue)
433 end
434 end
435 end
437 function request_pt:_assert_not_faulty()
438 assert(not self._faulty, "Tried to use faulty request handle")
439 end
441 function request_pt:_write_yield()
442 self:_consume_input()
443 self._poll(self._socket_set, self._socket_set)
444 end
446 function request_pt:_write(...)
447 assert(self._socket:write_call(self._write_yield_closure, ...))
448 end
450 function request_pt:_flush(...)
451 assert(self._socket:write_call(self._write_yield_closure, ...))
452 end
454 function request_pt:_drain_input()
455 socket:drain_nb(self._input_chunk_size)
456 end
459 -- function creating a HTTP handler:
460 function generate_handler(handler, options)
461 -- swap arguments if necessary (for convenience):
462 if type(handler) ~= "function" and type(options) == "function" then
463 handler, options = options, handler
464 end
465 local request = setmetatable({}, request_mt)
466 request:_init(handler, options)
467 return request._handler_closure
468 end
470 return _M

Impressum / About Us