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