|
1 local tonumber = tonumber; |
|
2 local assert = assert; |
|
3 local url_parse = require "socket.url".parse; |
|
4 local urldecode = require "util.http".urldecode; |
|
5 |
|
6 local function preprocess_path(path) |
|
7 path = urldecode((path:gsub("//+", "/"))); |
|
8 if path:sub(1,1) ~= "/" then |
|
9 path = "/"..path; |
|
10 end |
|
11 local level = 0; |
|
12 for component in path:gmatch("([^/]+)/") do |
|
13 if component == ".." then |
|
14 level = level - 1; |
|
15 elseif component ~= "." then |
|
16 level = level + 1; |
|
17 end |
|
18 if level < 0 then |
|
19 return nil; |
|
20 end |
|
21 end |
|
22 return path; |
|
23 end |
|
24 |
|
25 local httpstream = {}; |
|
26 |
|
27 function httpstream.new(success_cb, error_cb, parser_type, options_cb) |
|
28 local client = true; |
|
29 if not parser_type or parser_type == "server" then client = false; else assert(parser_type == "client", "Invalid parser type"); end |
|
30 local buf = ""; |
|
31 local chunked, chunk_size, chunk_start; |
|
32 local success_on_chunk = (options_cb and options_cb().success_on_chunk); |
|
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 status_code = tonumber(status_code); |
|
69 if not status_code then error = true; return error_cb("invalid-status-line"); end |
|
70 have_body = not |
|
71 ( (options_cb and options_cb().method == "HEAD") |
|
72 or (status_code == 204 or status_code == 304 or status_code == 301) |
|
73 or (status_code >= 100 and status_code < 200) ); |
|
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 chunked = have_body and headers["transfer-encoding"] == "chunked"; |
|
82 len = tonumber(headers["content-length"]); -- TODO check for invalid len |
|
83 if client then |
|
84 -- FIXME handle '100 Continue' response (by skipping it) |
|
85 if not have_body then len = 0; end |
|
86 packet = { |
|
87 code = status_code; |
|
88 httpversion = httpversion; |
|
89 headers = headers; |
|
90 body = have_body and "" or nil; |
|
91 partial = chunked; |
|
92 -- COMPAT the properties below are deprecated |
|
93 responseversion = httpversion; |
|
94 responseheaders = headers; |
|
95 }; |
|
96 else |
|
97 local parsed_url; |
|
98 if path:byte() == 47 then -- starts with / |
|
99 local _path, _query = path:match("([^?]*).?(.*)"); |
|
100 if _query == "" then _query = nil; end |
|
101 parsed_url = { path = _path, query = _query }; |
|
102 else |
|
103 parsed_url = url_parse(path); |
|
104 if not(parsed_url and parsed_url.path) then error = true; return error_cb("invalid-url"); end |
|
105 end |
|
106 path = preprocess_path(parsed_url.path); |
|
107 headers.host = parsed_url.host or headers.host; |
|
108 |
|
109 len = len or 0; |
|
110 packet = { |
|
111 method = method; |
|
112 url = parsed_url; |
|
113 path = path; |
|
114 httpversion = httpversion; |
|
115 headers = headers; |
|
116 body = nil; |
|
117 }; |
|
118 end |
|
119 buf = buf:sub(index + 4); |
|
120 state = true; |
|
121 end |
|
122 if state then -- read body |
|
123 if client then |
|
124 if chunked then |
|
125 if not buf:find("\r\n", nil, true) then |
|
126 return; |
|
127 end -- not enough data |
|
128 if not chunk_size then |
|
129 chunk_size, chunk_start = buf:match("^(%x+)[^\r\n]*\r\n()"); |
|
130 chunk_size = chunk_size and tonumber(chunk_size, 16); |
|
131 if not chunk_size then error = true; return error_cb("invalid-chunk-size"); end |
|
132 end |
|
133 if chunk_size == 0 and buf:find("\r\n\r\n", chunk_start-2, true) then |
|
134 state, chunk_size = nil, nil; |
|
135 buf = buf:gsub("^.-\r\n\r\n", ""); -- This ensure extensions and trailers are stripped |
|
136 packet.partial = nil; |
|
137 success_cb(packet); |
|
138 elseif #buf - chunk_start + 2 >= chunk_size then -- we have a chunk |
|
139 packet.body = packet.body..buf:sub(chunk_start, chunk_start + (chunk_size-1)); |
|
140 buf = buf:sub(chunk_start + chunk_size + 2); |
|
141 chunk_size, chunk_start = nil, nil; |
|
142 if success_on_chunk then |
|
143 success_cb(packet); |
|
144 end |
|
145 else -- Partial chunk remaining |
|
146 break; |
|
147 end |
|
148 elseif len and #buf >= len then |
|
149 if packet.code == 101 then |
|
150 packet.body, buf = buf, "" |
|
151 else |
|
152 packet.body, buf = buf:sub(1, len), buf:sub(len + 1); |
|
153 end |
|
154 state = nil; success_cb(packet); |
|
155 else |
|
156 break; |
|
157 end |
|
158 elseif #buf >= len then |
|
159 packet.body, buf = buf:sub(1, len), buf:sub(len + 1); |
|
160 state = nil; success_cb(packet); |
|
161 else |
|
162 break; |
|
163 end |
|
164 end |
|
165 end |
|
166 end; |
|
167 }; |
|
168 end |
|
169 |
|
170 return httpstream; |