|
|
@ -10,15 +10,11 @@ |
|
|
|
start_link/1 |
|
|
|
, send_response/4 |
|
|
|
, send_file/5 |
|
|
|
, mk_req/8 |
|
|
|
, mk_req/11 |
|
|
|
|
|
|
|
%% Exported for looping with a fully-qualified module name |
|
|
|
, accept/4 |
|
|
|
, handle_request/4 |
|
|
|
, chunk_loop/1 |
|
|
|
, split_args/1 |
|
|
|
, parse_path/1 |
|
|
|
, keepalive_loop/3 |
|
|
|
, keepalive_loop/5 |
|
|
|
, close_or_keepalive/2 |
|
|
@ -47,7 +43,7 @@ start_link(WsMod) -> |
|
|
|
|
|
|
|
%% ******************************************** callback ************************************************************** |
|
|
|
init(WsMod) -> |
|
|
|
{ok, #state{wsMod = WsMod}}. |
|
|
|
{ok, #wsState{wsMod = WsMod}}. |
|
|
|
|
|
|
|
handleCall(_Msg, _State, _FROM) -> |
|
|
|
?wsErr("~p call receive unexpect msg ~p ~n ", [?MODULE, _Msg]), |
|
|
@ -59,12 +55,19 @@ handleCast(_Msg, _State) -> |
|
|
|
kpS. |
|
|
|
|
|
|
|
handleInfo({tcp, _Socket, Data}, State) -> |
|
|
|
#state{stage = Stage, buffer = Buffer, socket = Socket} = State, |
|
|
|
#wsState{stage = Stage, buffer = Buffer, socket = Socket} = State, |
|
|
|
case wsHttpProtocol:request(Stage, <<Buffer/binary, Data/binary>>, State) of |
|
|
|
{ok, NewState} -> |
|
|
|
{noreply, NewState}; |
|
|
|
{done, NewState} -> |
|
|
|
ok; |
|
|
|
Response = doHandle(NewState), |
|
|
|
case handle_response(Req1, B2, Response) of |
|
|
|
{keep_alive, NewBuffer} -> |
|
|
|
handleInfo({tcp, _Socket, NewBuffer}, NewState); |
|
|
|
{close, _} -> |
|
|
|
wsNet:close(Socket), |
|
|
|
ok |
|
|
|
end; |
|
|
|
Err -> |
|
|
|
?wsErr("recv the http data error ~p~n", [Err]), |
|
|
|
send_bad_request(Socket), |
|
|
@ -79,7 +82,7 @@ handleInfo({tcp_error, Socket, Reason}, _State) -> |
|
|
|
kpS; |
|
|
|
|
|
|
|
handleInfo({ssl, Socket, Data}, State) -> |
|
|
|
#state{stage = Stage, buffer = Buffer, socket = Socket} = State, |
|
|
|
#wsState{stage = Stage, buffer = Buffer, socket = Socket} = State, |
|
|
|
case wsHttpProtocol:request(Stage, <<Buffer/binary, Data/binary>>, State) of |
|
|
|
{ok, NewState} -> |
|
|
|
{noreply, NewState}; |
|
|
@ -99,12 +102,12 @@ handleInfo({ssl_error, Socket, Reason}, _State) -> |
|
|
|
kpS; |
|
|
|
handleInfo({?mSockReady, Sock}, _State) -> |
|
|
|
inet:setopts(Sock, [{packet, raw}, {active, true}]), |
|
|
|
{ok, #state{socket = Sock}}; |
|
|
|
{ok, #wsState{socket = Sock}}; |
|
|
|
handleInfo({?mSockReady, Sock, SslOpts, SslHSTet}, State) -> |
|
|
|
case ntSslAcceptor:handshake(Sock, SslOpts, SslHSTet) of |
|
|
|
{ok, SslSock} -> |
|
|
|
ssl:setopts(Sock, [{packet, raw}, {active, true}]), |
|
|
|
{ok, State#state{socket = SslSock, isSsl = true}}; |
|
|
|
{ok, State#wsState{socket = SslSock, isSsl = true}}; |
|
|
|
_Err -> |
|
|
|
?wsErr("ssl handshake error ~p~n", [_Err]), |
|
|
|
{stop, _Err, State} |
|
|
@ -119,32 +122,6 @@ terminate(_Reason, _State) -> |
|
|
|
code_change(_OldVsn, State, _Extra) -> |
|
|
|
{ok, State}. |
|
|
|
|
|
|
|
%% Handle the request, then loop if we're using keep alive or chunked transfer. |
|
|
|
%% If {@link elli_tcp:accept/3} doesn't return a socket within a configurable |
|
|
|
%% timeout, loop to allow code upgrades of this module. |
|
|
|
-spec accept(Server, ListenSocket, Options, Callback) -> ok when |
|
|
|
Server :: pid(), |
|
|
|
ListenSocket :: wsNet:socket(), |
|
|
|
Options :: proplists:proplist(), |
|
|
|
Callback :: wsHer:callback(). |
|
|
|
accept(Server, ListenSocket, Options, Callback) -> |
|
|
|
case catch wsNet:accept(ListenSocket, Server, accept_timeout(Options)) of |
|
|
|
{ok, Socket} -> |
|
|
|
t(accepted), |
|
|
|
?MODULE:keepalive_loop(Socket, Options, Callback); |
|
|
|
{error, timeout} -> |
|
|
|
?MODULE:accept(Server, ListenSocket, Options, Callback); |
|
|
|
{error, econnaborted} -> |
|
|
|
?MODULE:accept(Server, ListenSocket, Options, Callback); |
|
|
|
{error, {tls_alert, _}} -> |
|
|
|
?MODULE:accept(Server, ListenSocket, Options, Callback); |
|
|
|
{error, closed} -> |
|
|
|
ok; |
|
|
|
{error, Other} -> |
|
|
|
exit({error, Other}) |
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
%% @doc Handle multiple requests on the same connection, i.e. `"keep alive"'. |
|
|
|
keepalive_loop(Socket, Options, Callback) -> |
|
|
|
keepalive_loop(Socket, 0, <<>>, Options, Callback). |
|
|
@ -158,46 +135,6 @@ keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) -> |
|
|
|
ok |
|
|
|
end. |
|
|
|
|
|
|
|
%% @doc Handle a HTTP request that will possibly come on the socket. |
|
|
|
%% Returns the appropriate connection token and any buffer containing (parts of) |
|
|
|
%% the next request. |
|
|
|
-spec handle_request(Socket, PrevBin, Options, Callback) -> ConnToken when |
|
|
|
Socket :: wsNet:socket(), |
|
|
|
PrevBin :: binary(), |
|
|
|
Options :: proplists:proplist(), |
|
|
|
Callback :: wsHer:callback(), |
|
|
|
ConnToken :: {'keep_alive' | 'close', binary()}. |
|
|
|
handle_request(S, PrevB, Opts, {Mod, Args} = Callback) -> |
|
|
|
{Method, RawPath, V, B0} = get_request(S, PrevB, Opts, Callback), |
|
|
|
t(headers_start), |
|
|
|
{{RequestHeaders, ParsedRequestHeaders}, B1} = get_headers(S, V, B0, Opts, Callback), |
|
|
|
t(headers_end), |
|
|
|
Req = mk_req(Method, RawPath, RequestHeaders, ParsedRequestHeaders, <<>>, V, S, Callback), |
|
|
|
|
|
|
|
case init(Req) of |
|
|
|
{ok, standard} -> |
|
|
|
t(body_start), |
|
|
|
{RequestBody, B2} = get_body(S, ParsedRequestHeaders, B1, Opts, Callback), |
|
|
|
t(body_end), |
|
|
|
Req1 = Req#wsReq{body = RequestBody}, |
|
|
|
|
|
|
|
t(user_start), |
|
|
|
Response = execute_callback(Req1), |
|
|
|
t(user_end), |
|
|
|
|
|
|
|
handle_response(Req1, B2, Response); |
|
|
|
{ok, handover} -> |
|
|
|
Req1 = Req#wsReq{body = B1}, |
|
|
|
|
|
|
|
t(user_start), |
|
|
|
Response = Mod:handle(Req1, Args), |
|
|
|
t(user_end), |
|
|
|
|
|
|
|
t(request_end), |
|
|
|
handle_event(Mod, request_complete, [Req1, handover, [], <<>>, {get_timings(), get_sizes()}], Args), |
|
|
|
Response |
|
|
|
end. |
|
|
|
|
|
|
|
handle_response(Req, Buffer, {response, Code, UserHeaders, Body}) -> |
|
|
|
#wsReq{callback = {Mod, Args}} = Req, |
|
|
|
|
|
|
@ -213,8 +150,6 @@ handle_response(Req, Buffer, {response, Code, UserHeaders, Body}) -> |
|
|
|
[Req, Code, Headers, Body, {get_timings(), |
|
|
|
get_sizes()}], Args), |
|
|
|
{close_or_keepalive(Req, UserHeaders), Buffer}; |
|
|
|
|
|
|
|
|
|
|
|
handle_response(Req, _Buffer, {chunk, UserHeaders, Initial}) -> |
|
|
|
#wsReq{callback = {Mod, Args}} = Req, |
|
|
|
|
|
|
@ -237,8 +172,6 @@ handle_response(Req, _Buffer, {chunk, UserHeaders, Initial}) -> |
|
|
|
get_sizes()}], |
|
|
|
Args), |
|
|
|
{close, <<>>}; |
|
|
|
|
|
|
|
|
|
|
|
handle_response(Req, Buffer, {file, ResponseCode, UserHeaders, |
|
|
|
Filename, Range}) -> |
|
|
|
#wsReq{callback = {Mod, Args}} = Req, |
|
|
@ -282,7 +215,6 @@ handle_response(Req, Buffer, {file, ResponseCode, UserHeaders, |
|
|
|
{close_or_keepalive(Req, UserHeaders), Buffer} |
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
%% @doc Generate a HTTP response and send it to the client. |
|
|
|
send_response(Req, Code, Headers, UserBody) -> |
|
|
|
ResponseHeaders = assemble_response_headers(Code, Headers), |
|
|
@ -358,8 +290,9 @@ send_rescue_response(Socket, Code, Body) -> |
|
|
|
wsNet:send(Socket, Response). |
|
|
|
|
|
|
|
%% @doc Execute the user callback, translating failure into a proper response. |
|
|
|
execute_callback(#wsReq{callback = {Mod, Args}} = Req) -> |
|
|
|
try Mod:handle(Req, Args) of |
|
|
|
doHandle(State) -> |
|
|
|
#wsState{wsMod = WsMod, method = Method, path = Path, wsReq = WsReq} = State, |
|
|
|
try WsMod:handle(Method, Path, WsReq) of |
|
|
|
%% {ok,...{file,...}} |
|
|
|
{ok, Headers, {file, Filename}} -> |
|
|
|
{file, 200, Headers, Filename, []}; |
|
|
@ -381,19 +314,19 @@ execute_callback(#wsReq{callback = {Mod, Args}} = Req) -> |
|
|
|
{HttpCode, Body} -> {response, HttpCode, [], Body}; |
|
|
|
%% Unexpected |
|
|
|
Unexpected -> |
|
|
|
handle_event(Mod, invalid_return, [Req, Unexpected], Args), |
|
|
|
?wsErr("handle return error ~p ~p~n", [WsReq, Unexpected]), |
|
|
|
{response, 500, [], <<"Internal server error">>} |
|
|
|
catch |
|
|
|
throw:{ResponseCode, Headers, Body} when is_integer(ResponseCode) -> |
|
|
|
{response, ResponseCode, Headers, Body}; |
|
|
|
throw:Exc:Stacktrace -> |
|
|
|
handle_event(Mod, request_throw, [Req, Exc, Stacktrace], Args), |
|
|
|
?wsErr("handle catch throw ~p ~p ~p~n", [WsReq, Exc, Stacktrace]), |
|
|
|
{response, 500, [], <<"Internal server error">>}; |
|
|
|
error:Error:Stacktrace -> |
|
|
|
handle_event(Mod, request_error, [Req, Error, Stacktrace], Args), |
|
|
|
?wsErr("handle catch error ~p ~p ~p~n", [WsReq, Error, Stacktrace]), |
|
|
|
{response, 500, [], <<"Internal server error">>}; |
|
|
|
exit:Exit:Stacktrace -> |
|
|
|
handle_event(Mod, request_exit, [Req, Exit, Stacktrace], Args), |
|
|
|
?wsErr("handle catch exit ~p ~p ~p~n", [WsReq, Exit, Stacktrace]), |
|
|
|
{response, 500, [], <<"Internal server error">>} |
|
|
|
end. |
|
|
|
|
|
|
@ -466,159 +399,6 @@ send_chunk(Socket, Data) -> |
|
|
|
wsNet:send(Socket, Response) |
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
%% |
|
|
|
%% RECEIVE REQUEST |
|
|
|
%% |
|
|
|
|
|
|
|
%% @doc Retrieve the request line. |
|
|
|
get_request(Socket, <<>>, Options, Callback) -> |
|
|
|
NewBuffer = recv_request(Socket, <<>>, Options, Callback), |
|
|
|
get_request(Socket, NewBuffer, Options, Callback); |
|
|
|
get_request(Socket, Buffer, Options, Callback) -> |
|
|
|
t(request_start), |
|
|
|
get_request_(Socket, Buffer, Options, Callback). |
|
|
|
|
|
|
|
get_request_(Socket, Buffer, Options, {Mod, Args} = Callback) -> |
|
|
|
case erlang:decode_packet(http_bin, Buffer, []) of |
|
|
|
{more, _} -> |
|
|
|
NewBuffer = recv_request(Socket, Buffer, Options, Callback), |
|
|
|
get_request_(Socket, NewBuffer, Options, Callback); |
|
|
|
{ok, {http_request, Method, RawPath, Version}, Rest} -> |
|
|
|
{Method, RawPath, Version, Rest}; |
|
|
|
{ok, {http_error, _}, _} -> |
|
|
|
handle_event(Mod, request_parse_error, [Buffer], Args), |
|
|
|
send_bad_request(Socket), |
|
|
|
wsNet:close(Socket), |
|
|
|
exit(normal); |
|
|
|
{ok, {http_response, _, _, _}, _} -> |
|
|
|
wsNet:close(Socket), |
|
|
|
exit(normal) |
|
|
|
end. |
|
|
|
|
|
|
|
recv_request(Socket, Buffer, Options, {Mod, Args} = _Callback) -> |
|
|
|
case wsNet:recv(Socket, 0, request_timeout(Options)) of |
|
|
|
{ok, Data} -> |
|
|
|
<<Buffer/binary, Data/binary>>; |
|
|
|
{error, timeout} -> |
|
|
|
handle_event(Mod, request_timeout, [], Args), |
|
|
|
wsNet:close(Socket), |
|
|
|
exit(normal); |
|
|
|
{error, Closed} when Closed =:= closed orelse |
|
|
|
Closed =:= enotconn -> |
|
|
|
handle_event(Mod, request_closed, [], Args), |
|
|
|
wsNet:close(Socket), |
|
|
|
exit(normal) |
|
|
|
end. |
|
|
|
|
|
|
|
-spec get_headers(Socket, V, Buffer, Opts, Callback) -> Headers when |
|
|
|
Socket :: wsNet:socket(), |
|
|
|
V :: version(), |
|
|
|
Buffer :: binary(), |
|
|
|
Opts :: proplists:proplist(), |
|
|
|
Callback :: wsHer:callback(), |
|
|
|
Headers :: {{elli:headers(), elli:headers()}, any()}. % TODO: refine |
|
|
|
get_headers(_Socket, {0, 9}, _, _, _) -> |
|
|
|
{{[], []}, <<>>}; |
|
|
|
get_headers(Socket, {1, _}, Buffer, Opts, Callback) -> |
|
|
|
get_headers(Socket, Buffer, {[], []}, 0, Opts, Callback). |
|
|
|
|
|
|
|
get_headers(Socket, _, {Headers, _}, HeadersCount, _Opts, {Mod, Args}) when HeadersCount >= 100 -> |
|
|
|
handle_event(Mod, bad_request, [{too_many_headers, Headers}], Args), |
|
|
|
send_bad_request(Socket), |
|
|
|
wsNet:close(Socket), |
|
|
|
exit(normal); |
|
|
|
get_headers(Socket, Buffer, {Headers, ParsedHeaders}, Count, Opts, {Mod, Args} = Callback) -> |
|
|
|
case erlang:decode_packet(httph_bin, Buffer, []) of |
|
|
|
{ok, {http_header, _, Key, _, Value}, Rest} -> |
|
|
|
BinKey = ensure_binary(Key), |
|
|
|
NewHeaders = [{BinKey, Value} | Headers], |
|
|
|
NewParsedHeaders = [{string:casefold(BinKey), Value} | ParsedHeaders], |
|
|
|
get_headers(Socket, Rest, {NewHeaders, NewParsedHeaders}, Count + 1, Opts, Callback); |
|
|
|
{ok, http_eoh, Rest} -> |
|
|
|
{{Headers, ParsedHeaders}, Rest}; |
|
|
|
{ok, {http_error, _}, Rest} -> |
|
|
|
get_headers(Socket, Rest, {Headers, ParsedHeaders}, Count, Opts, Callback); |
|
|
|
{more, _} -> |
|
|
|
case wsNet:recv(Socket, 0, header_timeout(Opts)) of |
|
|
|
{ok, Data} -> |
|
|
|
get_headers(Socket, <<Buffer/binary, Data/binary>>, |
|
|
|
{Headers, ParsedHeaders}, Count, Opts, Callback); |
|
|
|
{error, Closed} when Closed =:= closed orelse |
|
|
|
Closed =:= enotconn -> |
|
|
|
handle_event(Mod, client_closed, [receiving_headers], Args), |
|
|
|
wsNet:close(Socket), |
|
|
|
exit(normal); |
|
|
|
{error, timeout} -> |
|
|
|
handle_event(Mod, client_timeout, |
|
|
|
[receiving_headers], Args), |
|
|
|
wsNet:close(Socket), |
|
|
|
exit(normal) |
|
|
|
end |
|
|
|
end. |
|
|
|
|
|
|
|
%% @doc Fetch the full body of the request, if any is available. |
|
|
|
%% |
|
|
|
%% At the moment we don't need to handle large requests, so there is |
|
|
|
%% no need for streaming or lazily fetching the body in the user |
|
|
|
%% code. Fully receiving the body allows us to avoid the complex |
|
|
|
%% request object threading in Cowboy and the caching in Mochiweb. |
|
|
|
%% |
|
|
|
%% As we are always receiving whatever the client sends, we might have |
|
|
|
%% buffered too much and get parts of the next pipelined request. In |
|
|
|
%% that case, push it back in the buffer and handle the first request. |
|
|
|
-spec get_body(Socket, Headers, Buffer, Opts, Callback) -> FullBody when |
|
|
|
Socket :: undefined | wsNet:socket(), |
|
|
|
Headers :: elli:headers(), |
|
|
|
Buffer :: binary(), |
|
|
|
Opts :: proplists:proplist(), |
|
|
|
Callback :: wsHer:callback(), |
|
|
|
FullBody :: {elli:body(), binary()}. |
|
|
|
get_body(Socket, Headers, Buffer, Opts, Callback) -> |
|
|
|
case get_header(?CONTENT_LENGTH_HEADER, Headers, undefined) of |
|
|
|
undefined -> |
|
|
|
{<<>>, Buffer}; |
|
|
|
ContentLengthBin -> |
|
|
|
maybe_send_continue(Socket, Headers), |
|
|
|
|
|
|
|
ContentLength = binary_to_integer(binary:replace(ContentLengthBin, |
|
|
|
<<" ">>, <<>>, |
|
|
|
[global])), |
|
|
|
|
|
|
|
ok = check_max_size(Socket, ContentLength, Buffer, Opts, Callback), |
|
|
|
|
|
|
|
Result = case ContentLength - byte_size(Buffer) of |
|
|
|
0 -> |
|
|
|
{Buffer, <<>>}; |
|
|
|
N when N > 0 -> |
|
|
|
do_get_body(Socket, Buffer, Opts, N, Callback); |
|
|
|
_ -> |
|
|
|
<<Body:ContentLength/binary, R/binary>> = Buffer, |
|
|
|
{Body, R} |
|
|
|
end, |
|
|
|
%% set the size here so if do_get_body exits it won't have |
|
|
|
%% req_body in sizes |
|
|
|
s(req_body, ContentLength), |
|
|
|
Result |
|
|
|
end. |
|
|
|
|
|
|
|
do_get_body(Socket, Buffer, Opts, N, {Mod, Args}) -> |
|
|
|
case wsNet:recv(Socket, N, body_timeout(Opts)) of |
|
|
|
{ok, Data} -> |
|
|
|
{<<Buffer/binary, Data/binary>>, <<>>}; |
|
|
|
{error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> |
|
|
|
handle_event(Mod, client_closed, [receiving_body], Args), |
|
|
|
ok = wsNet:close(Socket), |
|
|
|
exit(normal); |
|
|
|
{error, timeout} -> |
|
|
|
handle_event(Mod, client_timeout, [receiving_body], Args), |
|
|
|
ok = wsNet:close(Socket), |
|
|
|
exit(normal) |
|
|
|
end. |
|
|
|
|
|
|
|
ensure_binary(Bin) when is_binary(Bin) -> Bin; |
|
|
|
ensure_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom, latin1). |
|
|
|
|
|
|
|
maybe_send_continue(Socket, Headers) -> |
|
|
|
% According to RFC2616 section 8.2.3 an origin server must respond with |
|
|
|
% either a "100 Continue" or a final response code when the client |
|
|
@ -631,62 +411,6 @@ maybe_send_continue(Socket, Headers) -> |
|
|
|
ok |
|
|
|
end. |
|
|
|
|
|
|
|
%% @doc To send a response, we must first receive anything the client is |
|
|
|
%% sending. To avoid allowing clients to use all our bandwidth, if the request |
|
|
|
%% size is too big, we simply close the socket. |
|
|
|
check_max_size(Socket, ContentLength, Buffer, Opts, {Mod, Args}) -> |
|
|
|
MaxSize = max_body_size(Opts), |
|
|
|
do_check_max_size(Socket, ContentLength, Buffer, MaxSize, {Mod, Args}). |
|
|
|
|
|
|
|
do_check_max_size(Socket, ContentLength, Buffer, MaxSize, {Mod, Args}) |
|
|
|
when ContentLength > MaxSize -> |
|
|
|
handle_event(Mod, bad_request, [{body_size, ContentLength}], Args), |
|
|
|
do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize), |
|
|
|
wsNet:close(Socket), |
|
|
|
exit(normal); |
|
|
|
do_check_max_size(_, _, _, _, _) -> ok. |
|
|
|
|
|
|
|
do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize) |
|
|
|
when ContentLength < MaxSize * 2 -> |
|
|
|
OnSocket = ContentLength - size(Buffer), |
|
|
|
wsNet:recv(Socket, OnSocket, 60000), |
|
|
|
Response = http_response(413), |
|
|
|
wsNet:send(Socket, Response); |
|
|
|
do_check_max_size_x2(_, _, _, _) -> ok. |
|
|
|
|
|
|
|
-spec mk_req(Method, PathTuple, Headers, Headers, Body, V, Socket, Callback) -> Req when |
|
|
|
Method :: elli:method(), |
|
|
|
PathTuple :: {PathType :: atom(), RawPath :: binary()}, |
|
|
|
Headers :: elli:headers(), |
|
|
|
Body :: elli:body(), |
|
|
|
V :: version(), |
|
|
|
Socket :: wsNet:socket() | undefined, |
|
|
|
Callback :: wsHer:callback(), |
|
|
|
Req :: elli:req(). |
|
|
|
mk_req(Method, PathTuple, Headers, ParsedHeaders, Body, V, Socket, {Mod, Args} = Callback) -> |
|
|
|
case parse_path(PathTuple) of |
|
|
|
{ok, {Scheme, Host, Port}, {Path, URL, URLArgs}} -> |
|
|
|
#wsReq{method = Method, scheme = Scheme, host = Host, |
|
|
|
port = Port, path = URL, args = URLArgs, |
|
|
|
version = V, raw_path = Path, original_headers = Headers, |
|
|
|
body = Body, pid = self(), socket = Socket, |
|
|
|
callback = Callback, headers = ParsedHeaders}; |
|
|
|
{error, Reason} -> |
|
|
|
handle_event(Mod, request_parse_error, |
|
|
|
[{Reason, {Method, PathTuple}}], Args), |
|
|
|
send_bad_request(Socket), |
|
|
|
wsNet:close(Socket), |
|
|
|
exit(normal) |
|
|
|
end. |
|
|
|
|
|
|
|
mk_req(Method, Scheme, Host, Port, PathTuple, Headers, ParsedHeaders, Body, V, Socket, Callback) -> |
|
|
|
Req = mk_req(Method, PathTuple, Headers, ParsedHeaders, Body, V, Socket, Callback), |
|
|
|
Req#wsReq{scheme = Scheme, host = Host, port = Port}. |
|
|
|
|
|
|
|
%% |
|
|
|
%% HEADERS |
|
|
|
%% |
|
|
|
|
|
|
|
http_response(Code) -> |
|
|
|
http_response(Code, <<>>). |
|
|
|
|
|
|
@ -764,7 +488,6 @@ is_header_defined(Key, Headers) -> |
|
|
|
get_header(Key, Headers) -> |
|
|
|
get_header(Key, Headers, undefined). |
|
|
|
|
|
|
|
-ifdef(OTP_RELEASE). |
|
|
|
get_header(Key, Headers, Default) -> |
|
|
|
CaseFoldedKey = string:casefold(Key), |
|
|
|
case lists:search(fun({N, _}) -> string:equal(CaseFoldedKey, N, true) end, Headers) of |
|
|
@ -773,72 +496,6 @@ get_header(Key, Headers, Default) -> |
|
|
|
false -> |
|
|
|
Default |
|
|
|
end. |
|
|
|
-else. |
|
|
|
get_header(Key, Headers, Default) -> |
|
|
|
CaseFoldedKey = string:casefold(Key), |
|
|
|
case search(fun({N, _}) -> string:equal(CaseFoldedKey, N, true) end, Headers) of |
|
|
|
{value, {_, Value}} -> |
|
|
|
Value; |
|
|
|
false -> |
|
|
|
Default |
|
|
|
end. |
|
|
|
|
|
|
|
search(Pred, [Hd | Tail]) -> |
|
|
|
case Pred(Hd) of |
|
|
|
true -> {value, Hd}; |
|
|
|
false -> search(Pred, Tail) |
|
|
|
end; |
|
|
|
search(Pred, []) when is_function(Pred, 1) -> |
|
|
|
false. |
|
|
|
-endif. |
|
|
|
|
|
|
|
%% |
|
|
|
%% PATH HELPERS |
|
|
|
%% |
|
|
|
|
|
|
|
-ifdef(OTP_RELEASE). |
|
|
|
- if (?OTP_RELEASE >= 22). |
|
|
|
parse_path({abs_path, FullPath}) -> |
|
|
|
URIMap = uri_string:parse(FullPath), |
|
|
|
Host = maps:get(host, URIMap, undefined), |
|
|
|
Scheme = maps:get(scheme, URIMap, undefined), |
|
|
|
Path = maps:get(path, URIMap, <<>>), |
|
|
|
Query = maps:get(query, URIMap, <<>>), |
|
|
|
Port = maps:get(port, URIMap, case Scheme of http -> 80; https -> 443; _ -> undefined end), |
|
|
|
{ok, {Scheme, Host, Port}, {Path, split_path(Path), uri_string:dissect_query(Query)}}; |
|
|
|
parse_path({absoluteURI, Scheme, Host, Port, Path}) -> |
|
|
|
setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port}); |
|
|
|
parse_path(_) -> |
|
|
|
{error, unsupported_uri}. |
|
|
|
-else. |
|
|
|
parse_path({abs_path, FullPath}) -> |
|
|
|
Parsed = case binary:split(FullPath, [<<"?">>]) of |
|
|
|
[URL] -> {FullPath, split_path(URL), []}; |
|
|
|
[URL, Args] -> {FullPath, split_path(URL), split_args(Args)} |
|
|
|
end, |
|
|
|
{ok, {undefined, undefined, undefined}, Parsed}; |
|
|
|
parse_path({absoluteURI, Scheme, Host, Port, Path}) -> |
|
|
|
setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port}); |
|
|
|
parse_path(_) -> |
|
|
|
{error, unsupported_uri}. |
|
|
|
-endif. |
|
|
|
-else. |
|
|
|
%% same as else branch above. can drop this when only OTP 21+ is supported |
|
|
|
parse_path({abs_path, FullPath}) -> |
|
|
|
Parsed = case binary:split(FullPath, [<<"?">>]) of |
|
|
|
[URL] -> {FullPath, split_path(URL), []}; |
|
|
|
[URL, Args] -> {FullPath, split_path(URL), split_args(Args)} |
|
|
|
end, |
|
|
|
{ok, {undefined, undefined, undefined}, Parsed}; |
|
|
|
parse_path({absoluteURI, Scheme, Host, Port, Path}) -> |
|
|
|
setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port}); |
|
|
|
parse_path(_) -> |
|
|
|
{error, unsupported_uri}. |
|
|
|
-endif. |
|
|
|
|
|
|
|
split_path(Path) -> |
|
|
|
[P || P <- binary:split(Path, [<<"/">>], [global]), |
|
|
|
P =/= <<>>]. |
|
|
|
|
|
|
|
%% @doc Split the URL arguments into a proplist. |
|
|
|
%% Lifted from `cowboy_http:x_www_form_urlencoded/2'. |
|
|
@ -853,17 +510,7 @@ split_args(Qs) -> |
|
|
|
end || Token <- Tokens]. |
|
|
|
|
|
|
|
|
|
|
|
%% |
|
|
|
%% CALLBACK HELPERS |
|
|
|
%% |
|
|
|
|
|
|
|
init(#wsReq{callback = {Mod, Args}} = Req) -> |
|
|
|
?IIF(erlang:function_exported(Mod, init, 2), |
|
|
|
case Mod:init(Req, Args) of |
|
|
|
ignore -> {ok, standard}; |
|
|
|
{ok, Behaviour} -> {ok, Behaviour} |
|
|
|
end, |
|
|
|
{ok, standard}). |
|
|
|
|
|
|
|
handle_event(Mod, Name, EventArgs, ElliArgs) -> |
|
|
|
try |
|
|
@ -873,26 +520,6 @@ handle_event(Mod, Name, EventArgs, ElliArgs) -> |
|
|
|
?wsErr("~p:handle_event/3 crashed ~p:~p~n~p", [Mod, EvClass, EvError, Stacktrace]) |
|
|
|
end. |
|
|
|
|
|
|
|
%% |
|
|
|
%% TIMING HELPERS |
|
|
|
%% |
|
|
|
|
|
|
|
%% @doc Record the current monotonic time in the process dictionary. |
|
|
|
%% This allows easily adding time tracing wherever, |
|
|
|
%% without passing along any variables. |
|
|
|
t(Key) -> |
|
|
|
put({time, Key}, erlang:monotonic_time()). |
|
|
|
|
|
|
|
get_timings() -> |
|
|
|
lists:filtermap(fun get_timings/1, get()). |
|
|
|
|
|
|
|
get_timings({{time, accepted}, Value}) -> |
|
|
|
{true, {accepted, Value}}; |
|
|
|
get_timings({{time, Key}, Value}) -> |
|
|
|
erase({time, Key}), |
|
|
|
{true, {Key, Value}}; |
|
|
|
get_timings(_) -> |
|
|
|
false. |
|
|
|
|
|
|
|
%% |
|
|
|
%% SIZE HELPERS |
|
|
@ -918,18 +545,6 @@ get_sizes({{size, Key}, Value}) -> |
|
|
|
get_sizes(_) -> |
|
|
|
false. |
|
|
|
|
|
|
|
|
|
|
|
%% |
|
|
|
%% OPTIONS |
|
|
|
%% |
|
|
|
|
|
|
|
accept_timeout(Opts) -> proplists:get_value(accept_timeout, Opts). |
|
|
|
request_timeout(Opts) -> proplists:get_value(request_timeout, Opts). |
|
|
|
header_timeout(Opts) -> proplists:get_value(header_timeout, Opts). |
|
|
|
body_timeout(Opts) -> proplists:get_value(body_timeout, Opts). |
|
|
|
max_body_size(Opts) -> proplists:get_value(max_body_size, Opts). |
|
|
|
|
|
|
|
|
|
|
|
%% |
|
|
|
%% HTTP STATUS CODES |
|
|
|
%% |
|
|
@ -994,21 +609,3 @@ status(510) -> <<"510 Not Extended">>; |
|
|
|
status(511) -> <<"511 Network Authentication Required">>; |
|
|
|
status(I) when is_integer(I), I >= 100, I < 1000 -> <<(integer_to_binary(I))/binary, "Status">>; |
|
|
|
status(B) when is_binary(B) -> B. |
|
|
|
|
|
|
|
|
|
|
|
%% |
|
|
|
%% UNIT TESTS |
|
|
|
%% |
|
|
|
|
|
|
|
-ifdef(TEST). |
|
|
|
-include_lib("eunit/include/eunit.hrl"). |
|
|
|
|
|
|
|
get_body_test() -> |
|
|
|
Socket = undefined, |
|
|
|
Headers = [{<<"Content-Length">>, <<" 42 ">>}], |
|
|
|
Buffer = binary:copy(<<".">>, 42), |
|
|
|
Opts = [], |
|
|
|
Callback = {no_mod, []}, |
|
|
|
?assertMatch({Buffer, <<>>}, |
|
|
|
get_body(Socket, Headers, Buffer, Opts, Callback)). |
|
|
|
-endif. |