1 local tonumber = tonumber;
3 local t_insert
, t_concat
= table.insert
, table.concat
;
4 local url_parse
= require
"socket.url".parse
;
5 local urldecode
= require
"util.http".urldecode
;
7 local function preprocess_path(path
)
8 path
= urldecode((path
:gsub("//+", "/")));
9 if path
:sub(1,1) ~= "/" then
13 for component
in path
:gmatch("([^/]+)/") do
14 if component
== ".." then
16 elseif component
~= "." then
26 local httpstream
= {};
28 function httpstream
.new(success_cb
, error_cb
, parser_type
, options_cb
)
30 if not parser_type
or parser_type
== "server" then client
= false; else assert(parser_type
== "client", "Invalid parser type"); end
31 local buf
, buflen
, buftable
= {}, 0, true;
32 local bodylimit
= tonumber(options_cb
and options_cb().body_size_limit
) or 10*1024*1024;
33 local buflimit
= tonumber(options_cb
and options_cb().buffer_size_limit
) or bodylimit
* 2;
34 local chunked
, chunk_size
, chunk_start
;
41 feed
= function(_
, data
)
42 if error then return nil, "parse has failed"; end
43 if not data
then -- EOF
44 if buftable
then buf
, buftable
= t_concat(buf
), false; end
45 if state
and client
and not len
then -- reading client body until EOF
48 elseif buf
~= "" then -- unexpected EOF
49 error = true; return error_cb("unexpected-eof");
59 buflen
= buflen
+ #data
;
60 if buflen
> buflimit
then error = true; return error_cb("max-buffer-size-exceeded"); end
62 if state
== nil then -- read request
63 if buftable
then buf
, buftable
= t_concat(buf
), false; end
64 local index
= buf
:find("\r\n\r\n", nil, true);
65 if not index
then return; end -- not enough data
66 local method
, path
, httpversion
, status_code
, reason_phrase
;
69 for line
in buf
:sub(1,index
+1):gmatch("([^\r\n]+)\r\n") do -- parse request
71 local key
, val
= line
:match("^([^%s:]+): *(.*)$");
72 if not key
then error = true; return error_cb("invalid-header-line"); end -- TODO handle multi-line and invalid headers
74 headers
[key
] = headers
[key
] and headers
[key
]..","..val
or val
;
78 httpversion
, status_code
, reason_phrase
= line
:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$");
79 status_code
= tonumber(status_code
);
80 if not status_code
then error = true; return error_cb("invalid-status-line"); end
82 ( (options_cb
and options_cb().method
== "HEAD")
83 or (status_code
== 204 or status_code
== 304 or status_code
== 301)
84 or (status_code
>= 100 and status_code
< 200) );
86 method
, path
, httpversion
= line
:match("^(%w+) (%S+) HTTP/(1%.[01])$");
87 if not method
then error = true; return error_cb("invalid-status-line"); end
91 if not first_line
then error = true; return error_cb("invalid-status-line"); end
92 chunked
= have_body
and headers
["transfer-encoding"] == "chunked";
93 len
= tonumber(headers
["content-length"]); -- TODO check for invalid len
94 if len
and len
> bodylimit
then error = true; return error_cb("content-length-limit-exceeded"); end
96 -- FIXME handle '100 Continue' response (by skipping it)
97 if not have_body
then len
= 0; end
100 httpversion
= httpversion
;
102 body
= have_body
and "" or nil;
103 -- COMPAT the properties below are deprecated
104 responseversion
= httpversion
;
105 responseheaders
= headers
;
109 if path
:byte() == 47 then -- starts with /
110 local _path
, _query
= path
:match("([^?]*).?(.*)");
111 if _query
== "" then _query
= nil; end
112 parsed_url
= { path
= _path
, query
= _query
};
114 parsed_url
= url_parse(path
);
115 if not(parsed_url
and parsed_url
.path
) then error = true; return error_cb("invalid-url"); end
117 path
= preprocess_path(parsed_url
.path
);
118 headers
.host
= parsed_url
.host
or headers
.host
;
125 httpversion
= httpversion
;
130 buf
= buf
:sub(index
+ 4);
134 if state
then -- read body
137 if chunk_start
and buflen
- chunk_start
- 2 < chunk_size
then
139 end -- not enough data
140 if buftable
then buf
, buftable
= t_concat(buf
), false; end
141 if not buf
:find("\r\n", nil, true) then
143 end -- not enough data
144 if not chunk_size
then
145 chunk_size
, chunk_start
= buf
:match("^(%x+)[^\r\n]*\r\n()");
146 chunk_size
= chunk_size
and tonumber(chunk_size
, 16);
147 if not chunk_size
then error = true; return error_cb("invalid-chunk-size"); end
149 if chunk_size
== 0 and buf
:find("\r\n\r\n", chunk_start
-2, true) then
150 state
, chunk_size
= nil, nil;
151 buf
= buf
:gsub("^.-\r\n\r\n", ""); -- This ensure extensions and trailers are stripped
153 elseif buflen
- chunk_start
- 2 >= chunk_size
then -- we have a chunk
154 packet
.body
= packet
.body
..buf
:sub(chunk_start
, chunk_start
+ (chunk_size
-1));
155 buf
= buf
:sub(chunk_start
+ chunk_size
+ 2);
156 buflen
= buflen
- (chunk_start
+ chunk_size
+ 2 - 1);
157 chunk_size
, chunk_start
= nil, nil;
158 else -- Partial chunk remaining
161 elseif len
and buflen
>= len
then
162 if buftable
then buf
, buftable
= t_concat(buf
), false; end
163 if packet
.code
== 101 then
164 packet
.body
, buf
, buflen
, buftable
= buf
, {}, 0, true;
166 packet
.body
, buf
= buf
:sub(1, len
), buf
:sub(len
+ 1);
169 state
= nil; success_cb(packet
);
173 elseif buflen
>= len
then
174 if buftable
then buf
, buftable
= t_concat(buf
), false; end
175 packet
.body
, buf
= buf
:sub(1, len
), buf
:sub(len
+ 1);
177 state
= nil; success_cb(packet
);