diff --git a/README.md b/README.md index 11447c6..c85dfe1 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,80 @@ Build ----- $ rebar3 compile + +## Usage + +```sh +$ rebar3 shell +``` + +```erlang +%% starting elli +1 > {ok, Pid} = elli:start_link([{callback, elli_example_callback}, {port, 3000}]). +``` + +## Examples + +### Callback Module + +The best source to learn how to write a callback module +is [src/elli_example_callback.erl](src/elli_example_callback.erl) and +its [generated documentation](doc/elli_example_callback.md). There are a bunch of examples used in the tests as well as +descriptions of all the events. + +A minimal callback module could look like this: + +```erlang +-module(elli_minimal_callback). +-export([handle/2, handle_event/3]). + +-include_lib("elli/include/elli.hrl"). +-behaviour(elli_handler). + +handle(Req, _Args) -> + %% Delegate to our handler function + handle(Req#req.method, elli_request:path(Req), Req). + +handle('GET', [<<"hello">>, <<"world">>], _Req) -> + %% Reply with a normal response. `ok' can be used instead of `200' + %% to signal success. + {ok, [], <<"Hello World!">>}; + +handle(_, _, _Req) -> + {404, [], <<"Not Found">>}. + +%% @doc Handle request events, like request completed, exception +%% thrown, client timeout, etc. Must return `ok'. +handle_event(_Event, _Data, _Args) -> + ok. +``` + +### Supervisor Childspec + +To add `elli` to a supervisor you can use the following example and adapt it to your needs. + +```erlang +-module(fancyapi_sup). +-behaviour(supervisor). +-export([start_link/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + ElliOpts = [{callback, fancyapi_callback}, {port, 3000}], + ElliSpec = { + fancy_http, + {elli, start_link, [ElliOpts]}, + permanent, + 5000, + worker, + [elli]}, + + {ok, {{one_for_one, 5, 10}, [ElliSpec]}}. +``` + +## Further Reading + +For more information about the features and design philosophy of `elli` check out the [overview](doc/README.md). diff --git a/rebar.config b/rebar.config index 60a4d96..67245ac 100644 --- a/rebar.config +++ b/rebar.config @@ -8,7 +8,7 @@ ]}. {deps, [ - {eNet, ".*", {git, "http://sismaker.tpddns.cn:53000/SisMaker/eNets.git", {branch, "master"}}}, + {eNet, ".*", {git, "http://sismaker.tpddns.cn:53000/SisMaker/eNet.git", {branch, "master"}}}, {eFmt, ".*", {git, "http://sismaker.tpddns.cn:53000/SisMaker/eFmt.git", {branch, "master"}}}, {eGbh, ".*", {git, "http://sismaker.tpddns.cn:53000/SisMaker/eGbh.git", {branch, "master"}}}, {eSync, ".*", {git, "http://sismaker.tpddns.cn:53000/SisMaker/eSync.git", {branch, "master"}}}, diff --git a/src/test/elli_handover_tests.erl b/src/test/elli_handover_tests.erl index 7154e80..c49a568 100644 --- a/src/test/elli_handover_tests.erl +++ b/src/test/elli_handover_tests.erl @@ -3,35 +3,35 @@ -include("elli_test.hrl"). elli_test_() -> - {setup, - fun setup/0, fun teardown/1, - [ - ?_test(hello_world()), - ?_test(echo()) - ]}. + {setup, + fun setup/0, fun teardown/1, + [ + ?_test(hello_world()), + ?_test(echo()) + ]}. setup() -> - application:start(crypto), - application:start(public_key), - application:start(ssl), - {ok, _} = application:ensure_all_started(hackney), - - Config = [ - {mods, [ - {elli_example_callback_handover, []} - ]} - ], - - {ok, P} = elli:start_link([{callback, elli_middleware}, - {callback_args, Config}, - {port, 3003}]), - unlink(P), - [P]. + application:start(crypto), + application:start(public_key), + application:start(ssl), + {ok, _} = application:ensure_all_started(hackney), + + Config = [ + {mods, [ + {elli_example_callback_handover, []} + ]} + ], + + {ok, P} = elli:start_link([{callback, elli_middleware}, + {callback_args, Config}, + {port, 3003}]), + unlink(P), + [P]. teardown(Pids) -> - [elli:stop(P) || P <- Pids]. + [elli:stop(P) || P <- Pids]. %% @@ -40,13 +40,13 @@ teardown(Pids) -> %% hello_world() -> - Response = hackney:get("http://localhost:3003/hello/world"), - ?assertMatch(200, status(Response)), - ?assertMatch([{<<"Connection">>, <<"close">>}, - {<<"Content-Length">>, <<"12">>}], headers(Response)), - ?assertMatch(<<"Hello World!">>, body(Response)). + Response = hackney:get("http://localhost:3003/hello/world"), + ?assertMatch(200, status(Response)), + ?assertMatch([{<<"Connection">>, <<"close">>}, + {<<"Content-Length">>, <<"12">>}], headers(Response)), + ?assertMatch(<<"Hello World!">>, body(Response)). echo() -> - Response = hackney:get("http://localhost:3003/hello?name=knut"), - ?assertMatch(200, status(Response)), - ?assertMatch(<<"Hello knut">>, body(Response)). + Response = hackney:get("http://localhost:3003/hello?name=knut"), + ?assertMatch(200, status(Response)), + ?assertMatch(<<"Hello knut">>, body(Response)). diff --git a/src/test/elli_http_tests.erl b/src/test/elli_http_tests.erl index 1555b25..3e88cb0 100644 --- a/src/test/elli_http_tests.erl +++ b/src/test/elli_http_tests.erl @@ -4,24 +4,24 @@ %% 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. + 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 = elli_http:chunk_loop({some_type, some_socket}), - Here ! Result, - ok - end. + fun() -> + Result = elli_http:chunk_loop({some_type, some_socket}), + Here ! Result, + ok + end. receive_message() -> - receive - X -> X - after - 1 -> fail - end. + receive + X -> X + after + 1 -> fail + end. diff --git a/src/test/elli_metrics_middleware.erl b/src/test/elli_metrics_middleware.erl index 3861c68..950a426 100644 --- a/src/test/elli_metrics_middleware.erl +++ b/src/test/elli_metrics_middleware.erl @@ -8,27 +8,27 @@ %% init(_Req, _Args) -> - ignore. + ignore. preprocess(Req, _Args) -> - Req. + Req. handle(_Req, _Args) -> - ignore. + ignore. postprocess(_Req, Res, _Args) -> - Res. + Res. %% %% ELLI EVENT CALLBACKS %% -handle_event(request_complete, [_Req,_C,_Hs,_B, {Timings, Sizes}], _) -> - ets:insert(elli_stat_table, {timings, Timings}), - ets:insert(elli_stat_table, {sizes, Sizes}); -handle_event(chunk_complete, [_Req,_C,_Hs,_B, {Timings, Sizes}], _) -> - ets:insert(elli_stat_table, {timings, Timings}), - ets:insert(elli_stat_table, {sizes, Sizes}); +handle_event(request_complete, [_Req, _C, _Hs, _B, {Timings, Sizes}], _) -> + ets:insert(elli_stat_table, {timings, Timings}), + ets:insert(elli_stat_table, {sizes, Sizes}); +handle_event(chunk_complete, [_Req, _C, _Hs, _B, {Timings, Sizes}], _) -> + ets:insert(elli_stat_table, {timings, Timings}), + ets:insert(elli_stat_table, {sizes, Sizes}); handle_event(_Event, _Data, _Args) -> - ok. + ok. diff --git a/src/test/elli_middleware_tests.erl b/src/test/elli_middleware_tests.erl index ea303ee..20f6635 100644 --- a/src/test/elli_middleware_tests.erl +++ b/src/test/elli_middleware_tests.erl @@ -3,14 +3,14 @@ -include("elli_test.hrl"). elli_test_() -> - {setup, - fun setup/0, fun teardown/1, - [ - ?_test(hello_world()), - ?_test(short_circuit()), - ?_test(compress()), - ?_test(no_callbacks()) - ]}. + {setup, + fun setup/0, fun teardown/1, + [ + ?_test(hello_world()), + ?_test(short_circuit()), + ?_test(compress()), + ?_test(no_callbacks()) + ]}. %% @@ -18,53 +18,53 @@ elli_test_() -> %% short_circuit() -> - URL = "http://localhost:3002/middleware/short-circuit", - Response = hackney:get(URL), - ?assertMatch(<<"short circuit!">>, body(Response)). + URL = "http://localhost:3002/middleware/short-circuit", + Response = hackney:get(URL), + ?assertMatch(<<"short circuit!">>, body(Response)). hello_world() -> - URL = "http://localhost:3002/hello/world", - Response = hackney:get(URL), - ?assertMatch(<<"Hello World!">>, body(Response)). + URL = "http://localhost:3002/hello/world", + Response = hackney:get(URL), + ?assertMatch(<<"Hello World!">>, body(Response)). compress() -> - Url = "http://localhost:3002/compressed", - Headers = [{<<"Accept-Encoding">>, <<"gzip">>}], - Response = hackney:get(Url, Headers), - ?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>}, - {<<"Content-Encoding">>, <<"gzip">>}, - {<<"Content-Length">>, <<"41">>}], - headers(Response)), - ?assertEqual(binary:copy(<<"Hello World!">>, 86), - zlib:gunzip(body(Response))), - Response1 = hackney:get("http://localhost:3002/compressed"), - ?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>}, - {<<"Content-Length">>, <<"1032">>}], - headers(Response1)), - ?assertEqual(iolist_to_binary(lists:duplicate(86, "Hello World!")), - body(Response1)), - Url2 = "http://localhost:3002/compressed-io_list", - Headers2 = [{<<"Accept-Encoding">>, <<"gzip">>}], - Response2 = hackney:get(Url2, Headers2), - ?assertMatch(200, status(Response2)), - ?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>}, - {<<"Content-Encoding">>, <<"gzip">>}, - {<<"Content-Length">>, <<"41">>}], - headers(Response2)), - ?assertEqual(binary:copy(<<"Hello World!">>, 86), - zlib:gunzip(body(Response2))), - Response3 = hackney:request("http://localhost:3002/compressed-io_list"), - ?assertMatch(200, status(Response3)), - ?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>}, - {<<"Content-Length">>, <<"1032">>}], - headers(Response3)), - ?assertEqual(iolist_to_binary(lists:duplicate(86, "Hello World!")), - body(Response3)). + Url = "http://localhost:3002/compressed", + Headers = [{<<"Accept-Encoding">>, <<"gzip">>}], + Response = hackney:get(Url, Headers), + ?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>}, + {<<"Content-Encoding">>, <<"gzip">>}, + {<<"Content-Length">>, <<"41">>}], + headers(Response)), + ?assertEqual(binary:copy(<<"Hello World!">>, 86), + zlib:gunzip(body(Response))), + Response1 = hackney:get("http://localhost:3002/compressed"), + ?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>}, + {<<"Content-Length">>, <<"1032">>}], + headers(Response1)), + ?assertEqual(iolist_to_binary(lists:duplicate(86, "Hello World!")), + body(Response1)), + Url2 = "http://localhost:3002/compressed-io_list", + Headers2 = [{<<"Accept-Encoding">>, <<"gzip">>}], + Response2 = hackney:get(Url2, Headers2), + ?assertMatch(200, status(Response2)), + ?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>}, + {<<"Content-Encoding">>, <<"gzip">>}, + {<<"Content-Length">>, <<"41">>}], + headers(Response2)), + ?assertEqual(binary:copy(<<"Hello World!">>, 86), + zlib:gunzip(body(Response2))), + Response3 = hackney:request("http://localhost:3002/compressed-io_list"), + ?assertMatch(200, status(Response3)), + ?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>}, + {<<"Content-Length">>, <<"1032">>}], + headers(Response3)), + ?assertEqual(iolist_to_binary(lists:duplicate(86, "Hello World!")), + body(Response3)). no_callbacks() -> - Response = hackney:get("http://localhost:3004/whatever"), - ?assertMatch(404, status(Response)), - ?assertMatch(<<"Not Found">>, body(Response)). + Response = hackney:get("http://localhost:3004/whatever"), + ?assertMatch(404, status(Response)), + ?assertMatch(<<"Not Found">>, body(Response)). %% @@ -72,31 +72,31 @@ no_callbacks() -> %% setup() -> - application:start(crypto), - application:start(public_key), - application:start(ssl), - {ok, _} = application:ensure_all_started(hackney), + application:start(crypto), + application:start(public_key), + application:start(ssl), + {ok, _} = application:ensure_all_started(hackney), - Config = [ - {mods, [ - {elli_access_log, [{name, elli_syslog}, - {ip, "127.0.0.1"}, - {port, 514}]}, - {elli_example_middleware, []}, - {elli_middleware_compress, []}, - {elli_example_callback, []} - ]} - ], + Config = [ + {mods, [ + {elli_access_log, [{name, elli_syslog}, + {ip, "127.0.0.1"}, + {port, 514}]}, + {elli_example_middleware, []}, + {elli_middleware_compress, []}, + {elli_example_callback, []} + ]} + ], - {ok, P1} = elli:start_link([{callback, elli_middleware}, - {callback_args, Config}, - {port, 3002}]), - unlink(P1), - {ok, P2} = elli:start_link([{callback, elli_middleware}, - {callback_args, [{mods, []}]}, - {port, 3004}]), - unlink(P2), - [P1, P2]. + {ok, P1} = elli:start_link([{callback, elli_middleware}, + {callback_args, Config}, + {port, 3002}]), + unlink(P1), + {ok, P2} = elli:start_link([{callback, elli_middleware}, + {callback_args, [{mods, []}]}, + {port, 3004}]), + unlink(P2), + [P1, P2]. teardown(Pids) -> - [elli:stop(P) || P <- Pids]. + [elli:stop(P) || P <- Pids]. diff --git a/src/test/elli_ssl_tests.erl b/src/test/elli_ssl_tests.erl index 7a7d1d5..6a8293d 100644 --- a/src/test/elli_ssl_tests.erl +++ b/src/test/elli_ssl_tests.erl @@ -5,120 +5,120 @@ -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()) - ]} - ]}. + {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). + [{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). + [{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). + Response = hackney:get("https://localhost:3443/hello/world", + [], <<>>, [insecure]), + ?assertMatch(200, status(Response)), + ?assertMatch({ok, 200, _, _}, Response). chunked() -> - Expected = <<"chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1">>, + Expected = <<"chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1">>, - Response = hackney:get("https://localhost:3443/chunked", [], <<>>, [insecure]), + 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)). + ?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)). + 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)). + {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]. + 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). + [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]). + ets:new(elli_stat_table, [set, named_table, public]). clear_stats(_) -> - ets:delete(elli_stat_table). + ets:delete(elli_stat_table). diff --git a/src/test/elli_test.hrl b/src/test/elli_test.hrl index a451c58..1e2c61e 100644 --- a/src/test/elli_test.hrl +++ b/src/test/elli_test.hrl @@ -1,21 +1,21 @@ -define(I2B(I), list_to_binary(integer_to_list(I))). -define(assertHeadersEqual(H1, H2), ?assertEqual(lists:sort([{string:casefold(K), V} || {K, V} <- H1]), - lists:sort([{string:casefold(K), V} || {K, V} <- H2]))). + lists:sort([{string:casefold(K), V} || {K, V} <- H2]))). status({ok, Status, _Headers, _ClientRef}) -> - Status; + Status; status({ok, Status, _Headers}) -> - Status. + Status. body({ok, _Status, _Headers, ClientRef}) -> - {ok, Body} = hackney:body(ClientRef), - Body. + {ok, Body} = hackney:body(ClientRef), + Body. headers({ok, _Status, Headers, _ClientRef}) -> - lists:sort(Headers); + lists:sort(Headers); headers({ok, _Status, Headers}) -> - lists:sort(Headers). + lists:sort(Headers). diff --git a/src/test/elli_tests.erl b/src/test/elli_tests.erl index c89ba17..ab34a9b 100644 --- a/src/test/elli_tests.erl +++ b/src/test/elli_tests.erl @@ -5,268 +5,268 @@ -define(README, "README.md"). -define(VTB(T1, T2, LB, UB), - time_diff_to_micro_seconds(T1, T2) >= LB andalso - time_diff_to_micro_seconds(T1, T2) =< UB). + time_diff_to_micro_seconds(T1, T2) >= LB andalso + time_diff_to_micro_seconds(T1, T2) =< UB). -ifdef(OTP_RELEASE). -include_lib("kernel/include/logger.hrl"). -else. -define(LOG_ERROR(Str), error_logger:error_msg(Str)). --define(LOG_ERROR(Format,Data), error_logger:error_msg(Format, Data)). --define(LOG_INFO(Format,Data), error_logger:info_msg(Format, Data)). +-define(LOG_ERROR(Format, Data), error_logger:error_msg(Format, Data)). +-define(LOG_INFO(Format, Data), error_logger:info_msg(Format, Data)). -endif. time_diff_to_micro_seconds(T1, T2) -> - erlang:convert_time_unit( + erlang:convert_time_unit( get_timing_value(T2) - - get_timing_value(T1), + get_timing_value(T1), native, micro_seconds). elli_test_() -> - {setup, - fun setup/0, fun teardown/1, - [{foreach, - fun init_stats/0, fun clear_stats/1, - [?_test(hello_world()), - ?_test(keep_alive_timings()), - ?_test(not_found()), - ?_test(crash()), - ?_test(invalid_return()), - ?_test(no_compress()), - ?_test(gzip()), - ?_test(deflate()), - ?_test(exception_flow()), - ?_test(hello_iolist()), - ?_test(accept_content_type()), - ?_test(user_connection()), - ?_test(get_args()), - ?_test(decoded_get_args()), - ?_test(decoded_get_args_list()), - ?_test(post_args()), - ?_test(shorthand()), - ?_test(ip()), - ?_test(found()), - ?_test(too_many_headers()), - ?_test(too_big_body()), - ?_test(way_too_big_body()), - ?_test(bad_request_line()), - ?_test(content_length()), - ?_test(user_content_length()), - ?_test(headers()), - ?_test(chunked()), - ?_test(sendfile()), - ?_test(send_no_file()), - ?_test(sendfile_error()), - ?_test(sendfile_range()), - ?_test(slow_client()), - ?_test(post_pipeline()), - ?_test(get_pipeline()), - ?_test(head()), - ?_test(no_body()), - ?_test(sends_continue()) - ]} - ]}. + {setup, + fun setup/0, fun teardown/1, + [{foreach, + fun init_stats/0, fun clear_stats/1, + [?_test(hello_world()), + ?_test(keep_alive_timings()), + ?_test(not_found()), + ?_test(crash()), + ?_test(invalid_return()), + ?_test(no_compress()), + ?_test(gzip()), + ?_test(deflate()), + ?_test(exception_flow()), + ?_test(hello_iolist()), + ?_test(accept_content_type()), + ?_test(user_connection()), + ?_test(get_args()), + ?_test(decoded_get_args()), + ?_test(decoded_get_args_list()), + ?_test(post_args()), + ?_test(shorthand()), + ?_test(ip()), + ?_test(found()), + ?_test(too_many_headers()), + ?_test(too_big_body()), + ?_test(way_too_big_body()), + ?_test(bad_request_line()), + ?_test(content_length()), + ?_test(user_content_length()), + ?_test(headers()), + ?_test(chunked()), + ?_test(sendfile()), + ?_test(send_no_file()), + ?_test(sendfile_error()), + ?_test(sendfile_range()), + ?_test(slow_client()), + ?_test(post_pipeline()), + ?_test(get_pipeline()), + ?_test(head()), + ?_test(no_body()), + ?_test(sends_continue()) + ]} + ]}. get_timing_value(Key) -> - [{timings, Timings}] = ets:lookup(elli_stat_table, timings), - proplists:get_value(Key, Timings). + [{timings, Timings}] = ets:lookup(elli_stat_table, timings), + proplists:get_value(Key, Timings). get_size_value(Key) -> - [{sizes, Sizes}] = ets:lookup(elli_stat_table, sizes), - proplists:get_value(Key, Sizes). + [{sizes, Sizes}] = ets:lookup(elli_stat_table, sizes), + proplists:get_value(Key, Sizes). setup() -> - application:start(crypto), - application:start(public_key), - application:start(ssl), - {ok, _} = application:ensure_all_started(hackney), - - Config = [ - {mods, [ - {elli_metrics_middleware, []}, - {elli_middleware_compress, []}, - {elli_example_callback, []} - ]} - ], - {ok, P} = elli:start_link([{callback, elli_middleware}, - {callback_args, Config}, - {port, 3001}]), - unlink(P), - [P]. + application:start(crypto), + application:start(public_key), + application:start(ssl), + {ok, _} = application:ensure_all_started(hackney), + + Config = [ + {mods, [ + {elli_metrics_middleware, []}, + {elli_middleware_compress, []}, + {elli_example_callback, []} + ]} + ], + {ok, P} = elli:start_link([{callback, elli_middleware}, + {callback_args, Config}, + {port, 3001}]), + unlink(P), + [P]. teardown(Pids) -> - [elli:stop(P) || P <- Pids]. + [elli:stop(P) || P <- Pids]. init_stats() -> - ets:new(elli_stat_table, [set, named_table, public]). + ets:new(elli_stat_table, [set, named_table, public]). clear_stats(_) -> - ets:delete(elli_stat_table). + ets:delete(elli_stat_table). accessors_test_() -> - RawPath = <<"/foo/bar">>, - Headers = [{<<"content-type">>, <<"application/x-www-form-urlencoded">>}], - Method = 'POST', - Body = <<"name=knut%3D">>, - Name = <<"knut=">>, - Req1 = #req{raw_path = RawPath, - original_headers = Headers, - headers = Headers, - method = Method, - body = Body}, - Args = [{<<"name">>, Name}], - Req2 = #req{original_headers = Headers, headers = Headers, args = Args, body = <<>>}, - - [ - %% POST /foo/bar - ?_assertMatch(RawPath, elli_request:raw_path(Req1)), - ?_assertMatch(Headers, elli_request:headers(Req1)), - ?_assertMatch(Method, elli_request:method(Req1)), - ?_assertMatch(Body, elli_request:body(Req1)), - ?_assertMatch(Args, elli_request:post_args_decoded(Req1)), - ?_assertMatch(undefined, elli_request:post_arg(<<"foo">>, Req1)), - ?_assertMatch(undefined, elli_request:post_arg_decoded(<<"foo">>, Req1)), - ?_assertMatch(Name, elli_request:post_arg_decoded(<<"name">>, Req1)), - %% GET /foo/bar - ?_assertMatch(Headers, elli_request:headers(Req2)), - - ?_assertMatch(Args, elli_request:get_args(Req2)), - ?_assertMatch(undefined, elli_request:get_arg_decoded(<<"foo">>, Req2)), - ?_assertMatch(Name, elli_request:get_arg_decoded(<<"name">>, Req2)), - ?_assertMatch([], elli_request:post_args(Req2)), - - ?_assertMatch({error, not_supported}, elli_request:chunk_ref(#req{})) - ]. + RawPath = <<"/foo/bar">>, + Headers = [{<<"content-type">>, <<"application/x-www-form-urlencoded">>}], + Method = 'POST', + Body = <<"name=knut%3D">>, + Name = <<"knut=">>, + Req1 = #req{raw_path = RawPath, + original_headers = Headers, + headers = Headers, + method = Method, + body = Body}, + Args = [{<<"name">>, Name}], + Req2 = #req{original_headers = Headers, headers = Headers, args = Args, body = <<>>}, + + [ + %% POST /foo/bar + ?_assertMatch(RawPath, elli_request:raw_path(Req1)), + ?_assertMatch(Headers, elli_request:headers(Req1)), + ?_assertMatch(Method, elli_request:method(Req1)), + ?_assertMatch(Body, elli_request:body(Req1)), + ?_assertMatch(Args, elli_request:post_args_decoded(Req1)), + ?_assertMatch(undefined, elli_request:post_arg(<<"foo">>, Req1)), + ?_assertMatch(undefined, elli_request:post_arg_decoded(<<"foo">>, Req1)), + ?_assertMatch(Name, elli_request:post_arg_decoded(<<"name">>, Req1)), + %% GET /foo/bar + ?_assertMatch(Headers, elli_request:headers(Req2)), + + ?_assertMatch(Args, elli_request:get_args(Req2)), + ?_assertMatch(undefined, elli_request:get_arg_decoded(<<"foo">>, Req2)), + ?_assertMatch(Name, elli_request:get_arg_decoded(<<"name">>, Req2)), + ?_assertMatch([], elli_request:post_args(Req2)), + + ?_assertMatch({error, not_supported}, elli_request:chunk_ref(#req{})) + ]. %%% Integration tests %%% Use hackney to actually call Elli over the network. hello_world() -> - Response = hackney:get("http://localhost:3001/hello/world"), - ?assertMatch(200, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"12">>}], headers(Response)), - ?assertMatch(<<"Hello World!">>, body(Response)), - %% sizes - ?assertMatch(63, get_size_value(resp_headers)), - ?assertMatch(12, get_size_value(resp_body)), - ?assertMatch(undefined, get_size_value(req_body)), - - %% 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)), - %% check timings - ?assert(?VTB(request_start, request_end, 1000000, 1200000)), - ?assert(?VTB(headers_start, headers_end, 1, 100)), - ?assert(?VTB(body_start, body_end, 1, 100)), - ?assert(?VTB(user_start, user_end, 1000000, 1200000)), - ?assert(?VTB(send_start, send_end, 1, 2000)). + Response = hackney:get("http://localhost:3001/hello/world"), + ?assertMatch(200, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"12">>}], headers(Response)), + ?assertMatch(<<"Hello World!">>, body(Response)), + %% sizes + ?assertMatch(63, get_size_value(resp_headers)), + ?assertMatch(12, get_size_value(resp_body)), + ?assertMatch(undefined, get_size_value(req_body)), + + %% 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)), + %% check timings + ?assert(?VTB(request_start, request_end, 1000000, 1200000)), + ?assert(?VTB(headers_start, headers_end, 1, 100)), + ?assert(?VTB(body_start, body_end, 1, 100)), + ?assert(?VTB(user_start, user_end, 1000000, 1200000)), + ?assert(?VTB(send_start, send_end, 1, 2000)). keep_alive_timings() -> - Transport = hackney_tcp, - Host = <<"localhost">>, - Port = 3001, - Options = [], - {ok, ConnRef} = hackney:connect(Transport, Host, Port, Options), + Transport = hackney_tcp, + Host = <<"localhost">>, + Port = 3001, + Options = [], + {ok, ConnRef} = hackney:connect(Transport, Host, Port, Options), - ReqBody = <<>>, - ReqHeaders = [], - ReqPath = <<"/hello/world">>, - ReqMethod = get, - Req = {ReqMethod, ReqPath, ReqHeaders, ReqBody}, + ReqBody = <<>>, + ReqHeaders = [], + ReqPath = <<"/hello/world">>, + ReqMethod = get, + Req = {ReqMethod, ReqPath, ReqHeaders, ReqBody}, - {ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req), - keep_alive_timings(Status, Headers, HCRef), + {ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req), + keep_alive_timings(Status, Headers, HCRef), - %% pause between keep-alive requests, - %% request_start is a timestamp of - %% the first bytes of the second request - timer:sleep(1000), + %% pause between keep-alive requests, + %% request_start is a timestamp of + %% the first bytes of the second request + timer:sleep(1000), - {ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req), - keep_alive_timings(Status, Headers, HCRef), + {ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req), + keep_alive_timings(Status, Headers, HCRef), - hackney:close(ConnRef). + hackney:close(ConnRef). keep_alive_timings(Status, Headers, HCRef) -> - ?assertMatch(200, Status), - ?assertHeadersEqual([{<<"connection">>,<<"Keep-Alive">>}, - {<<"content-length">>,<<"12">>}], Headers), - ?assertMatch({ok, <<"Hello World!">>}, hackney:body(HCRef)), - %% sizes - ?assertMatch(63, get_size_value(resp_headers)), - ?assertMatch(12, get_size_value(resp_body)), - %% 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)), - %% check timings - ?assert(?VTB(request_start, request_end, 1000000, 1200000)), - ?assert(?VTB(headers_start, headers_end, 1, 100)), - ?assert(?VTB(body_start, body_end, 1, 100)), - ?assert(?VTB(user_start, user_end, 1000000, 1200000)), - ?assert(?VTB(send_start, send_end, 1, 2000)). + ?assertMatch(200, Status), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"12">>}], Headers), + ?assertMatch({ok, <<"Hello World!">>}, hackney:body(HCRef)), + %% sizes + ?assertMatch(63, get_size_value(resp_headers)), + ?assertMatch(12, get_size_value(resp_body)), + %% 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)), + %% check timings + ?assert(?VTB(request_start, request_end, 1000000, 1200000)), + ?assert(?VTB(headers_start, headers_end, 1, 100)), + ?assert(?VTB(body_start, body_end, 1, 100)), + ?assert(?VTB(user_start, user_end, 1000000, 1200000)), + ?assert(?VTB(send_start, send_end, 1, 2000)). not_found() -> - Response = hackney:get("http://localhost:3001/foobarbaz"), - ?assertMatch(404, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"9">>}], headers(Response)), - ?assertMatch(<<"Not Found">>, body(Response)). + Response = hackney:get("http://localhost:3001/foobarbaz"), + ?assertMatch(404, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"9">>}], headers(Response)), + ?assertMatch(<<"Not Found">>, body(Response)). crash() -> - Response = hackney:get("http://localhost:3001/crash"), - ?assertMatch(500, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"21">>}], headers(Response)), - ?assertMatch(<<"Internal server error">>, body(Response)). + Response = hackney:get("http://localhost:3001/crash"), + ?assertMatch(500, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"21">>}], headers(Response)), + ?assertMatch(<<"Internal server error">>, body(Response)). invalid_return() -> - %% Elli should return 500 for handlers returning bogus responses. - Response = hackney:get("http://localhost:3001/invalid_return"), - ?assertMatch(500, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"21">>}], headers(Response)), - ?assertMatch(<<"Internal server error">>, body(Response)). + %% Elli should return 500 for handlers returning bogus responses. + Response = hackney:get("http://localhost:3001/invalid_return"), + ?assertMatch(500, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"21">>}], headers(Response)), + ?assertMatch(<<"Internal server error">>, body(Response)). no_compress() -> - Response = hackney:get("http://localhost:3001/compressed"), - ?assertMatch(200, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"1032">>}], headers(Response)), - ?assertEqual(binary:copy(<<"Hello World!">>, 86), - body(Response)). + Response = hackney:get("http://localhost:3001/compressed"), + ?assertMatch(200, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"1032">>}], headers(Response)), + ?assertEqual(binary:copy(<<"Hello World!">>, 86), + body(Response)). compress(Encoding, Length) -> - Response = hackney:get("http://localhost:3001/compressed", - [{<<"Accept-Encoding">>, Encoding}]), - ?assertMatch(200, status(Response)), - ?assertHeadersEqual([{<<"Content-Encoding">>, Encoding}, - {<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, Length}], headers(Response)), - ?assertEqual(binary:copy(<<"Hello World!">>, 86), - uncompress(Encoding, body(Response))). - -uncompress(<<"gzip">>, Data) -> zlib:gunzip(Data); + Response = hackney:get("http://localhost:3001/compressed", + [{<<"Accept-Encoding">>, Encoding}]), + ?assertMatch(200, status(Response)), + ?assertHeadersEqual([{<<"Content-Encoding">>, Encoding}, + {<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, Length}], headers(Response)), + ?assertEqual(binary:copy(<<"Hello World!">>, 86), + uncompress(Encoding, body(Response))). + +uncompress(<<"gzip">>, Data) -> zlib:gunzip(Data); uncompress(<<"deflate">>, Data) -> zlib:uncompress(Data). gzip() -> compress(<<"gzip">>, <<"41">>). @@ -274,474 +274,474 @@ gzip() -> compress(<<"gzip">>, <<"41">>). deflate() -> compress(<<"deflate">>, <<"29">>). exception_flow() -> - Response = hackney:get("http://localhost:3001/403"), - ?assertMatch(403, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"9">>}], headers(Response)), - ?assertMatch(<<"Forbidden">>, body(Response)). + Response = hackney:get("http://localhost:3001/403"), + ?assertMatch(403, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"9">>}], headers(Response)), + ?assertMatch(<<"Forbidden">>, body(Response)). hello_iolist() -> - Url = "http://localhost:3001/hello/iolist?name=knut", - Response = hackney:get(Url), - ?assertMatch(<<"Hello knut">>, body(Response)). + Url = "http://localhost:3001/hello/iolist?name=knut", + Response = hackney:get(Url), + ?assertMatch(<<"Hello knut">>, body(Response)). accept_content_type() -> - Json = hackney:get("http://localhost:3001/type?name=knut", - [{"Accept", "application/json"}]), - ?assertMatch(<<"{\"name\" : \"knut\"}">>, body(Json)), - Text = hackney:get("http://localhost:3001/type?name=knut", - [{"Accept", "text/plain"}]), - ?assertMatch(<<"name: knut">>, body(Text)). + Json = hackney:get("http://localhost:3001/type?name=knut", + [{"Accept", "application/json"}]), + ?assertMatch(<<"{\"name\" : \"knut\"}">>, body(Json)), + Text = hackney:get("http://localhost:3001/type?name=knut", + [{"Accept", "text/plain"}]), + ?assertMatch(<<"name: knut">>, body(Text)). user_connection() -> - Url = "http://localhost:3001/user/defined/behaviour", - Response = hackney:get(Url), - ?assertMatch(304, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"close">>}, - {<<"content-length">>, <<"123">>}], headers(Response)), - ?assertMatch(<<>>, body(Response)). + Url = "http://localhost:3001/user/defined/behaviour", + Response = hackney:get(Url), + ?assertMatch(304, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"close">>}, + {<<"content-length">>, <<"123">>}], headers(Response)), + ?assertMatch(<<>>, body(Response)). get_args() -> - Response = hackney:get("http://localhost:3001/hello?name=knut"), - ?assertMatch(<<"Hello knut">>, body(Response)). + Response = hackney:get("http://localhost:3001/hello?name=knut"), + ?assertMatch(<<"Hello knut">>, body(Response)). decoded_get_args() -> - Url = "http://localhost:3001/decoded-hello?name=knut%3D", - Response = hackney:get(Url), - ?assertMatch(<<"Hello knut=">>, body(Response)). + Url = "http://localhost:3001/decoded-hello?name=knut%3D", + Response = hackney:get(Url), + ?assertMatch(<<"Hello knut=">>, body(Response)). decoded_get_args_list() -> - Url = "http://localhost:3001/decoded-list?name=knut%3D&foo", - Response = hackney:get(Url), - ?assertMatch(<<"Hello knut=">>, body(Response)). + Url = "http://localhost:3001/decoded-list?name=knut%3D&foo", + Response = hackney:get(Url), + ?assertMatch(<<"Hello knut=">>, body(Response)). post_args() -> - Body = <<"name=foo&city=New%20York">>, - ContentType = <<"application/x-www-form-urlencoded">>, + Body = <<"name=foo&city=New%20York">>, + ContentType = <<"application/x-www-form-urlencoded">>, - Response = hackney:post("http://localhost:3001/hello", - [{<<"content-type">>, ContentType}], - Body), - ?assertMatch(200, status(Response)), - ?assertMatch(<<"Hello foo of New York">>, body(Response)). + Response = hackney:post("http://localhost:3001/hello", + [{<<"content-type">>, ContentType}], + Body), + ?assertMatch(200, status(Response)), + ?assertMatch(<<"Hello foo of New York">>, body(Response)). shorthand() -> - Response = hackney:get("http://localhost:3001/shorthand"), - ?assertMatch(200, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"5">>}], headers(Response)), - ?assertMatch(<<"hello">>, body(Response)). + Response = hackney:get("http://localhost:3001/shorthand"), + ?assertMatch(200, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"5">>}], headers(Response)), + ?assertMatch(<<"hello">>, body(Response)). ip() -> - Response = hackney:get("http://localhost:3001/ip"), - ?assertMatch(200, status(Response)), - ?assertMatch(<<"127.0.0.1">>, body(Response)). + Response = hackney:get("http://localhost:3001/ip"), + ?assertMatch(200, status(Response)), + ?assertMatch(<<"127.0.0.1">>, body(Response)). found() -> - Response = hackney:get("http://localhost:3001/302"), - ?assertMatch(302, status(Response)), - ?assertHeadersEqual([{<<"Location">>, <<"/hello/world">>}, - {<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"0">>}], headers(Response)), - ?assertMatch(<<>>, body(Response)). + Response = hackney:get("http://localhost:3001/302"), + ?assertMatch(302, status(Response)), + ?assertHeadersEqual([{<<"Location">>, <<"/hello/world">>}, + {<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"0">>}], headers(Response)), + ?assertMatch(<<>>, body(Response)). too_many_headers() -> - Headers = lists:duplicate(100, {<<"X-Foo">>, <<"Bar">>}), - Response = hackney:get("http://localhost:3001/foo", Headers), - ?assertMatch(400, status(Response)). + Headers = lists:duplicate(100, {<<"X-Foo">>, <<"Bar">>}), + Response = hackney:get("http://localhost:3001/foo", Headers), + ?assertMatch(400, status(Response)). too_big_body() -> - Body = binary:copy(<<"x">>, (1024 * 1000) + 1), - Response = hackney:post("http://localhost:3001/foo", [], Body), - ?assertMatch(413, status(Response)). + Body = binary:copy(<<"x">>, (1024 * 1000) + 1), + Response = hackney:post("http://localhost:3001/foo", [], Body), + ?assertMatch(413, status(Response)). way_too_big_body() -> - Body = binary:copy(<<"x">>, (1024 * 2000) + 1), - ?assertMatch({error, closed}, - hackney:post("http://localhost:3001/foo", [], Body)). + Body = binary:copy(<<"x">>, (1024 * 2000) + 1), + ?assertMatch({error, closed}, + hackney:post("http://localhost:3001/foo", [], Body)). bad_request_line() -> - {ok, Socket} = gen_tcp:connect("127.0.0.1", 3001, - [{active, false}, binary]), + {ok, Socket} = gen_tcp:connect("127.0.0.1", 3001, + [{active, false}, binary]), - Req = <<"FOO BAR /hello HTTP/1.1\r\n">>, - gen_tcp:send(Socket, <>), - ?assertMatch({ok, <<"HTTP/1.1 400 Bad Request\r\n" - "content-length: 11\r\n\r\nBad Request">>}, - gen_tcp:recv(Socket, 0)). + Req = <<"FOO BAR /hello HTTP/1.1\r\n">>, + gen_tcp:send(Socket, <>), + ?assertMatch({ok, <<"HTTP/1.1 400 Bad Request\r\n" + "content-length: 11\r\n\r\nBad Request">>}, + gen_tcp:recv(Socket, 0)). content_length() -> - Response = hackney:get("http://localhost:3001/304"), + Response = hackney:get("http://localhost:3001/304"), - ?assertMatch(304, status(Response)), - ?assertHeadersEqual([{<<"Etag">>, <<"foobar">>}, - {<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"7">>}], headers(Response)), - ?assertMatch(<<>>, body(Response)). + ?assertMatch(304, status(Response)), + ?assertHeadersEqual([{<<"Etag">>, <<"foobar">>}, + {<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"7">>}], headers(Response)), + ?assertMatch(<<>>, body(Response)). user_content_length() -> - Headers = <<"Foo: bar\n\n">>, - Client = start_slow_client(3001, "/user/content-length"), - send(Client, Headers, 128), - ?assertMatch({ok, <<"HTTP/1.1 200 OK\r\n" - "connection: Keep-Alive\r\n" - "Content-Length: 123\r\n" - "\r\n" - "foobar">>}, - gen_tcp:recv(Client, 0)). + Headers = <<"Foo: bar\n\n">>, + Client = start_slow_client(3001, "/user/content-length"), + send(Client, Headers, 128), + ?assertMatch({ok, <<"HTTP/1.1 200 OK\r\n" + "connection: Keep-Alive\r\n" + "Content-Length: 123\r\n" + "\r\n" + "foobar">>}, + gen_tcp:recv(Client, 0)). headers() -> - Response = hackney:get("http://localhost:3001/headers.html"), - Headers = headers(Response), + Response = hackney:get("http://localhost:3001/headers.html"), + Headers = headers(Response), - ?assert(proplists:is_defined(<<"X-Custom">>, Headers)), - ?assertMatch(<<"foobar">>, proplists:get_value(<<"X-Custom">>, Headers)). + ?assert(proplists:is_defined(<<"X-Custom">>, Headers)), + ?assertMatch(<<"foobar">>, proplists:get_value(<<"X-Custom">>, Headers)). chunked() -> - Expected = <<"chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1">>, - - Response = hackney:get("http://localhost:3001/chunked"), - - ?assertMatch(200, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-type">>, <<"text/event-stream">>}, - {<<"transfer-encoding">>, <<"chunked">>}], headers(Response)), - ?assertMatch(Expected, body(Response)), - %% sizes - ?assertMatch(104, get_size_value(resp_headers)), - ?assertMatch(111, get_size_value(chunks)), - %% 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)). + Expected = <<"chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1">>, + + Response = hackney:get("http://localhost:3001/chunked"), + + ?assertMatch(200, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-type">>, <<"text/event-stream">>}, + {<<"transfer-encoding">>, <<"chunked">>}], headers(Response)), + ?assertMatch(Expected, body(Response)), + %% sizes + ?assertMatch(104, get_size_value(resp_headers)), + ?assertMatch(111, get_size_value(chunks)), + %% 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)). sendfile() -> - Response = hackney:get("http://localhost:3001/sendfile"), - F = ?README, - {ok, Expected} = file:read_file(F), - - ?assertMatch(200, status(Response)), - ?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)). + Response = hackney:get("http://localhost:3001/sendfile"), + F = ?README, + {ok, Expected} = file:read_file(F), + + ?assertMatch(200, status(Response)), + ?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)). send_no_file() -> - Response = hackney:get("http://localhost:3001/send_no_file"), + Response = hackney:get("http://localhost:3001/send_no_file"), - ?assertMatch(500, status(Response)), - ?assertHeadersEqual([{<<"content-length">>, <<"12">>}], - headers(Response)), - ?assertMatch(<<"Server Error">>, body(Response)). + ?assertMatch(500, status(Response)), + ?assertHeadersEqual([{<<"content-length">>, <<"12">>}], + headers(Response)), + ?assertMatch(<<"Server Error">>, body(Response)). sendfile_error() -> - Response = hackney:get("http://localhost:3001/sendfile/error"), + Response = hackney:get("http://localhost:3001/sendfile/error"), - ?assertMatch(500, status(Response)), - ?assertHeadersEqual([{<<"content-length">>, <<"12">>}], - headers(Response)), - ?assertMatch(<<"Server Error">>, body(Response)). + ?assertMatch(500, status(Response)), + ?assertHeadersEqual([{<<"content-length">>, <<"12">>}], + headers(Response)), + ?assertMatch(<<"Server Error">>, body(Response)). sendfile_range() -> - Url = "http://localhost:3001/sendfile/range", - Headers = [{"Range", "bytes=300-699"}], - Response = hackney:get(Url, Headers), - F = ?README, - {ok, Fd} = file:open(F, [read, raw, binary]), - {ok, Expected} = file:pread(Fd, 300, 400), - file:close(Fd), - Size = elli_util:file_size(F), - ?assertMatch(206, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"400">>}, - {<<"Content-Range">>, iolist_to_binary(["bytes 300-699/", integer_to_binary(Size)])}], - headers(Response)), - ?assertEqual(Expected, body(Response)). + Url = "http://localhost:3001/sendfile/range", + Headers = [{"Range", "bytes=300-699"}], + Response = hackney:get(Url, Headers), + F = ?README, + {ok, Fd} = file:open(F, [read, raw, binary]), + {ok, Expected} = file:pread(Fd, 300, 400), + file:close(Fd), + Size = elli_util:file_size(F), + ?assertMatch(206, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"400">>}, + {<<"Content-Range">>, iolist_to_binary(["bytes 300-699/", integer_to_binary(Size)])}], + headers(Response)), + ?assertEqual(Expected, body(Response)). slow_client() -> - Body = <<"name=foobarbaz">>, - Headers = <<"content-length: ", (?I2B(size(Body)))/binary, "\r\n\r\n">>, - Client = start_slow_client(3001, "/hello"), - - send(Client, Headers, 1), - send(Client, Body, size(Body)), - - ?assertMatch({ok, <<"HTTP/1.1 200 OK\r\n" - "connection: Keep-Alive\r\n" - "content-length: 15\r\n" - "\r\n" - "Hello undefined">>}, - gen_tcp:recv(Client, 0)), - %% check timings - ?assert(?VTB(request_start, request_end, 30000, 70000)), - ?assert(?VTB(headers_start, headers_end, 30000, 70000)), - ?assert(?VTB(body_start, body_end, 1, 3000)), - ?assert(?VTB(user_start, user_end, 1, 100)), - ?assert(?VTB(send_start, send_end, 1, 200)). + Body = <<"name=foobarbaz">>, + Headers = <<"content-length: ", (?I2B(size(Body)))/binary, "\r\n\r\n">>, + Client = start_slow_client(3001, "/hello"), + + send(Client, Headers, 1), + send(Client, Body, size(Body)), + + ?assertMatch({ok, <<"HTTP/1.1 200 OK\r\n" + "connection: Keep-Alive\r\n" + "content-length: 15\r\n" + "\r\n" + "Hello undefined">>}, + gen_tcp:recv(Client, 0)), + %% check timings + ?assert(?VTB(request_start, request_end, 30000, 70000)), + ?assert(?VTB(headers_start, headers_end, 30000, 70000)), + ?assert(?VTB(body_start, body_end, 1, 3000)), + ?assert(?VTB(user_start, user_end, 1, 100)), + ?assert(?VTB(send_start, send_end, 1, 200)). post_pipeline() -> - Body = <<"name=elli&city=New%20York">>, - Headers = <<"content-length: ", (?I2B(size(Body)))/binary, "\r\n", - "Content-Type: application/x-www-form-urlencoded", "\r\n", - "\r\n">>, + Body = <<"name=elli&city=New%20York">>, + Headers = <<"content-length: ", (?I2B(size(Body)))/binary, "\r\n", + "Content-Type: application/x-www-form-urlencoded", "\r\n", + "\r\n">>, - {ok, Socket} = gen_tcp:connect("127.0.0.1", 3001, - [{active, false}, binary]), + {ok, Socket} = gen_tcp:connect("127.0.0.1", 3001, + [{active, false}, binary]), - Req = <<"POST /hello HTTP/1.1\r\n", - Headers/binary, - Body/binary>>, - gen_tcp:send(Socket, <>), + Req = <<"POST /hello HTTP/1.1\r\n", + Headers/binary, + Body/binary>>, + gen_tcp:send(Socket, <>), - ExpectedResponse = <<"HTTP/1.1 200 OK\r\n" - "connection: Keep-Alive\r\n" - "content-length: 22\r\n" - "\r\n" - "Hello elli of New York">>, + ExpectedResponse = <<"HTTP/1.1 200 OK\r\n" + "connection: Keep-Alive\r\n" + "content-length: 22\r\n" + "\r\n" + "Hello elli of New York">>, - {ok, Res} = gen_tcp:recv(Socket, size(ExpectedResponse) * 2), + {ok, Res} = gen_tcp:recv(Socket, size(ExpectedResponse) * 2), - Size = size(Body), - ?assertMatch(Size, get_size_value(req_body)), - ?assertEqual(binary:copy(ExpectedResponse, 2), - Res). + Size = size(Body), + ?assertMatch(Size, get_size_value(req_body)), + ?assertEqual(binary:copy(ExpectedResponse, 2), + Res). get_pipeline() -> - Headers = <<"User-Agent: sloow\r\n\r\n">>, - Req = <<"GET /hello?name=elli HTTP/1.1\r\n", - Headers/binary>>, - {ok, Socket} = gen_tcp:connect("127.0.0.1", 3001, - [{active, false}, binary]), - gen_tcp:send(Socket, <>), - ExpectedResponse = <<"HTTP/1.1 200 OK\r\n" - "connection: Keep-Alive\r\n" - "content-length: 10\r\n" - "\r\n" - "Hello elli">>, - {ok, Res} = gen_tcp:recv(Socket, size(ExpectedResponse) * 2), - case binary:copy(ExpectedResponse, 2) =:= Res of - true -> - ok; - false -> - ?LOG_INFO("Expected: ~p~nResult: ~p~n", - [binary:copy(ExpectedResponse, 2), Res]) - end, - - ?assertEqual(binary:copy(ExpectedResponse, 2), - Res). + Headers = <<"User-Agent: sloow\r\n\r\n">>, + Req = <<"GET /hello?name=elli HTTP/1.1\r\n", + Headers/binary>>, + {ok, Socket} = gen_tcp:connect("127.0.0.1", 3001, + [{active, false}, binary]), + gen_tcp:send(Socket, <>), + ExpectedResponse = <<"HTTP/1.1 200 OK\r\n" + "connection: Keep-Alive\r\n" + "content-length: 10\r\n" + "\r\n" + "Hello elli">>, + {ok, Res} = gen_tcp:recv(Socket, size(ExpectedResponse) * 2), + case binary:copy(ExpectedResponse, 2) =:= Res of + true -> + ok; + false -> + ?LOG_INFO("Expected: ~p~nResult: ~p~n", + [binary:copy(ExpectedResponse, 2), Res]) + end, + + ?assertEqual(binary:copy(ExpectedResponse, 2), + Res). head() -> - Response = hackney:head("http://localhost:3001/head"), - ?assertMatch(200, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"20">>}], headers(Response)). + Response = hackney:head("http://localhost:3001/head"), + ?assertMatch(200, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"20">>}], headers(Response)). no_body() -> - Response = hackney:get("http://localhost:3001/304"), - ?assertMatch(304, status(Response)), - ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, - {<<"content-length">>, <<"7">>}, - {<<"Etag">>, <<"foobar">>}], headers(Response)), - ?assertMatch(<<>>, body(Response)). + Response = hackney:get("http://localhost:3001/304"), + ?assertMatch(304, status(Response)), + ?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>}, + {<<"content-length">>, <<"7">>}, + {<<"Etag">>, <<"foobar">>}], headers(Response)), + ?assertMatch(<<>>, body(Response)). sends_continue() -> - {ok, Socket} = gen_tcp:connect("127.0.0.1", 3001, [{active, false}, binary]), - - Body = <<"name=elli&city=New%20York">>, - Length = ?I2B(size(Body)), - - Req = <<"POST /hello HTTP/1.1\r\n", - "Host: localhost\r\n", - "Content-Type: application/x-www-form-urlencoded\r\n", - "content-length: ", Length/binary, "\r\n", - "Expect: 100-continue\r\n\r\n">>, - - gen_tcp:send(Socket, Req), - ?assertMatch({ok, <<"HTTP/1.1 100 Continue\r\n" - "content-length: 0\r\n\r\n">>}, - gen_tcp:recv(Socket, 0)), - % Send Result of the body - gen_tcp:send(Socket, Body), - ExpectedResponse = <<"HTTP/1.1 200 OK\r\n" - "connection: Keep-Alive\r\n" - "content-length: 22\r\n" - "\r\n" - "Hello elli of New York">>, - ?assertMatch({ok, ExpectedResponse}, - gen_tcp:recv(Socket, size(ExpectedResponse))). + {ok, Socket} = gen_tcp:connect("127.0.0.1", 3001, [{active, false}, binary]), + + Body = <<"name=elli&city=New%20York">>, + Length = ?I2B(size(Body)), + + Req = <<"POST /hello HTTP/1.1\r\n", + "Host: localhost\r\n", + "Content-Type: application/x-www-form-urlencoded\r\n", + "content-length: ", Length/binary, "\r\n", + "Expect: 100-continue\r\n\r\n">>, + + gen_tcp:send(Socket, Req), + ?assertMatch({ok, <<"HTTP/1.1 100 Continue\r\n" + "content-length: 0\r\n\r\n">>}, + gen_tcp:recv(Socket, 0)), + % Send Result of the body + gen_tcp:send(Socket, Body), + ExpectedResponse = <<"HTTP/1.1 200 OK\r\n" + "connection: Keep-Alive\r\n" + "content-length: 22\r\n" + "\r\n" + "Hello elli of New York">>, + ?assertMatch({ok, ExpectedResponse}, + gen_tcp:recv(Socket, size(ExpectedResponse))). %%% Slow client, sending only the specified byte size every millisecond start_slow_client(Port, Url) -> - case gen_tcp:connect("127.0.0.1", Port, [{active, false}, binary]) of - {ok, Socket} -> - gen_tcp:send(Socket, "GET " ++ Url ++ " HTTP/1.1\r\n"), - Socket; - {error, Reason} -> - throw({slow_client_error, Reason}) - end. + case gen_tcp:connect("127.0.0.1", Port, [{active, false}, binary]) of + {ok, Socket} -> + gen_tcp:send(Socket, "GET " ++ Url ++ " HTTP/1.1\r\n"), + Socket; + {error, Reason} -> + throw({slow_client_error, Reason}) + end. send(_Socket, <<>>, _) -> - ok; + ok; send(Socket, B, ChunkSize) -> - {Part, Rest} = case B of - <> -> {P, R}; - P -> {P, <<>>} - end, - %%?LOG_INFO("~p~n", [Part]), - gen_tcp:send(Socket, Part), - timer:sleep(1), - send(Socket, Rest, ChunkSize). + {Part, Rest} = case B of + <> -> {P, R}; + P -> {P, <<>>} + end, + %%?LOG_INFO("~p~n", [Part]), + gen_tcp:send(Socket, Part), + timer:sleep(1), + send(Socket, Rest, ChunkSize). %%% Unit tests body_qs_test() -> - Expected = [{<<"foo">>, <<"bar">>}, - {<<"baz">>, <<"bang">>}, - {<<"found">>, true}], - Body = <<"foo=bar&baz=bang&found">>, - Headers = [{<<"content-type">>, <<"application/x-www-form-urlencoded">>}], - ?assertMatch(Expected, elli_request:body_qs(#req{body = Body, - original_headers = Headers, - headers = Headers})). + Expected = [{<<"foo">>, <<"bar">>}, + {<<"baz">>, <<"bang">>}, + {<<"found">>, true}], + Body = <<"foo=bar&baz=bang&found">>, + Headers = [{<<"content-type">>, <<"application/x-www-form-urlencoded">>}], + ?assertMatch(Expected, elli_request:body_qs(#req{body = Body, + original_headers = Headers, + headers = Headers})). to_proplist_test() -> - Req = #req{method = 'GET', - path = [<<"crash">>], - args = [], - version = {1, 1}, - raw_path = <<"/crash">>, - original_headers = [{<<"Host">>, <<"localhost:3001">>}], - headers = [{<<"host">>, <<"localhost:3001">>}], - body = <<>>, - pid = self(), - socket = socket, - callback = {mod, []}}, - - Prop = [{method, 'GET'}, - {scheme, undefined}, - {host, undefined}, - {port, undefined}, - {path, [<<"crash">>]}, - {args, []}, - {raw_path, <<"/crash">>}, - {version, {1, 1}}, - {headers, [{<<"host">>, <<"localhost:3001">>}]}, - {original_headers, [{<<"Host">>, <<"localhost:3001">>}]}, - {body, <<>>}, - {pid, self()}, - {socket, socket}, - {callback, {mod, []}}], - ?assertEqual(Prop, elli_request:to_proplist(Req)). + Req = #req{method = 'GET', + path = [<<"crash">>], + args = [], + version = {1, 1}, + raw_path = <<"/crash">>, + original_headers = [{<<"Host">>, <<"localhost:3001">>}], + headers = [{<<"host">>, <<"localhost:3001">>}], + body = <<>>, + pid = self(), + socket = socket, + callback = {mod, []}}, + + Prop = [{method, 'GET'}, + {scheme, undefined}, + {host, undefined}, + {port, undefined}, + {path, [<<"crash">>]}, + {args, []}, + {raw_path, <<"/crash">>}, + {version, {1, 1}}, + {headers, [{<<"host">>, <<"localhost:3001">>}]}, + {original_headers, [{<<"Host">>, <<"localhost:3001">>}]}, + {body, <<>>}, + {pid, self()}, + {socket, socket}, + {callback, {mod, []}}], + ?assertEqual(Prop, elli_request:to_proplist(Req)). is_request_test() -> - ?assert(elli_request:is_request(#req{})), - ?assertNot(elli_request:is_request({req, foobar})). + ?assert(elli_request:is_request(#req{})), + ?assertNot(elli_request:is_request({req, foobar})). query_str_test_() -> - MakeReq = fun(Path) -> #req{raw_path = Path} end, - [ - %% For empty query strings, expect `query_str` to return an empty binary. - ?_assertMatch(<<>>, elli_request:query_str(MakeReq(<<"/foo">>))), - ?_assertMatch(<<>>, elli_request:query_str(MakeReq(<<"/foo?">>))), - %% Otherwise it should return everything to the right hand side of `?`. - ?_assertMatch(<<"bar=baz&baz=bang">>, - elli_request:query_str(MakeReq(<<"/foo?bar=baz&baz=bang">>))) - ]. + MakeReq = fun(Path) -> #req{raw_path = Path} end, + [ + %% For empty query strings, expect `query_str` to return an empty binary. + ?_assertMatch(<<>>, elli_request:query_str(MakeReq(<<"/foo">>))), + ?_assertMatch(<<>>, elli_request:query_str(MakeReq(<<"/foo?">>))), + %% Otherwise it should return everything to the right hand side of `?`. + ?_assertMatch(<<"bar=baz&baz=bang">>, + elli_request:query_str(MakeReq(<<"/foo?bar=baz&baz=bang">>))) + ]. get_range_test_() -> - Req = #req{headers = [{<<"range">>, - <<"bytes=0-99 ,500-999 , -800">>}]}, - OffsetReq = #req{headers = [{<<"range">>, <<"bytes=200-">>}]}, - UndefReq = #req{headers = []}, - BadReq = #req{headers = [{<<"range">>, <<"bytes=--99,hallo-world">>}]}, + Req = #req{headers = [{<<"range">>, + <<"bytes=0-99 ,500-999 , -800">>}]}, + OffsetReq = #req{headers = [{<<"range">>, <<"bytes=200-">>}]}, + UndefReq = #req{headers = []}, + BadReq = #req{headers = [{<<"range">>, <<"bytes=--99,hallo-world">>}]}, - ByteRangeSet = [{bytes, 0, 99}, {bytes, 500, 999}, {suffix, 800}], + ByteRangeSet = [{bytes, 0, 99}, {bytes, 500, 999}, {suffix, 800}], - [?_assertMatch(ByteRangeSet, elli_request:get_range(Req)), - ?_assertMatch([{offset, 200}], elli_request:get_range(OffsetReq)), - ?_assertMatch([], elli_request:get_range(UndefReq)), - ?_assertMatch(parse_error, elli_request:get_range(BadReq))]. + [?_assertMatch(ByteRangeSet, elli_request:get_range(Req)), + ?_assertMatch([{offset, 200}], elli_request:get_range(OffsetReq)), + ?_assertMatch([], elli_request:get_range(UndefReq)), + ?_assertMatch(parse_error, elli_request:get_range(BadReq))]. normalize_range_test_() -> - Size = 1000, - - Bytes1 = {bytes, 200, 400}, - Bytes2 = {bytes, 0, 1000000}, - Suffix = {suffix, 303}, - Offset = {offset, 42}, - Normal = {200, 400}, - Set = [{bytes, 0, 999}], - EmptySet = [], - Invalid1 = {bytes, 400, 200}, - Invalid2 = {bytes, 1200, 2000}, - Invalid3 = {offset, -10}, - Invalid4 = {offset, 2000}, - Invalid5 = parse_error, - Invalid6 = [{bytes, 0, 100}, {suffix, 42}], - - [?_assertMatch({200, 201}, elli_util:normalize_range(Bytes1, Size)), - ?_assertMatch({0, Size}, elli_util:normalize_range(Bytes2, Size)), - ?_assertEqual({Size - 303, 303}, elli_util:normalize_range(Suffix, Size)), - ?_assertEqual({42, Size - 42}, elli_util:normalize_range(Offset, Size)), - ?_assertMatch({200, 400}, elli_util:normalize_range(Normal, Size)), - ?_assertMatch({0, 1000}, elli_util:normalize_range(Set, Size)), - ?_assertMatch(undefined, elli_util:normalize_range(EmptySet, Size)), - ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid1, Size)), - ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid2, Size)), - ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid3, Size)), - ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid4, Size)), - ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid5, Size)), - ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid6, Size))]. + Size = 1000, + + Bytes1 = {bytes, 200, 400}, + Bytes2 = {bytes, 0, 1000000}, + Suffix = {suffix, 303}, + Offset = {offset, 42}, + Normal = {200, 400}, + Set = [{bytes, 0, 999}], + EmptySet = [], + Invalid1 = {bytes, 400, 200}, + Invalid2 = {bytes, 1200, 2000}, + Invalid3 = {offset, -10}, + Invalid4 = {offset, 2000}, + Invalid5 = parse_error, + Invalid6 = [{bytes, 0, 100}, {suffix, 42}], + + [?_assertMatch({200, 201}, elli_util:normalize_range(Bytes1, Size)), + ?_assertMatch({0, Size}, elli_util:normalize_range(Bytes2, Size)), + ?_assertEqual({Size - 303, 303}, elli_util:normalize_range(Suffix, Size)), + ?_assertEqual({42, Size - 42}, elli_util:normalize_range(Offset, Size)), + ?_assertMatch({200, 400}, elli_util:normalize_range(Normal, Size)), + ?_assertMatch({0, 1000}, elli_util:normalize_range(Set, Size)), + ?_assertMatch(undefined, elli_util:normalize_range(EmptySet, Size)), + ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid1, Size)), + ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid2, Size)), + ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid3, Size)), + ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid4, Size)), + ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid5, Size)), + ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid6, Size))]. encode_range_test() -> - Expected = [<<"bytes ">>,<<"*">>,<<"/">>,<<"42">>], - ?assertMatch(Expected, elli_util:encode_range(invalid_range, 42)). + Expected = [<<"bytes ">>, <<"*">>, <<"/">>, <<"42">>], + ?assertMatch(Expected, elli_util:encode_range(invalid_range, 42)). register_test() -> - ?assertMatch(undefined, whereis(elli)), - Config = [ - {name, {local, elli}}, - {callback, elli_middleware}, - {callback_args, [{mods, [ - elli_example_callback, - elli_metrics_middleware - ]}]} - ], - {ok, Pid} = elli:start_link(Config), - ?assertMatch(Pid, whereis(elli)), - ok. + ?assertMatch(undefined, whereis(elli)), + Config = [ + {name, {local, elli}}, + {callback, elli_middleware}, + {callback_args, [{mods, [ + elli_example_callback, + elli_metrics_middleware + ]}]} + ], + {ok, Pid} = elli:start_link(Config), + ?assertMatch(Pid, whereis(elli)), + ok. invalid_callback_test() -> - case catch elli:start_link([{callback, elli}]) of - E -> - ?assertMatch(invalid_callback, E) - end. + case catch elli:start_link([{callback, elli}]) of + E -> + ?assertMatch(invalid_callback, E) + end. diff --git a/src/wsSrv/elli.erl b/src/wsSrv/elli.erl index 9f233ce..8f7630f 100644 --- a/src/wsSrv/elli.erl +++ b/src/wsSrv/elli.erl @@ -12,17 +12,17 @@ %% API -export([start_link/0, - start_link/1, - stop/1, - get_acceptors/1, - get_open_reqs/1, - get_open_reqs/2, - set_callback/3 - ]). + start_link/1, + stop/1, + get_acceptors/1, + get_open_reqs/1, + get_open_reqs/2, + set_callback/3 +]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). + terminate/2, code_change/3]). -export_type([req/0, http_method/0, body/0, headers/0, response_code/0]). @@ -32,22 +32,22 @@ %% @type http_method(). An uppercase atom representing a known HTTP verb or a %% binary for other verbs. -type http_method() :: 'OPTIONS' | 'GET' | 'HEAD' | 'POST' - | 'PUT' | 'DELETE' | 'TRACE' | binary(). +| 'PUT' | 'DELETE' | 'TRACE' | binary(). %% @type body(). A binary or iolist. -type body() :: binary() | iolist(). --type header() :: {Key::binary(), Value::binary() | string()}. +-type header() :: {Key :: binary(), Value :: binary() | string()}. -type headers() :: [header()]. -type response_code() :: 100..999. --record(state, {socket :: elli_tcp:socket(), - acceptors :: ets:tid(), - open_reqs = 0 :: non_neg_integer(), - options = [] :: [{_, _}], % TODO: refine - callback :: elli_handler:callback() - }). +-record(state, {socket :: elli_tcp:socket(), + acceptors :: ets:tid(), + open_reqs = 0 :: non_neg_integer(), + options = [] :: [{_, _}], % TODO: refine + callback :: elli_handler:callback() +}). %% @type state(). Internal state. -opaque state() :: #state{}. -export_type([state/0]). @@ -58,57 +58,57 @@ %%%=================================================================== -spec start_link() -> Result when - Result :: {ok, Pid} | ignore | {error, Error}, - Pid :: pid(), - Error :: {already_started, Pid} | term(). + Result :: {ok, Pid} | ignore | {error, Error}, + Pid :: pid(), + Error :: {already_started, Pid} | term(). %% @equiv start_link({@EXAMPLE_CONF}) %% @doc Create an Elli server process as part of a supervision tree, using the %% default configuration. start_link() -> start_link(?EXAMPLE_CONF). -spec start_link(Opts) -> Result when - Opts :: [{_, _}], % TODO: refine - Result :: {ok, Pid} | ignore | {error, Error}, - Pid :: pid(), - Error :: {already_started, Pid} | term(). + Opts :: [{_, _}], % TODO: refine + Result :: {ok, Pid} | ignore | {error, Error}, + Pid :: pid(), + Error :: {already_started, Pid} | term(). start_link(Opts) -> - valid_callback(required_opt(callback, Opts)) - orelse throw(invalid_callback), + valid_callback(required_opt(callback, Opts)) + orelse throw(invalid_callback), - case proplists:get_value(name, Opts) of - undefined -> - gen_server:start_link(?MODULE, [Opts], []); - Name -> - gen_server:start_link(Name, ?MODULE, [Opts], []) - end. + case proplists:get_value(name, Opts) of + undefined -> + gen_server:start_link(?MODULE, [Opts], []); + Name -> + gen_server:start_link(Name, ?MODULE, [Opts], []) + end. -spec get_acceptors(atom()) -> {reply, {ok, [ets:tid()]}, state()}. get_acceptors(S) -> - gen_server:call(S, get_acceptors). + gen_server:call(S, get_acceptors). -spec get_open_reqs(S :: atom()) -> {reply, {ok, non_neg_integer()}, state()}. %% @equiv get_open_reqs(S, 5000) get_open_reqs(S) -> - get_open_reqs(S, 5000). + get_open_reqs(S, 5000). -spec get_open_reqs(S :: atom(), Timeout :: non_neg_integer()) -> Reply when - Reply :: {reply, {ok, non_neg_integer()}, state()}. + Reply :: {reply, {ok, non_neg_integer()}, state()}. get_open_reqs(S, Timeout) -> - gen_server:call(S, get_open_reqs, Timeout). + gen_server:call(S, get_open_reqs, Timeout). -spec set_callback(S, Callback, CallbackArgs) -> Reply when - S :: atom(), - Callback :: elli_handler:callback_mod(), - CallbackArgs :: elli_handler:callback_args(), - Reply :: {reply, ok, state()}. + S :: atom(), + Callback :: elli_handler:callback_mod(), + CallbackArgs :: elli_handler:callback_args(), + Reply :: {reply, ok, state()}. set_callback(S, Callback, CallbackArgs) -> - valid_callback(Callback) orelse throw(invalid_callback), - gen_server:call(S, {set_callback, Callback, CallbackArgs}). + valid_callback(Callback) orelse throw(invalid_callback), + gen_server:call(S, {set_callback, Callback, CallbackArgs}). %% @doc Stop `Server'. -spec stop(Server :: atom()) -> {stop, normal, ok, state()}. stop(S) -> - gen_server:call(S, stop). + gen_server:call(S, stop). %%%=================================================================== @@ -118,126 +118,126 @@ stop(S) -> %% @hidden -spec init([Opts :: [{_, _}]]) -> {ok, state()}. init([Opts]) -> - %% Use the exit signal from the acceptor processes to know when - %% they exit - process_flag(trap_exit, true), - - Callback = required_opt(callback, Opts), - CallbackArgs = proplists:get_value(callback_args, Opts), - IPAddress = proplists:get_value(ip, Opts, {0, 0, 0, 0}), - Port = proplists:get_value(port, Opts, 8080), - MinAcceptors = proplists:get_value(min_acceptors, Opts, 20), - - UseSSL = proplists:get_value(ssl, Opts, false), - KeyFile = proplists:get_value(keyfile, Opts), - CertFile = proplists:get_value(certfile, Opts), - SockType = ?IF(UseSSL, ssl, plain), - SSLSockOpts = ?IF(UseSSL, - [{keyfile, KeyFile}, {certfile, CertFile}], - []), - - AcceptTimeout = proplists:get_value(accept_timeout, Opts, 10000), - RequestTimeout = proplists:get_value(request_timeout, Opts, 60000), - HeaderTimeout = proplists:get_value(header_timeout, Opts, 10000), - BodyTimeout = proplists:get_value(body_timeout, Opts, 30000), - MaxBodySize = proplists:get_value(max_body_size, Opts, 1024000), - - Options = [{accept_timeout, AcceptTimeout}, - {request_timeout, RequestTimeout}, - {header_timeout, HeaderTimeout}, - {body_timeout, BodyTimeout}, - {max_body_size, MaxBodySize}], - - %% Notify the handler that we are about to start accepting - %% requests, so it can create necessary supporting processes, ETS - %% tables, etc. - ok = Callback:handle_event(elli_startup, [], CallbackArgs), - - {ok, Socket} = elli_tcp:listen(SockType, Port, [binary, - {ip, IPAddress}, - {reuseaddr, true}, - {backlog, 32768}, - {packet, raw}, - {active, false} - | SSLSockOpts]), - - Acceptors = ets:new(acceptors, [private, set]), - [begin - Pid = elli_http:start_link(self(), Socket, Options, - {Callback, CallbackArgs}), - ets:insert(Acceptors, {Pid}) - end - || _ <- lists:seq(1, MinAcceptors)], - - {ok, #state{socket = Socket, - acceptors = Acceptors, - open_reqs = 0, - options = Options, - callback = {Callback, CallbackArgs}}}. + %% Use the exit signal from the acceptor processes to know when + %% they exit + process_flag(trap_exit, true), + + Callback = required_opt(callback, Opts), + CallbackArgs = proplists:get_value(callback_args, Opts), + IPAddress = proplists:get_value(ip, Opts, {0, 0, 0, 0}), + Port = proplists:get_value(port, Opts, 8080), + MinAcceptors = proplists:get_value(min_acceptors, Opts, 20), + + UseSSL = proplists:get_value(ssl, Opts, false), + KeyFile = proplists:get_value(keyfile, Opts), + CertFile = proplists:get_value(certfile, Opts), + SockType = ?IF(UseSSL, ssl, plain), + SSLSockOpts = ?IF(UseSSL, + [{keyfile, KeyFile}, {certfile, CertFile}], + []), + + AcceptTimeout = proplists:get_value(accept_timeout, Opts, 10000), + RequestTimeout = proplists:get_value(request_timeout, Opts, 60000), + HeaderTimeout = proplists:get_value(header_timeout, Opts, 10000), + BodyTimeout = proplists:get_value(body_timeout, Opts, 30000), + MaxBodySize = proplists:get_value(max_body_size, Opts, 1024000), + + Options = [{accept_timeout, AcceptTimeout}, + {request_timeout, RequestTimeout}, + {header_timeout, HeaderTimeout}, + {body_timeout, BodyTimeout}, + {max_body_size, MaxBodySize}], + + %% Notify the handler that we are about to start accepting + %% requests, so it can create necessary supporting processes, ETS + %% tables, etc. + ok = Callback:handle_event(elli_startup, [], CallbackArgs), + + {ok, Socket} = elli_tcp:listen(SockType, Port, [binary, + {ip, IPAddress}, + {reuseaddr, true}, + {backlog, 32768}, + {packet, raw}, + {active, false} + | SSLSockOpts]), + + Acceptors = ets:new(acceptors, [private, set]), + [begin + Pid = elli_http:start_link(self(), Socket, Options, + {Callback, CallbackArgs}), + ets:insert(Acceptors, {Pid}) + end + || _ <- lists:seq(1, MinAcceptors)], + + {ok, #state{socket = Socket, + acceptors = Acceptors, + open_reqs = 0, + options = Options, + callback = {Callback, CallbackArgs}}}. %% @hidden -spec handle_call(get_acceptors, {pid(), _Tag}, state()) -> - {reply, {ok, [ets:tid()]}, state()}; - (get_open_reqs, {pid(), _Tag}, state()) -> - {reply, {ok, OpenReqs :: non_neg_integer()}, state()}; - (stop, {pid(), _Tag}, state()) -> {stop, normal, ok, state()}; - ({set_callback, Mod, Args}, {pid(), _Tag}, state()) -> - {reply, ok, state()} when - Mod :: elli_handler:callback_mod(), - Args :: elli_handler:callback_args(). + {reply, {ok, [ets:tid()]}, state()}; + (get_open_reqs, {pid(), _Tag}, state()) -> + {reply, {ok, OpenReqs :: non_neg_integer()}, state()}; + (stop, {pid(), _Tag}, state()) -> {stop, normal, ok, state()}; + ({set_callback, Mod, Args}, {pid(), _Tag}, state()) -> + {reply, ok, state()} when + Mod :: elli_handler:callback_mod(), + Args :: elli_handler:callback_args(). handle_call(get_acceptors, _From, State) -> - Acceptors = [Pid || {Pid} <- ets:tab2list(State#state.acceptors)], - {reply, {ok, Acceptors}, State}; + Acceptors = [Pid || {Pid} <- ets:tab2list(State#state.acceptors)], + {reply, {ok, Acceptors}, State}; handle_call(get_open_reqs, _From, State) -> - {reply, {ok, State#state.open_reqs}, State}; + {reply, {ok, State#state.open_reqs}, State}; handle_call({set_callback, Callback, CallbackArgs}, _From, State) -> - ok = Callback:handle_event(elli_reconfigure, [], CallbackArgs), - {reply, ok, State#state{callback = {Callback, CallbackArgs}}}; + ok = Callback:handle_event(elli_reconfigure, [], CallbackArgs), + {reply, ok, State#state{callback = {Callback, CallbackArgs}}}; handle_call(stop, _From, State) -> - {stop, normal, ok, State}. + {stop, normal, ok, State}. %% @hidden -spec handle_cast(accepted | _Msg, State0) -> {noreply, State1} when - State0 :: state(), - State1 :: state(). + State0 :: state(), + State1 :: state(). handle_cast(accepted, State) -> - {noreply, start_add_acceptor(State)}; + {noreply, start_add_acceptor(State)}; handle_cast(_Msg, State) -> - {noreply, State}. + {noreply, State}. %% @hidden -spec handle_info({'EXIT', _Pid, Reason}, State0) -> Result when - State0 :: state(), - Reason :: {error, emfile}, - Result :: {stop, emfile, State0} - | {noreply, State1 :: state()}. + State0 :: state(), + Reason :: {error, emfile}, + Result :: {stop, emfile, State0} + | {noreply, State1 :: state()}. handle_info({'EXIT', _Pid, {error, emfile}}, State) -> - ?LOG_ERROR("No more file descriptors, shutting down~n"), - {stop, emfile, State}; + ?LOG_ERROR("No more file descriptors, shutting down~n"), + {stop, emfile, State}; handle_info({'EXIT', Pid, normal}, State) -> - {noreply, remove_acceptor(State, Pid)}; + {noreply, remove_acceptor(State, Pid)}; handle_info({'EXIT', Pid, Reason}, State) -> - ?LOG_ERROR("Elli request (pid ~p) unexpectedly crashed:~n~p~n", [Pid, Reason]), - {noreply, remove_acceptor(State, Pid)}. + ?LOG_ERROR("Elli request (pid ~p) unexpectedly crashed:~n~p~n", [Pid, Reason]), + {noreply, remove_acceptor(State, Pid)}. %% @hidden -spec terminate(_Reason, _State) -> ok. terminate(_Reason, _State) -> - ok. + ok. %% @hidden -spec code_change(_OldVsn, State, _Extra) -> {ok, State} when State :: state(). code_change(_OldVsn, State, _Extra) -> - {ok, State}. + {ok, State}. %%%=================================================================== %%% Internal functions @@ -245,41 +245,41 @@ code_change(_OldVsn, State, _Extra) -> -spec remove_acceptor(State0 :: state(), Pid :: pid()) -> State1 :: state(). remove_acceptor(State, Pid) -> - ets:delete(State#state.acceptors, Pid), - dec_open_reqs(State). + ets:delete(State#state.acceptors, Pid), + dec_open_reqs(State). -spec start_add_acceptor(State0 :: state()) -> State1 :: state(). start_add_acceptor(State) -> - Pid = elli_http:start_link(self(), State#state.socket, - State#state.options, State#state.callback), - add_acceptor(State, Pid). + Pid = elli_http:start_link(self(), State#state.socket, + State#state.options, State#state.callback), + add_acceptor(State, Pid). -spec add_acceptor(State0 :: state(), Pid :: pid()) -> State1 :: state(). add_acceptor(#state{acceptors = As} = State, Pid) -> - ets:insert(As, {Pid}), - inc_open_reqs(State). + ets:insert(As, {Pid}), + inc_open_reqs(State). -spec required_opt(Name, Opts) -> Value when - Name :: any(), - Opts :: proplists:proplist(), - Value :: term(). + Name :: any(), + Opts :: proplists:proplist(), + Value :: term(). required_opt(Name, Opts) -> - case proplists:get_value(Name, Opts) of - undefined -> - throw(badarg); - Value -> - Value - end. + case proplists:get_value(Name, Opts) of + undefined -> + throw(badarg); + Value -> + Value + end. -spec valid_callback(Mod :: module()) -> Exported :: boolean(). valid_callback(Mod) -> - lists:member({handle, 2}, Mod:module_info(exports)) andalso - lists:member({handle_event, 3}, Mod:module_info(exports)). + lists:member({handle, 2}, Mod:module_info(exports)) andalso + lists:member({handle_event, 3}, Mod:module_info(exports)). -spec dec_open_reqs(State0 :: state()) -> State1 :: state(). dec_open_reqs(#state{open_reqs = OpenReqs} = State) -> - State#state{open_reqs = OpenReqs - 1}. + State#state{open_reqs = OpenReqs - 1}. -spec inc_open_reqs(State0 :: state()) -> State1 :: state(). inc_open_reqs(#state{open_reqs = OpenReqs} = State) -> - State#state{open_reqs = OpenReqs + 1}. + State#state{open_reqs = OpenReqs + 1}. diff --git a/src/wsSrv/elli_example_callback.erl b/src/wsSrv/elli_example_callback.erl index 7eda4b8..f977ca6 100644 --- a/src/wsSrv/elli_example_callback.erl +++ b/src/wsSrv/elli_example_callback.erl @@ -24,13 +24,12 @@ %% Delegate to our handler function. %% @see handle/3 -spec handle(Req, _Args) -> Result when - Req :: elli:req(), - _Args :: elli_handler:callback_args(), - Result :: elli_handler:result(). + Req :: elli:req(), + _Args :: elli_handler:callback_args(), + Result :: elli_handler:result(). handle(Req, _Args) -> handle(Req#req.method, elli_request:path(Req), Req). - %% @doc Route `Method' and `Path' to the appropriate clause. %% %% `ok' can be used instead of `200' to signal success. @@ -64,172 +63,171 @@ handle(Req, _Args) -> handle(Req#req.method, elli_request:path(Req), Req). %% @see elli_request:chunk_ref/1 %% @see chunk_loop/1 -spec handle(Method, Path, Req) -> elli_handler:result() when - Method :: elli:http_method(), - Path :: [binary()], - Req :: elli:req(). + Method :: elli:http_method(), + Path :: [binary()], + Req :: elli:req(). handle('GET', [<<"hello">>, <<"world">>], _Req) -> - %% Reply with a normal response. - timer:sleep(1000), - {ok, [], <<"Hello World!">>}; + %% Reply with a normal response. + timer:sleep(1000), + {ok, [], <<"Hello World!">>}; handle('GET', [<<"hello">>], Req) -> - %% Fetch a GET argument from the URL. - Name = elli_request:get_arg(<<"name">>, Req, <<"undefined">>), - {ok, [], <<"Hello ", Name/binary>>}; + %% Fetch a GET argument from the URL. + Name = elli_request:get_arg(<<"name">>, Req, <<"undefined">>), + {ok, [], <<"Hello ", Name/binary>>}; handle('POST', [<<"hello">>], Req) -> - %% Fetch a POST argument from the POST body. - Name = elli_request:post_arg(<<"name">>, Req, <<"undefined">>), - %% Fetch and decode - City = elli_request:post_arg_decoded(<<"city">>, Req, <<"undefined">>), - {ok, [], <<"Hello ", Name/binary, " of ", City/binary>>}; + %% Fetch a POST argument from the POST body. + Name = elli_request:post_arg(<<"name">>, Req, <<"undefined">>), + %% Fetch and decode + City = elli_request:post_arg_decoded(<<"city">>, Req, <<"undefined">>), + {ok, [], <<"Hello ", Name/binary, " of ", City/binary>>}; handle('GET', [<<"hello">>, <<"iolist">>], Req) -> - %% Iolists will be kept as iolists all the way to the socket. - Name = elli_request:get_arg(<<"name">>, Req), - {ok, [], [<<"Hello ">>, Name]}; + %% Iolists will be kept as iolists all the way to the socket. + Name = elli_request:get_arg(<<"name">>, Req), + {ok, [], [<<"Hello ">>, Name]}; handle('GET', [<<"type">>], Req) -> - Name = elli_request:get_arg(<<"name">>, Req), - %% Fetch a header. - case elli_request:get_header(<<"Accept">>, Req, <<"text/plain">>) of - <<"text/plain">> -> - {ok, [{<<"content-type">>, <<"text/plain; charset=ISO-8859-1">>}], - <<"name: ", Name/binary>>}; - <<"application/json">> -> - {ok, [{<<"content-type">>, - <<"application/json; charset=ISO-8859-1">>}], - <<"{\"name\" : \"", Name/binary, "\"}">>} - end; + Name = elli_request:get_arg(<<"name">>, Req), + %% Fetch a header. + case elli_request:get_header(<<"Accept">>, Req, <<"text/plain">>) of + <<"text/plain">> -> + {ok, [{<<"content-type">>, <<"text/plain; charset=ISO-8859-1">>}], + <<"name: ", Name/binary>>}; + <<"application/json">> -> + {ok, [{<<"content-type">>, + <<"application/json; charset=ISO-8859-1">>}], + <<"{\"name\" : \"", Name/binary, "\"}">>} + end; handle('GET', [<<"headers.html">>], _Req) -> - %% Set custom headers, for example 'Content-Type' - {ok, [{<<"X-Custom">>, <<"foobar">>}], <<"see headers">>}; + %% 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) -> - {304, [{<<"Connection">>, <<"close">>}, - {<<"Content-Length">>, <<"123">>}], <<"ignored">>}; + {304, [{<<"Connection">>, <<"close">>}, + {<<"Content-Length">>, <<"123">>}], <<"ignored">>}; handle('GET', [<<"user">>, <<"content-length">>], _Req) -> - {200, [{<<"Content-Length">>, 123}], <<"foobar">>}; + {200, [{<<"Content-Length">>, 123}], <<"foobar">>}; handle('GET', [<<"crash">>], _Req) -> - %% Throwing an exception results in a 500 response and - %% request_throw being called - throw(foobar); + %% Throwing an exception results in a 500 response and + %% request_throw being called + throw(foobar); handle('GET', [<<"decoded-hello">>], Req) -> - %% Fetch a URI decoded GET argument from the URL. - Name = elli_request:get_arg_decoded(<<"name">>, Req, <<"undefined">>), - {ok, [], <<"Hello ", Name/binary>>}; + %% Fetch a URI decoded GET argument from the URL. + Name = elli_request:get_arg_decoded(<<"name">>, Req, <<"undefined">>), + {ok, [], <<"Hello ", Name/binary>>}; handle('GET', [<<"decoded-list">>], Req) -> - %% Fetch a URI decoded GET argument from the URL. - [{<<"name">>, Name}, {<<"foo">>, true}] = - elli_request:get_args_decoded(Req), - {ok, [], <<"Hello ", Name/binary>>}; + %% Fetch a URI decoded GET argument from the URL. + [{<<"name">>, Name}, {<<"foo">>, true}] = + elli_request:get_args_decoded(Req), + {ok, [], <<"Hello ", Name/binary>>}; handle('GET', [<<"sendfile">>], _Req) -> - %% Returning {file, "/path/to/file"} instead of the body results - %% in Elli using sendfile. - F = "README.md", - {ok, [], {file, F}}; + %% 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) -> - %% Returning {file, "/path/to/file"} instead of the body results - %% in Elli using sendfile. - F = "README", - {ok, [], {file, F}}; + %% Returning {file, "/path/to/file"} instead of the body results + %% in Elli using sendfile. + F = "README", + {ok, [], {file, F}}; handle('GET', [<<"sendfile">>, <<"error">>], _Req) -> - F = "test", - {ok, [], {file, F}}; + F = "test", + {ok, [], {file, F}}; handle('GET', [<<"sendfile">>, <<"range">>], Req) -> - %% 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, elli_request:get_range(Req)}}; + %% 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, elli_request:get_range(Req)}}; handle('GET', [<<"compressed">>], _Req) -> - %% Body with a byte size over 1024 are automatically gzipped by - %% elli_middleware_compress - {ok, binary:copy(<<"Hello World!">>, 86)}; + %% 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) -> - %% Body with a iolist size over 1024 are automatically gzipped by - %% elli_middleware_compress - {ok, lists:duplicate(86, [<<"Hello World!">>])}; + %% Body with a iolist size over 1024 are automatically gzipped by + %% elli_middleware_compress + {ok, lists:duplicate(86, [<<"Hello World!">>])}; handle('HEAD', [<<"head">>], _Req) -> - {200, [], <<"body must be ignored">>}; + {200, [], <<"body must be ignored">>}; handle('GET', [<<"chunked">>], Req) -> - %% Start a chunked response for streaming real-time events to the - %% browser. - %% - %% Calling elli_request:send_chunk(ChunkRef, Body) will send that - %% part to the client. elli_request:close_chunk(ChunkRef) will - %% close the response. - %% - %% Return immediately {chunk, Headers} to signal we want to chunk. - Ref = elli_request:chunk_ref(Req), - spawn(fun() -> ?MODULE:chunk_loop(Ref) end), - {chunk, [{<<"Content-Type">>, <<"text/event-stream">>}]}; + %% Start a chunked response for streaming real-time events to the + %% browser. + %% + %% Calling elli_request:send_chunk(ChunkRef, Body) will send that + %% part to the client. elli_request:close_chunk(ChunkRef) will + %% close the response. + %% + %% Return immediately {chunk, Headers} to signal we want to chunk. + Ref = elli_request:chunk_ref(Req), + spawn(fun() -> ?MODULE:chunk_loop(Ref) end), + {chunk, [{<<"Content-Type">>, <<"text/event-stream">>}]}; handle('GET', [<<"shorthand">>], _Req) -> - {200, <<"hello">>}; + {200, <<"hello">>}; handle('GET', [<<"ip">>], Req) -> - {<<"200 OK">>, elli_request:peer(Req)}; + {<<"200 OK">>, elli_request:peer(Req)}; handle('GET', [<<"304">>], _Req) -> - %% 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">>}; + %% 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) -> - {302, [{<<"Location">>, <<"/hello/world">>}], <<>>}; + {302, [{<<"Location">>, <<"/hello/world">>}], <<>>}; handle('GET', [<<"403">>], _Req) -> - %% Exceptions formatted as return codes can be used to - %% short-circuit a response, for example in case of - %% authentication/authorization - throw({403, [], <<"Forbidden">>}); + %% 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) -> - {invalid_return}; + {invalid_return}; handle(_, _, _Req) -> - {404, [], <<"Not Found">>}. - + {404, [], <<"Not Found">>}. %% @doc Send 10 separate chunks to the client. %% @equiv chunk_loop(Ref, 10) chunk_loop(Ref) -> - chunk_loop(Ref, 10). + chunk_loop(Ref, 10). %% @doc If `N > 0', send a chunk to the client, checking for errors, %% as the user might have disconnected. %% When `N == 0', call {@link elli_request:close_chunk/1. %% elli_request:close_chunk(Ref)}. chunk_loop(Ref, 0) -> - elli_request:close_chunk(Ref); + elli_request:close_chunk(Ref); chunk_loop(Ref, N) -> - timer:sleep(10), + timer:sleep(10), - case elli_request:send_chunk(Ref, [<<"chunk">>, integer_to_binary(N)]) of - ok -> ok; - {error, Reason} -> ?LOG_ERROR("error in sending chunk: ~p~n", [Reason]) - end, + case elli_request:send_chunk(Ref, [<<"chunk">>, integer_to_binary(N)]) of + ok -> ok; + {error, Reason} -> ?LOG_ERROR("error in sending chunk: ~p~n", [Reason]) + end, - chunk_loop(Ref, N-1). + chunk_loop(Ref, N - 1). %% @@ -297,13 +295,13 @@ chunk_loop(Ref, N) -> %% `file_error' is sent when the user wants to return a file as a %% response, but for some reason it cannot be opened. -spec handle_event(Event, Args, Config) -> ok when - Event :: elli:event(), - Args :: elli_handler:callback_args(), - Config :: [tuple()]. + Event :: elli:event(), + Args :: elli_handler:callback_args(), + Config :: [tuple()]. handle_event(elli_startup, [], _) -> ok; handle_event(request_complete, [_Request, - _ResponseCode, _ResponseHeaders, _ResponseBody, - {_Timings, _Sizes}], _) -> ok; + _ResponseCode, _ResponseHeaders, _ResponseBody, + {_Timings, _Sizes}], _) -> ok; handle_event(request_throw, [_Request, _Exception, _Stacktrace], _) -> ok; handle_event(request_error, [_Request, _Exception, _Stacktrace], _) -> ok; handle_event(request_exit, [_Request, _Exception, _Stacktrace], _) -> ok; @@ -311,8 +309,8 @@ handle_event(request_exit, [_Request, _Exception, _Stacktrace], _) -> ok; handle_event(invalid_return, [_Request, _ReturnValue], _) -> ok; handle_event(chunk_complete, [_Request, - _ResponseCode, _ResponseHeaders, _ClosingEnd, - {_Timings, _Sizes}], _) -> ok; + _ResponseCode, _ResponseHeaders, _ClosingEnd, + {_Timings, _Sizes}], _) -> ok; handle_event(request_closed, [], _) -> ok; diff --git a/src/wsSrv/elli_example_callback_handover.erl b/src/wsSrv/elli_example_callback_handover.erl index 052f43e..896a73e 100644 --- a/src/wsSrv/elli_example_callback_handover.erl +++ b/src/wsSrv/elli_example_callback_handover.erl @@ -7,36 +7,36 @@ %% @doc Return `{ok, handover}' if `Req''s path is `/hello/world', %% otherwise `ignore'. init(Req, _Args) -> - case elli_request:path(Req) of - [<<"hello">>, <<"world">>] -> - {ok, handover}; - _ -> - ignore - end. + case elli_request:path(Req) of + [<<"hello">>, <<"world">>] -> + {ok, handover}; + _ -> + ignore + end. %% TODO: write docstring -spec handle(Req, Args) -> Result when - Req :: elli:req(), - Args :: elli_handler:callback_args(), - Result :: elli_handler:result(). + Req :: elli:req(), + Args :: elli_handler:callback_args(), + Result :: elli_handler:result(). handle(Req, Args) -> - handle(elli_request:method(Req), elli_request:path(Req), Req, Args). + handle(elli_request:method(Req), elli_request:path(Req), Req, Args). handle('GET', [<<"hello">>, <<"world">>], Req, _Args) -> - Body = <<"Hello World!">>, - Size = integer_to_binary(size(Body)), - Headers = [{"Connection", "close"}, {"Content-Length", Size}], - elli_http:send_response(Req, 200, Headers, Body), - {close, <<>>}; + Body = <<"Hello World!">>, + Size = integer_to_binary(size(Body)), + Headers = [{"Connection", "close"}, {"Content-Length", Size}], + elli_http:send_response(Req, 200, Headers, Body), + {close, <<>>}; handle('GET', [<<"hello">>], Req, _Args) -> - %% Fetch a GET argument from the URL. - Name = elli_request:get_arg(<<"name">>, Req, <<"undefined">>), - {ok, [], <<"Hello ", Name/binary>>}. + %% Fetch a GET argument from the URL. + Name = elli_request:get_arg(<<"name">>, Req, <<"undefined">>), + {ok, [], <<"Hello ", Name/binary>>}. %% @hidden handle_event(_, _, _) -> - ok. + ok. diff --git a/src/wsSrv/elli_example_middleware.erl b/src/wsSrv/elli_example_middleware.erl index aec9af8..2f1baf8 100644 --- a/src/wsSrv/elli_example_middleware.erl +++ b/src/wsSrv/elli_example_middleware.erl @@ -9,12 +9,12 @@ %% handle(Req, _Args) -> - do_handle(elli_request:path(Req)). + do_handle(elli_request:path(Req)). do_handle([<<"middleware">>, <<"short-circuit">>]) -> - {200, [], <<"short circuit!">>}; + {200, [], <<"short circuit!">>}; do_handle(_) -> - ignore. + ignore. %% @@ -23,4 +23,4 @@ do_handle(_) -> handle_event(_Event, _Data, _Args) -> - ok. + ok. diff --git a/src/wsSrv/elli_handler.erl b/src/wsSrv/elli_handler.erl index 481b051..e264a89 100644 --- a/src/wsSrv/elli_handler.erl +++ b/src/wsSrv/elli_handler.erl @@ -9,7 +9,7 @@ -type callback() :: {callback_mod(), callback_args()}. %% @type callback_mod(). A callback module. --type callback_mod() :: module(). +-type callback_mod() :: module(). %% @type callback_args(). Arguments to pass to a {@type callback_mod()}. -type callback_args() :: list(). @@ -17,41 +17,41 @@ %% @type event(). Fired throughout processing a request. %% See {@link elli_example_callback:handle_event/3} for descriptions. -type event() :: elli_startup - | bad_request | file_error - | chunk_complete | request_complete - | request_throw | request_error | request_exit - | request_closed | request_parse_error - | client_closed | client_timeout - | invalid_return. +| bad_request | file_error +| chunk_complete | request_complete +| request_throw | request_error | request_exit +| request_closed | request_parse_error +| client_closed | client_timeout +| invalid_return. -type result() :: {elli:response_code() | ok, - elli:headers(), - {file, file:name_all()} - | {file, file:name_all(), elli_util:range()}} - | {elli:response_code() | ok, elli:headers(), elli:body()} - | {elli:response_code() | ok, elli:body()} - | {chunk, elli:headers()} - | {chunk, elli:headers(), elli:body()} - | ignore. + elli:headers(), + {file, file:name_all()} + | {file, file:name_all(), elli_util:range()}} +| {elli:response_code() | ok, elli:headers(), elli:body()} +| {elli:response_code() | ok, elli:body()} +| {chunk, elli:headers()} +| {chunk, elli:headers(), elli:body()} +| ignore. -callback handle(Req :: elli:req(), callback_args()) -> result(). -callback handle_event(Event, Args, Config) -> ok when - Event :: event(), - Args :: callback_args(), - Config :: [tuple()]. + Event :: event(), + Args :: callback_args(), + Config :: [tuple()]. -callback init(Req, Args) -> {ok, standard | handover} when - Req :: elli:req(), - Args :: callback_args(). + Req :: elli:req(), + Args :: callback_args(). -callback preprocess(Req1, Args) -> Req2 when - Req1 :: elli:req(), - Args :: callback_args(), - Req2 :: elli:req(). + Req1 :: elli:req(), + Args :: callback_args(), + Req2 :: elli:req(). -callback postprocess(Req, Res1, Args) -> Res2 when - Req :: elli:req(), - Res1 :: result(), - Args :: callback_args(), - Res2 :: result(). + Req :: elli:req(), + Res1 :: result(), + Args :: callback_args(), + Res2 :: result(). diff --git a/src/wsSrv/elli_http.erl b/src/wsSrv/elli_http.erl index 4404a76..e2a746f 100644 --- a/src/wsSrv/elli_http.erl +++ b/src/wsSrv/elli_http.erl @@ -19,7 +19,7 @@ %% Exported for looping with a fully-qualified module name -export([accept/4, handle_request/4, chunk_loop/1, split_args/1, - parse_path/1, keepalive_loop/3, keepalive_loop/5]). + parse_path/1, keepalive_loop/3, keepalive_loop/5]). %% Exported for correctly handling session keep-alive for handlers %% operating in handler mode. @@ -40,305 +40,305 @@ %% -type connection_token() :: keep_alive | close. -spec start_link(Server, ListenSocket, Options, Callback) -> pid() when - Server :: pid(), - ListenSocket :: elli_tcp:socket(), - Options :: proplists:proplist(), - Callback :: elli_handler:callback(). + Server :: pid(), + ListenSocket :: elli_tcp:socket(), + Options :: proplists:proplist(), + Callback :: elli_handler:callback(). start_link(Server, ListenSocket, Options, Callback) -> - proc_lib:spawn_link(?MODULE, accept, - [Server, ListenSocket, Options, Callback]). + proc_lib:spawn_link(?MODULE, accept, + [Server, ListenSocket, Options, Callback]). %% @doc Accept on the socket until a client connects. %% Handle the request, then loop if we're using keep alive or chunked transfer. %% If {@link elli_tcp:accept/3} doesn't return a socket within a configurable %% timeout, loop to allow code upgrades of this module. -spec accept(Server, ListenSocket, Options, Callback) -> ok when - Server :: pid(), - ListenSocket :: elli_tcp:socket(), - Options :: proplists:proplist(), - Callback :: elli_handler:callback(). + Server :: pid(), + ListenSocket :: elli_tcp:socket(), + Options :: proplists:proplist(), + Callback :: elli_handler:callback(). accept(Server, ListenSocket, Options, Callback) -> - case catch elli_tcp:accept(ListenSocket, Server, accept_timeout(Options)) of - {ok, Socket} -> - t(accepted), - ?MODULE:keepalive_loop(Socket, Options, Callback); - {error, timeout} -> - ?MODULE:accept(Server, ListenSocket, Options, Callback); - {error, econnaborted} -> - ?MODULE:accept(Server, ListenSocket, Options, Callback); - {error, {tls_alert, _}} -> - ?MODULE:accept(Server, ListenSocket, Options, Callback); - {error, closed} -> - ok; - {error, Other} -> - exit({error, Other}) - end. + case catch elli_tcp:accept(ListenSocket, Server, accept_timeout(Options)) of + {ok, Socket} -> + t(accepted), + ?MODULE:keepalive_loop(Socket, Options, Callback); + {error, timeout} -> + ?MODULE:accept(Server, ListenSocket, Options, Callback); + {error, econnaborted} -> + ?MODULE:accept(Server, ListenSocket, Options, Callback); + {error, {tls_alert, _}} -> + ?MODULE:accept(Server, ListenSocket, Options, Callback); + {error, closed} -> + ok; + {error, Other} -> + exit({error, Other}) + end. %% @doc Handle multiple requests on the same connection, i.e. `"keep alive"'. keepalive_loop(Socket, Options, Callback) -> - keepalive_loop(Socket, 0, <<>>, Options, Callback). + keepalive_loop(Socket, 0, <<>>, Options, Callback). keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) -> - case ?MODULE:handle_request(Socket, Buffer, Options, Callback) of - {keep_alive, NewBuffer} -> - ?MODULE:keepalive_loop(Socket, NumRequests + 1, - NewBuffer, Options, Callback); - {close, _} -> - elli_tcp:close(Socket), - ok - end. + case ?MODULE:handle_request(Socket, Buffer, Options, Callback) of + {keep_alive, NewBuffer} -> + ?MODULE:keepalive_loop(Socket, NumRequests + 1, + NewBuffer, Options, Callback); + {close, _} -> + elli_tcp:close(Socket), + ok + end. %% @doc Handle a HTTP request that will possibly come on the socket. %% Returns the appropriate connection token and any buffer containing (parts of) %% the next request. -spec handle_request(Socket, PrevBin, Options, Callback) -> ConnToken when - Socket :: elli_tcp:socket(), - PrevBin :: binary(), - Options :: proplists:proplist(), - Callback :: elli_handler:callback(), - ConnToken :: {'keep_alive' | 'close', binary()}. + Socket :: elli_tcp:socket(), + PrevBin :: binary(), + Options :: proplists:proplist(), + Callback :: elli_handler:callback(), + ConnToken :: {'keep_alive' | 'close', binary()}. handle_request(S, PrevB, Opts, {Mod, Args} = Callback) -> - {Method, RawPath, V, B0} = get_request(S, PrevB, Opts, Callback), - t(headers_start), - {{RequestHeaders, ParsedRequestHeaders}, B1} = get_headers(S, V, B0, Opts, Callback), - t(headers_end), - Req = mk_req(Method, RawPath, RequestHeaders, ParsedRequestHeaders, <<>>, V, S, Callback), - - case init(Req) of - {ok, standard} -> - t(body_start), - {RequestBody, B2} = get_body(S, ParsedRequestHeaders, B1, Opts, Callback), - t(body_end), - Req1 = Req#req{body = RequestBody}, - - t(user_start), - Response = execute_callback(Req1), - t(user_end), - - handle_response(Req1, B2, Response); - {ok, handover} -> - Req1 = Req#req{body = B1}, - - t(user_start), - Response = Mod:handle(Req1, Args), - t(user_end), - - t(request_end), - handle_event(Mod, request_complete, - [Req1, handover, [], <<>>, {get_timings(), - get_sizes()}], Args), - Response - end. + {Method, RawPath, V, B0} = get_request(S, PrevB, Opts, Callback), + t(headers_start), + {{RequestHeaders, ParsedRequestHeaders}, B1} = get_headers(S, V, B0, Opts, Callback), + t(headers_end), + Req = mk_req(Method, RawPath, RequestHeaders, ParsedRequestHeaders, <<>>, V, S, Callback), + + case init(Req) of + {ok, standard} -> + t(body_start), + {RequestBody, B2} = get_body(S, ParsedRequestHeaders, B1, Opts, Callback), + t(body_end), + Req1 = Req#req{body = RequestBody}, + + t(user_start), + Response = execute_callback(Req1), + t(user_end), + + handle_response(Req1, B2, Response); + {ok, handover} -> + Req1 = Req#req{body = B1}, + + t(user_start), + Response = Mod:handle(Req1, Args), + t(user_end), + + t(request_end), + handle_event(Mod, request_complete, + [Req1, handover, [], <<>>, {get_timings(), + get_sizes()}], Args), + Response + end. handle_response(Req, Buffer, {response, Code, UserHeaders, Body}) -> - #req{callback = {Mod, Args}} = Req, + #req{callback = {Mod, Args}} = Req, - Headers = [connection(Req, UserHeaders), - content_length(UserHeaders, Body) - | UserHeaders], - t(send_start), - send_response(Req, Code, Headers, Body), - t(send_end), + Headers = [connection(Req, UserHeaders), + content_length(UserHeaders, Body) + | UserHeaders], + t(send_start), + send_response(Req, Code, Headers, Body), + t(send_end), - t(request_end), - handle_event(Mod, request_complete, - [Req, Code, Headers, Body, {get_timings(), - get_sizes()}], Args), - {close_or_keepalive(Req, UserHeaders), Buffer}; + t(request_end), + handle_event(Mod, request_complete, + [Req, Code, Headers, Body, {get_timings(), + get_sizes()}], Args), + {close_or_keepalive(Req, UserHeaders), Buffer}; handle_response(Req, _Buffer, {chunk, UserHeaders, Initial}) -> - #req{callback = {Mod, Args}} = Req, + #req{callback = {Mod, Args}} = Req, - ResponseHeaders = [{?TRANSFER_ENCODING_HEADER, <<"chunked">>}, - connection(Req, UserHeaders) - | UserHeaders], - send_response(Req, 200, ResponseHeaders, <<"">>), + ResponseHeaders = [{?TRANSFER_ENCODING_HEADER, <<"chunked">>}, + connection(Req, UserHeaders) + | UserHeaders], + send_response(Req, 200, ResponseHeaders, <<"">>), - t(send_start), - Initial =:= <<"">> orelse send_chunk(Req#req.socket, Initial), - ClosingEnd = case start_chunk_loop(Req#req.socket) of - {error, client_closed} -> client; - ok -> server - end, - t(send_end), + t(send_start), + Initial =:= <<"">> orelse send_chunk(Req#req.socket, Initial), + ClosingEnd = case start_chunk_loop(Req#req.socket) of + {error, client_closed} -> client; + ok -> server + end, + t(send_end), - t(request_end), - handle_event(Mod, chunk_complete, - [Req, 200, ResponseHeaders, ClosingEnd, {get_timings(), - get_sizes()}], - Args), - {close, <<>>}; + t(request_end), + handle_event(Mod, chunk_complete, + [Req, 200, ResponseHeaders, ClosingEnd, {get_timings(), + get_sizes()}], + Args), + {close, <<>>}; handle_response(Req, Buffer, {file, ResponseCode, UserHeaders, - Filename, Range}) -> - #req{callback = {Mod, Args}} = Req, - - ResponseHeaders = [connection(Req, UserHeaders) | UserHeaders], - - case elli_util:file_size(Filename) of - {error, FileError} -> - handle_event(Mod, file_error, [FileError], Args), - send_server_error(Req#req.socket), - elli_tcp:close(Req#req.socket), - exit(normal); - Size -> - t(send_start), - case elli_util:normalize_range(Range, Size) of - undefined -> - send_file(Req, ResponseCode, - [{<<"Content-Length">>, Size} | - ResponseHeaders], - Filename, {0, 0}); - {Offset, Length} -> - ERange = elli_util:encode_range({Offset, Length}, Size), - send_file(Req, 206, - lists:append(ResponseHeaders, - [{<<"Content-Length">>, Length}, - {<<"Content-Range">>, ERange}]), - Filename, {Offset, Length}); - invalid_range -> - ERange = elli_util:encode_range(invalid_range, Size), - send_response(Req, 416, - lists:append(ResponseHeaders, - [{<<"Content-Length">>, 0}, - {<<"Content-Range">>, ERange}]), - []) - end, - t(send_end), - - t(request_end), - handle_event(Mod, request_complete, - [Req, ResponseCode, ResponseHeaders, <<>>, - {get_timings(), - get_sizes()}], - Args), - - {close_or_keepalive(Req, UserHeaders), Buffer} - end. + Filename, Range}) -> + #req{callback = {Mod, Args}} = Req, + + ResponseHeaders = [connection(Req, UserHeaders) | UserHeaders], + + case elli_util:file_size(Filename) of + {error, FileError} -> + handle_event(Mod, file_error, [FileError], Args), + send_server_error(Req#req.socket), + elli_tcp:close(Req#req.socket), + exit(normal); + Size -> + t(send_start), + case elli_util:normalize_range(Range, Size) of + undefined -> + send_file(Req, ResponseCode, + [{<<"Content-Length">>, Size} | + ResponseHeaders], + Filename, {0, 0}); + {Offset, Length} -> + ERange = elli_util:encode_range({Offset, Length}, Size), + send_file(Req, 206, + lists:append(ResponseHeaders, + [{<<"Content-Length">>, Length}, + {<<"Content-Range">>, ERange}]), + Filename, {Offset, Length}); + invalid_range -> + ERange = elli_util:encode_range(invalid_range, Size), + send_response(Req, 416, + lists:append(ResponseHeaders, + [{<<"Content-Length">>, 0}, + {<<"Content-Range">>, ERange}]), + []) + end, + t(send_end), + + t(request_end), + handle_event(Mod, request_complete, + [Req, ResponseCode, ResponseHeaders, <<>>, + {get_timings(), + get_sizes()}], + Args), + + {close_or_keepalive(Req, UserHeaders), Buffer} + end. %% @doc Generate a HTTP response and send it to the client. send_response(Req, Code, Headers, UserBody) -> - ResponseHeaders = assemble_response_headers(Code, Headers), - - Body = case {Req#req.method, Code} of - {'HEAD', _} -> <<>>; - {_, 304} -> <<>>; - {_, 204} -> <<>>; - _ -> UserBody - end, - s(resp_body, iolist_size(Body)), - - Response = [ResponseHeaders, - Body], - - case elli_tcp:send(Req#req.socket, Response) of - ok -> ok; - {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> - #req{callback = {Mod, Args}} = Req, - handle_event(Mod, client_closed, [before_response], Args), - ok - end. + ResponseHeaders = assemble_response_headers(Code, Headers), + + Body = case {Req#req.method, Code} of + {'HEAD', _} -> <<>>; + {_, 304} -> <<>>; + {_, 204} -> <<>>; + _ -> UserBody + end, + s(resp_body, iolist_size(Body)), + + Response = [ResponseHeaders, + Body], + + case elli_tcp:send(Req#req.socket, Response) of + ok -> ok; + {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> + #req{callback = {Mod, Args}} = Req, + handle_event(Mod, client_closed, [before_response], Args), + ok + end. %% @doc Send a HTTP response to the client where the body is the %% contents of the given file. Assumes correctly set response code %% and headers. -spec send_file(Req, Code, Headers, Filename, Range) -> ok when - Req :: elli:req(), - Code :: elli:response_code(), - Headers :: elli:headers(), - Filename :: file:filename(), - Range :: elli_util:range(). -send_file(#req{callback={Mod, Args}} = Req, Code, Headers, Filename, Range) -> - ResponseHeaders = assemble_response_headers(Code, Headers), - - case file:open(Filename, [read, raw, binary]) of - {ok, Fd} -> do_send_file(Fd, Range, Req, ResponseHeaders); - {error, FileError} -> - handle_event(Mod, file_error, [FileError], Args), - send_server_error(Req#req.socket), - elli_tcp:close(Req#req.socket), - exit(normal) - end, - ok. - -do_send_file(Fd, {Offset, Length}, #req{callback={Mod, Args}} = Req, Headers) -> - try elli_tcp:send(Req#req.socket, Headers) of - ok -> - case elli_tcp:sendfile(Fd, Req#req.socket, Offset, Length, []) of - {ok, BytesSent} -> s(file, BytesSent), ok; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - handle_event(Mod, client_closed, [before_response], Args) - end; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - handle_event(Mod, client_closed, [before_response], Args) - after - file:close(Fd) - end. + Req :: elli:req(), + Code :: elli:response_code(), + Headers :: elli:headers(), + Filename :: file:filename(), + Range :: elli_util:range(). +send_file(#req{callback = {Mod, Args}} = Req, Code, Headers, Filename, Range) -> + ResponseHeaders = assemble_response_headers(Code, Headers), + + case file:open(Filename, [read, raw, binary]) of + {ok, Fd} -> do_send_file(Fd, Range, Req, ResponseHeaders); + {error, FileError} -> + handle_event(Mod, file_error, [FileError], Args), + send_server_error(Req#req.socket), + elli_tcp:close(Req#req.socket), + exit(normal) + end, + ok. + +do_send_file(Fd, {Offset, Length}, #req{callback = {Mod, Args}} = Req, Headers) -> + try elli_tcp:send(Req#req.socket, Headers) of + ok -> + case elli_tcp:sendfile(Fd, Req#req.socket, Offset, Length, []) of + {ok, BytesSent} -> s(file, BytesSent), ok; + {error, Closed} when Closed =:= closed orelse + Closed =:= enotconn -> + handle_event(Mod, client_closed, [before_response], Args) + end; + {error, Closed} when Closed =:= closed orelse + Closed =:= enotconn -> + handle_event(Mod, client_closed, [before_response], Args) + after + file:close(Fd) + end. %% @doc To send a response, we must first have received everything the %% client is sending. If this is not the case, {@link send_bad_request/1} %% might reset the client connection. send_bad_request(Socket) -> - send_rescue_response(Socket, 400, <<"Bad Request">>). + send_rescue_response(Socket, 400, <<"Bad Request">>). send_server_error(Socket) -> - send_rescue_response(Socket, 500, <<"Server Error">>). + send_rescue_response(Socket, 500, <<"Server Error">>). send_rescue_response(Socket, Code, Body) -> - Response = http_response(Code, Body), - elli_tcp:send(Socket, Response). + Response = http_response(Code, Body), + elli_tcp:send(Socket, Response). %% @doc Execute the user callback, translating failure into a proper response. execute_callback(#req{callback = {Mod, Args}} = Req) -> - try Mod:handle(Req, Args) of - %% {ok,...{file,...}} - {ok, Headers, {file, Filename}} -> - {file, 200, Headers, Filename, []}; - {ok, Headers, {file, Filename, Range}} -> - {file, 200, Headers, Filename, Range}; - %% ok simple - {ok, Headers, Body} -> {response, 200, Headers, Body}; - {ok, Body} -> {response, 200, [], Body}; - %% Chunk - {chunk, Headers} -> {chunk, Headers, <<"">>}; - {chunk, Headers, Initial} -> {chunk, Headers, Initial}; - %% File - {HttpCode, Headers, {file, Filename}} -> - {file, HttpCode, Headers, Filename, {0, 0}}; - {HttpCode, Headers, {file, Filename, Range}} -> - {file, HttpCode, Headers, Filename, Range}; - %% Simple - {HttpCode, Headers, Body} -> {response, HttpCode, Headers, Body}; - {HttpCode, Body} -> {response, HttpCode, [], Body}; - %% Unexpected - Unexpected -> - handle_event(Mod, invalid_return, [Req, Unexpected], Args), - {response, 500, [], <<"Internal server error">>} - catch - throw:{ResponseCode, Headers, Body} when is_integer(ResponseCode) -> - {response, ResponseCode, Headers, Body}; - ?WITH_STACKTRACE(throw, Exc, Stacktrace) - handle_event(Mod, request_throw, - [Req, Exc, Stacktrace], - Args), - {response, 500, [], <<"Internal server error">>}; - ?WITH_STACKTRACE(error, Error, Stacktrace) - handle_event(Mod, request_error, - [Req, Error, Stacktrace], - Args), - {response, 500, [], <<"Internal server error">>}; - ?WITH_STACKTRACE(exit, Exit, Stacktrace) - handle_event(Mod, request_exit, - [Req, Exit, Stacktrace], - Args), - {response, 500, [], <<"Internal server error">>} - end. + try Mod:handle(Req, Args) of + %% {ok,...{file,...}} + {ok, Headers, {file, Filename}} -> + {file, 200, Headers, Filename, []}; + {ok, Headers, {file, Filename, Range}} -> + {file, 200, Headers, Filename, Range}; + %% ok simple + {ok, Headers, Body} -> {response, 200, Headers, Body}; + {ok, Body} -> {response, 200, [], Body}; + %% Chunk + {chunk, Headers} -> {chunk, Headers, <<"">>}; + {chunk, Headers, Initial} -> {chunk, Headers, Initial}; + %% File + {HttpCode, Headers, {file, Filename}} -> + {file, HttpCode, Headers, Filename, {0, 0}}; + {HttpCode, Headers, {file, Filename, Range}} -> + {file, HttpCode, Headers, Filename, Range}; + %% Simple + {HttpCode, Headers, Body} -> {response, HttpCode, Headers, Body}; + {HttpCode, Body} -> {response, HttpCode, [], Body}; + %% Unexpected + Unexpected -> + handle_event(Mod, invalid_return, [Req, Unexpected], Args), + {response, 500, [], <<"Internal server error">>} + catch + throw:{ResponseCode, Headers, Body} when is_integer(ResponseCode) -> + {response, ResponseCode, Headers, Body}; + ?WITH_STACKTRACE(throw, Exc, Stacktrace) +handle_event(Mod, request_throw, + [Req, Exc, Stacktrace], + Args), +{response, 500, [], <<"Internal server error">>}; +?WITH_STACKTRACE(error, Error, Stacktrace) +handle_event(Mod, request_error, +[Req, Error, Stacktrace], +Args), +{response, 500, [], <<"Internal server error">>}; +?WITH_STACKTRACE(exit, Exit, Stacktrace) +handle_event(Mod, request_exit, +[Req, Exit, Stacktrace], +Args), +{response, 500, [], <<"Internal server error">>} +end . %% %% CHUNKED-TRANSFER @@ -350,64 +350,64 @@ execute_callback(#req{callback = {Mod, Args}} = Req) -> %% empty response, which signals that the connection should be %% closed. When the client closes the socket, the loop exits. start_chunk_loop(Socket) -> - %% Set the socket to active so we receive the tcp_closed message - %% if the client closes the connection - elli_tcp:setopts(Socket, [{active, once}]), - ?MODULE:chunk_loop(Socket). + %% Set the socket to active so we receive the tcp_closed message + %% if the client closes the connection + elli_tcp:setopts(Socket, [{active, once}]), + ?MODULE:chunk_loop(Socket). chunk_loop(Socket) -> - {_SockType, InnerSocket} = Socket, - receive - {tcp_closed, InnerSocket} -> - {error, client_closed}; - - {chunk, close} -> - case elli_tcp:send(Socket, <<"0\r\n\r\n">>) of - ok -> - elli_tcp:close(Socket), - ok; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - {error, client_closed} - end; - {chunk, close, From} -> - case elli_tcp:send(Socket, <<"0\r\n\r\n">>) of - ok -> - elli_tcp:close(Socket), - From ! {self(), ok}, - ok; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - From ! {self(), {error, closed}}, - ok - end; - - {chunk, Data} -> - send_chunk(Socket, Data), - ?MODULE:chunk_loop(Socket); - {chunk, Data, From} -> - case send_chunk(Socket, Data) of - ok -> - From ! {self(), ok}; - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - From ! {self(), {error, closed}} - end, - ?MODULE:chunk_loop(Socket) - after 10000 -> - ?MODULE:chunk_loop(Socket) - end. + {_SockType, InnerSocket} = Socket, + receive + {tcp_closed, InnerSocket} -> + {error, client_closed}; + + {chunk, close} -> + case elli_tcp:send(Socket, <<"0\r\n\r\n">>) of + ok -> + elli_tcp:close(Socket), + ok; + {error, Closed} when Closed =:= closed orelse + Closed =:= enotconn -> + {error, client_closed} + end; + {chunk, close, From} -> + case elli_tcp:send(Socket, <<"0\r\n\r\n">>) of + ok -> + elli_tcp:close(Socket), + From ! {self(), ok}, + ok; + {error, Closed} when Closed =:= closed orelse + Closed =:= enotconn -> + From ! {self(), {error, closed}}, + ok + end; + + {chunk, Data} -> + send_chunk(Socket, Data), + ?MODULE:chunk_loop(Socket); + {chunk, Data, From} -> + case send_chunk(Socket, Data) of + ok -> + From ! {self(), ok}; + {error, Closed} when Closed =:= closed orelse + Closed =:= enotconn -> + From ! {self(), {error, closed}} + end, + ?MODULE:chunk_loop(Socket) + after 10000 -> + ?MODULE:chunk_loop(Socket) + end. send_chunk(Socket, Data) -> - case iolist_size(Data) of - 0 -> ok; - Size -> - Response = [integer_to_list(Size, 16), - <<"\r\n">>, Data, <<"\r\n">>], - s(chunks, iolist_size(Response)), - elli_tcp:send(Socket, Response) - end. + case iolist_size(Data) of + 0 -> ok; + Size -> + Response = [integer_to_list(Size, 16), + <<"\r\n">>, Data, <<"\r\n">>], + s(chunks, iolist_size(Response)), + elli_tcp:send(Socket, Response) + end. %% @@ -416,90 +416,90 @@ send_chunk(Socket, Data) -> %% @doc Retrieve the request line. get_request(Socket, <<>>, Options, Callback) -> - NewBuffer = recv_request(Socket, <<>>, Options, Callback), - get_request(Socket, NewBuffer, Options, Callback); + NewBuffer = recv_request(Socket, <<>>, Options, Callback), + get_request(Socket, NewBuffer, Options, Callback); get_request(Socket, Buffer, Options, Callback) -> - t(request_start), - get_request_(Socket, Buffer, Options, Callback). + t(request_start), + get_request_(Socket, Buffer, Options, Callback). get_request_(Socket, Buffer, Options, {Mod, Args} = Callback) -> - case erlang:decode_packet(http_bin, Buffer, []) of - {more, _} -> - NewBuffer = recv_request(Socket, Buffer, Options, Callback), - get_request_(Socket, NewBuffer, Options, Callback); - {ok, {http_request, Method, RawPath, Version}, Rest} -> - {Method, RawPath, Version, Rest}; - {ok, {http_error, _}, _} -> - handle_event(Mod, request_parse_error, [Buffer], Args), - send_bad_request(Socket), - elli_tcp:close(Socket), - exit(normal); - {ok, {http_response, _, _, _}, _} -> - elli_tcp:close(Socket), - exit(normal) - end. + case erlang:decode_packet(http_bin, Buffer, []) of + {more, _} -> + NewBuffer = recv_request(Socket, Buffer, Options, Callback), + get_request_(Socket, NewBuffer, Options, Callback); + {ok, {http_request, Method, RawPath, Version}, Rest} -> + {Method, RawPath, Version, Rest}; + {ok, {http_error, _}, _} -> + handle_event(Mod, request_parse_error, [Buffer], Args), + send_bad_request(Socket), + elli_tcp:close(Socket), + exit(normal); + {ok, {http_response, _, _, _}, _} -> + elli_tcp:close(Socket), + exit(normal) + end. recv_request(Socket, Buffer, Options, {Mod, Args} = _Callback) -> - case elli_tcp:recv(Socket, 0, request_timeout(Options)) of - {ok, Data} -> - <>; - {error, timeout} -> - handle_event(Mod, request_timeout, [], Args), - elli_tcp:close(Socket), - exit(normal); - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - handle_event(Mod, request_closed, [], Args), - elli_tcp:close(Socket), - exit(normal) - end. + case elli_tcp:recv(Socket, 0, request_timeout(Options)) of + {ok, Data} -> + <>; + {error, timeout} -> + handle_event(Mod, request_timeout, [], Args), + elli_tcp:close(Socket), + exit(normal); + {error, Closed} when Closed =:= closed orelse + Closed =:= enotconn -> + handle_event(Mod, request_closed, [], Args), + elli_tcp:close(Socket), + exit(normal) + end. -spec get_headers(Socket, V, Buffer, Opts, Callback) -> Headers when - Socket :: elli_tcp:socket(), - V :: version(), - Buffer :: binary(), - Opts :: proplists:proplist(), - Callback :: elli_handler:callback(), - Headers :: {{elli:headers(), elli:headers()}, any()}. % TODO: refine + Socket :: elli_tcp:socket(), + V :: version(), + Buffer :: binary(), + Opts :: proplists:proplist(), + Callback :: elli_handler:callback(), + Headers :: {{elli:headers(), elli:headers()}, any()}. % TODO: refine get_headers(_Socket, {0, 9}, _, _, _) -> - {{[], []}, <<>>}; + {{[], []}, <<>>}; get_headers(Socket, {1, _}, Buffer, Opts, Callback) -> - get_headers(Socket, Buffer, {[], []}, 0, Opts, Callback). + get_headers(Socket, Buffer, {[], []}, 0, Opts, Callback). get_headers(Socket, _, {Headers, _}, HeadersCount, _Opts, {Mod, Args}) - when HeadersCount >= 100 -> - handle_event(Mod, bad_request, [{too_many_headers, Headers}], Args), - send_bad_request(Socket), - elli_tcp:close(Socket), - exit(normal); + when HeadersCount >= 100 -> + handle_event(Mod, bad_request, [{too_many_headers, Headers}], Args), + send_bad_request(Socket), + elli_tcp:close(Socket), + exit(normal); get_headers(Socket, Buffer, {Headers, ParsedHeaders}, Count, Opts, {Mod, Args} = Callback) -> - case erlang:decode_packet(httph_bin, Buffer, []) of - {ok, {http_header, _, Key, _, Value}, Rest} -> - BinKey = ensure_binary(Key), - NewHeaders = [{BinKey, Value} | Headers], - NewParsedHeaders = [{string:casefold(BinKey), Value} | ParsedHeaders], - get_headers(Socket, Rest, {NewHeaders, NewParsedHeaders}, Count + 1, Opts, Callback); - {ok, http_eoh, Rest} -> - {{Headers, ParsedHeaders}, Rest}; - {ok, {http_error, _}, Rest} -> - get_headers(Socket, Rest, {Headers, ParsedHeaders}, Count, Opts, Callback); - {more, _} -> - case elli_tcp:recv(Socket, 0, header_timeout(Opts)) of - {ok, Data} -> - get_headers(Socket, <>, - {Headers, ParsedHeaders}, Count, Opts, Callback); - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - handle_event(Mod, client_closed, [receiving_headers], Args), - elli_tcp:close(Socket), - exit(normal); - {error, timeout} -> - handle_event(Mod, client_timeout, - [receiving_headers], Args), - elli_tcp:close(Socket), - exit(normal) - end - end. + case erlang:decode_packet(httph_bin, Buffer, []) of + {ok, {http_header, _, Key, _, Value}, Rest} -> + BinKey = ensure_binary(Key), + NewHeaders = [{BinKey, Value} | Headers], + NewParsedHeaders = [{string:casefold(BinKey), Value} | ParsedHeaders], + get_headers(Socket, Rest, {NewHeaders, NewParsedHeaders}, Count + 1, Opts, Callback); + {ok, http_eoh, Rest} -> + {{Headers, ParsedHeaders}, Rest}; + {ok, {http_error, _}, Rest} -> + get_headers(Socket, Rest, {Headers, ParsedHeaders}, Count, Opts, Callback); + {more, _} -> + case elli_tcp:recv(Socket, 0, header_timeout(Opts)) of + {ok, Data} -> + get_headers(Socket, <>, + {Headers, ParsedHeaders}, Count, Opts, Callback); + {error, Closed} when Closed =:= closed orelse + Closed =:= enotconn -> + handle_event(Mod, client_closed, [receiving_headers], Args), + elli_tcp:close(Socket), + exit(normal); + {error, timeout} -> + handle_event(Mod, client_timeout, + [receiving_headers], Args), + elli_tcp:close(Socket), + exit(normal) + end + end. %% @doc Fetch the full body of the request, if any is available. %% @@ -512,228 +512,228 @@ get_headers(Socket, Buffer, {Headers, ParsedHeaders}, Count, Opts, {Mod, Args} = %% buffered too much and get parts of the next pipelined request. In %% that case, push it back in the buffer and handle the first request. -spec get_body(Socket, Headers, Buffer, Opts, Callback) -> FullBody when - Socket :: undefined | elli_tcp:socket(), - Headers :: elli:headers(), - Buffer :: binary(), - Opts :: proplists:proplist(), - Callback :: elli_handler:callback(), - FullBody :: {elli:body(), binary()}. + Socket :: undefined | elli_tcp:socket(), + Headers :: elli:headers(), + Buffer :: binary(), + Opts :: proplists:proplist(), + Callback :: elli_handler:callback(), + FullBody :: {elli:body(), binary()}. get_body(Socket, Headers, Buffer, Opts, Callback) -> - case get_header(?CONTENT_LENGTH_HEADER, Headers, undefined) of - undefined -> - {<<>>, Buffer}; - ContentLengthBin -> - maybe_send_continue(Socket, Headers), - - ContentLength = binary_to_integer(binary:replace(ContentLengthBin, - <<" ">>, <<>>, - [global])), - - ok = check_max_size(Socket, ContentLength, Buffer, Opts, Callback), - - Result = case ContentLength - byte_size(Buffer) of - 0 -> - {Buffer, <<>>}; - N when N > 0 -> - do_get_body(Socket, Buffer, Opts, N, Callback); - _ -> - <> = Buffer, - {Body, R} - end, - %% set the size here so if do_get_body exits it won't have - %% req_body in sizes - s(req_body, ContentLength), - Result - end. + case get_header(?CONTENT_LENGTH_HEADER, Headers, undefined) of + undefined -> + {<<>>, Buffer}; + ContentLengthBin -> + maybe_send_continue(Socket, Headers), + + ContentLength = binary_to_integer(binary:replace(ContentLengthBin, + <<" ">>, <<>>, + [global])), + + ok = check_max_size(Socket, ContentLength, Buffer, Opts, Callback), + + Result = case ContentLength - byte_size(Buffer) of + 0 -> + {Buffer, <<>>}; + N when N > 0 -> + do_get_body(Socket, Buffer, Opts, N, Callback); + _ -> + <> = Buffer, + {Body, R} + end, + %% set the size here so if do_get_body exits it won't have + %% req_body in sizes + s(req_body, ContentLength), + Result + end. do_get_body(Socket, Buffer, Opts, N, {Mod, Args}) -> - case elli_tcp:recv(Socket, N, body_timeout(Opts)) of - {ok, Data} -> - {<>, <<>>}; - {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> - handle_event(Mod, client_closed, [receiving_body], Args), - ok = elli_tcp:close(Socket), - exit(normal); - {error, timeout} -> - handle_event(Mod, client_timeout, [receiving_body], Args), - ok = elli_tcp:close(Socket), - exit(normal) - end. + case elli_tcp:recv(Socket, N, body_timeout(Opts)) of + {ok, Data} -> + {<>, <<>>}; + {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> + handle_event(Mod, client_closed, [receiving_body], Args), + ok = elli_tcp:close(Socket), + exit(normal); + {error, timeout} -> + handle_event(Mod, client_timeout, [receiving_body], Args), + ok = elli_tcp:close(Socket), + exit(normal) + end. ensure_binary(Bin) when is_binary(Bin) -> Bin; ensure_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom, latin1). maybe_send_continue(Socket, Headers) -> - % According to RFC2616 section 8.2.3 an origin server must respond with - % either a "100 Continue" or a final response code when the client - % headers contains "Expect:100-continue" - case get_header(?EXPECT_HEADER, Headers, undefined) of - <<"100-continue">> -> - Response = http_response(100), - elli_tcp:send(Socket, Response); - _Other -> - ok - end. + % According to RFC2616 section 8.2.3 an origin server must respond with + % either a "100 Continue" or a final response code when the client + % headers contains "Expect:100-continue" + case get_header(?EXPECT_HEADER, Headers, undefined) of + <<"100-continue">> -> + Response = http_response(100), + elli_tcp:send(Socket, Response); + _Other -> + ok + end. %% @doc To send a response, we must first receive anything the client is %% sending. To avoid allowing clients to use all our bandwidth, if the request %% size is too big, we simply close the socket. check_max_size(Socket, ContentLength, Buffer, Opts, {Mod, Args}) -> - MaxSize = max_body_size(Opts), - do_check_max_size(Socket, ContentLength, Buffer, MaxSize, {Mod, Args}). + MaxSize = max_body_size(Opts), + do_check_max_size(Socket, ContentLength, Buffer, MaxSize, {Mod, Args}). do_check_max_size(Socket, ContentLength, Buffer, MaxSize, {Mod, Args}) - when ContentLength > MaxSize -> - handle_event(Mod, bad_request, [{body_size, ContentLength}], Args), - do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize), - elli_tcp:close(Socket), - exit(normal); + when ContentLength > MaxSize -> + handle_event(Mod, bad_request, [{body_size, ContentLength}], Args), + do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize), + elli_tcp:close(Socket), + exit(normal); do_check_max_size(_, _, _, _, _) -> ok. do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize) - when ContentLength < MaxSize * 2 -> - OnSocket = ContentLength - size(Buffer), - elli_tcp:recv(Socket, OnSocket, 60000), - Response = http_response(413), - elli_tcp:send(Socket, Response); + when ContentLength < MaxSize * 2 -> + OnSocket = ContentLength - size(Buffer), + elli_tcp:recv(Socket, OnSocket, 60000), + Response = http_response(413), + elli_tcp:send(Socket, Response); do_check_max_size_x2(_, _, _, _) -> ok. -spec mk_req(Method, PathTuple, Headers, Headers, Body, V, Socket, Callback) -> Req when - Method :: elli:http_method(), - PathTuple :: {PathType :: atom(), RawPath :: binary()}, - Headers :: elli:headers(), - Body :: elli:body(), - V :: version(), - Socket :: elli_tcp:socket() | undefined, - Callback :: elli_handler:callback(), - Req :: elli:req(). + Method :: elli:http_method(), + PathTuple :: {PathType :: atom(), RawPath :: binary()}, + Headers :: elli:headers(), + Body :: elli:body(), + V :: version(), + Socket :: elli_tcp:socket() | undefined, + Callback :: elli_handler:callback(), + Req :: elli:req(). mk_req(Method, PathTuple, Headers, ParsedHeaders, Body, V, Socket, {Mod, Args} = Callback) -> - case parse_path(PathTuple) of - {ok, {Scheme, Host, Port}, {Path, URL, URLArgs}} -> - #req{method = Method, scheme = Scheme, host = Host, - port = Port, path = URL, args = URLArgs, - version = V, raw_path = Path, original_headers = Headers, - body = Body, pid = self(), socket = Socket, - callback = Callback, headers = ParsedHeaders}; - {error, Reason} -> - handle_event(Mod, request_parse_error, - [{Reason, {Method, PathTuple}}], Args), - send_bad_request(Socket), - elli_tcp:close(Socket), - exit(normal) - end. + case parse_path(PathTuple) of + {ok, {Scheme, Host, Port}, {Path, URL, URLArgs}} -> + #req{method = Method, scheme = Scheme, host = Host, + port = Port, path = URL, args = URLArgs, + version = V, raw_path = Path, original_headers = Headers, + body = Body, pid = self(), socket = Socket, + callback = Callback, headers = ParsedHeaders}; + {error, Reason} -> + handle_event(Mod, request_parse_error, + [{Reason, {Method, PathTuple}}], Args), + send_bad_request(Socket), + elli_tcp:close(Socket), + exit(normal) + end. mk_req(Method, Scheme, Host, Port, PathTuple, Headers, ParsedHeaders, Body, V, Socket, Callback) -> - Req = mk_req(Method, PathTuple, Headers, ParsedHeaders, Body, V, Socket, Callback), - Req#req{scheme = Scheme, host = Host, port = Port}. + Req = mk_req(Method, PathTuple, Headers, ParsedHeaders, Body, V, Socket, Callback), + Req#req{scheme = Scheme, host = Host, port = Port}. %% %% HEADERS %% http_response(Code) -> - http_response(Code, <<>>). + http_response(Code, <<>>). http_response(Code, Body) -> - http_response(Code, [{?CONTENT_LENGTH_HEADER, size(Body)}], Body). + http_response(Code, [{?CONTENT_LENGTH_HEADER, size(Body)}], Body). http_response(Code, Headers, <<>>) -> - [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, - encode_headers(Headers), <<"\r\n">>]; + [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, + encode_headers(Headers), <<"\r\n">>]; http_response(Code, Headers, Body) -> - [http_response(Code, Headers, <<>>), Body]. + [http_response(Code, Headers, <<>>), Body]. assemble_response_headers(Code, Headers) -> - ResponseHeaders = http_response(Code, Headers, <<>>), - s(resp_headers, iolist_size(ResponseHeaders)), - ResponseHeaders. + ResponseHeaders = http_response(Code, Headers, <<>>), + s(resp_headers, iolist_size(ResponseHeaders)), + ResponseHeaders. encode_headers([]) -> - []; + []; encode_headers([[] | H]) -> - encode_headers(H); + encode_headers(H); encode_headers([{K, V} | H]) -> - [encode_value(K), <<": ">>, encode_value(V), <<"\r\n">>, encode_headers(H)]. + [encode_value(K), <<": ">>, encode_value(V), <<"\r\n">>, encode_headers(H)]. encode_value(V) when is_integer(V) -> integer_to_binary(V); -encode_value(V) when is_binary(V) -> V; -encode_value(V) when is_list(V) -> list_to_binary(V). +encode_value(V) when is_binary(V) -> V; +encode_value(V) when is_list(V) -> list_to_binary(V). connection_token(#req{version = {1, 1}, headers = Headers}) -> - case get_header(?CONNECTION_HEADER, Headers) of - <<"close">> -> <<"close">>; - <<"Close">> -> <<"close">>; - _ -> <<"Keep-Alive">> - end; + case get_header(?CONNECTION_HEADER, Headers) of + <<"close">> -> <<"close">>; + <<"Close">> -> <<"close">>; + _ -> <<"Keep-Alive">> + end; connection_token(#req{version = {1, 0}, headers = Headers}) -> - case get_header(?CONNECTION_HEADER, Headers) of - <<"Keep-Alive">> -> <<"Keep-Alive">>; - _ -> <<"close">> - end; + case get_header(?CONNECTION_HEADER, Headers) of + <<"Keep-Alive">> -> <<"Keep-Alive">>; + _ -> <<"close">> + end; connection_token(#req{version = {0, 9}}) -> - <<"close">>. + <<"close">>. %% @doc Return the preferred session handling setting to close or keep the %% current session alive based on the presence of a header or the standard %% default based on the version of HTTP of the request. -spec close_or_keepalive(Req, Headers) -> KeepaliveOpt when - Req :: elli:req(), - Headers :: elli:headers(), - KeepaliveOpt :: close | keep_alive. + Req :: elli:req(), + Headers :: elli:headers(), + KeepaliveOpt :: close | keep_alive. close_or_keepalive(Req, UserHeaders) -> - case get_header(?CONNECTION_HEADER, UserHeaders, connection_token(Req)) of - <<"close">> -> close; - <<"Keep-Alive">> -> keep_alive - end. + case get_header(?CONNECTION_HEADER, UserHeaders, connection_token(Req)) of + <<"close">> -> close; + <<"Keep-Alive">> -> keep_alive + end. %% @doc Add appropriate connection header if the user did not add one already. connection(Req, UserHeaders) -> - case get_header(?CONNECTION_HEADER, UserHeaders) of - undefined -> - {?CONNECTION_HEADER, connection_token(Req)}; - _ -> - [] - end. + case get_header(?CONNECTION_HEADER, UserHeaders) of + undefined -> + {?CONNECTION_HEADER, connection_token(Req)}; + _ -> + [] + end. -content_length(Headers, Body)-> - ?IF(is_header_defined(?CONTENT_LENGTH_HEADER, Headers), [], - {?CONTENT_LENGTH_HEADER, iolist_size(Body)}). +content_length(Headers, Body) -> + ?IF(is_header_defined(?CONTENT_LENGTH_HEADER, Headers), [], + {?CONTENT_LENGTH_HEADER, iolist_size(Body)}). is_header_defined(Key, Headers) -> - Key1 = string:casefold(Key), - lists:any(fun({X, _}) -> string:equal(Key1, X, true) end, Headers). + Key1 = string:casefold(Key), + lists:any(fun({X, _}) -> string:equal(Key1, X, true) end, Headers). get_header(Key, Headers) -> - get_header(Key, Headers, undefined). + get_header(Key, Headers, undefined). -ifdef(OTP_RELEASE). get_header(Key, Headers, Default) -> - CaseFoldedKey = string:casefold(Key), - case lists:search(fun({N, _}) -> string:equal(CaseFoldedKey, N, true) end, Headers) of - {value, {_, Value}} -> - Value; - false -> - Default - end. + CaseFoldedKey = string:casefold(Key), + case lists:search(fun({N, _}) -> string:equal(CaseFoldedKey, N, true) end, Headers) of + {value, {_, Value}} -> + Value; + false -> + Default + end. -else. get_header(Key, Headers, Default) -> - CaseFoldedKey = string:casefold(Key), - case search(fun({N, _}) -> string:equal(CaseFoldedKey, N, true) end, Headers) of - {value, {_, Value}} -> - Value; - false -> - Default - end. - -search(Pred, [Hd|Tail]) -> - case Pred(Hd) of - true -> {value, Hd}; - false -> search(Pred, Tail) - end; + CaseFoldedKey = string:casefold(Key), + case search(fun({N, _}) -> string:equal(CaseFoldedKey, N, true) end, Headers) of + {value, {_, Value}} -> + Value; + false -> + Default + end. + +search(Pred, [Hd | Tail]) -> + case Pred(Hd) of + true -> {value, Hd}; + false -> search(Pred, Tail) + end; search(Pred, []) when is_function(Pred, 1) -> - false. + false. -endif. %% @@ -741,60 +741,60 @@ search(Pred, []) when is_function(Pred, 1) -> %% -ifdef(OTP_RELEASE). - -if(?OTP_RELEASE >= 22). - parse_path({abs_path, FullPath}) -> - URIMap = uri_string:parse(FullPath), - Host = maps:get(host, URIMap, undefined), - Scheme = maps:get(scheme, URIMap, undefined), - Path = maps:get(path, URIMap, <<>>), - Query = maps:get(query, URIMap, <<>>), - Port = maps:get(port, URIMap, case Scheme of http -> 80; https -> 443; _ -> undefined end), - {ok, {Scheme, Host, Port}, {Path, split_path(Path), uri_string:dissect_query(Query)}}; - parse_path({absoluteURI, Scheme, Host, Port, Path}) -> - setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port}); - parse_path(_) -> - {error, unsupported_uri}. - -else. - parse_path({abs_path, FullPath}) -> - Parsed = case binary:split(FullPath, [<<"?">>]) of - [URL] -> {FullPath, split_path(URL), []}; - [URL, Args] -> {FullPath, split_path(URL), split_args(Args)} - end, - {ok, {undefined, undefined, undefined}, Parsed}; - parse_path({absoluteURI, Scheme, Host, Port, Path}) -> - setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port}); - parse_path(_) -> - {error, unsupported_uri}. - -endif. +- if (?OTP_RELEASE >= 22). +parse_path({abs_path, FullPath}) -> + URIMap = uri_string:parse(FullPath), + Host = maps:get(host, URIMap, undefined), + Scheme = maps:get(scheme, URIMap, undefined), + Path = maps:get(path, URIMap, <<>>), + Query = maps:get(query, URIMap, <<>>), + Port = maps:get(port, URIMap, case Scheme of http -> 80; https -> 443; _ -> undefined end), + {ok, {Scheme, Host, Port}, {Path, split_path(Path), uri_string:dissect_query(Query)}}; +parse_path({absoluteURI, Scheme, Host, Port, Path}) -> + setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port}); +parse_path(_) -> + {error, unsupported_uri}. -else. - %% same as else branch above. can drop this when only OTP 21+ is supported - parse_path({abs_path, FullPath}) -> - Parsed = case binary:split(FullPath, [<<"?">>]) of - [URL] -> {FullPath, split_path(URL), []}; - [URL, Args] -> {FullPath, split_path(URL), split_args(Args)} - end, - {ok, {undefined, undefined, undefined}, Parsed}; - parse_path({absoluteURI, Scheme, Host, Port, Path}) -> - setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port}); - parse_path(_) -> - {error, unsupported_uri}. +parse_path({abs_path, FullPath}) -> + Parsed = case binary:split(FullPath, [<<"?">>]) of + [URL] -> {FullPath, split_path(URL), []}; + [URL, Args] -> {FullPath, split_path(URL), split_args(Args)} + end, + {ok, {undefined, undefined, undefined}, Parsed}; +parse_path({absoluteURI, Scheme, Host, Port, Path}) -> + setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port}); +parse_path(_) -> + {error, unsupported_uri}. +-endif. +-else. +%% same as else branch above. can drop this when only OTP 21+ is supported +parse_path({abs_path, FullPath}) -> + Parsed = case binary:split(FullPath, [<<"?">>]) of + [URL] -> {FullPath, split_path(URL), []}; + [URL, Args] -> {FullPath, split_path(URL), split_args(Args)} + end, + {ok, {undefined, undefined, undefined}, Parsed}; +parse_path({absoluteURI, Scheme, Host, Port, Path}) -> + setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port}); +parse_path(_) -> + {error, unsupported_uri}. -endif. split_path(Path) -> - [P || P <- binary:split(Path, [<<"/">>], [global]), - P =/= <<>>]. + [P || P <- binary:split(Path, [<<"/">>], [global]), + P =/= <<>>]. %% @doc Split the URL arguments into a proplist. %% Lifted from `cowboy_http:x_www_form_urlencoded/2'. -spec split_args(binary()) -> list({binary(), binary() | true}). split_args(<<>>) -> - []; + []; split_args(Qs) -> - Tokens = binary:split(Qs, <<"&">>, [global, trim]), - [case binary:split(Token, <<"=">>) of - [Token] -> {Token, true}; - [Name, Value] -> {Name, Value} - end || Token <- Tokens]. + Tokens = binary:split(Qs, <<"&">>, [global, trim]), + [case binary:split(Token, <<"=">>) of + [Token] -> {Token, true}; + [Name, Value] -> {Name, Value} + end || Token <- Tokens]. %% @@ -802,21 +802,21 @@ split_args(Qs) -> %% init(#req{callback = {Mod, Args}} = Req) -> - ?IF(erlang:function_exported(Mod, init, 2), - case Mod:init(Req, Args) of - ignore -> {ok, standard}; - {ok, Behaviour} -> {ok, Behaviour} - end, - {ok, standard}). + ?IF(erlang:function_exported(Mod, init, 2), + case Mod:init(Req, Args) of + ignore -> {ok, standard}; + {ok, Behaviour} -> {ok, Behaviour} + end, + {ok, standard}). handle_event(Mod, Name, EventArgs, ElliArgs) -> - try - Mod:handle_event(Name, EventArgs, ElliArgs) - catch - ?WITH_STACKTRACE(EvClass, EvError, Stacktrace) - ?LOG_ERROR("~p:handle_event/3 crashed ~p:~p~n~p", - [Mod, EvClass, EvError, Stacktrace]) - end. + try + Mod:handle_event(Name, EventArgs, ElliArgs) + catch + ?WITH_STACKTRACE(EvClass, EvError, Stacktrace) + ?LOG_ERROR("~p:handle_event/3 crashed ~p:~p~n~p", + [Mod, EvClass, EvError, Stacktrace]) +end. %% %% TIMING HELPERS @@ -826,18 +826,18 @@ handle_event(Mod, Name, EventArgs, ElliArgs) -> %% This allows easily adding time tracing wherever, %% without passing along any variables. t(Key) -> - put({time, Key}, erlang:monotonic_time()). + put({time, Key}, erlang:monotonic_time()). get_timings() -> - lists:filtermap(fun get_timings/1, get()). + lists:filtermap(fun get_timings/1, get()). get_timings({{time, accepted}, Value}) -> - {true, {accepted, Value}}; + {true, {accepted, Value}}; get_timings({{time, Key}, Value}) -> - erase({time, Key}), - {true, {Key, Value}}; + erase({time, Key}), + {true, {Key, Value}}; get_timings(_) -> - false. + false. %% %% SIZE HELPERS @@ -845,34 +845,34 @@ get_timings(_) -> %% @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; + case get({size, chunks}) of + undefined -> + put({size, chunks}, Size); + Sum -> + put({size, chunks}, Size + Sum) + end; s(Key, Size) -> - put({size, Key}, Size). + put({size, Key}, Size). get_sizes() -> - lists:filtermap(fun get_sizes/1, get()). + lists:filtermap(fun get_sizes/1, get()). get_sizes({{size, Key}, Value}) -> - erase({size, Key}), - {true, {Key, Value}}; + erase({size, Key}), + {true, {Key, Value}}; get_sizes(_) -> - false. + false. %% %% OPTIONS %% -accept_timeout(Opts) -> proplists:get_value(accept_timeout, Opts). +accept_timeout(Opts) -> proplists:get_value(accept_timeout, Opts). request_timeout(Opts) -> proplists:get_value(request_timeout, Opts). -header_timeout(Opts) -> proplists:get_value(header_timeout, Opts). -body_timeout(Opts) -> proplists:get_value(body_timeout, Opts). -max_body_size(Opts) -> proplists:get_value(max_body_size, Opts). +header_timeout(Opts) -> proplists:get_value(header_timeout, Opts). +body_timeout(Opts) -> proplists:get_value(body_timeout, Opts). +max_body_size(Opts) -> proplists:get_value(max_body_size, Opts). %% @@ -949,11 +949,11 @@ status(B) when is_binary(B) -> B. -include_lib("eunit/include/eunit.hrl"). get_body_test() -> - Socket = undefined, - Headers = [{<<"Content-Length">>, <<" 42 ">>}], - Buffer = binary:copy(<<".">>, 42), - Opts = [], - Callback = {no_mod, []}, - ?assertMatch({Buffer, <<>>}, - get_body(Socket, Headers, Buffer, Opts, Callback)). + Socket = undefined, + Headers = [{<<"Content-Length">>, <<" 42 ">>}], + Buffer = binary:copy(<<".">>, 42), + Opts = [], + Callback = {no_mod, []}, + ?assertMatch({Buffer, <<>>}, + get_body(Socket, Headers, Buffer, Opts, Callback)). -endif. diff --git a/src/wsSrv/elli_middleware.erl b/src/wsSrv/elli_middleware.erl index 4a9cbae..3c5ba56 100644 --- a/src/wsSrv/elli_middleware.erl +++ b/src/wsSrv/elli_middleware.erl @@ -42,7 +42,7 @@ %% Macros. -define(IF_NOT_EXPORTED(M, F, A, T, E), - case erlang:function_exported(M, F, A) of true -> E; false -> T end). + case erlang:function_exported(M, F, A) of true -> E; false -> T end). %% %% ELLI CALLBACKS @@ -50,34 +50,32 @@ %% @hidden -spec init(Req, Args) -> {ok, standard | handover} when - Req :: elli:req(), - Args :: elli_handler:callback_args(). + Req :: elli:req(), + Args :: elli_handler:callback_args(). init(Req, Args) -> - do_init(Req, callbacks(Args)). + do_init(Req, callbacks(Args)). %% @hidden -spec handle(Req :: elli:req(), Config :: [tuple()]) -> elli_handler:result(). handle(CleanReq, Config) -> - Callbacks = callbacks(Config), - PreReq = preprocess(CleanReq, Callbacks), - Res = process(PreReq, Callbacks), - postprocess(PreReq, Res, lists:reverse(Callbacks)). + Callbacks = callbacks(Config), + PreReq = preprocess(CleanReq, Callbacks), + Res = process(PreReq, Callbacks), + postprocess(PreReq, Res, lists:reverse(Callbacks)). %% @hidden -spec handle_event(Event, Args, Config) -> ok when - Event :: elli_handler:event(), - Args :: elli_handler:callback_args(), - Config :: [tuple()]. + Event :: elli_handler:event(), + Args :: elli_handler:callback_args(), + Config :: [tuple()]. handle_event(elli_startup, Args, Config) -> - Callbacks = callbacks(Config), - [code:ensure_loaded(M) || {M, _} <- Callbacks], - forward_event(elli_startup, Args, Callbacks); + Callbacks = callbacks(Config), + [code:ensure_loaded(M) || {M, _} <- Callbacks], + forward_event(elli_startup, Args, Callbacks); handle_event(Event, Args, Config) -> - forward_event(Event, Args, lists:reverse(callbacks(Config))). - - + forward_event(Event, Args, lists:reverse(callbacks(Config))). %% @@ -85,62 +83,62 @@ handle_event(Event, Args, Config) -> %% -spec do_init(Req, Callbacks) -> {ok, standard | handover} when - Req :: elli:req(), - Callbacks :: elli_handler:callbacks(). + Req :: elli:req(), + Callbacks :: elli_handler:callbacks(). do_init(_, []) -> - {ok, standard}; -do_init(Req, [{Mod, Args}|Mods]) -> - ?IF_NOT_EXPORTED(Mod, init, 2, do_init(Req, Mods), - case Mod:init(Req, Args) of - ignore -> do_init(Req, Mods); - Result -> Result - end). + {ok, standard}; +do_init(Req, [{Mod, Args} | Mods]) -> + ?IF_NOT_EXPORTED(Mod, init, 2, do_init(Req, Mods), + case Mod:init(Req, Args) of + ignore -> do_init(Req, Mods); + Result -> Result + end). -spec process(Req, Callbacks) -> Result when - Req :: elli:req(), - Callbacks :: [Callback :: elli_handler:callback()], - Result :: elli_handler:result(). + Req :: elli:req(), + Callbacks :: [Callback :: elli_handler:callback()], + Result :: elli_handler:result(). process(_Req, []) -> - {404, [], <<"Not Found">>}; + {404, [], <<"Not Found">>}; process(Req, [{Mod, Args} | Mods]) -> - ?IF_NOT_EXPORTED(Mod, handle, 2, process(Req, Mods), - case Mod:handle(Req, Args) of - ignore -> process(Req, Mods); - Response -> Response - end). + ?IF_NOT_EXPORTED(Mod, handle, 2, process(Req, Mods), + case Mod:handle(Req, Args) of + ignore -> process(Req, Mods); + Response -> Response + end). -spec preprocess(Req1, Callbacks) -> Req2 when - Req1 :: elli:req(), - Callbacks :: [elli_handler:callback()], - Req2 :: elli:req(). + Req1 :: elli:req(), + Callbacks :: [elli_handler:callback()], + Req2 :: elli:req(). preprocess(Req, []) -> - Req; + Req; preprocess(Req, [{Mod, Args} | Mods]) -> - ?IF_NOT_EXPORTED(Mod, preprocess, 2, preprocess(Req, Mods), - preprocess(Mod:preprocess(Req, Args), Mods)). + ?IF_NOT_EXPORTED(Mod, preprocess, 2, preprocess(Req, Mods), + preprocess(Mod:preprocess(Req, Args), Mods)). -spec postprocess(Req, Res1, Callbacks) -> Res2 when - Req :: elli:req(), - Res1 :: elli_handler:result(), - Callbacks :: [elli_handler:callback()], - Res2 :: elli_handler:result(). + Req :: elli:req(), + Res1 :: elli_handler:result(), + Callbacks :: [elli_handler:callback()], + Res2 :: elli_handler:result(). postprocess(_Req, Res, []) -> - Res; + Res; postprocess(Req, Res, [{Mod, Args} | Mods]) -> - ?IF_NOT_EXPORTED(Mod, postprocess, 3, postprocess(Req, Res, Mods), - postprocess(Req, Mod:postprocess(Req, Res, Args), Mods)). + ?IF_NOT_EXPORTED(Mod, postprocess, 3, postprocess(Req, Res, Mods), + postprocess(Req, Mod:postprocess(Req, Res, Args), Mods)). -spec forward_event(Event, Args, Callbacks) -> ok when - Event :: elli_handler:event(), - Args :: elli_handler:callback_args(), - Callbacks :: [elli_handler:callback()]. + Event :: elli_handler:event(), + Args :: elli_handler:callback_args(), + Callbacks :: [elli_handler:callback()]. forward_event(Event, Args, Callbacks) -> - [?IF_NOT_EXPORTED(M, handle_event, 3, ok, - M:handle_event(Event, Args, XArgs)) - || {M, XArgs} <- Callbacks], - ok. + [?IF_NOT_EXPORTED(M, handle_event, 3, ok, + M:handle_event(Event, Args, XArgs)) + || {M, XArgs} <- Callbacks], + ok. %% @@ -148,6 +146,6 @@ forward_event(Event, Args, Callbacks) -> %% -spec callbacks(Config :: [{mod, Callbacks} | tuple()]) -> Callbacks when - Callbacks :: [elli_handler:callback()]. + Callbacks :: [elli_handler:callback()]. callbacks(Config) -> - proplists:get_value(mods, Config, []). + proplists:get_value(mods, Config, []). diff --git a/src/wsSrv/elli_middleware_compress.erl b/src/wsSrv/elli_middleware_compress.erl index 3fd0e60..51ea4f5 100644 --- a/src/wsSrv/elli_middleware_compress.erl +++ b/src/wsSrv/elli_middleware_compress.erl @@ -10,26 +10,26 @@ %%% @doc Postprocess all requests and compress bodies larger than %%% `compress_byte_size' (`1024' by default). -spec postprocess(Req, Result, Config) -> Result when - Req :: elli:req(), - Result :: elli_handler:result(), - Config :: [{compress_byte_size, non_neg_integer()} | tuple()]. + Req :: elli:req(), + Result :: elli_handler:result(), + Config :: [{compress_byte_size, non_neg_integer()} | tuple()]. postprocess(Req, {ResponseCode, Body}, Config) - when is_integer(ResponseCode) orelse ResponseCode =:= ok -> - postprocess(Req, {ResponseCode, [], Body}, Config); + when is_integer(ResponseCode) orelse ResponseCode =:= ok -> + postprocess(Req, {ResponseCode, [], Body}, Config); postprocess(Req, {ResponseCode, Headers, Body} = Res, Config) - when is_integer(ResponseCode) orelse ResponseCode =:= ok -> - Threshold = proplists:get_value(compress_byte_size, Config, 1024), - ?IF(not should_compress(Body, Threshold), Res, - case compress(Body, Req) of - no_compress -> - Res; - {CompressedBody, Encoding} -> - NewHeaders = [{<<"Content-Encoding">>, Encoding} | Headers], - {ResponseCode, NewHeaders, CompressedBody} - end); + when is_integer(ResponseCode) orelse ResponseCode =:= ok -> + Threshold = proplists:get_value(compress_byte_size, Config, 1024), + ?IF(not should_compress(Body, Threshold), Res, + case compress(Body, Req) of + no_compress -> + Res; + {CompressedBody, Encoding} -> + NewHeaders = [{<<"Content-Encoding">>, Encoding} | Headers], + {ResponseCode, NewHeaders, CompressedBody} + end); postprocess(_, Res, _) -> - Res. + Res. %% %% INTERNALS @@ -37,21 +37,21 @@ postprocess(_, Res, _) -> %% NOTE: Algorithm is either `<<"gzip">>' or `<<"deflate">>'. -spec compress(Body0 :: elli:body(), Req :: elli:req()) -> Body1 when - Body1 :: {Compressed :: binary(), Algorithm :: binary()} | no_compress. + Body1 :: {Compressed :: binary(), Algorithm :: binary()} | no_compress. compress(Body, Req) -> - case accepted_encoding(Req) of - <<"gzip">> = E -> {zlib:gzip(Body), E}; - <<"deflate">> = E -> {zlib:compress(Body), E}; - _ -> no_compress - end. + case accepted_encoding(Req) of + <<"gzip">> = E -> {zlib:gzip(Body), E}; + <<"deflate">> = E -> {zlib:compress(Body), E}; + _ -> no_compress + end. accepted_encoding(Req) -> - hd(binary:split(elli_request:get_header(<<"Accept-Encoding">>, Req, <<>>), - [<<",">>, <<";">>], [global])). + hd(binary:split(elli_request:get_header(<<"Accept-Encoding">>, Req, <<>>), + [<<",">>, <<";">>], [global])). -spec should_compress(Body, Threshold) -> boolean() when - Body :: binary(), - Threshold :: non_neg_integer(). + Body :: binary(), + Threshold :: non_neg_integer(). should_compress(Body, S) -> - is_binary(Body) andalso byte_size(Body) >= S orelse - is_list(Body) andalso iolist_size(Body) >= S. + is_binary(Body) andalso byte_size(Body) >= S orelse + is_list(Body) andalso iolist_size(Body) >= S. diff --git a/src/wsSrv/elli_request.erl b/src/wsSrv/elli_request.erl index 6500ee2..4b6ecd7 100644 --- a/src/wsSrv/elli_request.erl +++ b/src/wsSrv/elli_request.erl @@ -3,46 +3,46 @@ -include("elli_util.hrl"). -export([send_chunk/2 - , async_send_chunk/2 - , chunk_ref/1 - , close_chunk/1 - , path/1 - , raw_path/1 - , query_str/1 - , get_header/2 - , get_header/3 - , get_arg_decoded/2 - , get_arg_decoded/3 - , get_arg/2 - , get_arg/3 - , get_args/1 - , get_args_decoded/1 - , post_arg/2 - , post_arg/3 - , post_arg_decoded/2 - , post_arg_decoded/3 - , post_args/1 - , post_args_decoded/1 - , body_qs/1 - , original_headers/1 - , headers/1 - , peer/1 - , method/1 - , body/1 - , scheme/1 - , host/1 - , port/1 - , get_range/1 - , to_proplist/1 - , is_request/1 - , uri_decode/1 - ]). + , async_send_chunk/2 + , chunk_ref/1 + , close_chunk/1 + , path/1 + , raw_path/1 + , query_str/1 + , get_header/2 + , get_header/3 + , get_arg_decoded/2 + , get_arg_decoded/3 + , get_arg/2 + , get_arg/3 + , get_args/1 + , get_args_decoded/1 + , post_arg/2 + , post_arg/3 + , post_arg_decoded/2 + , post_arg_decoded/3 + , post_args/1 + , post_args_decoded/1 + , body_qs/1 + , original_headers/1 + , headers/1 + , peer/1 + , method/1 + , body/1 + , scheme/1 + , host/1 + , port/1 + , get_range/1 + , to_proplist/1 + , is_request/1 + , uri_decode/1 +]). -export_type([http_range/0]). --type http_range() :: {First::non_neg_integer(), Last::non_neg_integer()} - | {offset, Offset::non_neg_integer()} - | {suffix, Length::pos_integer()}. +-type http_range() :: {First :: non_neg_integer(), Last :: non_neg_integer()} +| {offset, Offset :: non_neg_integer()} +| {suffix, Length :: pos_integer()}. %% @@ -51,249 +51,248 @@ %% @doc Return `path' split into binary parts. -path(#req{path = Path}) -> Path. +path(#req{path = Path}) -> Path. %% @doc Return the `raw_path', i.e. not split or parsed for query params. -raw_path(#req{raw_path = Path}) -> Path. +raw_path(#req{raw_path = Path}) -> Path. %% @doc Return the `headers' that have had `string:casefold/1' run on each key. headers(#req{headers = Headers}) -> Headers. %% @doc Return the original `headers'. original_headers(#req{original_headers = Headers}) -> Headers. %% @doc Return the `method'. -method(#req{method = Method}) -> Method. +method(#req{method = Method}) -> Method. %% @doc Return the `body'. -body(#req{body = Body}) -> Body. +body(#req{body = Body}) -> Body. %% @doc Return the `scheme'. -scheme(#req{scheme = Scheme}) -> Scheme. +scheme(#req{scheme = Scheme}) -> Scheme. %% @doc Return the `host'. -host(#req{host = Host}) -> Host. +host(#req{host = Host}) -> Host. %% @doc Return the `port'. -port(#req{port = Port}) -> Port. +port(#req{port = Port}) -> Port. peer(#req{socket = Socket} = _Req) -> - case elli_tcp:peername(Socket) of - {ok, {Address, _}} -> - list_to_binary(inet_parse:ntoa(Address)); - {error, _} -> - undefined - end. + case elli_tcp:peername(Socket) of + {ok, {Address, _}} -> + list_to_binary(inet_parse:ntoa(Address)); + {error, _} -> + undefined + end. get_header(Key, #req{headers = Headers}) -> - CaseFoldedKey = string:casefold(Key), - proplists:get_value(CaseFoldedKey, Headers). + CaseFoldedKey = string:casefold(Key), + proplists:get_value(CaseFoldedKey, Headers). get_header(Key, #req{headers = Headers}, Default) -> - CaseFoldedKey = string:casefold(Key), - proplists:get_value(CaseFoldedKey, Headers, Default). + CaseFoldedKey = string:casefold(Key), + proplists:get_value(CaseFoldedKey, Headers, Default). %% @equiv get_arg(Key, Req, undefined) get_arg(Key, #req{} = Req) -> - get_arg(Key, Req, undefined). + get_arg(Key, Req, undefined). %% @equiv proplists:get_value(Key, Args, Default) get_arg(Key, #req{args = Args}, Default) -> - proplists:get_value(Key, Args, Default). + proplists:get_value(Key, Args, Default). %% @equiv get_arg_decoded(Key, Req, undefined) get_arg_decoded(Key, #req{} = Req) -> - get_arg_decoded(Key, Req, undefined). + get_arg_decoded(Key, Req, undefined). get_arg_decoded(Key, #req{args = Args}, Default) -> - case proplists:get_value(Key, Args) of - undefined -> Default; - true -> true; - EncodedValue -> - uri_decode(EncodedValue) - end. + case proplists:get_value(Key, Args) of + undefined -> Default; + true -> true; + EncodedValue -> + uri_decode(EncodedValue) + end. %% @doc Parse `application/x-www-form-urlencoded' body into a proplist. body_qs(#req{body = <<>>}) -> []; body_qs(#req{body = Body} = Req) -> - case get_header(<<"Content-Type">>, Req) of - <<"application/x-www-form-urlencoded">> -> - elli_http:split_args(Body); - <<"application/x-www-form-urlencoded;", _/binary>> -> % ; charset=... - elli_http:split_args(Body); - _ -> - erlang:error(badarg) - end. + case get_header(<<"Content-Type">>, Req) of + <<"application/x-www-form-urlencoded">> -> + elli_http:split_args(Body); + <<"application/x-www-form-urlencoded;", _/binary>> -> % ; charset=... + elli_http:split_args(Body); + _ -> + erlang:error(badarg) + end. %% @equiv post_arg(Key, Req, undefined) post_arg(Key, #req{} = Req) -> - post_arg(Key, Req, undefined). + post_arg(Key, Req, undefined). post_arg(Key, #req{} = Req, Default) -> - proplists:get_value(Key, body_qs(Req), Default). + proplists:get_value(Key, body_qs(Req), Default). %% @equiv post_arg_decoded(Key, Req, undefined) post_arg_decoded(Key, #req{} = Req) -> - post_arg_decoded(Key, Req, undefined). + post_arg_decoded(Key, Req, undefined). post_arg_decoded(Key, #req{} = Req, Default) -> - case proplists:get_value(Key, body_qs(Req)) of - undefined -> Default; - true -> true; - EncodedValue -> - uri_decode(EncodedValue) - end. + case proplists:get_value(Key, body_qs(Req)) of + undefined -> Default; + true -> true; + EncodedValue -> + uri_decode(EncodedValue) + end. %% @doc Return a proplist of keys and values of the original query string. %% Both keys and values in the returned proplists will be binaries or the atom %% `true' in case no value was supplied for the query value. --spec get_args(elli:req()) -> QueryArgs :: proplists:proplist(). +-spec get_args(elli:req()) -> QueryArgs :: proplists:proplist(). get_args(#req{args = Args}) -> Args. get_args_decoded(#req{args = Args}) -> - lists:map(fun ({K, true}) -> - {K, true}; - ({K, V}) -> - {K, uri_decode(V)} - end, Args). + lists:map(fun({K, true}) -> + {K, true}; + ({K, V}) -> + {K, uri_decode(V)} + end, Args). post_args(#req{} = Req) -> - body_qs(Req). + body_qs(Req). post_args_decoded(#req{} = Req) -> - lists:map(fun ({K, true}) -> - {K, true}; - ({K, V}) -> - {K, uri_decode(V)} - end, body_qs(Req)). + lists:map(fun({K, true}) -> + {K, true}; + ({K, V}) -> + {K, uri_decode(V)} + end, body_qs(Req)). %% @doc Calculate the query string associated with a given `Request' %% as a binary. -spec query_str(elli:req()) -> QueryStr :: binary(). query_str(#req{raw_path = Path}) -> - case binary:split(Path, [<<"?">>]) of - [_, Qs] -> Qs; - [_] -> <<>> - end. + case binary:split(Path, [<<"?">>]) of + [_, Qs] -> Qs; + [_] -> <<>> + end. %% @doc Parse the `Range' header from the request. %% The result is either a `byte_range_set()' or the atom `parse_error'. %% Use {@link elli_util:normalize_range/2} to get a validated, normalized range. -spec get_range(elli:req()) -> [http_range()] | parse_error. -get_range(#req{headers = Headers}) -> - case proplists:get_value(<<"range">>, Headers) of - <<"bytes=", RangeSetBin/binary>> -> - parse_range_set(RangeSetBin); - _ -> [] - end. +get_range(#req{headers = Headers}) -> + case proplists:get_value(<<"range">>, Headers) of + <<"bytes=", RangeSetBin/binary>> -> + parse_range_set(RangeSetBin); + _ -> [] + end. --spec parse_range_set(Bin::binary()) -> [http_range()] | parse_error. +-spec parse_range_set(Bin :: binary()) -> [http_range()] | parse_error. parse_range_set(<>) -> - RangeBins = binary:split(ByteRangeSet, <<",">>, [global]), - Parsed = [parse_range(remove_whitespace(RangeBin)) - || RangeBin <- RangeBins], - case lists:member(parse_error, Parsed) of - true -> parse_error; - false -> Parsed - end. - --spec parse_range(Bin::binary()) -> http_range() | parse_error. + RangeBins = binary:split(ByteRangeSet, <<",">>, [global]), + Parsed = [parse_range(remove_whitespace(RangeBin)) + || RangeBin <- RangeBins], + case lists:member(parse_error, Parsed) of + true -> parse_error; + false -> Parsed + end. + +-spec parse_range(Bin :: binary()) -> http_range() | parse_error. parse_range(<<$-, SuffixBin/binary>>) -> - %% suffix-byte-range - try {suffix, binary_to_integer(SuffixBin)} - catch - error:badarg -> parse_error - end; + %% suffix-byte-range + try {suffix, binary_to_integer(SuffixBin)} + catch + error:badarg -> parse_error + end; parse_range(<>) -> - case binary:split(ByteRange, <<"-">>) of - %% byte-range without last-byte-pos - [FirstBytePosBin, <<>>] -> - try {offset, binary_to_integer(FirstBytePosBin)} - catch - error:badarg -> parse_error - end; - %% full byte-range - [FirstBytePosBin, LastBytePosBin] -> - try {bytes, - binary_to_integer(FirstBytePosBin), - binary_to_integer(LastBytePosBin)} - catch - error:badarg -> parse_error - end; - _ -> parse_error - end. + case binary:split(ByteRange, <<"-">>) of + %% byte-range without last-byte-pos + [FirstBytePosBin, <<>>] -> + try {offset, binary_to_integer(FirstBytePosBin)} + catch + error:badarg -> parse_error + end; + %% full byte-range + [FirstBytePosBin, LastBytePosBin] -> + try {bytes, + binary_to_integer(FirstBytePosBin), + binary_to_integer(LastBytePosBin)} + catch + error:badarg -> parse_error + end; + _ -> parse_error + end. -spec remove_whitespace(binary()) -> binary(). remove_whitespace(Bin) -> - binary:replace(Bin, <<" ">>, <<>>, [global]). + binary:replace(Bin, <<" ">>, <<>>, [global]). %% @doc Serialize the `Req'uest record to a proplist. %% Useful for logging. to_proplist(#req{} = Req) -> - lists:zip(record_info(fields, req), tl(tuple_to_list(Req))). - + lists:zip(record_info(fields, req), tl(tuple_to_list(Req))). %% @doc Return a reference that can be used to send chunks to the client. %% If the protocol does not support it, return `{error, not_supported}'. chunk_ref(#req{version = {1, 1}} = Req) -> - Req#req.pid; + Req#req.pid; chunk_ref(#req{}) -> - {error, not_supported}. + {error, not_supported}. %% @doc Explicitly close the chunked connection. %% Return `{error, closed}' if the client already closed the connection. %% @equiv send_chunk(Ref, close) close_chunk(Ref) -> - send_chunk(Ref, close). + send_chunk(Ref, close). %% @doc Send a chunk asynchronously. async_send_chunk(Ref, Data) -> - Ref ! {chunk, Data}. + Ref ! {chunk, Data}. %% @doc Send a chunk synchronously. %% If the referenced process is dead, return early with `{error, closed}', %% instead of timing out. send_chunk(Ref, Data) -> - ?IF(is_ref_alive(Ref), - send_chunk(Ref, Data, 5000), - {error, closed}). + ?IF(is_ref_alive(Ref), + send_chunk(Ref, Data, 5000), + {error, closed}). send_chunk(Ref, Data, Timeout) -> - Ref ! {chunk, Data, self()}, - receive - {Ref, ok} -> - ok; - {Ref, {error, Reason}} -> - {error, Reason} - after Timeout -> - {error, timeout} - end. + Ref ! {chunk, Data, self()}, + receive + {Ref, ok} -> + ok; + {Ref, {error, Reason}} -> + {error, Reason} + after Timeout -> + {error, timeout} + end. is_ref_alive(Ref) -> - ?IF(node(Ref) =:= node(), - is_process_alive(Ref), - rpc:call(node(Ref), erlang, is_process_alive, [Ref])). + ?IF(node(Ref) =:= node(), + is_process_alive(Ref), + rpc:call(node(Ref), erlang, is_process_alive, [Ref])). is_request(#req{}) -> true; -is_request(_) -> false. +is_request(_) -> false. uri_decode(Bin) -> - case binary:match(Bin, [<<"+">>, <<"%">>]) of - nomatch -> Bin; - {Pos, _} -> - <> = Bin, - uri_decode(Rest, Prefix) - end. + case binary:match(Bin, [<<"+">>, <<"%">>]) of + nomatch -> Bin; + {Pos, _} -> + <> = Bin, + uri_decode(Rest, Prefix) + end. uri_decode(<<>>, Acc) -> Acc; uri_decode(<<$+, Rest/binary>>, Acc) -> - uri_decode(Rest, <>); + uri_decode(Rest, <>); uri_decode(<<$%, H, L, Rest/binary>>, Acc) -> - uri_decode(Rest, <>); + uri_decode(Rest, <>); uri_decode(<>, Acc) -> - uri_decode(Rest, <>). + uri_decode(Rest, <>). -compile({inline, [hex_to_int/1]}). hex_to_int(X) when X >= $0, X =< $9 -> X - $0; -hex_to_int(X) when X >= $a, X =< $f -> X - ($a-10); -hex_to_int(X) when X >= $A, X =< $F -> X - ($A-10). +hex_to_int(X) when X >= $a, X =< $f -> X - ($a - 10); +hex_to_int(X) when X >= $A, X =< $F -> X - ($A - 10). diff --git a/src/wsSrv/elli_sendfile.erl b/src/wsSrv/elli_sendfile.erl index d67ded4..fff7a05 100644 --- a/src/wsSrv/elli_sendfile.erl +++ b/src/wsSrv/elli_sendfile.erl @@ -14,65 +14,65 @@ %% %% @end -spec sendfile(file:fd(), inet:socket() | ssl:sslsocket(), - non_neg_integer(), non_neg_integer(), sendfile_opts()) - -> {ok, non_neg_integer()} | {error, atom()}. + non_neg_integer(), non_neg_integer(), sendfile_opts()) + -> {ok, non_neg_integer()} | {error, atom()}. sendfile(RawFile, Socket, Offset, Bytes, Opts) -> - ChunkSize = chunk_size(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. + ChunkSize = chunk_size(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 chunk_size(sendfile_opts()) -> pos_integer(). chunk_size(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. + 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()}. + 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}; + 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. + 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(). + non_neg_integer(). read_size(0, _Sent, ChunkSize) -> - ChunkSize; + ChunkSize; read_size(Bytes, Sent, ChunkSize) -> - min(Bytes - Sent, ChunkSize). + min(Bytes - Sent, ChunkSize). diff --git a/src/wsSrv/elli_tcp.erl b/src/wsSrv/elli_tcp.erl index ee9ad39..1133ca5 100644 --- a/src/wsSrv/elli_tcp.erl +++ b/src/wsSrv/elli_tcp.erl @@ -3,95 +3,95 @@ -module(elli_tcp). -export([listen/3, accept/3, recv/3, send/2, close/1, - setopts/2, sendfile/5, peername/1]). + setopts/2, sendfile/5, peername/1]). -export_type([socket/0]). -type socket() :: {plain, inet:socket()} | {ssl, ssl:sslsocket()}. listen(plain, Port, Opts) -> - case gen_tcp:listen(Port, Opts) of - {ok, Socket} -> - {ok, {plain, Socket}}; - {error, Reason} -> - {error, Reason} - end; + case gen_tcp:listen(Port, Opts) of + {ok, Socket} -> + {ok, {plain, Socket}}; + {error, Reason} -> + {error, Reason} + end; listen(ssl, Port, Opts) -> - case ssl:listen(Port, Opts) of - {ok, Socket} -> - {ok, {ssl, Socket}}; - {error, Reason} -> - {error, Reason} - end. + case ssl:listen(Port, Opts) of + {ok, Socket} -> + {ok, {ssl, Socket}}; + {error, Reason} -> + {error, Reason} + end. accept({plain, Socket}, Server, Timeout) -> - case gen_tcp:accept(Socket, Timeout) of - {ok, S} -> - gen_server:cast(Server, accepted), - {ok, {plain, S}}; - {error, Reason} -> - {error, Reason} - end; + case gen_tcp:accept(Socket, Timeout) of + {ok, S} -> + gen_server:cast(Server, accepted), + {ok, {plain, S}}; + {error, Reason} -> + {error, Reason} + end; accept({ssl, Socket}, Server, Timeout) -> - case ssl:transport_accept(Socket, Timeout) of - {ok, S} -> - handshake(S, Server, Timeout); - {error, Reason} -> - {error, Reason} - end. + case ssl:transport_accept(Socket, Timeout) of + {ok, S} -> + handshake(S, Server, Timeout); + {error, Reason} -> + {error, Reason} + end. -ifdef(post20). handshake(S, Server, Timeout) -> - case ssl:handshake(S, Timeout) of - {ok, S1} -> - gen_server:cast(Server, accepted), - {ok, {ssl, S1}}; - {error, closed} -> - {error, econnaborted}; - {error, Reason} -> - {error, Reason} - end. + case ssl:handshake(S, Timeout) of + {ok, S1} -> + gen_server:cast(Server, accepted), + {ok, {ssl, S1}}; + {error, closed} -> + {error, econnaborted}; + {error, Reason} -> + {error, Reason} + end. -else. handshake(S, Server, Timeout) -> - case ssl:ssl_accept(S, Timeout) of - ok -> - gen_server:cast(Server, accepted), - {ok, {ssl, S}}; - {error, closed} -> - {error, econnaborted}; - {error, Reason} -> - {error, Reason} - end. + case ssl:ssl_accept(S, Timeout) of + ok -> + gen_server:cast(Server, accepted), + {ok, {ssl, S}}; + {error, closed} -> + {error, econnaborted}; + {error, Reason} -> + {error, Reason} + end. -endif. recv({plain, Socket}, Size, Timeout) -> - gen_tcp:recv(Socket, Size, Timeout); + gen_tcp:recv(Socket, Size, Timeout); recv({ssl, Socket}, Size, Timeout) -> - ssl:recv(Socket, Size, Timeout). + ssl:recv(Socket, Size, Timeout). send({plain, Socket}, Data) -> - gen_tcp:send(Socket, Data); + gen_tcp:send(Socket, Data); send({ssl, Socket}, Data) -> - ssl:send(Socket, Data). + ssl:send(Socket, Data). close({plain, Socket}) -> - gen_tcp:close(Socket); + gen_tcp:close(Socket); close({ssl, Socket}) -> - ssl:close(Socket). + ssl:close(Socket). setopts({plain, Socket}, Opts) -> - inet:setopts(Socket, Opts); + inet:setopts(Socket, Opts); setopts({ssl, Socket}, Opts) -> - ssl:setopts(Socket, Opts). + ssl:setopts(Socket, Opts). sendfile(Fd, {plain, Socket}, Offset, Length, Opts) -> - file:sendfile(Fd, Socket, Offset, Length, Opts); + file:sendfile(Fd, Socket, Offset, Length, Opts); sendfile(Fd, {ssl, Socket}, Offset, Length, Opts) -> - elli_sendfile:sendfile(Fd, Socket, Offset, Length, Opts). + elli_sendfile:sendfile(Fd, Socket, Offset, Length, Opts). peername({plain, Socket}) -> - inet:peername(Socket); + inet:peername(Socket); peername({ssl, Socket}) -> - ssl:peername(Socket). + ssl:peername(Socket). diff --git a/src/wsSrv/elli_test.erl b/src/wsSrv/elli_test.erl index 0fa1fbb..51ae107 100644 --- a/src/wsSrv/elli_test.erl +++ b/src/wsSrv/elli_test.erl @@ -13,35 +13,35 @@ -export([call/5]). -spec call(Method, Path, Headers, Body, Opts) -> elli_handler:result() when - Method :: elli:http_method(), - Path :: binary(), - Headers :: elli:headers(), - Body :: elli:body(), - Opts :: proplists:proplist(). + Method :: elli:http_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 = elli_http: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). + Callback = proplists:get_value(callback, Opts), + CallbackArgs = proplists:get_value(callback_args, Opts), + Req = elli_http: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)). + ?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/wsSrv/elli_util.erl b/src/wsSrv/elli_util.erl index b8aa5cf..6875dd4 100644 --- a/src/wsSrv/elli_util.erl +++ b/src/wsSrv/elli_util.erl @@ -5,67 +5,67 @@ -include_lib("kernel/include/file.hrl"). -export([normalize_range/2 - , encode_range/2 - , file_size/1 - ]). + , encode_range/2 + , file_size/1 +]). -export_type([range/0]). --type range() :: {Offset::non_neg_integer(), Length::non_neg_integer()}. +-type range() :: {Offset :: non_neg_integer(), Length :: non_neg_integer()}. -spec normalize_range(RangeOrSet, Size) -> Normalized when - RangeOrSet :: any(), - Size :: integer(), - Normalized :: range() | undefined | invalid_range. + RangeOrSet :: any(), + Size :: integer(), + Normalized :: range() | undefined | invalid_range. %% @doc: If a valid byte-range, or byte-range-set of size 1 %% 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 -> - Length0 = erlang:min(Length, Size), - {Size - Length0, Length0}; + 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 -> - {Offset, Size - Offset}; + 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); + 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), - Offset >= 0, Length >= 0, Offset < Size -> - Length0 = erlang:min(Length, Size - Offset), - {Offset, Length0}; + 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(ByteRange, Size); normalize_range([], _Size) -> undefined; -normalize_range(_, _Size) -> invalid_range. +normalize_range(_, _Size) -> invalid_range. --spec encode_range(Range::range() | invalid_range, - Size::non_neg_integer()) -> ByteRange::iolist(). +-spec encode_range(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)]. + [<<"bytes ">>, encode_range_bytes(Range), + <<"/">>, integer_to_binary(Size)]. encode_range_bytes({Offset, Length}) -> - [integer_to_binary(Offset), - <<"-">>, - integer_to_binary(Offset + Length - 1)]; + [integer_to_binary(Offset), + <<"-">>, + integer_to_binary(Offset + Length - 1)]; encode_range_bytes(invalid_range) -> <<"*">>. -spec file_size(Filename) -> Size | {error, Reason} when - Filename :: file:name_all(), - Size :: non_neg_integer(), - Reason :: file:posix() | badarg | invalid_file. + Filename :: file:name_all(), + Size :: non_neg_integer(), + Reason :: file:posix() | badarg | invalid_file. %% @doc: Get the size in bytes of the file. file_size(Filename) -> - case file:read_file_info(Filename) of - {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. + case file:read_file_info(Filename) of + {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. diff --git a/src/wsSrv/elli_util.hrl b/src/wsSrv/elli_util.hrl index 557ec28..204fbfc 100644 --- a/src/wsSrv/elli_util.hrl +++ b/src/wsSrv/elli_util.hrl @@ -1,10 +1,9 @@ - -ifdef(OTP_RELEASE). -include_lib("kernel/include/logger.hrl"). -else. -define(LOG_ERROR(Str), error_logger:error_msg(Str)). --define(LOG_ERROR(Format,Data), error_logger:error_msg(Format, Data)). --define(LOG_INFO(Format,Data), error_logger:info_msg(Format, Data)). +-define(LOG_ERROR(Format, Data), error_logger:error_msg(Format, Data)). +-define(LOG_INFO(Format, Data), error_logger:info_msg(Format, Data)). -endif. -ifdef(OTP_RELEASE). @@ -14,4 +13,4 @@ -endif. %% Bloody useful --define(IF(Test,True,False), case Test of true -> True; false -> False end). +-define(IF(Test, True, False), case Test of true -> True; false -> False end).