From 924b8f215922fbb3bf44d35a485f16b65ff88beb Mon Sep 17 00:00:00 2001 From: SisMaker <1713699517@qq.com> Date: Thu, 20 Jan 2022 10:48:07 +0800 Subject: [PATCH] =?UTF-8?q?ft:=20=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/wsEgHer.erl | 127 ++++++++++++++--- src/wsSrv/wsHttpProtocol.erl | 11 +- src/wsSrv/wsReq.erl | 258 ----------------------------------- 3 files changed, 117 insertions(+), 279 deletions(-) delete mode 100644 src/wsSrv/wsReq.erl diff --git a/src/test/wsEgHer.erl b/src/test/wsEgHer.erl index 946a773..0b21176 100644 --- a/src/test/wsEgHer.erl +++ b/src/test/wsEgHer.erl @@ -23,28 +23,28 @@ handle('GET', <<"/hello/world">>, WsReq) -> handle('GET', <<"/hello">>, WsReq) -> io:format("receive WsReq: ~p~n", [WsReq]), %% Fetch a GET argument from the URL. - Name = wsReq:get_arg(<<"name">>, WsReq, <<"undefined">>), + Name = proplists:get_value(<<"name">>, WsReq, <<"undefined">>), {ok, [], <<"Hello ", Name/binary>>}; handle('POST', <<"hello">>, WsReq) -> io:format("receive WsReq: ~p~n", [WsReq]), %% Fetch a POST argument from the POST body. - Name = wsReq:post_arg(<<"name">>, WsReq, <<"undefined">>), + Name = proplists:get_value(<<"name">>, WsReq, <<"undefined">>), %% Fetch and decode - City = wsReq:post_arg_decoded(<<"city">>, WsReq, <<"undefined">>), + City = proplists:get_value(<<"city">>, WsReq, <<"undefined">>), {ok, [], <<"Hello ", Name/binary, " of ", City/binary>>}; handle('GET', [<<"hello">>, <<"iolist">>], WsReq) -> io:format("receive WsReq: ~p~n", [WsReq]), %% Iolists will be kept as iolists all the way to the socket. - Name = wsReq:get_arg(<<"name">>, WsReq), + Name = proplists:get_value(<<"name">>, WsReq), {ok, [], [<<"Hello ">>, Name]}; handle('GET', [<<"type">>], WsReq) -> io:format("receive WsReq: ~p~n", [WsReq]), - Name = wsReq:get_arg(<<"name">>, WsReq), + Name = proplists:get_value(<<"name">>, WsReq), %% Fetch a header. - case wsReq:get_header(<<"Accept">>, WsReq, <<"text/plain">>) of + case proplists:get_value(<<"Accept">>, WsReq, <<"text/plain">>) of <<"text/plain">> -> {ok, [{<<"content-type">>, <<"text/plain; charset=ISO-8859-1">>}], <<"name: ", Name/binary>>}; @@ -79,15 +79,12 @@ handle('GET', [<<"crash">>], WsReq) -> handle('GET', [<<"decoded-hello">>], WsReq) -> io:format("receive WsReq: ~p~n", [WsReq]), %% Fetch a URI decoded GET argument from the URL. - Name = wsReq:get_arg_decoded(<<"name">>, WsReq, <<"undefined">>), + Name = proplists:get_value(<<"name">>, WsReq, <<"undefined">>), {ok, [], <<"Hello ", Name/binary>>}; handle('GET', [<<"decoded-list">>], WsReq) -> io:format("receive WsReq: ~p~n", [WsReq]), - %% Fetch a URI decoded GET argument from the URL. - [{<<"name">>, Name}, {<<"foo">>, true}] = - wsReq:get_args_decoded(WsReq), - {ok, [], <<"Hello ", Name/binary>>}; + {ok, [], <<"Hello">>}; handle('GET', [<<"sendfile">>], WsReq) -> @@ -115,7 +112,7 @@ handle('GET', [<<"sendfile">>, <<"range">>], WsReq) -> %% range with sendfile, otherwise send the entire file when %% no range is present, or respond with a 416 if the range is invalid. F = "README.md", - {ok, [], {file, F, wsReq:get_range(WsReq)}}; + {ok, [], {file, F, get_range(WsReq)}}; handle('GET', [<<"compressed">>], WsReq) -> io:format("receive WsReq: ~p~n", [WsReq]), @@ -144,8 +141,7 @@ handle('GET', [<<"chunked">>], WsReq) -> %% close the response. %% %% Return immediately {chunk, Headers} to signal we want to chunk. - Ref = wsReq:chunk_ref(WsReq), - spawn(fun() -> ?MODULE:chunk_loop(Ref) end), + spawn(fun() -> ?MODULE:chunk_loop(not_support) end), {chunk, [{<<"Content-Type">>, <<"text/event-stream">>}]}; handle('GET', [<<"shorthand">>], WsReq) -> @@ -154,7 +150,7 @@ handle('GET', [<<"shorthand">>], WsReq) -> handle('GET', [<<"ip">>], WsReq) -> io:format("receive WsReq: ~p~n", [WsReq]), - {<<"200 OK">>, wsReq:peer(WsReq)}; + {<<"200 OK">>, wsNet:peername(WsReq)}; handle('GET', [<<"304">>], WsReq) -> io:format("receive WsReq: ~p~n", [WsReq]), @@ -182,6 +178,62 @@ handle(_, _, WsReq) -> {404, [], <<"Not Found">>}. +%% @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:wsReq()) -> [http_range()] | parse_error. +get_range(#wsReq{headers = Headers}) -> + case proplists:get_value(<<"range">>, Headers) of + <<"bytes=", RangeSetBin/binary>> -> + parse_range_set(RangeSetBin); + _ -> [] + end. + + +-spec parse_range_set(Bin :: binary()) -> [http_range()] | parse_error. +parse_range_set(<>) -> + RangeBins = binary:split(ByteRangeSet, <<",">>, [global]), + Parsed = [parse_range(remove_whitespace(RangeBin)) + || RangeBin <- RangeBins], + case lists:member(parse_error, Parsed) of + true -> parse_error; + false -> Parsed + end. + +-spec remove_whitespace(binary()) -> binary(). +remove_whitespace(Bin) -> + binary:replace(Bin, <<" ">>, <<>>, [global]). + +-type http_range() :: {First :: non_neg_integer(), Last :: non_neg_integer()} +| {offset, Offset :: non_neg_integer()} +| {suffix, Length :: pos_integer()}. + +-spec parse_range(Bin :: binary()) -> http_range() | parse_error. +parse_range(<<$-, SuffixBin/binary>>) -> + %% suffix-byte-range + try {suffix, binary_to_integer(SuffixBin)} + catch + error:badarg -> parse_error + end; +parse_range(<>) -> + case binary:split(ByteRange, <<"-">>) of + %% byte-range without last-byte-pos + [FirstBytePosBin, <<>>] -> + try {offset, binary_to_integer(FirstBytePosBin)} + catch + error:badarg -> parse_error + end; + %% full byte-range + [FirstBytePosBin, LastBytePosBin] -> + try {bytes, + binary_to_integer(FirstBytePosBin), + binary_to_integer(LastBytePosBin)} + catch + error:badarg -> parse_error + end; + _ -> parse_error + end. + %% @doc Send 10 separate chunks to the client. %% @equiv chunk_loop(Ref, 10) chunk_loop(Ref) -> @@ -192,12 +244,51 @@ chunk_loop(Ref) -> %% When `N == 0', call {@link elli_request:close_chunk/1. %% elli_request:close_chunk(Ref)}. chunk_loop(Ref, 0) -> - wsReq:close_chunk(Ref); + close_chunk(Ref); chunk_loop(Ref, N) -> timer:sleep(10), - case wsReq:send_chunk(Ref, [<<"chunk">>, integer_to_binary(N)]) of + case send_chunk(Ref, [<<"chunk">>, integer_to_binary(N)]) of ok -> ok; {error, Reason} -> ?wsErr("error in sending chunk: ~p~n", [Reason]) end, - chunk_loop(Ref, N - 1). \ No newline at end of file + chunk_loop(Ref, N - 1). + +%% @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{}) -> +% {error, not_supported}. + +%% @doc Explicitly close the chunked connection. +%% Return `{error, closed}' if the client already closed the connection. +%% @equiv send_chunk(Ref, close) +close_chunk(Ref) -> + send_chunk(Ref, close). + +% %% @doc Send a chunk asynchronously. +% async_send_chunk(Ref, Data) -> +% Ref ! {chunk, Data}. + +%% @doc Send a chunk synchronously. +%% If the referenced process is dead, return early with `{error, closed}', +%% instead of timing out. +send_chunk(Ref, Data) -> + ?IIF(is_ref_alive(Ref), + send_chunk(Ref, Data, 5000), + {error, closed}). + +is_ref_alive(Ref) -> + ?IIF(node(Ref) =:= node(), + is_process_alive(Ref), + rpc:call(node(Ref), erlang, is_process_alive, [Ref])). + +send_chunk(Ref, Data, Timeout) -> + Ref ! {chunk, Data, self()}, + receive + {Ref, ok} -> + ok; + {Ref, {error, Reason}} -> + {error, Reason} + after Timeout -> + {error, timeout} + end. \ No newline at end of file diff --git a/src/wsSrv/wsHttpProtocol.erl b/src/wsSrv/wsHttpProtocol.erl index 1625b17..880e857 100644 --- a/src/wsSrv/wsHttpProtocol.erl +++ b/src/wsSrv/wsHttpProtocol.erl @@ -5,9 +5,7 @@ -compile(inline). -compile({inline_size, 128}). --export([ - request/3 -]). +-export([request/3]). -spec request(Stage :: stage(), Data :: binary(), State :: #wsState{}) -> {ok, NewState :: #wsState{}} | {done, NewState :: #wsState{}} | {error, term()}. request(reqLine, Data, State) -> @@ -64,6 +62,13 @@ request(header, Data, State) -> _ -> request(header, Rest, State#wsState{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader, contentLength = chunked}) end; + <<"Chunked">> -> + case Rn of + undefined -> + request(header, Rest, State#wsState{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader, contentLength = chunked, rn = binary:compile_pattern(<<"\r\n">>)}); + _ -> + request(header, Rest, State#wsState{buffer = Rest, headerCnt = NewHeaderCnt, temHeader = NewTemHeader, contentLength = chunked}) + end; _ -> {error, 'Transfer-Encoding'} end; diff --git a/src/wsSrv/wsReq.erl b/src/wsSrv/wsReq.erl deleted file mode 100644 index d7352e6..0000000 --- a/src/wsSrv/wsReq.erl +++ /dev/null @@ -1,258 +0,0 @@ --module(wsReq). - --include("wsCom.hrl"). - --export([ - send_chunk/2 - , async_send_chunk/2 - , chunk_ref/1 - , close_chunk/1 - , query_str/1 - , get_header/2 - , get_header/3 - , get_arg_decoded/2 - , get_arg_decoded/3 - , get_arg/2 - , get_arg/3 - , get_args/1 - , get_args_decoded/1 - , post_arg/2 - , post_arg/3 - , post_arg_decoded/2 - , post_arg_decoded/3 - , post_args/1 - , post_args_decoded/1 - , body_qs/1 - , get_range/1 - , to_proplist/1 - , is_request/1 - , uri_decode/1 -]). - --export_type([http_range/0]). - --type http_range() :: {First :: non_neg_integer(), Last :: non_neg_integer()} -| {offset, Offset :: non_neg_integer()} -| {suffix, Length :: pos_integer()}. - - -%% -%% Helpers for working with a #req{} -%% - -get_header(Key, #wsReq{headers = Headers}) -> - CaseFoldedKey = string:casefold(Key), - proplists:get_value(CaseFoldedKey, Headers). - -get_header(Key, #wsReq{headers = Headers}, Default) -> - CaseFoldedKey = string:casefold(Key), - proplists:get_value(CaseFoldedKey, Headers, Default). - - -%% @equiv get_arg(Key, Req, undefined) -get_arg(Key, #wsReq{} = Req) -> - get_arg(Key, Req, undefined). - -%% @equiv proplists:get_value(Key, Args, Default) -get_arg(Key, #wsReq{args = Args}, Default) -> - proplists:get_value(Key, Args, Default). - -%% @equiv get_arg_decoded(Key, Req, undefined) -get_arg_decoded(Key, #wsReq{} = Req) -> - get_arg_decoded(Key, Req, undefined). - -get_arg_decoded(Key, #wsReq{args = Args}, Default) -> - case proplists:get_value(Key, Args) of - undefined -> Default; - true -> true; - EncodedValue -> - uri_decode(EncodedValue) - end. - -%% @doc Parse `application/x-www-form-urlencoded' body into a proplist. -body_qs(#wsReq{body = <<>>}) -> []; -body_qs(#wsReq{body = Body} = Req) -> - case get_header(<<"Content-Type">>, Req) of - <<"application/x-www-form-urlencoded">> -> - wsHttp:splitArgs(Body); - <<"application/x-www-form-urlencoded;", _/binary>> -> % ; charset=... - wsHttp:splitArgs(Body); - _ -> - erlang:error(badarg) - end. - -%% @equiv post_arg(Key, Req, undefined) -post_arg(Key, #wsReq{} = Req) -> - post_arg(Key, Req, undefined). - -post_arg(Key, #wsReq{} = Req, Default) -> - proplists:get_value(Key, body_qs(Req), Default). - -%% @equiv post_arg_decoded(Key, Req, undefined) -post_arg_decoded(Key, #wsReq{} = Req) -> - post_arg_decoded(Key, Req, undefined). - -post_arg_decoded(Key, #wsReq{} = Req, Default) -> - case proplists:get_value(Key, body_qs(Req)) of - undefined -> Default; - true -> true; - EncodedValue -> - uri_decode(EncodedValue) - end. - - -%% @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:wsReq()) -> QueryArgs :: proplists:proplist(). -get_args(#wsReq{args = Args}) -> Args. - -get_args_decoded(#wsReq{args = Args}) -> - lists:map(fun({K, true}) -> - {K, true}; - ({K, V}) -> - {K, uri_decode(V)} - end, Args). - - -post_args(#wsReq{} = Req) -> - body_qs(Req). - -post_args_decoded(#wsReq{} = Req) -> - lists:map(fun({K, true}) -> - {K, true}; - ({K, V}) -> - {K, uri_decode(V)} - end, body_qs(Req)). - -%% @doc Calculate the query string associated with a given `Request' -%% as a binary. --spec query_str(elli:wsReq()) -> QueryStr :: binary(). -query_str(#wsReq{path = Path}) -> - case binary:split(Path, [<<"?">>]) of - [_, Qs] -> Qs; - [_] -> <<>> - end. - - -%% @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:wsReq()) -> [http_range()] | parse_error. -get_range(#wsReq{headers = Headers}) -> - case proplists:get_value(<<"range">>, Headers) of - <<"bytes=", RangeSetBin/binary>> -> - parse_range_set(RangeSetBin); - _ -> [] - end. - - --spec parse_range_set(Bin :: binary()) -> [http_range()] | parse_error. -parse_range_set(<>) -> - RangeBins = binary:split(ByteRangeSet, <<",">>, [global]), - Parsed = [parse_range(remove_whitespace(RangeBin)) - || RangeBin <- RangeBins], - case lists:member(parse_error, Parsed) of - true -> parse_error; - false -> Parsed - end. - --spec parse_range(Bin :: binary()) -> http_range() | parse_error. -parse_range(<<$-, SuffixBin/binary>>) -> - %% suffix-byte-range - try {suffix, binary_to_integer(SuffixBin)} - catch - error:badarg -> parse_error - end; -parse_range(<>) -> - case binary:split(ByteRange, <<"-">>) of - %% byte-range without last-byte-pos - [FirstBytePosBin, <<>>] -> - try {offset, binary_to_integer(FirstBytePosBin)} - catch - error:badarg -> parse_error - end; - %% full byte-range - [FirstBytePosBin, LastBytePosBin] -> - try {bytes, - binary_to_integer(FirstBytePosBin), - binary_to_integer(LastBytePosBin)} - catch - error:badarg -> parse_error - end; - _ -> parse_error - end. - --spec remove_whitespace(binary()) -> binary(). -remove_whitespace(Bin) -> - binary:replace(Bin, <<" ">>, <<>>, [global]). - -%% @doc Serialize the `Req'uest record to a proplist. -%% Useful for logging. -to_proplist(#wsReq{} = 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{}) -> - {error, not_supported}. - -%% @doc Explicitly close the chunked connection. -%% Return `{error, closed}' if the client already closed the connection. -%% @equiv send_chunk(Ref, close) -close_chunk(Ref) -> - send_chunk(Ref, close). - -%% @doc Send a chunk asynchronously. -async_send_chunk(Ref, Data) -> - Ref ! {chunk, Data}. - -%% @doc Send a chunk synchronously. -%% If the referenced process is dead, return early with `{error, closed}', -%% instead of timing out. -send_chunk(Ref, Data) -> - ?IIF(is_ref_alive(Ref), - send_chunk(Ref, Data, 5000), - {error, closed}). - -send_chunk(Ref, Data, Timeout) -> - Ref ! {chunk, Data, self()}, - receive - {Ref, ok} -> - ok; - {Ref, {error, Reason}} -> - {error, Reason} - after Timeout -> - {error, timeout} - end. - -is_ref_alive(Ref) -> - ?IIF(node(Ref) =:= node(), - is_process_alive(Ref), - rpc:call(node(Ref), erlang, is_process_alive, [Ref])). - -is_request(#wsReq{}) -> true; -is_request(_) -> false. - -uri_decode(Bin) -> - case binary:match(Bin, [<<"+">>, <<"%">>]) of - nomatch -> Bin; - {Pos, _} -> - <> = Bin, - uri_decode(Rest, Prefix) - end. - -uri_decode(<<>>, Acc) -> Acc; -uri_decode(<<$+, Rest/binary>>, Acc) -> - uri_decode(Rest, <>); -uri_decode(<<$%, H, L, Rest/binary>>, Acc) -> - uri_decode(Rest, <>); -uri_decode(<>, Acc) -> - uri_decode(Rest, <>). - --compile({inline, [hex_to_int/1]}). - -hex_to_int(X) when X >= $0, X =< $9 -> X - $0; -hex_to_int(X) when X >= $a, X =< $f -> X - ($a - 10); -hex_to_int(X) when X >= $A, X =< $F -> X - ($A - 10).