diff --git a/include/eWSrv.hrl b/include/eWSrv.hrl index 28b29f1..4f019f8 100644 --- a/include/eWSrv.hrl +++ b/include/eWSrv.hrl @@ -35,10 +35,10 @@ body = <<>> :: body() }). --export_type([req/0, method/0, body/0, headers/0, response_code/0]). +-export_type([wsReq/0, method/0, body/0, headers/0, response_code/0]). %% @type req(). A record representing an HTTP request. --type req() :: #wsReq{}. +-type wsReq() :: #wsReq{}. %% @type http_method(). An uppercase atom representing a known HTTP verb or a %% binary for other verbs. @@ -53,51 +53,9 @@ -type response_code() :: 100..999. --define(EXAMPLE_CONF, [{callback, elli_example_callback}, {callback_args, []}]). - --define(CONTENT_LENGTH_HEADER, <<"content-length">>). +-define(CONTENT_LENGTH_HEADER, 'Content-Length'). -define(EXPECT_HEADER, <<"expect">>). --define(CONNECTION_HEADER, <<"connection">>). +-define(CONNECTION_HEADER, 'Connection'). -define(TRANSFER_ENCODING_HEADER, <<"Transfer-Encoding">>). - - --export_type([callback/0, callback_mod/0, callback_args/0, event/0, result/0]). - -%% @type callback(). A tuple of a {@type callback_mod()} and {@type -%% callback_args()}. --type callback() :: {callback_mod(), callback_args()}. - -%% @type callback_mod(). A callback module. --type callback_mod() :: module(). - -%% @type callback_args(). Arguments to pass to a {@type callback_mod()}. --type callback_args() :: list(). - -%% @type event(). Fired throughout processing a request. -%% See {@link elli_example_callback:handle_event/3} for descriptions. --type event() :: -elli_startup| -bad_request | -file_error| -chunk_complete | -request_complete| -request_throw | -request_error | -request_exit| -request_closed | -request_parse_error| -client_closed | -client_timeout| -invalid_return. - --type result() :: -{elli:response_code() |ok, elli:headers(), {file, file:name_all()}| -{file, file:name_all(), wsUtil:range()}}| -{elli:response_code() | ok, elli:headers(), elli:body()}| -{elli:response_code() | ok, elli:body()}| -{chunk, elli:headers()}| -{chunk, elli:headers(), elli:body()}| -ignore. - -type sendfile_opts() :: [{chunk_size, non_neg_integer()}]. \ No newline at end of file diff --git a/include/wsCom.hrl b/include/wsCom.hrl index d52c4a3..dea1ad8 100644 --- a/include/wsCom.hrl +++ b/include/wsCom.hrl @@ -24,7 +24,7 @@ , rn :: undefined | binary:cp() , socket :: undefined | wsNet:socket() , isSsl = false :: boolean() - , wsMod :: callback() + , wsMod :: module() , maxSize = 0 :: pos_integer() %% 允许接收的最大长度 , maxChunkCnt = 0 :: pos_integer() %% 允许接收的最大chunk 数量 , maxRecvCnt = 0 :: pos_integer() %% 允许允许recv最大的次数 diff --git a/src/example/wsEgHandover.erl b/src/example/wsEgHandover.erl index 0f0c92a..30fc596 100644 --- a/src/example/wsEgHandover.erl +++ b/src/example/wsEgHandover.erl @@ -16,7 +16,7 @@ init(Req, _Args) -> %% TODO: write docstring -spec handle(Req, Args) -> Result when - Req :: elli:req(), + Req :: elli:wsReq(), Args :: wsHer:callback_args(), Result :: wsHer:result(). handle(Req, Args) -> diff --git a/src/example/wsEgHer.erl b/src/example/wsEgHer.erl index 47949d3..a7f81d4 100644 --- a/src/example/wsEgHer.erl +++ b/src/example/wsEgHer.erl @@ -23,7 +23,7 @@ %% Delegate to our handler function. %% @see handle/3 -spec handle(Req, _Args) -> Result when - Req :: elli:req(), + Req :: elli:wsReq(), _Args :: wsHer:callback_args(), Result :: wsHer:result(). handle(Req, _Args) -> handle(Req#wsReq.method, wsReq:path(Req), Req). @@ -64,7 +64,7 @@ handle(Req, _Args) -> handle(Req#wsReq.method, wsReq:path(Req), Req). -spec handle(Method, Path, Req) -> wsHer:result() when Method :: elli:method(), Path :: [binary()], - Req :: elli:req(). + Req :: elli:wsReq(). handle('GET', [<<"hello">>, <<"world">>], _Req) -> %% Reply with a normal response. timer:sleep(1000), diff --git a/src/test/elli_tests.erl b/src/test/elli_tests.erl index cf43c3b..456a3fe 100644 --- a/src/test/elli_tests.erl +++ b/src/test/elli_tests.erl @@ -112,12 +112,11 @@ accessors_test_() -> Body = <<"name=knut%3D">>, Name = <<"knut=">>, Req1 = #wsReq{path = RawPath, - original_headers = Headers, headers = Headers, method = Method, body = Body}, Args = [{<<"name">>, Name}], - Req2 = #wsReq{original_headers = Headers, headers = Headers, args = Args, body = <<>>}, + Req2 = #wsReq{ headers = Headers, args = Args, body = <<>>}, [ %% POST /foo/bar @@ -627,22 +626,16 @@ body_qs_test() -> {<<"found">>, true}], Body = <<"foo=bar&baz=bang&found">>, Headers = [{<<"content-type">>, <<"application/x-www-form-urlencoded">>}], - ?assertMatch(Expected, wsReq:body_qs(#wsReq{body = Body, - original_headers = Headers, - headers = Headers})). + ?assertMatch(Expected, wsReq:body_qs(#wsReq{body = Body, headers = Headers})). to_proplist_test() -> Req = #wsReq{method = 'GET', path = [<<"crash">>], args = [], version = {1, 1}, - path = <<"/crash">>, - original_headers = [{<<"Host">>, <<"localhost:3001">>}], headers = [{<<"host">>, <<"localhost:3001">>}], - body = <<>>, - pid = self(), - socket = socket, - callback = {mod, []}}, + body = <<>> + }, Prop = [{method, 'GET'}, {scheme, undefined}, diff --git a/src/test/wsTest.erl b/src/test/wsTest.erl deleted file mode 100644 index 3f81bf4..0000000 --- a/src/test/wsTest.erl +++ /dev/null @@ -1,56 +0,0 @@ --module(wsTest). - --compile([export_all]). - --define(Line, <<"GET /test/tttt HTTP/1.1\r\n">>). - -test1(Rn) -> - parseRequestLine(?Line, Rn). - -test2() -> - erlang:decode_packet(http_bin, ?Line, []). - -parseRequestLine(Data, Rn) -> - case binary:split(Data, Rn) of - [Data] -> - not_enough_data; - [Line, Rest] -> - case binary:split(Line, <<" ">>, [global]) of - [Method, RawPath, V] -> - {Method, RawPath, V, Rest}; - _ -> - {error, request_line} - end - end. - - -%% <<"Content-Type: application/json; charset=utf-8">>, --spec request(boolean(), body(), method(), host(), binary(), path(), headers()) -> iolist(). -request(true, undefined, Method, Host, _DbName, Path, Headers) -> - [ - Method, <<"/_db/_system">>, Path, <<" HTTP/1.1\r\nHost: ">>, Host, - <<"\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 0\r\n">>, - spellHeaders(Headers), <<"\r\n">> - ]; -request(false, undefined, Method, Host, DbName, Path, Headers) -> - [ - Method, DbName, Path, <<" HTTP/1.1\r\nHost: ">>, Host, - <<"\r\nContent-Type: application/json; charset=utf-8\r\nContent-Length: 0\r\n">>, - spellHeaders(Headers), <<"\r\n">> - ]; -request(false, Body, Method, Host, DbName, Path, Headers) -> - ContentLength = integer_to_binary(iolist_size(Body)), - NewHeaders = [{<<"Content-Length">>, ContentLength} | Headers], - [ - Method, DbName, Path, <<" HTTP/1.1\r\nHost: ">>, Host, - <<"\r\nContent-Type: application/json; charset=utf-8\r\n">>, - spellHeaders(NewHeaders), <<"\r\n">>, Body - ]; -request(true, Body, Method, Host, _DbName, Path, Headers) -> - ContentLength = integer_to_binary(iolist_size(Body)), - NewHeaders = [{<<"Content-Length">>, ContentLength} | Headers], - [ - Method, <<"/_db/_system">>, Path, <<" HTTP/1.1\r\nHost: ">>, Host, - <<"\r\nContent-Type: application/json; charset=utf-8\r\n">>, - spellHeaders(NewHeaders), <<"\r\n">>, Body - ]. \ No newline at end of file diff --git a/src/wsSrv/elli.erl b/src/wsSrv/elli.erl deleted file mode 100644 index ddf9a8f..0000000 --- a/src/wsSrv/elli.erl +++ /dev/null @@ -1,264 +0,0 @@ --module(elli). - --behaviour(gen_server). - --include("wsCom.hrl"). - --export([ - start_link/0, - start_link/1, - stop/1, - get_acceptors/1, - get_open_reqs/1, - get_open_reqs/2, - set_callback/3 -]). - --export([ - init/1 - , handle_call/3 - , handle_cast/2 - , handle_info/2 - , terminate/2 - , code_change/3 -]). - --record(state, {socket :: wsNet:socket(), - acceptors :: ets:tid(), - open_reqs = 0 :: non_neg_integer(), - options = [] :: [{_, _}], % TODO: refine - callback :: wsHer:callback() -}). -%% @type state(). Internal state. --opaque state() :: #state{}. --export_type([state/0]). - - -%%%=================================================================== -%%% API -%%%=================================================================== - --spec start_link() -> Result when - Result :: {ok, Pid} | ignore | {error, Error}, - Pid :: pid(), - Error :: {already_started, Pid} | term(). -%% @equiv start_link({@EXAMPLE_CONF}) -%% @doc Create an Elli server process as part of a supervision tree, using the -%% default configuration. -start_link() -> start_link(?EXAMPLE_CONF). - --spec start_link(Opts) -> Result when - Opts :: [{_, _}], % TODO: refine - Result :: {ok, Pid} | ignore | {error, Error}, - Pid :: pid(), - Error :: {already_started, Pid} | term(). -start_link(Opts) -> - valid_callback(required_opt(callback, Opts)) orelse throw(invalid_callback), - - case proplists:get_value(name, Opts) of - undefined -> - gen_server:start_link(?MODULE, [Opts], []); - Name -> - gen_server:start_link(Name, ?MODULE, [Opts], []) - end. - --spec get_acceptors(atom()) -> {reply, {ok, [ets:tid()]}, state()}. -get_acceptors(S) -> - gen_server:call(S, get_acceptors). - --spec get_open_reqs(S :: atom()) -> {reply, {ok, non_neg_integer()}, state()}. -%% @equiv get_open_reqs(S, 5000) -get_open_reqs(S) -> - get_open_reqs(S, 5000). - --spec get_open_reqs(S :: atom(), Timeout :: non_neg_integer()) -> Reply when - Reply :: {reply, {ok, non_neg_integer()}, state()}. -get_open_reqs(S, Timeout) -> - gen_server:call(S, get_open_reqs, Timeout). - --spec set_callback(S, Callback, CallbackArgs) -> Reply when - S :: atom(), - Callback :: wsHer:callback_mod(), - CallbackArgs :: wsHer:callback_args(), - Reply :: {reply, ok, state()}. -set_callback(S, Callback, CallbackArgs) -> - valid_callback(Callback) orelse throw(invalid_callback), - gen_server:call(S, {set_callback, Callback, CallbackArgs}). - -%% @doc Stop `Server'. --spec stop(Server :: atom()) -> {stop, normal, ok, state()}. -stop(S) -> - gen_server:call(S, stop). - - -%%%=================================================================== -%%% gen_server callbacks -%%%=================================================================== - -%% @hidden --spec init([Opts :: [{_, _}]]) -> {ok, state()}. -init([Opts]) -> - %% Use the exit signal from the acceptor processes to know when - %% they exit - process_flag(trap_exit, true), - - Callback = required_opt(callback, Opts), - CallbackArgs = proplists:get_value(callback_args, Opts), - IPAddress = proplists:get_value(ip, Opts, {0, 0, 0, 0}), - Port = proplists:get_value(port, Opts, 8080), - MinAcceptors = proplists:get_value(min_acceptors, Opts, 20), - - UseSSL = proplists:get_value(ssl, Opts, false), - KeyFile = proplists:get_value(keyfile, Opts), - CertFile = proplists:get_value(certfile, Opts), - SockType = ?IIF(UseSSL, ssl, plain), - SSLSockOpts = ?IIF(UseSSL, - [{keyfile, KeyFile}, {certfile, CertFile}], - []), - - AcceptTimeout = proplists:get_value(accept_timeout, Opts, 10000), - RequestTimeout = proplists:get_value(request_timeout, Opts, 60000), - HeaderTimeout = proplists:get_value(header_timeout, Opts, 10000), - BodyTimeout = proplists:get_value(body_timeout, Opts, 30000), - MaxBodySize = proplists:get_value(max_body_size, Opts, 1024000), - - Options = [{accept_timeout, AcceptTimeout}, - {request_timeout, RequestTimeout}, - {header_timeout, HeaderTimeout}, - {body_timeout, BodyTimeout}, - {max_body_size, MaxBodySize}], - - %% Notify the handler that we are about to start accepting - %% requests, so it can create necessary supporting processes, ETS - %% tables, etc. - ok = Callback:handle_event(elli_startup, [], CallbackArgs), - - {ok, Socket} = wsNet:listen(SockType, Port, [binary, - {ip, IPAddress}, - {reuseaddr, true}, - {backlog, 32768}, - {packet, raw}, - {active, false} - | SSLSockOpts]), - - Acceptors = ets:new(acceptors, [private, set]), - [begin - Pid = wsHttp:start_link(self(), Socket, Options, {Callback, CallbackArgs}), - ets:insert(Acceptors, {Pid}) - end - || _ <- lists:seq(1, MinAcceptors)], - - {ok, #state{socket = Socket, - acceptors = Acceptors, - open_reqs = 0, - options = Options, - callback = {Callback, CallbackArgs}}}. - - -%% @hidden --spec handle_call(get_acceptors, {pid(), _Tag}, state()) -> - {reply, {ok, [ets:tid()]}, state()}; - (get_open_reqs, {pid(), _Tag}, state()) -> - {reply, {ok, OpenReqs :: non_neg_integer()}, state()}; - (stop, {pid(), _Tag}, state()) -> {stop, normal, ok, state()}; - ({set_callback, Mod, Args}, {pid(), _Tag}, state()) -> - {reply, ok, state()} when - Mod :: wsHer:callback_mod(), - Args :: wsHer:callback_args(). -handle_call(get_acceptors, _From, State) -> - Acceptors = [Pid || {Pid} <- ets:tab2list(State#state.acceptors)], - {reply, {ok, Acceptors}, State}; - -handle_call(get_open_reqs, _From, State) -> - {reply, {ok, State#state.open_reqs}, State}; - -handle_call({set_callback, Callback, CallbackArgs}, _From, State) -> - ok = Callback:handle_event(elli_reconfigure, [], CallbackArgs), - {reply, ok, State#state{callback = {Callback, CallbackArgs}}}; - -handle_call(stop, _From, State) -> - {stop, normal, ok, State}. - -%% @hidden --spec handle_cast(accepted | _Msg, State0) -> {noreply, State1} when - State0 :: state(), - State1 :: state(). -handle_cast(accepted, State) -> - {noreply, start_add_acceptor(State)}; - -handle_cast(_Msg, State) -> - {noreply, State}. - - -%% @hidden --spec handle_info({'EXIT', _Pid, Reason}, State0) -> Result when - State0 :: state(), - Reason :: {error, emfile}, - Result :: {stop, emfile, State0} - | {noreply, State1 :: state()}. -handle_info({'EXIT', _Pid, {error, emfile}}, State) -> - ?wsErr("No more file descriptors, shutting down~n"), - {stop, emfile, State}; - -handle_info({'EXIT', Pid, normal}, State) -> - {noreply, remove_acceptor(State, Pid)}; - -handle_info({'EXIT', Pid, Reason}, State) -> - ?wsErr("Elli request (pid ~p) unexpectedly crashed:~n~p~n", [Pid, Reason]), - {noreply, remove_acceptor(State, Pid)}. - - -%% @hidden --spec terminate(_Reason, _State) -> ok. -terminate(_Reason, _State) -> - ok. - -%% @hidden --spec code_change(_OldVsn, State, _Extra) -> {ok, State} when State :: state(). -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%%=================================================================== -%%% Internal functions -%%%=================================================================== - --spec remove_acceptor(State0 :: state(), Pid :: pid()) -> State1 :: state(). -remove_acceptor(State, Pid) -> - ets:delete(State#state.acceptors, Pid), - dec_open_reqs(State). - --spec start_add_acceptor(State0 :: state()) -> State1 :: state(). -start_add_acceptor(State) -> - Pid = wsHttp:start_link(self(), State#state.socket, - State#state.options, State#state.callback), - add_acceptor(State, Pid). - --spec add_acceptor(State0 :: state(), Pid :: pid()) -> State1 :: state(). -add_acceptor(#state{acceptors = As} = State, Pid) -> - ets:insert(As, {Pid}), - inc_open_reqs(State). - --spec required_opt(Name, Opts) -> Value when - Name :: any(), - Opts :: proplists:proplist(), - Value :: term(). -required_opt(Name, Opts) -> - case proplists:get_value(Name, Opts) of - undefined -> - throw(badarg); - Value -> - Value - end. - --spec valid_callback(Mod :: module()) -> Exported :: boolean(). -valid_callback(Mod) -> - lists:member({handle, 2}, Mod:module_info(exports)) andalso - lists:member({handle_event, 3}, Mod:module_info(exports)). - --spec dec_open_reqs(State0 :: state()) -> State1 :: state(). -dec_open_reqs(#state{open_reqs = OpenReqs} = State) -> - State#state{open_reqs = OpenReqs - 1}. - --spec inc_open_reqs(State0 :: state()) -> State1 :: state(). -inc_open_reqs(#state{open_reqs = OpenReqs} = State) -> - State#state{open_reqs = OpenReqs + 1}. diff --git a/src/wsSrv/wsHer.erl b/src/wsSrv/wsHer.erl index 7790440..2cc02fc 100644 --- a/src/wsSrv/wsHer.erl +++ b/src/wsSrv/wsHer.erl @@ -2,15 +2,35 @@ -include("eWSrv.hrl"). --callback init(Req :: elli:req(), Args :: callback_args()) -> {ok, standard | handover}. +-export_type([callback/0, callback_mod/0, callback_args/0, result/0]). --callback handle(Req :: elli:req(), callback_args()) -> result(). +%% @type callback(). A tuple of a {@type callback_mod()} and {@type +%% callback_args()}. +-type callback() :: {callback_mod(), callback_args()}. --callback handle_event(Event :: event(), Args :: callback_args(), Config :: [tuple()]) -> ok. +%% @type callback_mod(). A callback module. +-type callback_mod() :: module(). --callback preprocess(Req1 :: elli:req(), Args :: callback_args()) -> Req2 :: elli:req(). +%% @type callback_args(). Arguments to pass to a {@type callback_mod()}. +-type callback_args() :: list(). --callback postprocess(Req :: elli:req(), Res1 :: result(), Args :: callback_args()) -> Res2 :: result(). +-type result() :: + {response_code() |ok, headers(), {file, file:name_all()}| + {file, file:name_all(), wsUtil:range()}}| + {response_code() | ok, headers(), body()}| + {response_code() | ok, body()}| + {chunk, headers()}| + {chunk, headers(), body()}| + ignore. + +-callback init(Req :: wsReq(), Args :: callback_args()) -> {ok, standard | handover}. + +-callback handle(Req :: wsReq(), callback_args()) -> result(). + + +-callback preprocess(Req1 :: wsReq(), Args :: callback_args()) -> Req2 :: wsReq(). + +-callback postprocess(Req :: wsReq(), Res1 :: result(), Args :: callback_args()) -> Res2 :: result(). -optional_callbacks([ init/2 diff --git a/src/wsSrv/wsHttp.erl b/src/wsSrv/wsHttp.erl index 15694ee..0fcb0ed 100644 --- a/src/wsSrv/wsHttp.erl +++ b/src/wsSrv/wsHttp.erl @@ -8,16 +8,12 @@ -export([ start_link/1 - , send_response/4 + , sendResponse/5 , send_file/5 - %% Exported for looping with a fully-qualified module name - , handle_request/4 , chunk_loop/1 , split_args/1 - , keepalive_loop/3 - , keepalive_loop/5 - , close_or_keepalive/2 + , closeOrKeepAlive/2 ]). %% eNet callback @@ -61,9 +57,10 @@ handleInfo({tcp, _Socket, Data}, State) -> {noreply, NewState}; {done, NewState} -> Response = doHandle(NewState), - case handle_response(Req1, B2, Response) of - {keep_alive, NewBuffer} -> - handleInfo({tcp, _Socket, NewBuffer}, NewState); + #wsState{buffer = Buffer, socket = Socket, temHeader = TemHeader, method = Method} = NewState, + case doResponse(Response, Socket, TemHeader, Method) of + keep_alive -> + handleInfo({tcp, _Socket, Buffer}, newWsState(NewState)); {close, _} -> wsNet:close(Socket), ok @@ -104,14 +101,14 @@ handleInfo({?mSockReady, Sock}, _State) -> inet:setopts(Sock, [{packet, raw}, {active, true}]), {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#wsState{socket = SslSock, isSsl = true}}; - _Err -> - ?wsErr("ssl handshake error ~p~n", [_Err]), - {stop, _Err, State} - end; + case ntSslAcceptor:handshake(Sock, SslOpts, SslHSTet) of + {ok, SslSock} -> + ssl:setopts(Sock, [{packet, raw}, {active, true}]), + {ok, State#wsState{socket = SslSock, isSsl = true}}; + _Err -> + ?wsErr("ssl handshake error ~p~n", [_Err]), + {stop, _Err, State} + end; handleInfo(_Msg, _State) -> ?wsErr("~p info receive unexpect msg ~p ~n ", [?MODULE, _Msg]), kpS. @@ -122,156 +119,152 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. -%% @doc Handle multiple requests on the same connection, i.e. `"keep alive"'. -keepalive_loop(Socket, Options, Callback) -> - keepalive_loop(Socket, 0, <<>>, Options, Callback). +newWsState(WsState) -> + WsState#wsState{ + stage = reqLine + , buffer = <<>> + , wsReq = undefined + , headerCnt = 0 + , temHeader = [] + , contentLength = undefined + , temChunked = <<>> + }. -keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) -> - case handle_request(Socket, Buffer, Options, Callback) of - {keep_alive, NewBuffer} -> - keepalive_loop(Socket, NumRequests + 1, NewBuffer, Options, Callback); - {close, _} -> - wsNet:close(Socket), - ok +%% @doc Execute the user callback, translating failure into a proper response. +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, []}; + {ok, Headers, {file, Filename, Range}} -> + {file, 200, Headers, Filename, Range}; + %% ok simple + {ok, Headers, Body} -> {response, 200, Headers, Body}; + {ok, Body} -> {response, 200, [], Body}; + %% Chunk + {chunk, Headers} -> {chunk, Headers, <<"">>}; + {chunk, Headers, Initial} -> {chunk, Headers, Initial}; + %% File + {HttpCode, Headers, {file, Filename}} -> + {file, HttpCode, Headers, Filename, {0, 0}}; + {HttpCode, Headers, {file, Filename, Range}} -> + {file, HttpCode, Headers, Filename, Range}; + %% Simple + {HttpCode, Headers, Body} -> {response, HttpCode, Headers, Body}; + {HttpCode, Body} -> {response, HttpCode, [], Body}; + %% Unexpected + Unexpected -> + ?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 -> + ?wsErr("handle catch throw ~p ~p ~p~n", [WsReq, Exc, Stacktrace]), + {response, 500, [], <<"Internal server error">>}; + error:Error:Stacktrace -> + ?wsErr("handle catch error ~p ~p ~p~n", [WsReq, Error, Stacktrace]), + {response, 500, [], <<"Internal server error">>}; + exit:Exit:Stacktrace -> + ?wsErr("handle catch exit ~p ~p ~p~n", [WsReq, Exit, Stacktrace]), + {response, 500, [], <<"Internal server error">>} end. -handle_response(Req, Buffer, {response, Code, UserHeaders, Body}) -> - #wsReq{callback = {Mod, Args}} = Req, - - Headers = [connection(Req, UserHeaders), - content_length(UserHeaders, Body) - | UserHeaders], - t(send_start), - send_response(Req, Code, Headers, Body), - t(send_end), - - t(request_end), - handle_event(Mod, request_complete, - [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, - - ResponseHeaders = [{?TRANSFER_ENCODING_HEADER, <<"chunked">>}, - connection(Req, UserHeaders) - | UserHeaders], - send_response(Req, 200, ResponseHeaders, <<"">>), - - t(send_start), - Initial =:= <<"">> orelse send_chunk(Req#wsReq.socket, Initial), - ClosingEnd = case start_chunk_loop(Req#wsReq.socket) of - {error, client_closed} -> client; - ok -> server - end, - t(send_end), - - t(request_end), - handle_event(Mod, chunk_complete, - [Req, 200, ResponseHeaders, ClosingEnd, {get_timings(), - get_sizes()}], - Args), - {close, <<>>}; -handle_response(Req, Buffer, {file, ResponseCode, UserHeaders, - Filename, Range}) -> - #wsReq{callback = {Mod, Args}} = Req, - - ResponseHeaders = [connection(Req, UserHeaders) | UserHeaders], - +doResponse({response, Code, UserHeaders, Body}, Socket, TemHeader, Method) -> + Headers = [connection(UserHeaders, TemHeader), contentLength(UserHeaders, Body) | UserHeaders], + sendResponse(Socket, Method, Code, Headers, Body), + closeOrKeepAlive(UserHeaders, TemHeader); +doResponse({chunk, UserHeaders, Initial}, Socket, TemHeader, Method) -> + ResponseHeaders = [{?TRANSFER_ENCODING_HEADER, <<"chunked">>}, connection(UserHeaders, TemHeader) | UserHeaders], + sendResponse(Socket, Method, 200, ResponseHeaders, <<>>), + Initial =:= <<"">> orelse send_chunk(Socket, Initial), + case start_chunk_loop(Socket) of + {error, client_closed} -> client; + ok -> server + end, + close; +doResponse({file, ResponseCode, UserHeaders, Filename, Range}, Socket, TemHeader, Method) -> + ResponseHeaders = [connection(UserHeaders, TemHeader) | UserHeaders], case wsUtil:file_size(Filename) of - {error, FileError} -> - handle_event(Mod, file_error, [FileError], Args), - send_server_error(Req#wsReq.socket), - wsNet:close(Req#wsReq.socket), + {error, _FileError} -> + send_server_error(Socket), + wsNet:close(Socket), exit(normal); Size -> - t(send_start), case wsUtil:normalize_range(Range, Size) of undefined -> - send_file(Req, ResponseCode, - [{<<"Content-Length">>, Size} | - ResponseHeaders], - Filename, {0, 0}); + send_file(Socket, ResponseCode, [{<<"Content-Length">>, Size} | ResponseHeaders], Filename, {0, 0}); {Offset, Length} -> ERange = wsUtil:encode_range({Offset, Length}, Size), - send_file(Req, 206, - lists:append(ResponseHeaders, - [{<<"Content-Length">>, Length}, - {<<"Content-Range">>, ERange}]), + send_file(Socket, 206, + lists:append(ResponseHeaders, [{<<"Content-Length">>, Length}, {<<"Content-Range">>, ERange}]), Filename, {Offset, Length}); invalid_range -> ERange = wsUtil:encode_range(invalid_range, Size), - send_response(Req, 416, - lists:append(ResponseHeaders, - [{<<"Content-Length">>, 0}, - {<<"Content-Range">>, ERange}]), - []) + sendResponse(Socket, Method, 416, lists:append(ResponseHeaders, [{<<"Content-Length">>, 0}, {<<"Content-Range">>, ERange}]), <<>>) end, - t(send_end), - - t(request_end), - handle_event(Mod, request_complete, [Req, ResponseCode, ResponseHeaders, <<>>, {get_timings(), get_sizes()}], Args), - - {close_or_keepalive(Req, UserHeaders), Buffer} + closeOrKeepAlive(UserHeaders, TemHeader) end. %% @doc Generate a HTTP response and send it to the client. -send_response(Req, Code, Headers, UserBody) -> - ResponseHeaders = assemble_response_headers(Code, Headers), - - Body = case {Req#wsReq.method, Code} of - {'HEAD', _} -> <<>>; - {_, 304} -> <<>>; - {_, 204} -> <<>>; - _ -> UserBody - end, - s(resp_body, iolist_size(Body)), - - Response = [ResponseHeaders, - Body], - - case wsNet:send(Req#wsReq.socket, Response) of - ok -> ok; - {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> - #wsReq{callback = {Mod, Args}} = Req, - handle_event(Mod, client_closed, [before_response], Args), - ok +sendResponse(Socket, Method, Code, Headers, UserBody) -> + Body = + case Method of + 'HEAD' -> + <<>>; + _ -> + case Code of + 304 -> + <<>>; + 204 -> + <<>>; + _ -> + UserBody + end + end, + + Response = http_response(Code, Headers, Body), + + case wsNet:send(Socket, Response) of + ok -> + ok; + _Err -> + ?wsErr("send_response error ~p~n", [_Err]) end. + %% @doc Send a HTTP response to the client where the body is the %% contents of the given file. Assumes correctly set response code %% and headers. -spec send_file(Req, Code, Headers, Filename, Range) -> ok when - Req :: elli:req(), + Req :: elli:wsReq(), Code :: elli:response_code(), Headers :: elli:headers(), Filename :: file:filename(), Range :: wsUtil:range(). -send_file(#wsReq{callback = {Mod, Args}} = Req, Code, Headers, Filename, Range) -> +send_file(Socket, Code, Headers, Filename, Range) -> ResponseHeaders = assemble_response_headers(Code, Headers), case file:open(Filename, [read, raw, binary]) of - {ok, Fd} -> do_send_file(Fd, Range, Req, ResponseHeaders); + {ok, Fd} -> do_send_file(Fd, Range, Socket, ResponseHeaders); {error, FileError} -> - handle_event(Mod, file_error, [FileError], Args), - send_server_error(Req#wsReq.socket), - wsNet:close(Req#wsReq.socket), + send_server_error(Socket), + wsNet:close(Socket), exit(normal) end, ok. -do_send_file(Fd, {Offset, Length}, #wsReq{callback = {Mod, Args}} = Req, Headers) -> - try wsNet:send(Req#wsReq.socket, Headers) of +do_send_file(Fd, {Offset, Length}, Socket, Headers) -> + try wsNet:send(Socket, Headers) of ok -> - case wsNet:sendfile(Fd, Req#wsReq.socket, Offset, Length, []) of + case wsNet:sendfile(Fd, Socket, Offset, Length, []) of {ok, BytesSent} -> s(file, BytesSent), ok; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - handle_event(Mod, client_closed, [before_response], Args) + {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> + ?wsErr("send file error") end; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - handle_event(Mod, client_closed, [before_response], Args) + {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> + ?wsErr("send file error") after file:close(Fd) end. @@ -289,46 +282,6 @@ send_rescue_response(Socket, Code, Body) -> Response = http_response(Code, Body), wsNet:send(Socket, Response). -%% @doc Execute the user callback, translating failure into a proper response. -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, []}; - {ok, Headers, {file, Filename, Range}} -> - {file, 200, Headers, Filename, Range}; - %% ok simple - {ok, Headers, Body} -> {response, 200, Headers, Body}; - {ok, Body} -> {response, 200, [], Body}; - %% Chunk - {chunk, Headers} -> {chunk, Headers, <<"">>}; - {chunk, Headers, Initial} -> {chunk, Headers, Initial}; - %% File - {HttpCode, Headers, {file, Filename}} -> - {file, HttpCode, Headers, Filename, {0, 0}}; - {HttpCode, Headers, {file, Filename, Range}} -> - {file, HttpCode, Headers, Filename, Range}; - %% Simple - {HttpCode, Headers, Body} -> {response, HttpCode, Headers, Body}; - {HttpCode, Body} -> {response, HttpCode, [], Body}; - %% Unexpected - Unexpected -> - ?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 -> - ?wsErr("handle catch throw ~p ~p ~p~n", [WsReq, Exc, Stacktrace]), - {response, 500, [], <<"Internal server error">>}; - error:Error:Stacktrace -> - ?wsErr("handle catch error ~p ~p ~p~n", [WsReq, Error, Stacktrace]), - {response, 500, [], <<"Internal server error">>}; - exit:Exit:Stacktrace -> - ?wsErr("handle catch exit ~p ~p ~p~n", [WsReq, Exit, Stacktrace]), - {response, 500, [], <<"Internal server error">>} - end. %% %% CHUNKED-TRANSFER @@ -403,7 +356,7 @@ 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 % headers contains "Expect:100-continue" - case get_header(?EXPECT_HEADER, Headers, undefined) of + case lists:keyfind(?EXPECT_HEADER, 1, Headers) of <<"100-continue">> -> Response = http_response(100), wsNet:send(Socket, Response); @@ -417,84 +370,56 @@ http_response(Code) -> http_response(Code, Body) -> http_response(Code, [{?CONTENT_LENGTH_HEADER, size(Body)}], Body). -http_response(Code, Headers, <<>>) -> - [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, - encode_headers(Headers), <<"\r\n">>]; http_response(Code, Headers, Body) -> - [http_response(Code, Headers, <<>>), Body]. + [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, spellHeaders(Headers), <<"\r\n">>, Body]. assemble_response_headers(Code, Headers) -> ResponseHeaders = http_response(Code, Headers, <<>>), s(resp_headers, iolist_size(ResponseHeaders)), ResponseHeaders. -encode_headers([]) -> - []; - -encode_headers([[] | H]) -> - encode_headers(H); -encode_headers([{K, V} | H]) -> - [encode_value(K), <<": ">>, encode_value(V), <<"\r\n">>, encode_headers(H)]. - -encode_value(V) when is_integer(V) -> integer_to_binary(V); -encode_value(V) when is_binary(V) -> V; -encode_value(V) when is_list(V) -> list_to_binary(V). - -connection_token(#wsReq{version = {1, 1}, headers = Headers}) -> - case get_header(?CONNECTION_HEADER, Headers) of - <<"close">> -> <<"close">>; - <<"Close">> -> <<"close">>; - _ -> <<"Keep-Alive">> - end; -connection_token(#wsReq{version = {1, 0}, headers = Headers}) -> - case get_header(?CONNECTION_HEADER, Headers) of - <<"Keep-Alive">> -> <<"Keep-Alive">>; - _ -> <<"close">> - end; -connection_token(#wsReq{version = {0, 9}}) -> - <<"close">>. - -%% @doc Return the preferred session handling setting to close or keep the -%% current session alive based on the presence of a header or the standard -%% default based on the version of HTTP of the request. --spec close_or_keepalive(Req, Headers) -> KeepaliveOpt when - Req :: elli:req(), - Headers :: elli:headers(), - KeepaliveOpt :: close | keep_alive. -close_or_keepalive(Req, UserHeaders) -> - case get_header(?CONNECTION_HEADER, UserHeaders, connection_token(Req)) of - <<"close">> -> close; - <<"Keep-Alive">> -> keep_alive +spellHeaders(Headers) -> + <<<<(toBinStr(Key))/binary, ": ", (toBinStr(Value))/binary, "\r\n">> || {Key, Value} <- Headers>>. + +toBinStr(V) when is_integer(V) -> integer_to_binary(V); +toBinStr(V) when is_binary(V) -> V; +toBinStr(V) when is_list(V) -> list_to_binary(V); +toBinStr(V) when is_atom(V) -> atom_to_binary(V). + +closeOrKeepAlive(UserHeaders, ReqHeader) -> + case lists:keyfind(?CONNECTION_HEADER, 1, UserHeaders) of + {_, <<"close">>} -> + case lists:keyfind(?CONNECTION_HEADER, 1, ReqHeader) of + {_, <<"close">>} -> + close; + _ -> + keep_alive + end; + _ -> + keep_alive end. - -%% @doc Add appropriate connection header if the user did not add one already. -connection(Req, UserHeaders) -> - case get_header(?CONNECTION_HEADER, UserHeaders) of - undefined -> - {?CONNECTION_HEADER, connection_token(Req)}; +connection(UserHeaders, ReqHeader) -> + case lists:keyfind(?CONNECTION_HEADER, 1, UserHeaders) of + false -> + case lists:keyfind(?CONNECTION_HEADER, 1, ReqHeader) of + {_, <<"close">>} -> + {?CONNECTION_HEADER, <<"Close">>}; + {_, <<"Close">>} -> + {?CONNECTION_HEADER, <<"Close">>}; + _ -> + {?CONNECTION_HEADER, <<"Keep-Alive">>} + end; _ -> [] end. -content_length(Headers, Body) -> - ?IIF(is_header_defined(?CONTENT_LENGTH_HEADER, Headers), [], - {?CONTENT_LENGTH_HEADER, iolist_size(Body)}). - -is_header_defined(Key, Headers) -> - Key1 = string:casefold(Key), - lists:any(fun({X, _}) -> string:equal(Key1, X, true) end, Headers). - -get_header(Key, Headers) -> - get_header(Key, Headers, undefined). - -get_header(Key, Headers, Default) -> - CaseFoldedKey = string:casefold(Key), - case lists:search(fun({N, _}) -> string:equal(CaseFoldedKey, N, true) end, Headers) of - {value, {_, Value}} -> - Value; +contentLength(Headers, Body) -> + case lists:keyfind(?CONTENT_LENGTH_HEADER, Headers) of false -> - Default + []; + _ -> + {?CONTENT_LENGTH_HEADER, iolist_size(Body)} end. %% @doc Split the URL arguments into a proplist. @@ -509,18 +434,6 @@ split_args(Qs) -> [Name, Value] -> {Name, Value} end || Token <- Tokens]. - - - -handle_event(Mod, Name, EventArgs, ElliArgs) -> - try - Mod:handle_event(Name, EventArgs, ElliArgs) - catch - EvClass:EvError:Stacktrace -> - ?wsErr("~p:handle_event/3 crashed ~p:~p~n~p", [Mod, EvClass, EvError, Stacktrace]) - end. - - %% %% SIZE HELPERS %% @@ -536,9 +449,6 @@ s(chunks, Size) -> s(Key, Size) -> put({size, Key}, Size). -get_sizes() -> - lists:filtermap(fun get_sizes/1, get()). - get_sizes({{size, Key}, Value}) -> erase({size, Key}), {true, {Key, Value}}; diff --git a/src/wsSrv/wsHttpProtocol.erl b/src/wsSrv/wsHttpProtocol.erl index 9b98c7e..6d2f744 100644 --- a/src/wsSrv/wsHttpProtocol.erl +++ b/src/wsSrv/wsHttpProtocol.erl @@ -1,6 +1,5 @@ -module(wsHttpProtocol). --include("eWSrv.hrl"). -include("wsCom.hrl"). -compile(inline). @@ -78,13 +77,13 @@ request(header, Data, State) -> undefined -> {error, content_length}; 0 -> - {done, State#wsState{buffer = Rest, wsReq = NewWsReq, temHeader = []}}; + {done, State#wsState{buffer = Rest, wsReq = NewWsReq}}; _ -> case byte_size(Rest) > 0 of true -> - request(body, Rest, State#wsState{stage = body, buffer = Rest, wsReq = NewWsReq, temHeader = []}); + request(body, Rest, State#wsState{stage = body, buffer = Rest, wsReq = NewWsReq}); _ -> - {ok, State#wsState{stage = body, buffer = <<>>, wsReq = NewWsReq, temHeader = []}} + {ok, State#wsState{stage = body, buffer = <<>>, wsReq = NewWsReq}} end end; {ok, {http_error, ErrStr}, _Rest} -> diff --git a/src/wsSrv/wsHttps.erl b/src/wsSrv/wsHttps.erl deleted file mode 100644 index 3eb9238..0000000 --- a/src/wsSrv/wsHttps.erl +++ /dev/null @@ -1,947 +0,0 @@ -%% @doc: Elli HTTP request implementation -%% -%% An elli_http process blocks in elli_tcp:accept/2 until a client -%% connects. It then handles requests on that connection until it's -%% closed either by the client timing out or explicitly by the user. --module(wsHttps). --include("wsCom.hrl"). - - -%% API --export([start_link/4]). - --export([send_response/4]). - --export([send_file/5]). - --export([mk_req/8, mk_req/11]). %% useful when testing. - -%% Exported for looping with a fully-qualified module name --export([accept/4, handle_request/4, chunk_loop/1, split_args/1, - parse_path/1, keepalive_loop/3, keepalive_loop/5]). - -%% Exported for correctly handling session keep-alive for handlers -%% operating in handler mode. --export([close_or_keepalive/2]). - - --define(CONTENT_LENGTH_HEADER, <<"content-length">>). --define(EXPECT_HEADER, <<"expect">>). --define(CONNECTION_HEADER, <<"connection">>). --define(TRANSFER_ENCODING_HEADER, <<"Transfer-Encoding">>). - -%% TODO: use this -%% -type connection_token() :: keep_alive | close. - --spec start_link(Server, ListenSocket, Options, Callback) -> pid() when - Server :: pid(), - ListenSocket :: wsNet:socket(), - Options :: proplists:proplist(), - Callback :: wsHer:callback(). -start_link(Server, ListenSocket, Options, Callback) -> - proc_lib:spawn_link(?MODULE, accept, - [Server, ListenSocket, Options, Callback]). - -%% @doc Accept on the socket until a client connects. -%% 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). - -keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) -> - case ?MODULE:handle_request(Socket, Buffer, Options, Callback) of - {keep_alive, NewBuffer} -> - ?MODULE:keepalive_loop(Socket, NumRequests + 1, - NewBuffer, Options, Callback); - {close, _} -> - wsNet:close(Socket), - 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, - - Headers = [connection(Req, UserHeaders), - content_length(UserHeaders, Body) - | UserHeaders], - t(send_start), - send_response(Req, Code, Headers, Body), - t(send_end), - - t(request_end), - handle_event(Mod, request_complete, - [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, - - ResponseHeaders = [{?TRANSFER_ENCODING_HEADER, <<"chunked">>}, - connection(Req, UserHeaders) - | UserHeaders], - send_response(Req, 200, ResponseHeaders, <<"">>), - - t(send_start), - Initial =:= <<"">> orelse send_chunk(Req#wsReq.socket, Initial), - ClosingEnd = case start_chunk_loop(Req#wsReq.socket) of - {error, client_closed} -> client; - ok -> server - end, - t(send_end), - - t(request_end), - handle_event(Mod, chunk_complete, - [Req, 200, ResponseHeaders, ClosingEnd, {get_timings(), - get_sizes()}], - Args), - {close, <<>>}; - - -handle_response(Req, Buffer, {file, ResponseCode, UserHeaders, - Filename, Range}) -> - #wsReq{callback = {Mod, Args}} = Req, - - ResponseHeaders = [connection(Req, UserHeaders) | UserHeaders], - - case wsUtil:file_size(Filename) of - {error, FileError} -> - handle_event(Mod, file_error, [FileError], Args), - send_server_error(Req#wsReq.socket), - wsNet:close(Req#wsReq.socket), - exit(normal); - Size -> - t(send_start), - case wsUtil:normalize_range(Range, Size) of - undefined -> - send_file(Req, ResponseCode, - [{<<"Content-Length">>, Size} | - ResponseHeaders], - Filename, {0, 0}); - {Offset, Length} -> - ERange = wsUtil:encode_range({Offset, Length}, Size), - send_file(Req, 206, - lists:append(ResponseHeaders, - [{<<"Content-Length">>, Length}, - {<<"Content-Range">>, ERange}]), - Filename, {Offset, Length}); - invalid_range -> - ERange = wsUtil:encode_range(invalid_range, Size), - send_response(Req, 416, - lists:append(ResponseHeaders, - [{<<"Content-Length">>, 0}, - {<<"Content-Range">>, ERange}]), - []) - end, - t(send_end), - - t(request_end), - handle_event(Mod, request_complete, - [Req, ResponseCode, ResponseHeaders, <<>>, - {get_timings(), - get_sizes()}], - Args), - - {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), - - Body = case {Req#wsReq.method, Code} of - {'HEAD', _} -> <<>>; - {_, 304} -> <<>>; - {_, 204} -> <<>>; - _ -> UserBody - end, - s(resp_body, iolist_size(Body)), - - Response = [ResponseHeaders, - Body], - - case wsNet:send(Req#wsReq.socket, Response) of - ok -> ok; - {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> - #wsReq{callback = {Mod, Args}} = Req, - handle_event(Mod, client_closed, [before_response], Args), - ok - end. - -%% @doc Send a HTTP response to the client where the body is the -%% contents of the given file. Assumes correctly set response code -%% and headers. --spec send_file(Req, Code, Headers, Filename, Range) -> ok when - Req :: elli:req(), - Code :: elli:response_code(), - Headers :: elli:headers(), - Filename :: file:filename(), - Range :: wsUtil:range(). -send_file(#wsReq{callback = {Mod, Args}} = Req, Code, Headers, Filename, Range) -> - ResponseHeaders = assemble_response_headers(Code, Headers), - - case file:open(Filename, [read, raw, binary]) of - {ok, Fd} -> do_send_file(Fd, Range, Req, ResponseHeaders); - {error, FileError} -> - handle_event(Mod, file_error, [FileError], Args), - send_server_error(Req#wsReq.socket), - wsNet:close(Req#wsReq.socket), - exit(normal) - end, - ok. - -do_send_file(Fd, {Offset, Length}, #wsReq{callback = {Mod, Args}} = Req, Headers) -> - try wsNet:send(Req#wsReq.socket, Headers) of - ok -> - case wsNet:sendfile(Fd, Req#wsReq.socket, Offset, Length, []) of - {ok, BytesSent} -> s(file, BytesSent), ok; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - handle_event(Mod, client_closed, [before_response], Args) - end; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - handle_event(Mod, client_closed, [before_response], Args) - after - file:close(Fd) - end. - -%% @doc To send a response, we must first have received everything the -%% client is sending. If this is not the case, {@link send_bad_request/1} -%% might reset the client connection. -send_bad_request(Socket) -> - send_rescue_response(Socket, 400, <<"Bad Request">>). - -send_server_error(Socket) -> - send_rescue_response(Socket, 500, <<"Server Error">>). - -send_rescue_response(Socket, Code, Body) -> - Response = http_response(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 - %% {ok,...{file,...}} - {ok, Headers, {file, Filename}} -> - {file, 200, Headers, Filename, []}; - {ok, Headers, {file, Filename, Range}} -> - {file, 200, Headers, Filename, Range}; - %% ok simple - {ok, Headers, Body} -> {response, 200, Headers, Body}; - {ok, Body} -> {response, 200, [], Body}; - %% Chunk - {chunk, Headers} -> {chunk, Headers, <<"">>}; - {chunk, Headers, Initial} -> {chunk, Headers, Initial}; - %% File - {HttpCode, Headers, {file, Filename}} -> - {file, HttpCode, Headers, Filename, {0, 0}}; - {HttpCode, Headers, {file, Filename, Range}} -> - {file, HttpCode, Headers, Filename, Range}; - %% Simple - {HttpCode, Headers, Body} -> {response, HttpCode, Headers, Body}; - {HttpCode, Body} -> {response, HttpCode, [], Body}; - %% Unexpected - Unexpected -> - handle_event(Mod, invalid_return, [Req, Unexpected], Args), - {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), - {response, 500, [], <<"Internal server error">>}; - error:Error:Stacktrace -> - handle_event(Mod, request_error, [Req, Error, Stacktrace], Args), - {response, 500, [], <<"Internal server error">>}; - exit:Exit:Stacktrace -> - handle_event(Mod, request_exit, [Req, Exit, Stacktrace], Args), - {response, 500, [], <<"Internal server error">>} -end . - -%% -%% CHUNKED-TRANSFER -%% - - -%% @doc The chunk loop is an intermediary between the socket and the -%% user. We forward anything the user sends until the user sends an -%% empty response, which signals that the connection should be -%% closed. When the client closes the socket, the loop exits. -start_chunk_loop(Socket) -> - %% Set the socket to active so we receive the tcp_closed message - %% if the client closes the connection - wsNet:setopts(Socket, [{active, once}]), - ?MODULE:chunk_loop(Socket). - -chunk_loop(Socket) -> - {_SockType, InnerSocket} = Socket, - receive - {tcp_closed, InnerSocket} -> - {error, client_closed}; - - {chunk, close} -> - case wsNet:send(Socket, <<"0\r\n\r\n">>) of - ok -> - wsNet:close(Socket), - ok; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - {error, client_closed} - end; - {chunk, close, From} -> - case wsNet:send(Socket, <<"0\r\n\r\n">>) of - ok -> - wsNet:close(Socket), - From ! {self(), ok}, - ok; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - From ! {self(), {error, closed}}, - ok - end; - - {chunk, Data} -> - send_chunk(Socket, Data), - ?MODULE:chunk_loop(Socket); - {chunk, Data, From} -> - case send_chunk(Socket, Data) of - ok -> - From ! {self(), ok}; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - From ! {self(), {error, closed}} - end, - ?MODULE:chunk_loop(Socket) - after 10000 -> - ?MODULE:chunk_loop(Socket) - end. - - -send_chunk(Socket, Data) -> - case iolist_size(Data) of - 0 -> ok; - Size -> - Response = [integer_to_list(Size, 16), - <<"\r\n">>, Data, <<"\r\n">>], - s(chunks, iolist_size(Response)), - 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 - % headers contains "Expect:100-continue" - case get_header(?EXPECT_HEADER, Headers, undefined) of - <<"100-continue">> -> - Response = http_response(100), - wsNet:send(Socket, Response); - _Other -> - 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, 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, <<>>). - -http_response(Code, Body) -> - http_response(Code, [{?CONTENT_LENGTH_HEADER, size(Body)}], Body). - -http_response(Code, Headers, <<>>) -> - [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, - encode_headers(Headers), <<"\r\n">>]; -http_response(Code, Headers, Body) -> - [http_response(Code, Headers, <<>>), Body]. - -assemble_response_headers(Code, Headers) -> - ResponseHeaders = http_response(Code, Headers, <<>>), - s(resp_headers, iolist_size(ResponseHeaders)), - ResponseHeaders. - -encode_headers([]) -> - []; - -encode_headers([[] | H]) -> - encode_headers(H); -encode_headers([{K, V} | H]) -> - [encode_value(K), <<": ">>, encode_value(V), <<"\r\n">>, encode_headers(H)]. - -encode_value(V) when is_integer(V) -> integer_to_binary(V); -encode_value(V) when is_binary(V) -> V; -encode_value(V) when is_list(V) -> list_to_binary(V). - -connection_token(#wsReq{version = {1, 1}, headers = Headers}) -> - case get_header(?CONNECTION_HEADER, Headers) of - <<"close">> -> <<"close">>; - <<"Close">> -> <<"close">>; - _ -> <<"Keep-Alive">> - end; -connection_token(#wsReq{version = {1, 0}, headers = Headers}) -> - case get_header(?CONNECTION_HEADER, Headers) of - <<"Keep-Alive">> -> <<"Keep-Alive">>; - _ -> <<"close">> - end; -connection_token(#wsReq{version = {0, 9}}) -> - <<"close">>. - -%% @doc Return the preferred session handling setting to close or keep the -%% current session alive based on the presence of a header or the standard -%% default based on the version of HTTP of the request. --spec close_or_keepalive(Req, Headers) -> KeepaliveOpt when - Req :: elli:req(), - Headers :: elli:headers(), - KeepaliveOpt :: close | keep_alive. -close_or_keepalive(Req, UserHeaders) -> - case get_header(?CONNECTION_HEADER, UserHeaders, connection_token(Req)) of - <<"close">> -> close; - <<"Keep-Alive">> -> keep_alive - end. - - -%% @doc Add appropriate connection header if the user did not add one already. -connection(Req, UserHeaders) -> - case get_header(?CONNECTION_HEADER, UserHeaders) of - undefined -> - {?CONNECTION_HEADER, connection_token(Req)}; - _ -> - [] - end. - -content_length(Headers, Body) -> - ?IIF(is_header_defined(?CONTENT_LENGTH_HEADER, Headers), [], - {?CONTENT_LENGTH_HEADER, iolist_size(Body)}). - -is_header_defined(Key, Headers) -> - Key1 = string:casefold(Key), - lists:any(fun({X, _}) -> string:equal(Key1, X, true) end, 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 - {value, {_, Value}} -> - Value; - 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'. --spec split_args(binary()) -> list({binary(), binary() | true}). -split_args(<<>>) -> - []; -split_args(Qs) -> - Tokens = binary:split(Qs, <<"&">>, [global, trim]), - [case binary:split(Token, <<"=">>) of - [Token] -> {Token, true}; - [Name, Value] -> {Name, Value} - 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 - Mod:handle_event(Name, EventArgs, ElliArgs) - catch - EvClass:EvError:Stacktrace -> - ?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 -%% - -%% @doc stores response part size in bytes -s(chunks, Size) -> - case get({size, chunks}) of - undefined -> - put({size, chunks}, Size); - Sum -> - put({size, chunks}, Size + Sum) - end; -s(Key, Size) -> - put({size, Key}, Size). - -get_sizes() -> - lists:filtermap(fun get_sizes/1, get()). - -get_sizes({{size, Key}, Value}) -> - erase({size, Key}), - {true, {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 -%% - -%% @doc Response code string. Lifted from `cowboy_http_req.erl'. -status(100) -> <<"100 Continue">>; -status(101) -> <<"101 Switching Protocols">>; -status(102) -> <<"102 Processing">>; -status(200) -> <<"200 OK">>; -status(201) -> <<"201 Created">>; -status(202) -> <<"202 Accepted">>; -status(203) -> <<"203 Non-Authoritative Information">>; -status(204) -> <<"204 No Content">>; -status(205) -> <<"205 Reset Content">>; -status(206) -> <<"206 Partial Content">>; -status(207) -> <<"207 Multi-Status">>; -status(226) -> <<"226 IM Used">>; -status(300) -> <<"300 Multiple Choices">>; -status(301) -> <<"301 Moved Permanently">>; -status(302) -> <<"302 Found">>; -status(303) -> <<"303 See Other">>; -status(304) -> <<"304 Not Modified">>; -status(305) -> <<"305 Use Proxy">>; -status(306) -> <<"306 Switch Proxy">>; -status(307) -> <<"307 Temporary Redirect">>; -status(400) -> <<"400 Bad Request">>; -status(401) -> <<"401 Unauthorized">>; -status(402) -> <<"402 Payment Required">>; -status(403) -> <<"403 Forbidden">>; -status(404) -> <<"404 Not Found">>; -status(405) -> <<"405 Method Not Allowed">>; -status(406) -> <<"406 Not Acceptable">>; -status(407) -> <<"407 Proxy Authentication Required">>; -status(408) -> <<"408 Request Timeout">>; -status(409) -> <<"409 Conflict">>; -status(410) -> <<"410 Gone">>; -status(411) -> <<"411 Length Required">>; -status(412) -> <<"412 Precondition Failed">>; -status(413) -> <<"413 Request Entity Too Large">>; -status(414) -> <<"414 Request-URI Too Long">>; -status(415) -> <<"415 Unsupported Media Type">>; -status(416) -> <<"416 Requested Range Not Satisfiable">>; -status(417) -> <<"417 Expectation Failed">>; -status(418) -> <<"418 I'm a teapot">>; -status(422) -> <<"422 Unprocessable Entity">>; -status(423) -> <<"423 Locked">>; -status(424) -> <<"424 Failed Dependency">>; -status(425) -> <<"425 Unordered Collection">>; -status(426) -> <<"426 Upgrade Required">>; -status(428) -> <<"428 Precondition Required">>; -status(429) -> <<"429 Too Many Requests">>; -status(431) -> <<"431 Request Header Fields Too Large">>; -status(500) -> <<"500 Internal Server Error">>; -status(501) -> <<"501 Not Implemented">>; -status(502) -> <<"502 Bad Gateway">>; -status(503) -> <<"503 Service Unavailable">>; -status(504) -> <<"504 Gateway Timeout">>; -status(505) -> <<"505 HTTP Version Not Supported">>; -status(506) -> <<"506 Variant Also Negotiates">>; -status(507) -> <<"507 Insufficient Storage">>; -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/wsMiddleware.erl b/src/wsSrv/wsMiddleware.erl index 7651c45..e33bc57 100644 --- a/src/wsSrv/wsMiddleware.erl +++ b/src/wsSrv/wsMiddleware.erl @@ -50,14 +50,14 @@ %% @hidden -spec init(Req, Args) -> {ok, standard | handover} when - Req :: elli:req(), + Req :: elli:wsReq(), Args :: wsHer:callback_args(). init(Req, Args) -> do_init(Req, callbacks(Args)). %% @hidden --spec handle(Req :: elli:req(), Config :: [tuple()]) -> wsHer:result(). +-spec handle(Req :: elli:wsReq(), Config :: [tuple()]) -> wsHer:result(). handle(CleanReq, Config) -> Callbacks = callbacks(Config), PreReq = preprocess(CleanReq, Callbacks), @@ -83,7 +83,7 @@ handle_event(Event, Args, Config) -> %% -spec do_init(Req, Callbacks) -> {ok, standard | handover} when - Req :: elli:req(), + Req :: elli:wsReq(), Callbacks :: wsHer:callbacks(). do_init(_, []) -> {ok, standard}; @@ -96,7 +96,7 @@ do_init(Req, [{Mod, Args} | Mods]) -> -spec process(Req, Callbacks) -> Result when - Req :: elli:req(), + Req :: elli:wsReq(), Callbacks :: [Callback :: wsHer:callback()], Result :: wsHer:result(). process(_Req, []) -> @@ -109,9 +109,9 @@ process(Req, [{Mod, Args} | Mods]) -> end). -spec preprocess(Req1, Callbacks) -> Req2 when - Req1 :: elli:req(), + Req1 :: elli:wsReq(), Callbacks :: [wsHer:callback()], - Req2 :: elli:req(). + Req2 :: elli:wsReq(). preprocess(Req, []) -> Req; preprocess(Req, [{Mod, Args} | Mods]) -> @@ -119,7 +119,7 @@ preprocess(Req, [{Mod, Args} | Mods]) -> preprocess(Mod:preprocess(Req, Args), Mods)). -spec postprocess(Req, Res1, Callbacks) -> Res2 when - Req :: elli:req(), + Req :: elli:wsReq(), Res1 :: wsHer:result(), Callbacks :: [wsHer:callback()], Res2 :: wsHer:result(). diff --git a/src/wsSrv/wsMiddlewareCompress.erl b/src/wsSrv/wsMiddlewareCompress.erl index ba292b2..77045c1 100644 --- a/src/wsSrv/wsMiddlewareCompress.erl +++ b/src/wsSrv/wsMiddlewareCompress.erl @@ -10,7 +10,7 @@ %%% @doc Postprocess all requests and compress bodies larger than %%% `compress_byte_size' (`1024' by default). -spec postprocess(Req, Result, Config) -> Result when - Req :: elli:req(), + Req :: elli:wsReq(), Result :: wsHer:result(), Config :: [{compress_byte_size, non_neg_integer()} | tuple()]. postprocess(Req, {ResponseCode, Body}, Config) @@ -36,7 +36,7 @@ postprocess(_, Res, _) -> %% %% NOTE: Algorithm is either `<<"gzip">>' or `<<"deflate">>'. --spec compress(Body0 :: elli:body(), Req :: elli:req()) -> Body1 when +-spec compress(Body0 :: elli:body(), Req :: elli:wsReq()) -> Body1 when Body1 :: {Compressed :: binary(), Algorithm :: binary()} | no_compress. compress(Body, Req) -> case accepted_encoding(Req) of diff --git a/src/wsSrv/wsNet.erl b/src/wsSrv/wsNet.erl index 9a0e418..752f629 100644 --- a/src/wsSrv/wsNet.erl +++ b/src/wsSrv/wsNet.erl @@ -2,8 +2,16 @@ %%% Based on `mochiweb_socket.erl'. -module(wsNet). --export([listen/3, accept/3, recv/3, send/2, close/1, - setopts/2, sendfile/5, peername/1]). +-export([ + listen/3 + , accept/3 + , recv/3 + , send/2 + , close/1 + , setopts/2 + , sendfile/5 + , peername/1 +]). -export_type([socket/0]). diff --git a/src/wsSrv/wsReq.erl b/src/wsSrv/wsReq.erl index ff3631f..10b66e6 100644 --- a/src/wsSrv/wsReq.erl +++ b/src/wsSrv/wsReq.erl @@ -1,6 +1,5 @@ -module(wsReq). --include("eWSrv.hrl"). -include("wsCom.hrl"). -export([ @@ -26,9 +25,7 @@ , post_args/1 , post_args_decoded/1 , body_qs/1 - , original_headers/1 , headers/1 - , peer/1 , method/1 , body/1 , scheme/1 @@ -59,7 +56,6 @@ 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'. -original_headers(#wsReq{original_headers = Headers}) -> Headers. %% @doc Return the `method'. method(#wsReq{method = Method}) -> Method. %% @doc Return the `body'. @@ -71,14 +67,6 @@ host(#wsReq{host = Host}) -> Host. %% @doc Return the `port'. port(#wsReq{port = Port}) -> Port. -peer(#wsReq{socket = Socket} = _Req) -> - case wsNet:peername(Socket) of - {ok, {Address, _}} -> - list_to_binary(inet_parse:ntoa(Address)); - {error, _} -> - undefined - end. - get_header(Key, #wsReq{headers = Headers}) -> CaseFoldedKey = string:casefold(Key), proplists:get_value(CaseFoldedKey, Headers). @@ -143,7 +131,7 @@ post_arg_decoded(Key, #wsReq{} = Req, Default) -> %% @doc Return a proplist of keys and values of the original query string. %% Both keys and values in the returned proplists will be binaries or the atom %% `true' in case no value was supplied for the query value. --spec get_args(elli:req()) -> QueryArgs :: proplists:proplist(). +-spec get_args(elli:wsReq()) -> QueryArgs :: proplists:proplist(). get_args(#wsReq{args = Args}) -> Args. get_args_decoded(#wsReq{args = Args}) -> @@ -166,7 +154,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(). +-spec query_str(elli:wsReq()) -> QueryStr :: binary(). query_str(#wsReq{path = Path}) -> case binary:split(Path, [<<"?">>]) of [_, Qs] -> Qs; @@ -177,7 +165,7 @@ query_str(#wsReq{path = Path}) -> %% @doc Parse the `Range' header from the request. %% The result is either a `byte_range_set()' or the atom `parse_error'. %% Use {@link elli_util:normalize_range/2} to get a validated, normalized range. --spec get_range(elli:req()) -> [http_range()] | parse_error. +-spec get_range(elli:wsReq()) -> [http_range()] | parse_error. get_range(#wsReq{headers = Headers}) -> case proplists:get_value(<<"range">>, Headers) of <<"bytes=", RangeSetBin/binary>> -> @@ -229,17 +217,14 @@ remove_whitespace(Bin) -> %% @doc Serialize the `Req'uest record to a proplist. %% Useful for logging. to_proplist(#wsReq{} = Req) -> - lists:zip(record_info(fields, req), tl(tuple_to_list(Req))). + lists:zip(record_info(fields, wsReq), tl(tuple_to_list(Req))). %% @doc Return a reference that can be used to send chunks to the client. %% If the protocol does not support it, return `{error, not_supported}'. -chunk_ref(#wsReq{version = {1, 1}} = Req) -> - Req#wsReq.pid; chunk_ref(#wsReq{}) -> {error, not_supported}. - %% @doc Explicitly close the chunked connection. %% Return `{error, closed}' if the client already closed the connection. %% @equiv send_chunk(Ref, close)