Parcourir la source

ft: 代码修改

master
SisMaker il y a 3 ans
Parent
révision
b6ab6919a5
17 fichiers modifiés avec 385 ajouts et 585 suppressions
  1. +59
    -2
      include/eWSrv.hrl
  2. +0
    -0
      priv/server_cert.pem
  3. +0
    -0
      priv/server_key.pem
  4. +1
    -1
      src/eWSrv.app.src
  5. +3
    -3
      src/eWSrv.erl
  6. +0
    -27
      src/test/elli_http_tests.erl
  7. +0
    -124
      src/test/elli_ssl_tests.erl
  8. +0
    -47
      src/test/elli_test.erl
  9. +64
    -59
      src/test/wsEgHer.erl
  10. +0
    -3
      src/test/wsTc.erl
  11. +18
    -17
      src/test/ws_test.erl
  12. +0
    -0
      src/test/ws_test.hrl
  13. +133
    -171
      src/wsSrv/wsHttp.erl
  14. +11
    -11
      src/wsSrv/wsNet.erl
  15. +2
    -2
      src/wsSrv/wsReq.erl
  16. +0
    -77
      src/wsSrv/wsSendFile.erl
  17. +94
    -41
      src/wsSrv/wsUtil.erl

+ 59
- 2
include/eWSrv.hrl Voir le fichier

@ -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().

src/test/server_cert.pem → priv/server_cert.pem Voir le fichier


src/test/server_key.pem → priv/server_key.pem Voir le fichier


+ 1
- 1
src/eWSrv.app.src Voir le fichier

@ -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"]},

+ 3
- 3
src/eWSrv.erl Voir le fichier

@ -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).

+ 0
- 27
src/test/elli_http_tests.erl Voir le fichier

@ -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.

+ 0
- 124
src/test/elli_ssl_tests.erl Voir le fichier

@ -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).

+ 0
- 47
src/test/elli_test.erl Voir le fichier

@ -1,47 +0,0 @@
%%% @author Andreas Hasselberg <andreas.hasselberg@gmail.com>
%%%
%%% @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

src/example/wsEgHer.erl → src/test/wsEgHer.erl Voir le fichier

@ -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">>}.

+ 0
- 3
src/test/wsTc.erl Voir le fichier

@ -212,6 +212,3 @@ test(N) ->
timer:sleep(N),
S2 = erlang:system_time(nanosecond),
io:format("IMY******************222 ~p~n", [S2 - S1]).

src/test/elli_tests.erl → src/test/ws_test.erl Voir le fichier

@ -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)),

src/test/elli_test.hrl → src/test/ws_test.hrl Voir le fichier


+ 133
- 171
src/wsSrv/wsHttp.erl Voir le fichier

@ -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, <<Buffer/binary, Data/binary>>, 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, <<Buffer/binary, Data/binary>>, 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">>;

+ 11
- 11
src/wsSrv/wsNet.erl Voir le fichier

@ -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).

+ 2
- 2
src/wsSrv/wsReq.erl Voir le fichier

@ -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.

+ 0
- 77
src/wsSrv/wsSendFile.erl Voir le fichier

@ -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).

+ 94
- 41
src/wsSrv/wsUtil.erl Voir le fichier

@ -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).

Chargement…
Annuler
Enregistrer