util/httpstream.lua

1
local coroutine = coroutine;
2
local tonumber = tonumber;
3
 
4
local deadroutine = coroutine.create(function() end);
5
coroutine.resume(deadroutine);
6
 
7
module("httpstream")
8
 
9
local function parser(success_cb, parser_type, options_cb)
10
	local data = coroutine.yield();
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
			path = path:gsub("^//+", "/"); -- TODO parse url more
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
			status_code = tonumber(status_code);
70
			if not status_code then coroutine.yield("invalid-status-line"); end
71
			local headers = readheaders();
72
			
73
			-- read body
74
			local have_body = not
75
				 ( (options_cb and options_cb().method == "HEAD")
76
				or (status_code == 204 or status_code == 304 or status_code == 301)
77
				or (status_code >= 100 and status_code < 200) );
78
			
79
			local body;
80
			if have_body then
81
				local len = tonumber(headers["content-length"]);
82
				if headers["transfer-encoding"] == "chunked" then
83
					body = "";
84
					while true do
85
						local chunk_size = readline():match("^%x+");
86
						if not chunk_size then coroutine.yield("invalid-chunk-size"); end
87
						chunk_size = tonumber(chunk_size, 16)
88
						if chunk_size == 0 then break; end
89
						body = body..readlength(chunk_size);
90
						if readline() ~= "" then coroutine.yield("invalid-chunk-ending"); end
91
					end
92
					local trailers = readheaders();
93
				elseif len then -- TODO check for invalid len
94
					body = readlength(len);
95
				else -- read to end
96
					repeat
97
						local newdata = coroutine.yield();
98
						data = data..newdata;
99
					until newdata == "";
100
					body, data = data, "";
101
				end
102
			end
103
			
104
			success_cb({
105
				code = status_code;
106
				httpversion = httpversion;
107
				headers = headers;
108
				body = body;
109
				-- COMPAT the properties below are deprecated
110
				responseversion = httpversion;
111
				responseheaders = headers;
112
			});
113
		end
114
	else coroutine.yield("unknown-parser-type"); end
115
end
116
 
117
function new(success_cb, error_cb, parser_type, options_cb)
118
	local co = coroutine.create(parser);
119
	coroutine.resume(co, success_cb, parser_type, options_cb)
120
	return {
121
		feed = function(self, data)
122
			if not data then
123
				if parser_type == "client" then coroutine.resume(co, ""); end
124
				co = deadroutine;
125
				return error_cb();
126
			end
127
			local success, result = coroutine.resume(co, data);
128
			if result then
129
				co = deadroutine;
130
				return error_cb(result);
131
			end
132
		end;
133
	};
134
end
135
 
136
return _M;