From 64d93271aa5b3a625579f0588dc6f316dd608c3e Mon Sep 17 00:00:00 2001 From: SisMaker <1713699517@qq.com> Date: Tue, 18 Jan 2022 12:35:35 +0800 Subject: [PATCH] =?UTF-8?q?ft:=20=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/eWSrv.hrl | 7 +- include/wsCom.hrl | 12 +- src/test/elli_tests.erl | 6 +- src/wsSrv/wsHttp.erl | 443 ++--------------------------------- src/wsSrv/wsHttpProtocol.erl | 67 ++++-- src/wsSrv/wsHttps.erl | 2 +- src/wsSrv/wsReq.erl | 4 +- 7 files changed, 81 insertions(+), 460 deletions(-) diff --git a/include/eWSrv.hrl b/include/eWSrv.hrl index f55dd85..28b29f1 100644 --- a/include/eWSrv.hrl +++ b/include/eWSrv.hrl @@ -25,17 +25,14 @@ -record(wsReq, { method :: method(), - raw_path :: binary(), + path :: binary(), version :: wsHttp:version(), scheme :: undefined | binary(), host :: undefined | binary(), port :: undefined | 1..65535, - path :: [binary()], args :: [{binary(), any()}], headers :: headers(), - original_headers :: headers(), - body = <<>> :: body(), - pid :: pid() + body = <<>> :: body() }). -export_type([req/0, method/0, body/0, headers/0, response_code/0]). diff --git a/include/wsCom.hrl b/include/wsCom.hrl index 6fccf90..d52c4a3 100644 --- a/include/wsCom.hrl +++ b/include/wsCom.hrl @@ -11,7 +11,7 @@ -type stage() :: reqLine | header | body | done. %% 接受http请求可能会有多个包 分四个阶接收 --record(state, { +-record(wsState, { stage = reqLine :: stage() %% 接收数据阶段 , buffer = <<>> :: binary() %% 缓存接收到的数据 , wsReq :: undefined | #wsReq{} %% 解析后的http @@ -19,11 +19,13 @@ , temHeader = [] :: [headers()] %% 解析header临时数据 , contentLength :: undefined | non_neg_integer() | chunked %% 长度 , temChunked = <<>> :: binary() + , method :: method() + , path :: binary() , rn :: undefined | binary:cp() - , wsMod :: callback() , socket :: undefined | wsNet:socket() , isSsl = false :: boolean() - , maxSize %% 允许接收的最大长度 - , maxChunkCnt %% 允许接收的最大chunk 数量 - , maxRecvCnt %% 允许允许recv最大的次数 + , wsMod :: callback() + , maxSize = 0 :: pos_integer() %% 允许接收的最大长度 + , maxChunkCnt = 0 :: pos_integer() %% 允许接收的最大chunk 数量 + , maxRecvCnt = 0 :: pos_integer() %% 允许允许recv最大的次数 }). diff --git a/src/test/elli_tests.erl b/src/test/elli_tests.erl index 756a7b7..cf43c3b 100644 --- a/src/test/elli_tests.erl +++ b/src/test/elli_tests.erl @@ -111,7 +111,7 @@ accessors_test_() -> Method = 'POST', Body = <<"name=knut%3D">>, Name = <<"knut=">>, - Req1 = #wsReq{raw_path = RawPath, + Req1 = #wsReq{path = RawPath, original_headers = Headers, headers = Headers, method = Method, @@ -636,7 +636,7 @@ to_proplist_test() -> path = [<<"crash">>], args = [], version = {1, 1}, - raw_path = <<"/crash">>, + path = <<"/crash">>, original_headers = [{<<"Host">>, <<"localhost:3001">>}], headers = [{<<"host">>, <<"localhost:3001">>}], body = <<>>, @@ -666,7 +666,7 @@ is_request_test() -> query_str_test_() -> - MakeReq = fun(Path) -> #wsReq{raw_path = Path} end, + MakeReq = fun(Path) -> #wsReq{path = Path} end, [ %% For empty query strings, expect `query_str` to return an empty binary. ?_assertMatch(<<>>, wsReq:query_str(MakeReq(<<"/foo">>))), diff --git a/src/wsSrv/wsHttp.erl b/src/wsSrv/wsHttp.erl index 3675fb5..15694ee 100644 --- a/src/wsSrv/wsHttp.erl +++ b/src/wsSrv/wsHttp.erl @@ -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, <>, 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, <>, 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} -> - <>; - {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, <>, - {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); - _ -> - <> = 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} -> - {<>, <<>>}; - {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. diff --git a/src/wsSrv/wsHttpProtocol.erl b/src/wsSrv/wsHttpProtocol.erl index 28ea4ff..9b98c7e 100644 --- a/src/wsSrv/wsHttpProtocol.erl +++ b/src/wsSrv/wsHttpProtocol.erl @@ -10,17 +10,22 @@ request/3 ]). --spec request(Stage :: stage(), Data :: binary(), State :: #state{}) -> {ok, NewState :: #state{}} | {done, NewState :: #state{}} | {error, term()}. +-spec request(Stage :: stage(), Data :: binary(), State :: #wsState{}) -> {ok, NewState :: #wsState{}} | {done, NewState :: #wsState{}} | {error, term()}. request(reqLine, Data, State) -> case erlang:decode_packet(http_bin, Data, []) of {more, _} -> - {ok, State#state{buffer = Data}}; + {ok, State#wsState{buffer = Data}}; {ok, {http_request, Method, RawPath, Version}, Rest} -> case byte_size(Rest) > 0 of true -> - request(header, Rest, State#state{stage = header, buffer = Rest, wsReq = #wsReq{method = Method, raw_path = RawPath, version = Version}, headerCnt = 0}); + case parsePath(RawPath) of + {ok, Scheme, Host, Port, Path, URLArgs} -> + request(header, Rest, State#wsState{stage = header, buffer = Rest, method = Method, path = Path, wsReq = #wsReq{method = Method, path = Path, version = Version, scheme = Scheme, host = Host, port = Port, args = URLArgs}, headerCnt = 0}); + _Err -> + _Err + end; _ -> - {ok, State#state{stage = header, buffer = <<>>, wsReq = #wsReq{method = Method, raw_path = RawPath, version = Version}, headerCnt = 0}} + {ok, State#wsState{stage = header, buffer = <<>>, wsReq = #wsReq{method = Method, path = RawPath, version = Version}, headerCnt = 0}} end; {ok, {http_error, ErrStr}, _} -> {error, ErrStr}; @@ -32,9 +37,9 @@ request(reqLine, Data, State) -> request(header, Data, State) -> case erlang:decode_packet(httph_bin, Data, []) of {more, _} -> - {ok, State#state{buffer = Data}}; + {ok, State#wsState{buffer = Data}}; {ok, {http_header, _, Key, _, Value}, Rest} -> - #state{headerCnt = HeaderCnt, temHeader = TemHeader, rn = Rn} = State, + #wsState{headerCnt = HeaderCnt, temHeader = TemHeader, rn = Rn, maxSize = MaxSize} = State, NewTemHeader = [{Key, Value} | TemHeader], NewHeaderCnt = HeaderCnt + 1, case NewHeaderCnt >= 100 of @@ -43,37 +48,43 @@ request(header, Data, State) -> _ -> case Key of 'Content-Length' -> - request(header, Rest, State#state{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader, contentLength = binary_to_integer(Value)}); + ContentLength = binary_to_integer(Value), + case ContentLength > MaxSize of + true -> + {err_code, 413}; + _ -> + request(header, Rest, State#wsState{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader, contentLength = ContentLength}) + end; 'Transfer-Encoding' -> case Value of <<"chunked">> -> case Rn of undefined -> - request(header, Rest, State#state{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader, contentLength = chunked, rn = binary:compile_pattern(<<"\r\n">>)}); + request(header, Rest, State#wsState{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader, contentLength = chunked, rn = binary:compile_pattern(<<"\r\n">>)}); _ -> - request(header, Rest, State#state{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader, contentLength = chunked}) + request(header, Rest, State#wsState{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader, contentLength = chunked}) end; _ -> {error, 'Transfer-Encoding'} end; _ -> - request(header, Rest, State#state{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader}) + request(header, Rest, State#wsState{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader}) end end; {ok, http_eoh, Rest} -> - #state{temHeader = TemHeader, contentLength = CLen, wsReq = WsReq} = State, + #wsState{temHeader = TemHeader, contentLength = CLen, wsReq = WsReq} = State, NewWsReq = WsReq#wsReq{headers = TemHeader}, case CLen of undefined -> {error, content_length}; 0 -> - {done, State#state{buffer = Rest, wsReq = NewWsReq, temHeader = []}}; + {done, State#wsState{buffer = Rest, wsReq = NewWsReq, temHeader = []}}; _ -> case byte_size(Rest) > 0 of true -> - request(body, Rest, State#state{stage = body, buffer = Rest, wsReq = NewWsReq, temHeader = []}); + request(body, Rest, State#wsState{stage = body, buffer = Rest, wsReq = NewWsReq, temHeader = []}); _ -> - {ok, State#state{stage = body, buffer = <<>>, wsReq = NewWsReq, temHeader = []}} + {ok, State#wsState{stage = body, buffer = <<>>, wsReq = NewWsReq, temHeader = []}} end end; {ok, {http_error, ErrStr}, _Rest} -> @@ -82,14 +93,14 @@ request(header, Data, State) -> Ret end; request(body, Data, State) -> - #state{contentLength = CLen, wsReq = WsReq, temChunked = TemChunked, rn = Rn} = State, + #wsState{contentLength = CLen, wsReq = WsReq, temChunked = TemChunked, rn = Rn} = State, case CLen of chunked -> case parseChunks(Data, Rn, TemChunked) of {ok, NewTemChunked, Rest} -> - {ok, State#state{buffer = Rest, temChunked = NewTemChunked}}; + {ok, State#wsState{buffer = Rest, temChunked = NewTemChunked}}; {over, LastTemChunked, Rest} -> - {done, State#state{buffer = Rest, temChunked = <<>>, wsReq = WsReq#wsReq{body = LastTemChunked}}}; + {done, State#wsState{buffer = Rest, temChunked = <<>>, wsReq = WsReq#wsReq{body = LastTemChunked}}}; {error, _Reason} = Ret -> Ret end; @@ -97,12 +108,12 @@ request(body, Data, State) -> BodySize = erlang:size(Data), if BodySize == CLen -> - {done, State#state{buffer = <<>>, wsReq = WsReq#wsReq{body = Data}}}; + {done, State#wsState{buffer = <<>>, wsReq = WsReq#wsReq{body = Data}}}; BodySize > CLen -> <> = Data, - {done, State#state{buffer = Rest, wsReq = WsReq#wsReq{body = Body}}}; + {done, State#wsState{buffer = Rest, wsReq = WsReq#wsReq{body = Body}}}; true -> - {ok, State#state{buffer = Data}} + {ok, State#wsState{buffer = Data}} end end. @@ -138,4 +149,18 @@ chunkSize(Bin) -> catch error:badarg -> undefined - end. \ No newline at end of file + end. + +parsePath({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, uri_string:dissect_query(Query)}; +parsePath({absoluteURI, Scheme, Host, Port, Path}) -> + {_, _Scheme, _Host, _Port, RetPath, RetQuery} = parsePath({abs_path, Path}), + {ok, Scheme, Host, Port, RetPath, RetQuery}; +parsePath(_) -> + {error, unsupported_uri}. \ No newline at end of file diff --git a/src/wsSrv/wsHttps.erl b/src/wsSrv/wsHttps.erl index a4f9510..3eb9238 100644 --- a/src/wsSrv/wsHttps.erl +++ b/src/wsSrv/wsHttps.erl @@ -600,7 +600,7 @@ mk_req(Method, PathTuple, Headers, ParsedHeaders, Body, V, Socket, {Mod, Args} = {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, + version = V, path = Path, original_headers = Headers, body = Body, pid = self(), socket = Socket, callback = Callback, headers = ParsedHeaders}; {error, Reason} -> diff --git a/src/wsSrv/wsReq.erl b/src/wsSrv/wsReq.erl index 7236a41..ff3631f 100644 --- a/src/wsSrv/wsReq.erl +++ b/src/wsSrv/wsReq.erl @@ -55,7 +55,7 @@ %% @doc Return `path' split into binary parts. path(#wsReq{path = Path}) -> Path. %% @doc Return the `raw_path', i.e. not split or parsed for query params. -raw_path(#wsReq{raw_path = Path}) -> Path. +raw_path(#wsReq{path = Path}) -> Path. %% @doc Return the `headers' that have had `string:casefold/1' run on each key. headers(#wsReq{headers = Headers}) -> Headers. %% @doc Return the original `headers'. @@ -167,7 +167,7 @@ post_args_decoded(#wsReq{} = Req) -> %% @doc Calculate the query string associated with a given `Request' %% as a binary. -spec query_str(elli:req()) -> QueryStr :: binary(). -query_str(#wsReq{raw_path = Path}) -> +query_str(#wsReq{path = Path}) -> case binary:split(Path, [<<"?">>]) of [_, Qs] -> Qs; [_] -> <<>>