瀏覽代碼

ft: 代码修改

master
SisMaker 3 年之前
父節點
當前提交
149dc94ec7
共有 5 個檔案被更改,包括 1014 行新增3 行删除
  1. +1
    -0
      rebar.config
  2. +28
    -3
      src/eWSrv.erl
  3. +28
    -0
      src/wsSrv/wsConSup.erl
  4. +953
    -0
      src/wsSrv/wsHttps.erl
  5. +4
    -0
      src/wsSrv/wsUtil.erl

+ 1
- 0
rebar.config 查看文件

@ -1,5 +1,6 @@
{erl_opts, [
debug_info,
{i, "include"},
verbose,
warn_export_vars,
warn_shadow_vars,

+ 28
- 3
src/eWSrv.erl 查看文件

@ -1,8 +1,33 @@
-module(eWSrv).
%% API
-export([]).
-include("wsCom.hrl").
-export([
startWSrv/3
, stopWSrv/1
]).
startHttp() ->
startWSrv(WSrvName, Port, WsOpts) ->
ListenName = wsUtil:lsName(WSrvName),
case ?wsGLV(sslOpts, WsOpts, false) of
false ->
{ok, _} = eNet:openTcp(ListenName, Port, WsOpts);
_ ->
{ok, _} = eNet:openSsl(ListenName, Port, WsOpts)
end,
ConSupSpec = #{
id => WSrvName,
start => {wsConSup, start_link, [ListenName]},
restart => permanent,
shutdown => infinity,
type => supervisor,
modules => [wsConSup]
},
supervisor:start_child(eWSrv_sup, ConSupSpec).
stopWSrv(WSrvName) ->
ListenName = wsUtil:lsName(WSrvName),
eNet:close(ListenName),
supervisor:terminate_child(eWSrv_sup, WSrvName),
supervisor:delete_child(eWSrv_sup, WSrvName).

+ 28
- 0
src/wsSrv/wsConSup.erl 查看文件

@ -0,0 +1,28 @@
-module(wsConSup).
-behaviour(supervisor).
-export([
start_link/1
]).
-export([
init/1
]).
-spec(start_link(SupName :: atom()) -> {ok, pid()}).
start_link(SupName) ->
supervisor:start_link({local, SupName}, ?MODULE, undefined).
init(_Args) ->
SupFlags = #{strategy => simple_one_for_one, intensity => 100, period => 3600},
WsHttpSpec = #{
id => wsHttp,
start => {wsHttp, start_link, []},
restart => transient,
shutdown => 3000,
type => worker,
modules => [wsHttp]
},
{ok, {SupFlags, [WsHttpSpec]}}.

+ 953
- 0
src/wsSrv/wsHttps.erl 查看文件

