net/http/parser.lua

changeset 0
d363a6692a10
child 1
7eff79ea9a01
equal deleted inserted replaced
-1:000000000000 0:d363a6692a10
1 local tonumber = tonumber;
2 local assert = assert;
3 local url_parse = require "socket.url".parse;
4 local urldecode = require "util.http".urldecode;
5
6 local function preprocess_path(path)
7 path = urldecode((path:gsub("//+", "/")));
8 if path:sub(1,1) ~= "/" then
9 path = "/"..path;
10 end
11 local level = 0;
12 for component in path:gmatch("([^/]+)/") do
13 if component == ".." then
14 level = level - 1;
15 elseif component ~= "." then
16 level = level + 1;
17 end
18 if level < 0 then
19 return nil;
20 end
21 end
22 return path;
23 end
24
25 local httpstream = {};
26
27 function httpstream.new(success_cb, error_cb, parser_type, options_cb)
28 local client = true;
29 if not parser_type or parser_type == "server" then client = false; else assert(parser_type == "client", "Invalid parser type"); end
30 local buf = "";
31 local chunked, chunk_size, chunk_start;
32 local success_on_chunk = (options_cb and options_cb().success_on_chunk);
33 local state = nil;
34 local packet;
35 local len;
36 local have_body;
37 local error;
38 return {
39 feed = function(self, data)
40 if error then return nil, "parse has failed"; end
41 if not data then -- EOF
42 if state and client and not len then -- reading client body until EOF
43 packet.body = buf;
44 success_cb(packet);
45 elseif buf ~= "" then -- unexpected EOF
46 error = true; return error_cb();
47 end
48 return;
49 end
50 buf = buf..data;
51 while #buf > 0 do
52 if state == nil then -- read request
53 local index = buf:find("\r\n\r\n", nil, true);
54 if not index then return; end -- not enough data
55 local method, path, httpversion, status_code, reason_phrase;
56 local first_line;
57 local headers = {};
58 for line in buf:sub(1,index+1):gmatch("([^\r\n]+)\r\n") do -- parse request
59 if first_line then
60 local key, val = line:match("^([^%s:]+): *(.*)$");
61 if not key then error = true; return error_cb("invalid-header-line"); end -- TODO handle multi-line and invalid headers
62 key = key:lower();
63 headers[key] = headers[key] and headers[key]..","..val or val;
64 else
65 first_line = line;
66 if client then
67 httpversion, status_code, reason_phrase = line:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$");
68 status_code = tonumber(status_code);
69 if not status_code then error = true; return error_cb("invalid-status-line"); end
70 have_body = not
71 ( (options_cb and options_cb().method == "HEAD")
72 or (status_code == 204 or status_code == 304 or status_code == 301)
73 or (status_code >= 100 and status_code < 200) );
74 else
75 method, path, httpversion = line:match("^(%w+) (%S+) HTTP/(1%.[01])$");
76 if not method then error = true; return error_cb("invalid-status-line"); end
77 end
78 end
79 end
80 if not first_line then error = true; return error_cb("invalid-status-line"); end
81 chunked = have_body and headers["transfer-encoding"] == "chunked";
82 len = tonumber(headers["content-length"]); -- TODO check for invalid len
83 if client then
84 -- FIXME handle '100 Continue' response (by skipping it)
85 if not have_body then len = 0; end
86 packet = {
87 code = status_code;
88 httpversion = httpversion;
89 headers = headers;
90 body = have_body and "" or nil;
91 partial = chunked;
92 -- COMPAT the properties below are deprecated
93 responseversion = httpversion;
94 responseheaders = headers;
95 };
96 else
97 local parsed_url;
98 if path:byte() == 47 then -- starts with /
99 local _path, _query = path:match("([^?]*).?(.*)");
100 if _query == "" then _query = nil; end
101 parsed_url = { path = _path, query = _query };
102 else
103 parsed_url = url_parse(path);
104 if not(parsed_url and parsed_url.path) then error = true; return error_cb("invalid-url"); end
105 end
106 path = preprocess_path(parsed_url.path);
107 headers.host = parsed_url.host or headers.host;
108
109 len = len or 0;
110 packet = {
111 method = method;
112 url = parsed_url;
113 path = path;
114 httpversion = httpversion;
115 headers = headers;
116 body = nil;
117 };
118 end
119 buf = buf:sub(index + 4);
120 state = true;
121 end
122 if state then -- read body
123 if client then
124 if chunked then
125 if not buf:find("\r\n", nil, true) then
126 return;
127 end -- not enough data
128 if not chunk_size then
129 chunk_size, chunk_start = buf:match("^(%x+)[^\r\n]*\r\n()");
130 chunk_size = chunk_size and tonumber(chunk_size, 16);
131 if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end
132 end
133 if chunk_size == 0 and buf:find("\r\n\r\n", chunk_start-2, true) then
134 state, chunk_size = nil, nil;
135 buf = buf:gsub("^.-\r\n\r\n", ""); -- This ensure extensions and trailers are stripped
136 packet.partial = nil;
137 success_cb(packet);
138 elseif #buf - chunk_start + 2 >= chunk_size then -- we have a chunk
139 packet.body = packet.body..buf:sub(chunk_start, chunk_start + (chunk_size-1));
140 buf = buf:sub(chunk_start + chunk_size + 2);
141 chunk_size, chunk_start = nil, nil;
142 if success_on_chunk then
143 success_cb(packet);
144 end
145 else -- Partial chunk remaining
146 break;
147 end
148 elseif len and #buf >= len then
149 if packet.code == 101 then
150 packet.body, buf = buf, ""
151 else
152 packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
153 end
154 state = nil; success_cb(packet);
155 else
156 break;
157 end
158 elseif #buf >= len then
159 packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
160 state = nil; success_cb(packet);
161 else
162 break;
163 end
164 end
165 end
166 end;
167 };
168 end
169
170 return httpstream;

mercurial