2d822598c9a7ccd4a759ab375519367d6161cf67
[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);
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                                         len = tonumber(headers["content-length"]); -- TODO check for invalid len
81                                         if client then
82                                                 -- FIXME handle '100 Continue' response (by skipping it)
83                                                 if not have_body then len = 0; end
84                                                 packet = {
85                                                         code = status_code;
86                                                         httpversion = httpversion;
87                                                         headers = headers;
88                                                         body = have_body and "" or nil;
89                                                         -- COMPAT the properties below are deprecated
90                                                         responseversion = httpversion;
91                                                         responseheaders = headers;
92                                                 };
93                                         else
94                                                 local parsed_url = url_parse(path);
95                                                 path = preprocess_path(parsed_url.path);
96                                                 headers.host = parsed_url.host or headers.host;
97
98                                                 len = len or 0;
99                                                 packet = {
100                                                         method = method;
101                                                         url = parsed_url;
102                                                         path = path;
103                                                         httpversion = httpversion;
104                                                         headers = headers;
105                                                         body = nil;
106                                                 };
107                                         end
108                                         buf = buf:sub(index + 4);
109                                         state = true;
110                                 end
111                                 if state then -- read body
112                                         if client then
113                                                 if chunked then
114                                                         local index = buf:find("\r\n", nil, true);
115                                                         if not index then return; end -- not enough data
116                                                         local chunk_size = buf:match("^%x+");
117                                                         if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end
118                                                         chunk_size = tonumber(chunk_size, 16);
119                                                         index = index + 2;
120                                                         if chunk_size == 0 then
121                                                                 state = nil; success_cb(packet);
122                                                         elseif #buf - index + 1 >= chunk_size then -- we have a chunk
123                                                                 packet.body = packet.body..buf:sub(index, index + chunk_size - 1);
124                                                                 buf = buf:sub(index + chunk_size);
125                                                         end
126                                                         error("trailers"); -- FIXME MUST read trailers
127                                                 elseif len and #buf >= len then
128                                                         packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
129                                                         state = nil; success_cb(packet);
130                                                 end
131                                         elseif #buf >= len then
132                                                 packet.body, buf = buf:sub(1, len), buf:sub(len + 1);
133                                                 state = nil; success_cb(packet);
134                                         end
135                                 end
136                         end
137                 end;
138         };
139 end
140
141 return httpstream;