aboutsummaryrefslogtreecommitdiffstats
path: root/util/httpstream.lua
blob: bdc3fce776c45827ffdf7cd533d7df69dc1a4cdc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

local coroutine = coroutine;
local tonumber = tonumber;

local deadroutine = coroutine.create(function() end);
coroutine.resume(deadroutine);

module("httpstream")

local function parser(success_cb, parser_type, options_cb)
	local data = coroutine.yield();
	local function readline()
		local pos = data:find("\r\n", nil, true);
		while not pos do
			data = data..coroutine.yield();
			pos = data:find("\r\n", nil, true);
		end
		local r = data:sub(1, pos-1);
		data = data:sub(pos+2);
		return r;
	end
	local function readlength(n)
		while #data < n do
			data = data..coroutine.yield();
		end
		local r = data:sub(1, n);
		data = data:sub(n + 1);
		return r;
	end
	local function readheaders()
		local headers = {}; -- read headers
		while true do
			local line = readline();
			if line == "" then break; end -- headers done
			local key, val = line:match("^([^%s:]+): *(.*)$");
			if not key then coroutine.yield("invalid-header-line"); end -- TODO handle multi-line and invalid headers
			key = key:lower();
			headers[key] = headers[key] and headers[key]..","..val or val;
		end
		return headers;
	end
	
	if not parser_type or parser_type == "server" then
		while true do
			-- read status line
			local status_line = readline();
			local method, path, httpversion = status_line:match("^(%S+)%s+(%S+)%s+HTTP/(%S+)$");
			if not method then coroutine.yield("invalid-status-line"); end
			path = path:gsub("^//+", "/"); -- TODO parse url more
			local headers = readheaders();
			
			-- read body
			local len = tonumber(headers["content-length"]);
			len = len or 0; -- TODO check for invalid len
			local body = readlength(len);
			
			success_cb({
				method = method;
				path = path;
				httpversion = httpversion;
				headers = headers;
				body = body;
			});
		end
	elseif parser_type == "client" then
		while true do
			-- read status line
			local status_line = readline();
			local httpversion, status_code, reason_phrase = status_line:match("^HTTP/(%S+)%s+(%d%d%d)%s+(.*)$");
			status_code = tonumber(status_code);
			if not status_code then coroutine.yield("invalid-status-line"); end
			local headers = readheaders();
			
			-- read body
			local have_body = not
				 ( (options_cb and options_cb().method == "HEAD")
				or (status_code == 204 or status_code == 304 or status_code == 301)
				or (status_code >= 100 and status_code < 200) );
			
			local body;
			if have_body then
				local len = tonumber(headers["content-length"]);
				if headers["transfer-encoding"] == "chunked" then
					body = "";
					while true do
						local chunk_size = readline():match("^%x+");
						if not chunk_size then coroutine.yield("invalid-chunk-size"); end
						chunk_size = tonumber(chunk_size, 16)
						if chunk_size == 0 then break; end
						body = body..readlength(chunk_size);
						if readline() ~= "" then coroutine.yield("invalid-chunk-ending"); end
					end
					local trailers = readheaders();
				elseif len then -- TODO check for invalid len
					body = readlength(len);
				else -- read to end
					repeat
						local newdata = coroutine.yield();
						data = data..newdata;
					until newdata == "";
					body, data = data, "";
				end
			end
			
			success_cb({
				code = status_code;
				httpversion = httpversion;
				headers = headers;
				body = body;
				-- COMPAT the properties below are deprecated
				responseversion = httpversion;
				responseheaders = headers;
			});
		end
	else coroutine.yield("unknown-parser-type"); end
end

function new(success_cb, error_cb, parser_type, options_cb)
	local co = coroutine.create(parser);
	coroutine.resume(co, success_cb, parser_type, options_cb)
	return {
		feed = function(self, data)
			if not data then
				if parser_type == "client" then coroutine.resume(co, ""); end
				co = deadroutine;
				return error_cb();
			end
			local success, result = coroutine.resume(co, data);
			if result then
				co = deadroutine;
				return error_cb(result);
			end
		end;
	};
end

return _M;