@ -0,0 +1,953 @@
%% @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("eWSrv.hrl").
-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]).
-export_type([version/0]).
%% @type version(). HTTP version as a tuple, i.e. `{0, 9} | {1, 0} | {1, 1}'.
-type version() :: {0, 9} | {1, 0} | {1, 1}.
-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#req{body = RequestBody},
t(user_start),
Response = execute_callback(Req1),
t(user_end),
handle_response(Req1, B2, Response);
{ok, handover} ->
Req1 = Req#req{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}) ->
#req{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}) ->
#req{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#req.socket, Initial),
ClosingEnd = case start_chunk_loop(Req#req.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}) ->
#req{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#req.socket),
wsNet:close(Req#req.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#req.method, Code} of
{'HEAD', _} -> <<>>;
{_, 304} -> <<>>;
{_, 204} -> <<>>;
_ -> UserBody
end,
s(resp_body, iolist_size(Body)),
Response = [ResponseHeaders,
Body],
case wsNet:send(Req#req.socket, Response) of
ok -> ok;
{error, Closed} when Closed =:= closed orelse Closed =:= enotconn ->
#req{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(#req{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#req.socket),
wsNet:close(Req#req.socket),
exit(normal)
end,
ok.
do_send_file(Fd, {Offset, Length}, #req{callback = {Mod, Args}} = Req, Headers) ->
try wsNet:send(Req#req.socket, Headers) of
ok ->
case wsNet:sendfile(Fd, Req#req.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(#req{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} ->
<<Buffer/binary, Data/binary>>;
{error, timeout} ->
handle_event(Mod, request_timeout, [], Args),
wsNet:close(Socket),
exit(normal);
{error, Closed} when Closed =:= closed orelse
Closed =:= enotconn ->
handle_event(Mod, request_closed, [], Args),
wsNet:close(Socket),
exit(normal)
end.
-spec get_headers(Socket, V, Buffer, Opts, Callback) -> Headers when
Socket :: wsNet:socket(),
V :: version(),
Buffer :: binary(),
Opts :: proplists:proplist(),
Callback :: wsHer:callback(),
Headers :: {{elli:headers(), elli:headers()}, any()}. % TODO: refine
get_headers(_Socket, {0, 9}, _, _, _) ->
{{[], []}, <<>>};
get_headers(Socket, {1, _}, Buffer, Opts, Callback) ->
get_headers(Socket, Buffer, {[], []}, 0, Opts, Callback).
get_headers(Socket, _, {Headers, _}, HeadersCount, _Opts, {Mod, Args})
when HeadersCount >= 100 ->
handle_event(Mod, bad_request, [{too_many_headers, Headers}], Args),
send_bad_request(Socket),
wsNet:close(Socket),
exit(normal);
get_headers(Socket, Buffer, {Headers, ParsedHeaders}, Count, Opts, {Mod, Args} = Callback) ->
case erlang:decode_packet(httph_bin, Buffer, []) of
{ok, {http_header, _, Key, _, Value}, Rest} ->
BinKey = ensure_binary(Key),
NewHeaders = [{BinKey, Value} | Headers],
NewParsedHeaders = [{string:casefold(BinKey), Value} | ParsedHeaders],
get_headers(Socket, Rest, {NewHeaders, NewParsedHeaders}, Count + 1, Opts, Callback);
{ok, http_eoh, Rest} ->
{{Headers, ParsedHeaders}, Rest};
{ok, {http_error, _}, Rest} ->
get_headers(Socket, Rest, {Headers, ParsedHeaders}, Count, Opts, Callback);
{more, _} ->
case wsNet:recv(Socket, 0, header_timeout(Opts)) of
{ok, Data} ->
get_headers(Socket, <<Buffer/binary, Data/binary>>,
{Headers, ParsedHeaders}, Count, Opts, Callback);
{error, Closed} when Closed =:= closed orelse
Closed =:= enotconn ->
handle_event(Mod, client_closed, [receiving_headers], Args),
wsNet:close(Socket),
exit(normal);
{error, timeout} ->
handle_event(Mod, client_timeout,
[receiving_headers], Args),
wsNet:close(Socket),
exit(normal)
end
end.
%% @doc Fetch the full body of the request, if any is available.
%%
%% At the moment we don't need to handle large requests, so there is
%% no need for streaming or lazily fetching the body in the user
%% code. Fully receiving the body allows us to avoid the complex
%% request object threading in Cowboy and the caching in Mochiweb.
%%
%% As we are always receiving whatever the client sends, we might have
%% buffered too much and get parts of the next pipelined request. In
%% that case, push it back in the buffer and handle the first request.
-spec get_body(Socket, Headers, Buffer, Opts, Callback) -> FullBody when
Socket :: undefined | wsNet:socket(),
Headers :: elli:headers(),
Buffer :: binary(),
Opts :: proplists:proplist(),
Callback :: wsHer:callback(),
FullBody :: {elli:body(), binary()}.
get_body(Socket, Headers, Buffer, Opts, Callback) ->
case get_header(?CONTENT_LENGTH_HEADER, Headers, undefined) of
undefined ->
{<<>>, Buffer};
ContentLengthBin ->
maybe_send_continue(Socket, Headers),
ContentLength = binary_to_integer(binary:replace(ContentLengthBin,
<<" ">>, <<>>,
[global])),
ok = check_max_size(Socket, ContentLength, Buffer, Opts, Callback),
Result = case ContentLength - byte_size(Buffer) of
0 ->
{Buffer, <<>>};
N when N > 0 ->
do_get_body(Socket, Buffer, Opts, N, Callback);
_ ->
<<Body:ContentLength/binary, R/binary>> = Buffer,
{Body, R}
end,
%% set the size here so if do_get_body exits it won't have
%% req_body in sizes
s(req_body, ContentLength),
Result
end.
do_get_body(Socket, Buffer, Opts, N, {Mod, Args}) ->
case wsNet:recv(Socket, N, body_timeout(Opts)) of
{ok, Data} ->
{<<Buffer/binary, Data/binary>>, <<>>};
{error, Closed} when Closed =:= closed orelse Closed =:= enotconn ->
handle_event(Mod, client_closed, [receiving_body], Args),
ok = wsNet:close(Socket),
exit(normal);
{error, timeout} ->
handle_event(Mod, client_timeout, [receiving_body], Args),
ok = wsNet:close(Socket),
exit(normal)
end.
ensure_binary(Bin) when is_binary(Bin) -> Bin;
ensure_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom, latin1).
maybe_send_continue(Socket, Headers) ->
% According to RFC2616 section 8.2.3 an origin server must respond with
% either a "100 Continue" or a final response code when the client
% 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:http_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}} ->
#req{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#req{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(#req{version = {1, 1}, headers = Headers}) ->
case get_header(?CONNECTION_HEADER, Headers) of
<<"close">> -> <<"close">>;
<<"Close">> -> <<"close">>;
_ -> <<"Keep-Alive">>
end;
connection_token(#req{version = {1, 0}, headers = Headers}) ->
case get_header(?CONNECTION_HEADER, Headers) of
<<"Keep-Alive">> -> <<"Keep-Alive">>;
_ -> <<"close">>
end;
connection_token(#req{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(#req{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.

+ 4
- 0
src/wsSrv/wsUtil.erl 查看文件

@ -9,6 +9,7 @@
, encode_range/2
, file_size/1
, gLV/3
, lsName/1
]).
-export_type([range/0]).
@ -79,3 +80,6 @@ gLV(Key, List, Default) ->
{Key, Value} ->
Value
end.
lsName(BaseName) ->
binary_to_atom(<<(atom_to_binary(BaseName))/binary, "Ls">>).

Loading…
取消
儲存