| 1 | local coroutine = coroutine; |
| 2 | local tonumber = tonumber; |
| 3 | |
| 4 | local deadroutine = coroutine.create(function() end); |
| 5 | coroutine.resume(deadroutine); |
| 6 | |
| 7 | module("httpstream") |
| 8 | |
| 9 | local function parser(success_cb, parser_type, options_cb) |
| 10 | local data = coroutine.yield(); |
| 11 | local function readline() |
| 12 | local pos = data:find("\r\n", nil, true); |
| 13 | while not pos do |
| 14 | data = data..coroutine.yield(); |
| 15 | pos = data:find("\r\n", nil, true); |
| 16 | end |
| 17 | local r = data:sub(1, pos-1); |
| 18 | data = data:sub(pos+2); |
| 19 | return r; |
| 20 | end |
| 21 | local function readlength(n) |
| 22 | while #data < n do |
| 23 | data = data..coroutine.yield(); |
| 24 | end |
| 25 | local r = data:sub(1, n); |
| 26 | data = data:sub(n + 1); |
| 27 | return r; |
| 28 | end |
| 29 | local function readheaders() |
| 30 | local headers = {}; -- read headers |
| 31 | while true do |
| 32 | local line = readline(); |
| 33 | if line == "" then break; end -- headers done |
| 34 | local key, val = line:match("^([^%s:]+): *(.*)$"); |
| 35 | if not key then coroutine.yield("invalid-header-line"); end -- TODO handle multi-line and invalid headers |
| 36 | key = key:lower(); |
| 37 | headers[key] = headers[key] and headers[key]..","..val or val; |
| 38 | end |
| 39 | return headers; |
| 40 | end |
| 41 | |
| 42 | if not parser_type or parser_type == "server" then |
| 43 | while true do |
| 44 | -- read status line |
| 45 | local status_line = readline(); |
| 46 | local method, path, httpversion = status_line:match("^(%S+)%s+(%S+)%s+HTTP/(%S+)$"); |
| 47 | if not method then coroutine.yield("invalid-status-line"); end |
| 48 | path = path:gsub("^//+", "/"); -- TODO parse url more |
| 49 | local headers = readheaders(); |
| 50 | |
| 51 | -- read body |
| 52 | local len = tonumber(headers["content-length"]); |
| 53 | len = len or 0; -- TODO check for invalid len |
| 54 | local body = readlength(len); |
| 55 | |
| 56 | success_cb({ |
| 57 | method = method; |
| 58 | path = path; |
| 59 | httpversion = httpversion; |
| 60 | headers = headers; |
| 61 | body = body; |
| 62 | }); |
| 63 | end |
| 64 | elseif parser_type == "client" then |
| 65 | while true do |
| 66 | -- read status line |
| 67 | local status_line = readline(); |
| 68 | local httpversion, status_code, reason_phrase = status_line:match("^HTTP/(%S+)%s+(%d%d%d)%s+(.*)$"); |
| 69 | status_code = tonumber(status_code); |
| 70 | if not status_code then coroutine.yield("invalid-status-line"); end |
| 71 | local headers = readheaders(); |
| 72 | |
| 73 | -- read body |
| 74 | local have_body = not |
| 75 | ( (options_cb and options_cb().method == "HEAD") |
| 76 | or (status_code == 204 or status_code == 304 or status_code == 301) |
| 77 | or (status_code >= 100 and status_code < 200) ); |
| 78 | |
| 79 | local body; |
| 80 | if have_body then |
| 81 | local len = tonumber(headers["content-length"]); |
| 82 | if headers["transfer-encoding"] == "chunked" then |
| 83 | body = ""; |
| 84 | while true do |
| 85 | local chunk_size = readline():match("^%x+"); |
| 86 | if not chunk_size then coroutine.yield("invalid-chunk-size"); end |
| 87 | chunk_size = tonumber(chunk_size, 16) |
| 88 | if chunk_size == 0 then break; end |
| 89 | body = body..readlength(chunk_size); |
| 90 | if readline() ~= "" then coroutine.yield("invalid-chunk-ending"); end |
| 91 | end |
| 92 | local trailers = readheaders(); |
| 93 | elseif len then -- TODO check for invalid len |
| 94 | body = readlength(len); |
| 95 | else -- read to end |
| 96 | repeat |
| 97 | local newdata = coroutine.yield(); |
| 98 | data = data..newdata; |
| 99 | until newdata == ""; |
| 100 | body, data = data, ""; |
| 101 | end |
| 102 | end |
| 103 | |
| 104 | success_cb({ |
| 105 | code = status_code; |
| 106 | httpversion = httpversion; |
| 107 | headers = headers; |
| 108 | body = body; |
| 109 | -- COMPAT the properties below are deprecated |
| 110 | responseversion = httpversion; |
| 111 | responseheaders = headers; |
| 112 | }); |
| 113 | end |
| 114 | else coroutine.yield("unknown-parser-type"); end |
| 115 | end |
| 116 | |
| 117 | function new(success_cb, error_cb, parser_type, options_cb) |
| 118 | local co = coroutine.create(parser); |
| 119 | coroutine.resume(co, success_cb, parser_type, options_cb) |
| 120 | return { |
| 121 | feed = function(self, data) |
| 122 | if not data then |
| 123 | if parser_type == "client" then coroutine.resume(co, ""); end |
| 124 | co = deadroutine; |
| 125 | return error_cb(); |
| 126 | end |
| 127 | local success, result = coroutine.resume(co, data); |
| 128 | if result then |
| 129 | co = deadroutine; |
| 130 | return error_cb(result); |
| 131 | end |
| 132 | end; |
| 133 | }; |
| 134 | end |
| 135 | |
| 136 | return _M; |