From b6ab6919a53027083b4e9f387f6cc44bd8020ac6 Mon Sep 17 00:00:00 2001 From: SisMaker <1713699517@qq.com> Date: Wed, 19 Jan 2022 15:17:36 +0800 Subject: [PATCH] =?UTF-8?q?ft:=20=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/eWSrv.hrl | 61 ++++- {src/test => priv}/server_cert.pem | 0 {src/test => priv}/server_key.pem | 0 src/eWSrv.app.src | 2 +- src/eWSrv.erl | 6 +- src/test/elli_http_tests.erl | 27 -- src/test/elli_ssl_tests.erl | 124 --------- src/test/elli_test.erl | 47 ---- src/{example => test}/wsEgHer.erl | 123 ++++----- src/test/wsTc.erl | 3 - src/test/{elli_tests.erl => ws_test.erl} | 35 +-- src/test/{elli_test.hrl => ws_test.hrl} | 0 src/wsSrv/wsHttp.erl | 304 ++++++++++------------- src/wsSrv/wsNet.erl | 22 +- src/wsSrv/wsReq.erl | 4 +- src/wsSrv/wsSendFile.erl | 77 ------ src/wsSrv/wsUtil.erl | 135 +++++++--- 17 files changed, 385 insertions(+), 585 deletions(-) rename {src/test => priv}/server_cert.pem (100%) rename {src/test => priv}/server_key.pem (100%) delete mode 100644 src/test/elli_http_tests.erl delete mode 100644 src/test/elli_ssl_tests.erl delete mode 100644 src/test/elli_test.erl rename src/{example => test}/wsEgHer.erl (57%) rename src/test/{elli_tests.erl => ws_test.erl} (95%) rename src/test/{elli_test.hrl => ws_test.hrl} (100%) delete mode 100644 src/wsSrv/wsSendFile.erl diff --git a/include/eWSrv.hrl b/include/eWSrv.hrl index ac6afea..6304dc9 100644 --- a/include/eWSrv.hrl +++ b/include/eWSrv.hrl @@ -25,6 +25,7 @@ , headers/0 , httpCode/0 , version/0 + , header_key/0 ]). -type wsReq() :: #wsReq{}. @@ -37,7 +38,63 @@ -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">>). +-define(TRANSFER_ENCODING_HEADER, 'Transfer-Encoding'). +-define(EXPECT_HEADER, <<"Expect">>). +%% http header 头 + -type header_key() :: + 'Cache-Control' | + 'Connection' | + 'Date' | + 'Pragma'| + 'Transfer-Encoding' | + 'Upgrade' | + 'Via' | + 'Accept' | + 'Accept-Charset'| + 'Accept-Encoding' | + 'Accept-Language' | + 'Authorization' | + 'From' | + 'Host' | + 'If-Modified-Since' | + 'If-Match' | + 'If-None-Match' | + 'If-Range'| + 'If-Unmodified-Since' | + 'Max-Forwards' | + 'Proxy-Authorization' | + 'Range'| + 'Referer' | + 'User-Agent' | + 'Age' | + 'Location' | + 'Proxy-Authenticate'| + 'Public' | + 'Retry-After' | + 'Server' | + 'Vary' | + 'Warning'| + 'Www-Authenticate' | + 'Allow' | + 'Content-Base' | + 'Content-Encoding'| + 'Content-Language' | + 'Content-Length' | + 'Content-Location'| + 'Content-Md5' | + 'Content-Range' | + 'Content-Type' | + 'Etag'| + 'Expires' | + 'Last-Modified' | + 'Accept-Ranges' | + 'Set-Cookie'| + 'Set-Cookie2' | + 'X-Forwarded-For' | + 'Cookie' | + 'Keep-Alive' | + 'Proxy-Connection' | + binary() | + string(). diff --git a/src/test/server_cert.pem b/priv/server_cert.pem similarity index 100% rename from src/test/server_cert.pem rename to priv/server_cert.pem diff --git a/src/test/server_key.pem b/priv/server_key.pem similarity index 100% rename from src/test/server_key.pem rename to priv/server_key.pem diff --git a/src/eWSrv.app.src b/src/eWSrv.app.src index 98e23c4..8ba4742 100644 --- a/src/eWSrv.app.src +++ b/src/eWSrv.app.src @@ -3,7 +3,7 @@ {vsn, "0.1.0"}, {registered, []}, {mod, {eWSrv_app, []}}, - {applications, [kernel, stdlib, crypto, ssl]}, + {applications, [kernel, stdlib, crypto, ssl, eNet]}, {env, []}, {modules, []}, {licenses, ["MIT"]}, diff --git a/src/eWSrv.erl b/src/eWSrv.erl index 5109244..749175d 100644 --- a/src/eWSrv.erl +++ b/src/eWSrv.erl @@ -13,14 +13,14 @@ startWSrv(WSrvName, Port, WsOpts) -> LWsOpts = lists:keystore(conArgs, 1, TWsOpts, {conArgs, WsMod}), case ?wsGLV(sslOpts, WsOpts, false) of false -> - {ok, _} = wsNet:openTcp(WSrvName, Port, LWsOpts); + {ok, _} = eNet:openTcp(WSrvName, Port, LWsOpts); _ -> - {ok, _} = wsNet:openSsl(WSrvName, Port, LWsOpts) + {ok, _} = eNet:openSsl(WSrvName, Port, LWsOpts) end. stopWSrv(WSrvName) -> ListenName = wsUtil:lsName(WSrvName), - wsNet:close(ListenName), + eNet:close(ListenName), supervisor:terminate_child(eWSrv_sup, WSrvName), supervisor:delete_child(eWSrv_sup, WSrvName). \ No newline at end of file diff --git a/src/test/elli_http_tests.erl b/src/test/elli_http_tests.erl deleted file mode 100644 index 14e17c8..0000000 --- a/src/test/elli_http_tests.erl +++ /dev/null @@ -1,27 +0,0 @@ --module(elli_http_tests). --include_lib("eunit/include/eunit.hrl"). - -%% UNIT TESTS - -chunk_loop_test_() -> - fun() -> - Here = self(), - Pid = spawn_link(chunk_loop_wrapper(Here)), - Pid ! {tcp_closed, some_socket}, - Message = receive_message(), - ?assertMatch({error, client_closed}, Message) - end. - -chunk_loop_wrapper(Here) -> - fun() -> - Result = wsHttp:chunk_loop({some_type, some_socket}), - Here ! Result, - ok - end. - -receive_message() -> - receive - X -> X - after - 1 -> fail - end. diff --git a/src/test/elli_ssl_tests.erl b/src/test/elli_ssl_tests.erl deleted file mode 100644 index 6a8293d..0000000 --- a/src/test/elli_ssl_tests.erl +++ /dev/null @@ -1,124 +0,0 @@ --module(elli_ssl_tests). --include_lib("eunit/include/eunit.hrl"). --include("elli_test.hrl"). - --define(README, "README.md"). - -elli_ssl_test_() -> - {setup, - fun setup/0, fun teardown/1, - [{foreach, - fun init_stats/0, fun clear_stats/1, - [ - ?_test(hello_world()), - ?_test(chunked()), - ?_test(sendfile()), - ?_test(acceptor_leak_regression()) - ]} - ]}. - -get_size_value(Key) -> - [{sizes, Sizes}] = ets:lookup(elli_stat_table, sizes), - proplists:get_value(Key, Sizes). - -get_timing_value(Key) -> - [{timings, Timings}] = ets:lookup(elli_stat_table, timings), - proplists:get_value(Key, Timings). - -%%% Tests - -hello_world() -> - Response = hackney:get("https://localhost:3443/hello/world", - [], <<>>, [insecure]), - ?assertMatch(200, status(Response)), - ?assertMatch({ok, 200, _, _}, Response). - -chunked() -> - Expected = <<"chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1">>, - - Response = hackney:get("https://localhost:3443/chunked", [], <<>>, [insecure]), - - ?assertMatch(200, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-type">>, <<"text/event-stream">>}, - {<<"transfer-encoding">>, <<"chunked">>}], headers(Response)), - ?assertMatch(Expected, body(Response)). - -sendfile() -> - Response = hackney:get("https://localhost:3443/sendfile", [], <<>>, [insecure]), - F = ?README, - {ok, Expected} = file:read_file(F), - - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, ?I2B(size(Expected))}], - headers(Response)), - ?assertEqual(Expected, body(Response)), - %% sizes - ?assertEqual(size(Expected), get_size_value(file)), - ?assertMatch(65, get_size_value(resp_headers)), - %% timings - ?assertNotMatch(undefined, get_timing_value(request_start)), - ?assertNotMatch(undefined, get_timing_value(headers_start)), - ?assertNotMatch(undefined, get_timing_value(headers_end)), - ?assertNotMatch(undefined, get_timing_value(body_start)), - ?assertNotMatch(undefined, get_timing_value(body_end)), - ?assertNotMatch(undefined, get_timing_value(user_start)), - ?assertNotMatch(undefined, get_timing_value(user_end)), - ?assertNotMatch(undefined, get_timing_value(send_start)), - ?assertNotMatch(undefined, get_timing_value(send_end)), - ?assertNotMatch(undefined, get_timing_value(request_end)). - -acceptor_leak_regression() -> - {ok, Before} = elli:get_acceptors(elli_under_test), - Opts = [{verify, verify_peer}, - {verify_fun, {fun(_, _) -> {fail, 23} end, []}}, - {reuse_sessions, false}], - {error, _} = ssl:connect("localhost", 3443, Opts), - {ok, After} = elli:get_acceptors(elli_under_test), - ?assertEqual(length(Before), length(After)). - -%%% Internal helpers - -setup() -> - application:start(asn1), - application:start(crypto), - application:start(public_key), - application:start(ssl), - {ok, _} = application:ensure_all_started(hackney), - - EbinDir = filename:dirname(code:which(?MODULE)), - CertDir = filename:join([EbinDir, "..", "test"]), - CertFile = filename:join(CertDir, "server_cert.pem"), - KeyFile = filename:join(CertDir, "server_key.pem"), - - Config = [ - {mods, [ - {elli_metrics_middleware, []}, - {elli_middleware_compress, []}, - {elli_example_callback, []} - ]} - ], - - {ok, P} = elli:start_link([ - {port, 3443}, - ssl, - {keyfile, KeyFile}, - {certfile, CertFile}, - {callback, elli_middleware}, - {callback_args, Config} - ]), - unlink(P), - erlang:register(elli_under_test, P), - [P]. - -teardown(Pids) -> - [elli:stop(P) || P <- Pids], - application:stop(ssl), - application:stop(public_key), - application:stop(crypto). - -init_stats() -> - ets:new(elli_stat_table, [set, named_table, public]). - -clear_stats(_) -> - ets:delete(elli_stat_table). diff --git a/src/test/elli_test.erl b/src/test/elli_test.erl deleted file mode 100644 index 7b726e9..0000000 --- a/src/test/elli_test.erl +++ /dev/null @@ -1,47 +0,0 @@ -%%% @author Andreas Hasselberg -%%% -%%% @doc Helper for calling your Elli callback in unit tests. -%%% Only the callback specified is actually run. Elli's response handling is not -%%% used, so the headers will for example not include a content length and the -%%% return format is not standardized. -%%% The unit tests below test `elli_example_callback'. - --module(elli_test). - --include("eWSrv.hrl"). - --export([call/5]). - --spec call(Method, Path, Headers, Body, Opts) -> wsHer:result() when - Method :: elli:method(), - Path :: binary(), - Headers :: elli:headers(), - Body :: elli:body(), - Opts :: proplists:proplist(). -call(Method, Path, Headers, Body, Opts) -> - Callback = proplists:get_value(callback, Opts), - CallbackArgs = proplists:get_value(callback_args, Opts), - Req = wsHttp:mk_req(Method, {abs_path, Path}, Headers, Headers, - Body, {1, 1}, undefined, {Callback, CallbackArgs}), - ok = Callback:handle_event(elli_startup, [], CallbackArgs), - Callback:handle(Req, CallbackArgs). - --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). - -hello_world_test() -> - ?assertMatch({ok, [], <<"Hello World!">>}, - elli_test:call('GET', <<"/hello/world/">>, [], <<>>, - ?EXAMPLE_CONF)), - ?assertMatch({ok, [], <<"Hello Test1">>}, - elli_test:call('GET', <<"/hello/?name=Test1">>, [], <<>>, - ?EXAMPLE_CONF)), - ?assertMatch({ok, - [{<<"content-type">>, - <<"application/json; charset=ISO-8859-1">>}], - <<"{\"name\" : \"Test2\"}">>}, - elli_test:call('GET', <<"/type?name=Test2">>, - [{<<"accept">>, <<"application/json">>}], <<>>, - ?EXAMPLE_CONF)). - --endif. %% TEST diff --git a/src/example/wsEgHer.erl b/src/test/wsEgHer.erl similarity index 57% rename from src/example/wsEgHer.erl rename to src/test/wsEgHer.erl index 5f4bc48..9b54118 100644 --- a/src/example/wsEgHer.erl +++ b/src/test/wsEgHer.erl @@ -13,54 +13,38 @@ chunk_loop/1 ]). -%% @doc Route `Method' and `Path' to the appropriate clause. -%% -%% `ok' can be used instead of `200' to signal success. -%% -%% If you return any of the following HTTP headers, you can -%% override the default behaviour of Elli: -%% -%% * **Connection**: By default Elli will use `keep-alive' if the protocol -%% supports it, setting `<<"close">>' will close the -%% connection immediately after Elli has sent the -%% response. If the client has already sent pipelined -%% requests, these will be discarded. -%% -%% * **Content-Length**: By default Elli looks at the size of the body you -%% returned to determine the `Content-Length' header. -%% Explicitly including your own `Content-Length' (with -%% the value as `integer()', `binary()' or `list()') -%% allows you to return an empty body. Useful for -%% implementing the `"304 Not Modified"' response. -%% - --spec handle(Method :: method(), Path :: path(), Req :: wsReq()) -> wsHer:response(). -handle('GET', <<"hello/world">>, wsReq) -> +-spec handle(Method :: method(), Path :: path(), WsReq :: wsReq()) -> wsHer:response(). +handle('GET', <<"hello/world">>, WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Reply with a normal response. timer:sleep(1000), {ok, [], <<"Hello World!">>}; -handle('GET', <<"hello">>, Req) -> +handle('GET', <<"hello">>, WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Fetch a GET argument from the URL. - Name = wsReq:get_arg(<<"name">>, Req, <<"undefined">>), + Name = wsReq:get_arg(<<"name">>, WsReq, <<"undefined">>), {ok, [], <<"Hello ", Name/binary>>}; -handle('POST', <<"hello">>, Req) -> +handle('POST', <<"hello">>, WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Fetch a POST argument from the POST body. - Name = wsReq:post_arg(<<"name">>, Req, <<"undefined">>), + Name = wsReq:post_arg(<<"name">>, WsReq, <<"undefined">>), %% Fetch and decode - City = wsReq:post_arg_decoded(<<"city">>, Req, <<"undefined">>), + City = wsReq:post_arg_decoded(<<"city">>, WsReq, <<"undefined">>), {ok, [], <<"Hello ", Name/binary, " of ", City/binary>>}; -handle('GET', [<<"hello">>, <<"iolist">>], Req) -> +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">>, Req), + Name = wsReq:get_arg(<<"name">>, WsReq), {ok, [], [<<"Hello ">>, Name]}; -handle('GET', [<<"type">>], Req) -> - Name = wsReq:get_arg(<<"name">>, Req), +handle('GET', [<<"type">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), + Name = wsReq:get_arg(<<"name">>, WsReq), %% Fetch a header. - case wsReq:get_header(<<"Accept">>, Req, <<"text/plain">>) of + case wsReq:get_header(<<"Accept">>, WsReq, <<"text/plain">>) of <<"text/plain">> -> {ok, [{<<"content-type">>, <<"text/plain; charset=ISO-8859-1">>}], <<"name: ", Name/binary>>}; @@ -70,74 +54,88 @@ handle('GET', [<<"type">>], Req) -> <<"{\"name\" : \"", Name/binary, "\"}">>} end; -handle('GET', [<<"headers.html">>], _Req) -> +handle('GET', [<<"headers.html">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Set custom headers, for example 'Content-Type' {ok, [{<<"X-Custom">>, <<"foobar">>}], <<"see headers">>}; %% See note in function doc re: overriding Elli's default behaviour %% via Connection and Content-Length headers. -handle('GET', [<<"user">>, <<"defined">>, <<"behaviour">>], _Req) -> +handle('GET', [<<"user">>, <<"defined">>, <<"behaviour">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), {304, [{<<"Connection">>, <<"close">>}, {<<"Content-Length">>, <<"123">>}], <<"ignored">>}; -handle('GET', [<<"user">>, <<"content-length">>], _Req) -> +handle('GET', [<<"user">>, <<"content-length">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), {200, [{<<"Content-Length">>, 123}], <<"foobar">>}; -handle('GET', [<<"crash">>], _Req) -> +handle('GET', [<<"crash">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Throwing an exception results in a 500 response and %% request_throw being called throw(foobar); -handle('GET', [<<"decoded-hello">>], Req) -> +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">>, Req, <<"undefined">>), + Name = wsReq:get_arg_decoded(<<"name">>, WsReq, <<"undefined">>), {ok, [], <<"Hello ", Name/binary>>}; -handle('GET', [<<"decoded-list">>], Req) -> +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(Req), + wsReq:get_args_decoded(WsReq), {ok, [], <<"Hello ", Name/binary>>}; -handle('GET', [<<"sendfile">>], _Req) -> +handle('GET', [<<"sendfile">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Returning {file, "/path/to/file"} instead of the body results %% in Elli using sendfile. F = "README.md", {ok, [], {file, F}}; -handle('GET', [<<"send_no_file">>], _Req) -> +handle('GET', [<<"send_no_file">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Returning {file, "/path/to/file"} instead of the body results %% in Elli using sendfile. F = "README", {ok, [], {file, F}}; -handle('GET', [<<"sendfile">>, <<"error">>], _Req) -> +handle('GET', [<<"sendfile">>, <<"error">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), F = "test", {ok, [], {file, F}}; -handle('GET', [<<"sendfile">>, <<"range">>], Req) -> +handle('GET', [<<"sendfile">>, <<"range">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Read the Range header of the request and use the normalized %% 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(Req)}}; + {ok, [], {file, F, wsReq:get_range(WsReq)}}; -handle('GET', [<<"compressed">>], _Req) -> +handle('GET', [<<"compressed">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Body with a byte size over 1024 are automatically gzipped by %% elli_middleware_compress {ok, binary:copy(<<"Hello World!">>, 86)}; -handle('GET', [<<"compressed-io_list">>], _Req) -> +handle('GET', [<<"compressed-io_list">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Body with a iolist size over 1024 are automatically gzipped by %% elli_middleware_compress {ok, lists:duplicate(86, [<<"Hello World!">>])}; -handle('HEAD', [<<"head">>], _Req) -> +handle('HEAD', [<<"head">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), {200, [], <<"body must be ignored">>}; -handle('GET', [<<"chunked">>], Req) -> +handle('GET', [<<"chunked">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Start a chunked response for streaming real-time events to the %% browser. %% @@ -146,34 +144,41 @@ handle('GET', [<<"chunked">>], Req) -> %% close the response. %% %% Return immediately {chunk, Headers} to signal we want to chunk. - Ref = wsReq:chunk_ref(Req), + Ref = wsReq:chunk_ref(WsReq), spawn(fun() -> ?MODULE:chunk_loop(Ref) end), {chunk, [{<<"Content-Type">>, <<"text/event-stream">>}]}; -handle('GET', [<<"shorthand">>], _Req) -> +handle('GET', [<<"shorthand">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), {200, <<"hello">>}; -handle('GET', [<<"ip">>], Req) -> - {<<"200 OK">>, wsReq:peer(Req)}; +handle('GET', [<<"ip">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), + {<<"200 OK">>, wsReq:peer(WsReq)}; -handle('GET', [<<"304">>], _Req) -> +handle('GET', [<<"304">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% A "Not Modified" response is exactly like a normal response (so %% Content-Length is included), but the body will not be sent. {304, [{<<"Etag">>, <<"foobar">>}], <<"Ignored">>}; -handle('GET', [<<"302">>], _Req) -> +handle('GET', [<<"302">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), {302, [{<<"Location">>, <<"/hello/world">>}], <<>>}; -handle('GET', [<<"403">>], _Req) -> +handle('GET', [<<"403">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), %% Exceptions formatted as return codes can be used to %% short-circuit a response, for example in case of %% authentication/authorization throw({403, [], <<"Forbidden">>}); -handle('GET', [<<"invalid_return">>], _Req) -> +handle('GET', [<<"invalid_return">>], WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), {invalid_return}; -handle(_, _, _Req) -> +handle(_, _, WsReq) -> + io:format("receive WsReq: ~p~n", [WsReq]), {404, [], <<"Not Found">>}. diff --git a/src/test/wsTc.erl b/src/test/wsTc.erl index c94230c..74eef5c 100644 --- a/src/test/wsTc.erl +++ b/src/test/wsTc.erl @@ -212,6 +212,3 @@ test(N) -> timer:sleep(N), S2 = erlang:system_time(nanosecond), io:format("IMY******************222 ~p~n", [S2 - S1]). - - - diff --git a/src/test/elli_tests.erl b/src/test/ws_test.erl similarity index 95% rename from src/test/elli_tests.erl rename to src/test/ws_test.erl index 456a3fe..0f3c6e2 100644 --- a/src/test/elli_tests.erl +++ b/src/test/ws_test.erl @@ -1,8 +1,9 @@ --module(elli_tests). +-module(ws_test). + -include_lib("eunit/include/eunit.hrl"). -include("wsCom.hrl"). --include("elli_test.hrl"). +-include("ws_test.hrl"). -define(README, "README.md"). -define(VTB(T1, T2, LB, UB), @@ -474,7 +475,7 @@ sendfile_range() -> {ok, Fd} = file:open(F, [read, raw, binary]), {ok, Expected} = file:pread(Fd, 300, 400), file:close(Fd), - Size = wsUtil:file_size(F), + Size = wsUtil:fileSize(F), ?assertMatch(206, status(Response)), ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, {<<"content-length">>, <<"400">>}, @@ -701,24 +702,24 @@ normalize_range_test_() -> Invalid5 = parse_error, Invalid6 = [{bytes, 0, 100}, {suffix, 42}], - [?_assertMatch({200, 201}, wsUtil:normalize_range(Bytes1, Size)), - ?_assertMatch({0, Size}, wsUtil:normalize_range(Bytes2, Size)), - ?_assertEqual({Size - 303, 303}, wsUtil:normalize_range(Suffix, Size)), - ?_assertEqual({42, Size - 42}, wsUtil:normalize_range(Offset, Size)), - ?_assertMatch({200, 400}, wsUtil:normalize_range(Normal, Size)), - ?_assertMatch({0, 1000}, wsUtil:normalize_range(Set, Size)), - ?_assertMatch(undefined, wsUtil:normalize_range(EmptySet, Size)), - ?_assertMatch(invalid_range, wsUtil:normalize_range(Invalid1, Size)), - ?_assertMatch(invalid_range, wsUtil:normalize_range(Invalid2, Size)), - ?_assertMatch(invalid_range, wsUtil:normalize_range(Invalid3, Size)), - ?_assertMatch(invalid_range, wsUtil:normalize_range(Invalid4, Size)), - ?_assertMatch(invalid_range, wsUtil:normalize_range(Invalid5, Size)), - ?_assertMatch(invalid_range, wsUtil:normalize_range(Invalid6, Size))]. + [?_assertMatch({200, 201}, wsUtil:normalizeRange(Bytes1, Size)), + ?_assertMatch({0, Size}, wsUtil:normalizeRange(Bytes2, Size)), + ?_assertEqual({Size - 303, 303}, wsUtil:normalizeRange(Suffix, Size)), + ?_assertEqual({42, Size - 42}, wsUtil:normalizeRange(Offset, Size)), + ?_assertMatch({200, 400}, wsUtil:normalizeRange(Normal, Size)), + ?_assertMatch({0, 1000}, wsUtil:normalizeRange(Set, Size)), + ?_assertMatch(undefined, wsUtil:normalizeRange(EmptySet, Size)), + ?_assertMatch(invalid_range, wsUtil:normalizeRange(Invalid1, Size)), + ?_assertMatch(invalid_range, wsUtil:normalizeRange(Invalid2, Size)), + ?_assertMatch(invalid_range, wsUtil:normalizeRange(Invalid3, Size)), + ?_assertMatch(invalid_range, wsUtil:normalizeRange(Invalid4, Size)), + ?_assertMatch(invalid_range, wsUtil:normalizeRange(Invalid5, Size)), + ?_assertMatch(invalid_range, wsUtil:normalizeRange(Invalid6, Size))]. encode_range_test() -> Expected = [<<"bytes ">>, <<"*">>, <<"/">>, <<"42">>], - ?assertMatch(Expected, wsUtil:encode_range(invalid_range, 42)). + ?assertMatch(Expected, wsUtil:encodeRange(invalid_range, 42)). register_test() -> ?assertMatch(undefined, whereis(elli)), diff --git a/src/test/elli_test.hrl b/src/test/ws_test.hrl similarity index 100% rename from src/test/elli_test.hrl rename to src/test/ws_test.hrl diff --git a/src/wsSrv/wsHttp.erl b/src/wsSrv/wsHttp.erl index 15c4575..7dd0004 100644 --- a/src/wsSrv/wsHttp.erl +++ b/src/wsSrv/wsHttp.erl @@ -6,11 +6,13 @@ -export([ start_link/1 , sendResponse/5 - , send_file/5 + , sendFile/5 %% Exported for looping with a fully-qualified module name - , chunk_loop/1 - , split_args/1 + , chunkLoop/1 + , spellHeaders/1 + , splitArgs/1 , closeOrKeepAlive/2 + , maybeSendContinue/2 ]). %% eNet callback @@ -25,7 +27,7 @@ ]). newCon(_Sock, WsMod) -> - wsHttp:start_link(WsMod). + ?MODULE:start_link(WsMod). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% genActor start %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec(start_link(atom()) -> {ok, pid()} | ignore | {error, term()}). @@ -81,62 +83,60 @@ loop(Parent, State) -> end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% genActor end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% ************************************************ API *************************************************************** - -%% ******************************************** API ******************************************************************* - - -%% ******************************************** callback ************************************************************** init(WsMod) -> {ok, #wsState{wsMod = WsMod}}. handleMsg({tcp, _Socket, Data}, State) -> #wsState{stage = Stage, buffer = Buffer, socket = Socket} = State, case wsHttpProtocol:request(Stage, <>, State) of - {ok, NewState} -> - {noreply, NewState}; + {ok, _NewState} = LRet -> + LRet; {done, NewState} -> Response = doHandle(NewState), #wsState{buffer = Buffer, socket = Socket, temHeader = TemHeader, method = Method} = NewState, case doResponse(Response, Socket, TemHeader, Method) of keep_alive -> handleMsg({tcp, _Socket, Buffer}, newWsState(NewState)); - {close, _} -> - wsNet:close(Socket), - ok + close -> + {stop, close} end; Err -> ?wsErr("recv the http data error ~p~n", [Err]), - send_bad_request(Socket), - gen_tcp:close(Socket) + sendBadRequest(Socket), + {stop, close} end; handleMsg({tcp_closed, _Socket}, _State) -> - ok; + {stop, tcp_closed}; +handleMsg({tcp_error, _Socket, Reason}, _State) -> + ?wsErr("the http tcp socket error ~p~n", [Reason]), + {stop, tcp_error}; -handleMsg({tcp_error, Socket, Reason}, _State) -> - ?wsErr("the http socket error ~p~n", [Reason]), - gen_tcp:close(Socket), - kpS; - -handleMsg({ssl, Socket, Data}, State) -> +handleMsg({ssl, _Socket, Data}, State) -> #wsState{stage = Stage, buffer = Buffer, socket = Socket} = State, case wsHttpProtocol:request(Stage, <>, State) of - {ok, NewState} -> - {noreply, NewState}; + {ok, _NewState} = LRet -> + LRet; {done, NewState} -> - ok; + Response = doHandle(NewState), + #wsState{buffer = Buffer, temHeader = TemHeader, method = Method} = NewState, + case doResponse(Response, Socket, TemHeader, Method) of + keep_alive -> + handleMsg({tcp, Socket, Buffer}, newWsState(NewState)); + close -> + {stop, close} + end; Err -> ?wsErr("recv the http data error ~p~n", [Err]), - send_bad_request(Socket), - ssl:close(Socket) + sendBadRequest(Socket), + {stop, close} end; -handleMsg({ssl_closed, Socket}, _State) -> - ok; - -handleMsg({ssl_error, Socket, Reason}, _State) -> - ?wsErr("the http socket error ~p~n", [Reason]), - ssl:close(Socket), - kpS; +handleMsg({ssl_closed, _Socket}, _State) -> + {stop, ssl_closed}; +handleMsg({ssl_error, _Socket, Reason}, _State) -> + ?wsErr("the http ssl socket error ~p~n", [Reason]), + {stop, ssl_error}; handleMsg({?mSockReady, Sock}, _State) -> inet:setopts(Sock, [{packet, raw}, {active, true}]), {ok, #wsState{socket = Sock}}; @@ -147,13 +147,14 @@ handleMsg({?mSockReady, Sock, SslOpts, SslHSTet}, State) -> {ok, State#wsState{socket = SslSock, isSsl = true}}; _Err -> ?wsErr("ssl handshake error ~p~n", [_Err]), - {stop, _Err, State} + {stop, handshake_error} end; handleMsg(_Msg, _State) -> ?wsErr("~p info receive unexpect msg ~p ~n ", [?MODULE, _Msg]), kpS. -terminate(_Reason, _State) -> +terminate(_Reason, #wsState{socket = Socket} = _State) -> + wsNet:close(Socket), ok. newWsState(WsState) -> @@ -213,35 +214,39 @@ doResponse({response, Code, UserHeaders, Body}, Socket, TemHeader, Method) -> 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], + ResponseHeaders = [transferEncoding(UserHeaders), connection(UserHeaders, TemHeader) | UserHeaders], sendResponse(Socket, Method, 200, ResponseHeaders, <<>>), - Initial =:= <<"">> orelse send_chunk(Socket, Initial), - case start_chunk_loop(Socket) of + Initial =:= <<"">> orelse sendChunk(Socket, Initial), + case startChunkLoop(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 + case wsUtil:fileSize(Filename) of {error, _FileError} -> - send_server_error(Socket), - wsNet:close(Socket), - exit(normal); + sendSrvError(Socket), + {stop, file_error}; Size -> - case wsUtil:normalize_range(Range, Size) of - undefined -> - send_file(Socket, ResponseCode, [{<<"Content-Length">>, Size} | ResponseHeaders], Filename, {0, 0}); - {Offset, Length} -> - ERange = wsUtil:encode_range({Offset, Length}, Size), - 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), - sendResponse(Socket, Method, 416, lists:append(ResponseHeaders, [{<<"Content-Length">>, 0}, {<<"Content-Range">>, ERange}]), <<>>) - end, - closeOrKeepAlive(UserHeaders, TemHeader) + Ret = + case wsUtil:normalizeRange(Range, Size) of + undefined -> + sendFile(Socket, ResponseCode, [{?CONTENT_LENGTH_HEADER, Size} | ResponseHeaders], Filename, {0, 0}); + {Offset, Length} -> + ERange = wsUtil:encodeRange({Offset, Length}, Size), + sendFile(Socket, 206, lists:append(ResponseHeaders, [{?CONTENT_LENGTH_HEADER, Length}, {<<"Content-Range">>, ERange}]), Filename, {Offset, Length}); + invalid_range -> + ERange = wsUtil:encodeRange(invalid_range, Size), + sendResponse(Socket, Method, 416, lists:append(ResponseHeaders, [{<<"Content-Length">>, 0}, {<<"Content-Range">>, ERange}]), <<>>), + {error, range} + end, + case Ret of + ok -> + closeOrKeepAlive(UserHeaders, TemHeader); + {error, Reason} -> + {stop, Reason} + end end. %% @doc Generate a HTTP response and send it to the client. @@ -261,7 +266,7 @@ sendResponse(Socket, Method, Code, Headers, UserBody) -> end end, - Response = http_response(Code, Headers, Body), + Response = httpResponse(Code, Headers, Body), case wsNet:send(Socket, Response) of ok -> @@ -270,38 +275,36 @@ sendResponse(Socket, Method, Code, Headers, UserBody) -> ?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 +-spec sendFile(Req, Code, Headers, Filename, Range) -> ok when Req :: elli:wsReq(), Code :: elli:httpCode(), Headers :: elli:headers(), Filename :: file:filename(), Range :: wsUtil:range(). -send_file(Socket, Code, Headers, Filename, Range) -> - ResponseHeaders = assemble_response_headers(Code, Headers), - +sendFile(Socket, Code, Headers, Filename, Range) -> + ResponseHeaders = httpResponse(Code, Headers, <<>>), case file:open(Filename, [read, raw, binary]) of - {ok, Fd} -> do_send_file(Fd, Range, Socket, ResponseHeaders); - {error, FileError} -> - send_server_error(Socket), - wsNet:close(Socket), - exit(normal) - end, - ok. + {ok, Fd} -> doSendFile(Fd, Range, Socket, ResponseHeaders); + {error, _FileError} = Err -> + sendSrvError(Socket), + Err + end. -do_send_file(Fd, {Offset, Length}, Socket, Headers) -> +doSendFile(Fd, {Offset, Length}, Socket, Headers) -> try wsNet:send(Socket, Headers) of ok -> case wsNet:sendfile(Fd, Socket, Offset, Length, []) of - {ok, BytesSent} -> s(file, BytesSent), ok; - {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> - ?wsErr("send file error") + {ok, _BytesSent} -> ok; + {error, Closed} = LErr when Closed =:= closed orelse Closed =:= enotconn -> + ?wsErr("send file error"), + LErr end; - {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> - ?wsErr("send file error") + {error, Closed} = LErr when Closed =:= closed orelse Closed =:= enotconn -> + ?wsErr("send file error"), + LErr after file:close(Fd) end. @@ -309,115 +312,101 @@ do_send_file(Fd, {Offset, Length}, Socket, Headers) -> %% @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">>). +sendBadRequest(Socket) -> + sendRescueResponse(Socket, 400, <<"Bad Request">>). -send_server_error(Socket) -> - send_rescue_response(Socket, 500, <<"Server Error">>). +sendSrvError(Socket) -> + sendRescueResponse(Socket, 500, <<"Server Error">>). -send_rescue_response(Socket, Code, Body) -> - Response = http_response(Code, Body), +sendRescueResponse(Socket, Code, Body) -> + Response = httpResponse(Code, Body), wsNet:send(Socket, Response). - -%% %% 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) -> +startChunkLoop(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). + ?MODULE:chunkLoop(Socket). -chunk_loop(Socket) -> - {_SockType, InnerSocket} = Socket, +chunkLoop(Socket) -> receive - {tcp_closed, InnerSocket} -> + {tcp_closed, Socket} -> {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, 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 -> + {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> From ! {self(), {error, closed}}, ok end; - {chunk, Data} -> - send_chunk(Socket, Data), - ?MODULE:chunk_loop(Socket); + sendChunk(Socket, Data), + ?MODULE:chunkLoop(Socket); {chunk, Data, From} -> - case send_chunk(Socket, Data) of + case sendChunk(Socket, Data) of ok -> From ! {self(), ok}; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> + {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> From ! {self(), {error, closed}} end, - ?MODULE:chunk_loop(Socket) + ?MODULE:chunkLoop(Socket) after 10000 -> - ?MODULE:chunk_loop(Socket) + ?MODULE:chunkLoop(Socket) end. - -send_chunk(Socket, Data) -> +sendChunk(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)), + Response = [integer_to_binary(Size, 16), <<"\r\n">>, Data, <<"\r\n">>], wsNet:send(Socket, Response) end. -maybe_send_continue(Socket, Headers) -> +maybeSendContinue(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 lists:keyfind(?EXPECT_HEADER, 1, Headers) of <<"100-continue">> -> - Response = http_response(100), + Response = httpResponse(100), wsNet:send(Socket, Response); _Other -> ok end. -http_response(Code) -> - http_response(Code, <<>>). +httpResponse(Code) -> + httpResponse(Code, <<>>). -http_response(Code, Body) -> - http_response(Code, [{?CONTENT_LENGTH_HEADER, size(Body)}], Body). +httpResponse(Code, Body) -> + httpResponse(Code, [{?CONTENT_LENGTH_HEADER, size(Body)}], Body). -http_response(Code, Headers, Body) -> +httpResponse(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. - spellHeaders(Headers) -> <<<<(toBinStr(Key))/binary, ": ", (toBinStr(Value))/binary, "\r\n">> || {Key, Value} <- Headers>>. +-spec splitArgs(binary()) -> list({binary(), binary() | true}). +splitArgs(<<>>) -> []; +splitArgs(Qs) -> + Tokens = binary:split(Qs, <<"&">>, [global, trim]), + [case binary:split(Token, <<"=">>) of [Token] -> {Token, true}; [Name, Value] -> + {Name, Value} end || Token <- Tokens]. + 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); @@ -425,27 +414,29 @@ toBinStr(V) when is_atom(V) -> atom_to_binary(V). closeOrKeepAlive(UserHeaders, ReqHeader) -> case lists:keyfind(?CONNECTION_HEADER, 1, UserHeaders) of + {_, <<"Close">>} -> + close; {_, <<"close">>} -> + close; + _ -> case lists:keyfind(?CONNECTION_HEADER, 1, ReqHeader) of + {_, <<"Close">>} -> + close; {_, <<"close">>} -> close; _ -> keep_alive - end; - _ -> - keep_alive + end end. 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">>} + false -> + {?CONNECTION_HEADER, <<"Keep-Alive">>}; + Tuple -> + Tuple end; _ -> [] @@ -454,49 +445,20 @@ connection(UserHeaders, ReqHeader) -> contentLength(Headers, Body) -> case lists:keyfind(?CONTENT_LENGTH_HEADER, Headers) of false -> - []; + {?CONTENT_LENGTH_HEADER, iolist_size(Body)}; _ -> - {?CONTENT_LENGTH_HEADER, iolist_size(Body)} + [] end. -%% @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]. - -%% -%% 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({{size, Key}, Value}) -> - erase({size, Key}), - {true, {Key, Value}}; -get_sizes(_) -> - false. +transferEncoding(Headers) -> + case lists:keyfind(?TRANSFER_ENCODING_HEADER, Headers) of + false -> + {?TRANSFER_ENCODING_HEADER, <<"chunked">>}; + _ -> + [] + end. -%% %% 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">>; diff --git a/src/wsSrv/wsNet.erl b/src/wsSrv/wsNet.erl index 7012c5a..9ce4681 100644 --- a/src/wsSrv/wsNet.erl +++ b/src/wsSrv/wsNet.erl @@ -10,27 +10,27 @@ , peername/1 ]). -send({plain, Socket}, Data) -> +send(Socket, Data) when is_port(Socket) -> gen_tcp:send(Socket, Data); -send({ssl, Socket}, Data) -> +send(Socket, Data) -> ssl:send(Socket, Data). -close({plain, Socket}) -> +close(Socket) when is_port(Socket) -> gen_tcp:close(Socket); -close({ssl, Socket}) -> +close(Socket) -> ssl:close(Socket). -setopts({plain, Socket}, Opts) -> +setopts(Socket, Opts) when is_port(Socket) -> inet:setopts(Socket, Opts); -setopts({ssl, Socket}, Opts) -> +setopts(Socket, Opts) -> ssl:setopts(Socket, Opts). -sendfile(Fd, {plain, Socket}, Offset, Length, Opts) -> +sendfile(Fd, Socket, Offset, Length, Opts) when is_port(Socket) -> file:sendfile(Fd, Socket, Offset, Length, Opts); -sendfile(Fd, {ssl, Socket}, Offset, Length, Opts) -> - wsSendFile:sendfile(Fd, Socket, Offset, Length, Opts). +sendfile(Fd, Socket, Offset, Length, Opts) -> + wsUtil:sendfile(Fd, Socket, Offset, Length, Opts). -peername({plain, Socket}) -> +peername(Socket) when is_port(Socket) -> inet:peername(Socket); -peername({ssl, Socket}) -> +peername(Socket) -> ssl:peername(Socket). diff --git a/src/wsSrv/wsReq.erl b/src/wsSrv/wsReq.erl index defda44..d7352e6 100644 --- a/src/wsSrv/wsReq.erl +++ b/src/wsSrv/wsReq.erl @@ -74,9 +74,9 @@ body_qs(#wsReq{body = <<>>}) -> []; body_qs(#wsReq{body = Body} = Req) -> case get_header(<<"Content-Type">>, Req) of <<"application/x-www-form-urlencoded">> -> - wsHttp:split_args(Body); + wsHttp:splitArgs(Body); <<"application/x-www-form-urlencoded;", _/binary>> -> % ; charset=... - wsHttp:split_args(Body); + wsHttp:splitArgs(Body); _ -> erlang:error(badarg) end. diff --git a/src/wsSrv/wsSendFile.erl b/src/wsSrv/wsSendFile.erl deleted file mode 100644 index 1253e96..0000000 --- a/src/wsSrv/wsSendFile.erl +++ /dev/null @@ -1,77 +0,0 @@ --module(wsSendFile). - --include("wsCom.hrl"). - --export([ - sendfile/5 -]). - - - -%% @doc Send part of a file on a socket. -%% -%% Basically, @see file:sendfile/5 but for ssl (i.e. not raw OS sockets). -%% Originally from https://github.com/ninenines/ranch/pull/41/files -%% -%% @end --spec sendfile(file:fd(), inet:socket() | ssl:sslsocket(), non_neg_integer(), non_neg_integer(), sendfile_opts()) -> {ok, non_neg_integer()} | {error, atom()}. -sendfile(RawFile, Socket, Offset, Bytes, Opts) -> - ChunkSize = chunkSize(Opts), - Initial2 = case file:position(RawFile, {cur, 0}) of - {ok, Offset} -> - Offset; - {ok, Initial} -> - {ok, _} = file:position(RawFile, {bof, Offset}), - Initial - end, - case sendfile_loop(Socket, RawFile, Bytes, 0, ChunkSize) of - {ok, _Sent} = Result -> - {ok, _} = file:position(RawFile, {bof, Initial2}), - Result; - {error, _Reason} = Error -> - Error - end. - --spec chunkSize(sendfile_opts()) -> pos_integer(). -chunkSize(Opts) -> - case lists:keyfind(chunk_size, 1, Opts) of - {chunk_size, ChunkSize} - when is_integer(ChunkSize) andalso ChunkSize > 0 -> - ChunkSize; - {chunk_size, 0} -> - 16#1FFF; - false -> - 16#1FFF - end. - --spec sendfile_loop(inet:socket() | ssl:sslsocket(), file:fd(), non_neg_integer(), - non_neg_integer(), pos_integer()) - -> {ok, non_neg_integer()} | {error, term()}. -sendfile_loop(_Socket, _RawFile, Sent, Sent, _ChunkSize) - when Sent =/= 0 -> - %% All requested data has been read and sent, return number of bytes sent. - {ok, Sent}; -sendfile_loop(Socket, RawFile, Bytes, Sent, ChunkSize) -> - ReadSize = read_size(Bytes, Sent, ChunkSize), - case file:read(RawFile, ReadSize) of - {ok, IoData} -> - case ssl:send(Socket, IoData) of - ok -> - Sent2 = iolist_size(IoData) + Sent, - sendfile_loop(Socket, RawFile, Bytes, Sent2, - ChunkSize); - {error, _Reason} = Error -> - Error - end; - eof -> - {ok, Sent}; - {error, _Reason} = Error -> - Error - end. - --spec read_size(non_neg_integer(), non_neg_integer(), non_neg_integer()) -> - non_neg_integer(). -read_size(0, _Sent, ChunkSize) -> - ChunkSize; -read_size(Bytes, Sent, ChunkSize) -> - min(Bytes - Sent, ChunkSize). diff --git a/src/wsSrv/wsUtil.erl b/src/wsSrv/wsUtil.erl index 322f691..9613baf 100644 --- a/src/wsSrv/wsUtil.erl +++ b/src/wsSrv/wsUtil.erl @@ -5,17 +5,26 @@ -include_lib("kernel/include/file.hrl"). -export([ - normalize_range/2 - , encode_range/2 - , file_size/1 - , gLV/3 + gLV/3 + , normalizeRange/2 + , encodeRange/2 + , fileSize/1 + , sendfile/5 ]). -export_type([range/0]). --type range() :: {Offset :: non_neg_integer(), Length :: non_neg_integer()}. --spec normalize_range(RangeOrSet, Size) -> Normalized when +gLV(Key, List, Default) -> + case lists:keyfind(Key, 1, List) of + false -> + Default; + {Key, Value} -> + Value + end. + +-type range() :: {Offset :: non_neg_integer(), Length :: non_neg_integer()}. +-spec normalizeRange(RangeOrSet, Size) -> Normalized when RangeOrSet :: any(), Size :: integer(), Normalized :: range() | undefined | invalid_range. @@ -23,59 +32,103 @@ %% is supplied, returns a normalized range in the format %% {Offset, Length}. Returns undefined when an empty byte-range-set %% is supplied and the atom `invalid_range' in all other cases. -normalize_range({suffix, Length}, Size) - when is_integer(Length), Length > 0 -> +normalizeRange({suffix, Length}, Size) when is_integer(Length), Length > 0 -> Length0 = erlang:min(Length, Size), {Size - Length0, Length0}; -normalize_range({offset, Offset}, Size) - when is_integer(Offset), Offset >= 0, Offset < Size -> +normalizeRange({offset, Offset}, Size) when is_integer(Offset), Offset >= 0, Offset < Size -> {Offset, Size - Offset}; -normalize_range({bytes, First, Last}, Size) - when is_integer(First), is_integer(Last), First =< Last -> - normalize_range({First, Last - First + 1}, Size); -normalize_range({Offset, Length}, Size) - when is_integer(Offset), is_integer(Length), +normalizeRange({bytes, First, Last}, Size) when is_integer(First), is_integer(Last), First =< Last -> + normalizeRange({First, Last - First + 1}, Size); +normalizeRange({Offset, Length}, Size) when is_integer(Offset), is_integer(Length), Offset >= 0, Length >= 0, Offset < Size -> Length0 = erlang:min(Length, Size - Offset), {Offset, Length0}; -normalize_range([ByteRange], Size) -> - normalize_range(ByteRange, Size); -normalize_range([], _Size) -> undefined; -normalize_range(_, _Size) -> invalid_range. +normalizeRange([ByteRange], Size) -> + normalizeRange(ByteRange, Size); +normalizeRange([], _Size) -> undefined; +normalizeRange(_, _Size) -> invalid_range. --spec encode_range(Range :: range() | invalid_range, - Size :: non_neg_integer()) -> ByteRange :: iolist(). +-spec encodeRange(Range :: range() | invalid_range, Size :: non_neg_integer()) -> ByteRange :: iolist(). %% @doc: Encode Range to a Content-Range value. -encode_range(Range, Size) -> - [<<"bytes ">>, encode_range_bytes(Range), - <<"/">>, integer_to_binary(Size)]. +encodeRange(Range, Size) -> + [<<"bytes ">>, encodeRangeBytes(Range), <<"/">>, integer_to_binary(Size)]. -encode_range_bytes({Offset, Length}) -> - [integer_to_binary(Offset), - <<"-">>, - integer_to_binary(Offset + Length - 1)]; -encode_range_bytes(invalid_range) -> <<"*">>. +encodeRangeBytes({Offset, Length}) -> + [integer_to_binary(Offset), <<"-">>, integer_to_binary(Offset + Length - 1)]; +encodeRangeBytes(invalid_range) -> <<"*">>. --spec file_size(Filename) -> Size | {error, Reason} when - Filename :: file:name_all(), - Size :: non_neg_integer(), - Reason :: file:posix() | badarg | invalid_file. +-spec fileSize(Filename :: file:name_all()) -> Size :: non_neg_integer() | {error, Reason :: file:posix() | badarg | invalid_file}. %% @doc: Get the size in bytes of the file. -file_size(Filename) -> +fileSize(Filename) -> case file:read_file_info(Filename) of - {ok, #file_info{type = regular, access = Perm, size = Size}} - when Perm =:= read orelse Perm =:= read_write -> + {ok, #file_info{type = regular, access = Perm, size = Size}} when Perm =:= read orelse Perm =:= read_write -> Size; {error, Reason} -> {error, Reason}; _ -> {error, invalid_file} end. -gLV(Key, List, Default) -> - case lists:keyfind(Key, 1, List) of +%% @doc Send part of a file on a socket. +%% +%% Basically, @see file:sendfile/5 but for ssl (i.e. not raw OS sockets). +%% Originally from https://github.com/ninenines/ranch/pull/41/files +%% +%% @end +-spec sendfile(file:fd(), inet:socket() | ssl:sslsocket(), non_neg_integer(), non_neg_integer(), sendfile_opts()) -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(RawFile, Socket, Offset, Bytes, Opts) -> + ChunkSize = chunkSize(Opts), + Initial2 = + case file:position(RawFile, {cur, 0}) of + {ok, Offset} -> + Offset; + {ok, Initial} -> + {ok, _} = file:position(RawFile, {bof, Offset}), + Initial + end, + case sendfileLoop(Socket, RawFile, Bytes, 0, ChunkSize) of + {ok, _Sent} = Result -> + {ok, _} = file:position(RawFile, {bof, Initial2}), + Result; + {error, _Reason} = Error -> + Error + end. + +-spec chunkSize(sendfile_opts()) -> pos_integer(). +chunkSize(Opts) -> + case lists:keyfind(chunk_size, 1, Opts) of + {chunk_size, ChunkSize} + when is_integer(ChunkSize) andalso ChunkSize > 0 -> + ChunkSize; + {chunk_size, 0} -> + 16#1FFF; false -> - Default; - {Key, Value} -> - Value + 16#1FFF end. + +-spec sendfileLoop(inet:socket() | ssl:sslsocket(), file:fd(), non_neg_integer(), non_neg_integer(), pos_integer()) -> {ok, non_neg_integer()} | {error, term()}. +sendfileLoop(_Socket, _RawFile, Sent, Sent, _ChunkSize) when Sent =/= 0 -> + %% All requested data has been read and sent, return number of bytes sent. + {ok, Sent}; +sendfileLoop(Socket, RawFile, Bytes, Sent, ChunkSize) -> + ReadSize = read_size(Bytes, Sent, ChunkSize), + case file:read(RawFile, ReadSize) of + {ok, IoData} -> + case ssl:send(Socket, IoData) of + ok -> + Sent2 = iolist_size(IoData) + Sent, + sendfileLoop(Socket, RawFile, Bytes, Sent2, ChunkSize); + {error, _Reason} = Error -> + Error + end; + eof -> + {ok, Sent}; + {error, _Reason} = Error -> + Error + end. + +-spec read_size(non_neg_integer(), non_neg_integer(), non_neg_integer()) -> non_neg_integer(). +read_size(0, _Sent, ChunkSize) -> + ChunkSize; +read_size(Bytes, Sent, ChunkSize) -> + min(Bytes - Sent, ChunkSize).