Merge 0.9->trunk
[prosody.git] / net / http / parser.lua
1
2 local tonumber = tonumber;
3 local assert = assert;
4 local url_parse = require "socket.url".parse;
5 local urldecode = require "net.http".urldecode;
6
7 local function preprocess_path(path)
8         path = urldecode((path:gsub("//+", "/")));
9         if path:sub(1,1) ~= "/" then
10                 path = "/"..path;
11         end
12         local level = 0;
13         for component in path:gmatch("([^/]+)/") do
14                 if component == ".." then
15                         level = level - 1;
16                 elseif component ~= "." then
17                         level = level + 1;
18                 end
19                 if level < 0 then
20                         return nil;
21                 end
22         end
23         return path;
24 end
25
26 local httpstream = {};
27
28 function httpstream.new(success_cb, error_cb, parser_type, options_cb)
29         local client = true;
30         if not parser_type or parser_type == "server" then client = false; else assert(parser_type == "client", "Invalid parser type"); end
31         local buf = "";
32         local chunked;
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                                                                 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                                                                 chunked = have_body and headers["transfer-encoding"] == "chunked";
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                                         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                                                 end
103                                                 path = preprocess_path(parsed_url.path);
104                                                 headers.host = parsed_url.host or headers.host;
105
106                                                 len = len or 0;
107                                                 packet = {
108                                                         method = method;
109                                                         url = parsed_url;
110                                                         path = path;
111                                                         httpversion = httpversion;
112                                                         headers = headers;
113                                                         body = nil;
114                                                 };
115                                         end
116                                         buf = buf:sub(index + 4);
117                                         state = true;
118                                 end
119                                 if state then -- read body
120                                         if client then
121                                                 if chunked then
122                                                         local index = buf:find("\r\n", nil, true);
123                                                         if not index then return; end -- not enough data
124                                                         local chunk_size = buf:match("^%x+");
125                                                         if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end
126                                                         chunk_size = tonumber(chunk_size, 16);
127                                                         index = index + 2;
128                                                         if chunk_size == 0 then
129                                                                 state = nil; success_cb(packet);
130                                                         elseif #buf - index + 1 >= chunk_size then -- we have a chunk
131                                                                 packet.body = packet.body..buf:sub(index, index + chunk_size - 1);
132                                                                 buf = buf:sub(index + chunk_size);
133                                                         end
134                                                         error("trailers"); -- FIXME MUST read trailers
135                                                 elseif len and #buf >= len then
136                                                         packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
137                                                         state = nil; success_cb(packet);
138                                                 end
139                                         elseif #buf >= len then
140                                                 packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
141                                                 state = nil; success_cb(packet);
142                                         else
143                                                 break;
144                                         end
145                                 end
146                         end
147                 end;
148         };
149 end
150
151 return httpstream;