util.httpstream: Added support for HTTP response parsing.
[prosody.git] / util / httpstream.lua
1
2 local coroutine = coroutine;
3 local tonumber = tonumber;
4
5 local deadroutine = coroutine.create(function() end);
6 coroutine.resume(deadroutine);
7
8 module("httpstream")
9
10 local function parser(data, success_cb, parser_type)
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                         -- TODO parse url
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                         if not httpversion then coroutine.yield("invalid-status-line"); end
70                         local headers = readheaders();
71                         
72                         -- read body
73                         local body;
74                         local len = tonumber(headers["content-length"]);
75                         if len then -- TODO check for invalid len
76                                 body = readlength(len);
77                         else -- read to end
78                                 repeat
79                                         local newdata = coroutine.yield();
80                                         data = data..newdata;
81                                 until newdata == "";
82                                 body, data = data, "";
83                         end
84                         
85                         success_cb({
86                                 code = status_code;
87                                 responseversion = httpversion;
88                                 responseheaders = headers;
89                                 body = body;
90                         });
91                 end
92         else coroutine.yield("unknown-parser-type"); end
93 end
94
95 function new(success_cb, error_cb, parser_type)
96         local co = coroutine.create(parser);
97         return {
98                 feed = function(self, data)
99                         if not data then
100                                 if parser_type == "client" then coroutine.resume(co, "", success_cb, parser_type); end
101                                 co = deadroutine;
102                                 return error_cb();
103                         end
104                         local success, result = coroutine.resume(co, data, success_cb, parser_type);
105                         if result then
106                                 co = deadroutine;
107                                 return error_cb(result);
108                         end
109                 end;
110         };
111 end
112
113 return _M;