diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/examples_SUITE.erl | 22 | ||||
-rw-r--r-- | test/handlers/echo_h.erl | 46 | ||||
-rw-r--r-- | test/handlers/long_polling_h.erl | 1 | ||||
-rw-r--r-- | test/handlers/loop_handler_body_h.erl | 7 | ||||
-rw-r--r-- | test/handlers/multipart_h.erl | 65 | ||||
-rw-r--r-- | test/handlers/resp_h.erl | 151 | ||||
-rw-r--r-- | test/http_SUITE.erl | 2 | ||||
-rw-r--r-- | test/http_SUITE_data/http_body_qs.erl | 2 | ||||
-rw-r--r-- | test/http_SUITE_data/http_loop_stream_recv.erl | 2 | ||||
-rw-r--r-- | test/http_SUITE_data/http_multipart.erl | 4 | ||||
-rw-r--r-- | test/http_SUITE_data/http_multipart_stream.erl | 6 | ||||
-rw-r--r-- | test/req_SUITE.erl | 444 | ||||
-rw-r--r-- | test/ws_SUITE.erl | 66 |
13 files changed, 751 insertions, 67 deletions
diff --git a/test/examples_SUITE.erl b/test/examples_SUITE.erl index b9cc7ba..56f612f 100644 --- a/test/examples_SUITE.erl +++ b/test/examples_SUITE.erl @@ -167,6 +167,7 @@ echo_get(Config) -> do_echo_get(Transport, Protocol, Config) -> {200, _, <<"this is fun">>} = do_get(Transport, Protocol, "/?echo=this+is+fun", Config), + {400, _, _} = do_get(Transport, Protocol, "/", Config), ok. %% Echo POST. @@ -393,12 +394,27 @@ websocket(_) -> Msg1 -> exit({connection_failed, Msg1}) end, + %% Check that we receive the message sent on timer on init. + receive + {gun_ws, Pid, {text, <<"Hello!">>}} -> + ok + after 2000 -> + exit(timeout) + end, + %% Check that we receive subsequent messages sent on timer. + receive + {gun_ws, Pid, {text, <<"How' you doin'?">>}} -> + ok + after 2000 -> + exit(timeout) + end, + %% Check that we receive the echoed message. gun:ws_send(Pid, {text, <<"hello">>}), receive {gun_ws, Pid, {text, <<"That's what she said! hello">>}} -> - ok; - Msg2 -> - exit({receive_failed, Msg2}) + ok + after 500 -> + exit(timeout) end, gun:ws_send(Pid, close) after diff --git a/test/handlers/echo_h.erl b/test/handlers/echo_h.erl index fd45c5f..fb7d8a8 100644 --- a/test/handlers/echo_h.erl +++ b/test/handlers/echo_h.erl @@ -12,10 +12,30 @@ init(Req, Opts) -> echo_arg(Arg, Req, Opts) end. -echo(<<"body">>, Req0, Opts) -> - {ok, Body, Req} = cowboy_req:read_body(Req0), - cowboy_req:reply(200, #{}, Body, Req), - {ok, Req, Opts}; +echo(<<"read_body">>, Req0, Opts) -> + case Opts of + #{crash := true} -> ct_helper:ignore(cowboy_req, read_body, 2); + _ -> ok + end, + {_, Body, Req} = case cowboy_req:path(Req0) of + <<"/full", _/bits>> -> read_body(Req0, <<>>); + <<"/opts", _/bits>> -> cowboy_req:read_body(Req0, Opts); + _ -> cowboy_req:read_body(Req0) + end, + {ok, cowboy_req:reply(200, #{}, Body, Req), Opts}; +echo(<<"read_urlencoded_body">>, Req0, Opts) -> + Path = cowboy_req:path(Req0), + case {Path, Opts} of + {<<"/opts", _/bits>>, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_body, 2); + {_, #{crash := true}} -> ct_helper:ignore(cowboy_req, read_urlencoded_body, 2); + _ -> ok + end, + {ok, Body, Req} = case Path of + <<"/opts", _/bits>> -> cowboy_req:read_urlencoded_body(Req0, Opts); + <<"/crash", _/bits>> -> cowboy_req:read_urlencoded_body(Req0, Opts); + _ -> cowboy_req:read_urlencoded_body(Req0) + end, + {ok, cowboy_req:reply(200, #{}, value_to_iodata(Body), Req), Opts}; echo(<<"uri">>, Req, Opts) -> Value = case cowboy_req:path_info(Req) of [<<"origin">>] -> cowboy_req:uri(Req, #{host => undefined}); @@ -25,8 +45,7 @@ echo(<<"uri">>, Req, Opts) -> [<<"set-port">>] -> cowboy_req:uri(Req, #{port => 123}); [] -> cowboy_req:uri(Req) end, - cowboy_req:reply(200, #{}, Value, Req), - {ok, Req, Opts}; + {ok, cowboy_req:reply(200, #{}, Value, Req), Opts}; echo(<<"match">>, Req, Opts) -> [Type|Fields0] = cowboy_req:path_info(Req), Fields = [binary_to_atom(F, latin1) || F <- Fields0], @@ -34,13 +53,11 @@ echo(<<"match">>, Req, Opts) -> <<"qs">> -> cowboy_req:match_qs(Fields, Req); <<"cookies">> -> cowboy_req:match_cookies(Fields, Req) end, - cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), - {ok, Req, Opts}; + {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts}; echo(What, Req, Opts) -> F = binary_to_atom(What, latin1), Value = cowboy_req:F(Req), - cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), - {ok, Req, Opts}. + {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts}. echo_arg(Arg0, Req, Opts) -> F = binary_to_atom(cowboy_req:binding(key, Req), latin1), @@ -52,8 +69,13 @@ echo_arg(Arg0, Req, Opts) -> undefined -> cowboy_req:F(Arg, Req); Default -> cowboy_req:F(Arg, Req, Default) end, - cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), - {ok, Req, Opts}. + {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts}. + +read_body(Req0, Acc) -> + case cowboy_req:read_body(Req0) of + {ok, Data, Req} -> {ok, << Acc/binary, Data/binary >>, Req}; + {more, Data, Req} -> read_body(Req, << Acc/binary, Data/binary >>) + end. value_to_iodata(V) when is_integer(V) -> integer_to_binary(V); value_to_iodata(V) when is_atom(V) -> atom_to_binary(V, latin1); diff --git a/test/handlers/long_polling_h.erl b/test/handlers/long_polling_h.erl index 4f8e23f..17afa0a 100644 --- a/test/handlers/long_polling_h.erl +++ b/test/handlers/long_polling_h.erl @@ -14,6 +14,7 @@ init(Req, _) -> {cowboy_loop, Req, 2, 5000, hibernate}. info(timeout, Req, 0) -> + %% @todo Why 102? {stop, cowboy_req:reply(102, Req), 0}; info(timeout, Req, Count) -> erlang:send_after(200, self(), timeout), diff --git a/test/handlers/loop_handler_body_h.erl b/test/handlers/loop_handler_body_h.erl index 38ba2c0..e0ea41b 100644 --- a/test/handlers/loop_handler_body_h.erl +++ b/test/handlers/loop_handler_body_h.erl @@ -13,11 +13,10 @@ init(Req, _) -> self() ! timeout, {cowboy_loop, Req, undefined, 5000, hibernate}. -info(timeout, Req, State) -> - {ok, Body, Req2} = cowboy_req:read_body(Req), +info(timeout, Req0, State) -> + {ok, Body, Req} = cowboy_req:read_body(Req0), 100000 = byte_size(Body), - cowboy_req:reply(200, Req2), - {stop, Req, State}. + {stop, cowboy_req:reply(200, Req), State}. terminate(stop, _, _) -> ok. diff --git a/test/handlers/multipart_h.erl b/test/handlers/multipart_h.erl new file mode 100644 index 0000000..1b9297b --- /dev/null +++ b/test/handlers/multipart_h.erl @@ -0,0 +1,65 @@ +%% This module reads a multipart body and echoes it back as an Erlang term. + +-module(multipart_h). + +-export([init/2]). + +init(Req0, State) -> + {Result, Req} = case cowboy_req:binding(key, Req0) of + undefined -> acc_multipart(Req0, []); + <<"skip_body">> -> skip_body_multipart(Req0, []); + <<"read_part2">> -> read_part2_multipart(Req0, []); + <<"read_part_body2">> -> read_part_body2_multipart(Req0, []) + end, + {ok, cowboy_req:reply(200, #{}, term_to_binary(Result), Req), State}. + +acc_multipart(Req0, Acc) -> + case cowboy_req:read_part(Req0) of + {ok, Headers, Req1} -> + {ok, Body, Req} = stream_body(Req1, <<>>), + acc_multipart(Req, [{Headers, Body}|Acc]); + {done, Req} -> + {lists:reverse(Acc), Req} + end. + +stream_body(Req0, Acc) -> + case cowboy_req:read_part_body(Req0) of + {more, Data, Req} -> + stream_body(Req, << Acc/binary, Data/binary >>); + {ok, Data, Req} -> + {ok, << Acc/binary, Data/binary >>, Req} + end. + +skip_body_multipart(Req0, Acc) -> + case cowboy_req:read_part(Req0) of + {ok, Headers, Req} -> + skip_body_multipart(Req, [Headers|Acc]); + {done, Req} -> + {lists:reverse(Acc), Req} + end. + +read_part2_multipart(Req0, Acc) -> + case cowboy_req:read_part(Req0, #{length => 1, period => 1}) of + {ok, Headers, Req1} -> + {ok, Body, Req} = stream_body(Req1, <<>>), + acc_multipart(Req, [{Headers, Body}|Acc]); + {done, Req} -> + {lists:reverse(Acc), Req} + end. + +read_part_body2_multipart(Req0, Acc) -> + case cowboy_req:read_part(Req0) of + {ok, Headers, Req1} -> + {ok, Body, Req} = stream_body2(Req1, <<>>), + acc_multipart(Req, [{Headers, Body}|Acc]); + {done, Req} -> + {lists:reverse(Acc), Req} + end. + +stream_body2(Req0, Acc) -> + case cowboy_req:read_part_body(Req0, #{length => 1, period => 1}) of + {more, Data, Req} -> + stream_body(Req, << Acc/binary, Data/binary >>); + {ok, Data, Req} -> + {ok, << Acc/binary, Data/binary >>, Req} + end. diff --git a/test/handlers/resp_h.erl b/test/handlers/resp_h.erl new file mode 100644 index 0000000..bb64906 --- /dev/null +++ b/test/handlers/resp_h.erl @@ -0,0 +1,151 @@ +%% This module echoes back the value the test is interested in. + +-module(resp_h). + +-export([init/2]). + +init(Req, Opts) -> + do(cowboy_req:binding(key, Req), Req, Opts). + +do(<<"set_resp_cookie3">>, Req0, Opts) -> + Req = case cowboy_req:binding(arg, Req0) of + undefined -> + cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", Req0); + <<"multiple">> -> + Req1 = cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", Req0), + cowboy_req:set_resp_cookie(<<"yourcookie">>, <<"yourvalue">>, Req1); + <<"overwrite">> -> + Req1 = cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", Req0), + cowboy_req:set_resp_cookie(<<"mycookie">>, <<"overwrite">>, Req1) + end, + {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; +do(<<"set_resp_cookie4">>, Req0, Opts) -> + Req = cowboy_req:set_resp_cookie(<<"mycookie">>, "myvalue", #{path => cowboy_req:path(Req0)}, Req0), + {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; +do(<<"set_resp_header">>, Req0, Opts) -> + Req = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Req0), + {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; +do(<<"set_resp_body">>, Req0, Opts) -> + Arg = cowboy_req:binding(arg, Req0), + Req1 = case Arg of + <<"sendfile">> -> + AppFile = code:where_is_file("cowboy.app"), + cowboy_req:set_resp_body({sendfile, 0, filelib:file_size(AppFile), AppFile}, Req0); + _ -> + cowboy_req:set_resp_body(<<"OK">>, Req0) + end, + Req = case Arg of + <<"override">> -> + cowboy_req:reply(200, #{}, <<"OVERRIDE">>, Req1); + _ -> + cowboy_req:reply(200, Req1) + end, + {ok, Req, Opts}; +do(<<"has_resp_header">>, Req0, Opts) -> + false = cowboy_req:has_resp_header(<<"content-type">>, Req0), + Req = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Req0), + true = cowboy_req:has_resp_header(<<"content-type">>, Req), + {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; +do(<<"has_resp_body">>, Req0, Opts) -> + case cowboy_req:binding(arg, Req0) of + <<"sendfile">> -> + %% @todo Cases for sendfile. Note that sendfile 0 is unallowed. + false = cowboy_req:has_resp_body(Req0), + Req = cowboy_req:set_resp_body({sendfile, 0, 10, code:where_is_file("cowboy.app")}, Req0), + true = cowboy_req:has_resp_body(Req), + {ok, cowboy_req:reply(200, #{}, <<"OK">>, Req), Opts}; + undefined -> + false = cowboy_req:has_resp_body(Req0), + Req = cowboy_req:set_resp_body(<<"OK">>, Req0), + true = cowboy_req:has_resp_body(Req), + {ok, cowboy_req:reply(200, #{}, Req), Opts} + end; +do(<<"delete_resp_header">>, Req0, Opts) -> + false = cowboy_req:has_resp_header(<<"content-type">>, Req0), + Req1 = cowboy_req:set_resp_header(<<"content-type">>, <<"text/plain">>, Req0), + true = cowboy_req:has_resp_header(<<"content-type">>, Req1), + Req = cowboy_req:delete_resp_header(<<"content-type">>, Req1), + false = cowboy_req:has_resp_header(<<"content-type">>, Req), + {ok, cowboy_req:reply(200, #{}, "OK", Req), Opts}; +do(<<"reply2">>, Req0, Opts) -> + Req = case cowboy_req:binding(arg, Req0) of + <<"binary">> -> + cowboy_req:reply(<<"200 GOOD">>, Req0); + <<"error">> -> + ct_helper:ignore(cowboy_req, reply, 4), + cowboy_req:reply(ok, Req0); + <<"twice">> -> + ct_helper:ignore(cowboy_req, reply, 4), + Req1 = cowboy_req:reply(200, Req0), + cowboy_req:reply(200, Req1); + Status -> + cowboy_req:reply(binary_to_integer(Status), Req0) + end, + {ok, Req, Opts}; +do(<<"reply3">>, Req0, Opts) -> + Req = case cowboy_req:binding(arg, Req0) of + <<"error">> -> + ct_helper:ignore(cowboy_req, reply, 4), + cowboy_req:reply(200, ok, Req0); + Status -> + cowboy_req:reply(binary_to_integer(Status), + #{<<"content-type">> => <<"text/plain">>}, Req0) + end, + {ok, Req, Opts}; +do(<<"reply4">>, Req0, Opts) -> + Req = case cowboy_req:binding(arg, Req0) of + <<"error">> -> + ct_helper:ignore(erlang, iolist_size, 1), + cowboy_req:reply(200, #{}, ok, Req0); + Status -> + cowboy_req:reply(binary_to_integer(Status), #{}, <<"OK">>, Req0) + end, + {ok, Req, Opts}; +do(<<"stream_reply2">>, Req0, Opts) -> + Req = case cowboy_req:binding(arg, Req0) of + <<"binary">> -> + cowboy_req:stream_reply(<<"200 GOOD">>, Req0); + <<"error">> -> + ct_helper:ignore(cowboy_req, stream_reply, 3), + cowboy_req:stream_reply(ok, Req0); + Status -> + cowboy_req:stream_reply(binary_to_integer(Status), Req0) + end, + stream_body(Req), + {ok, Req, Opts}; +do(<<"stream_reply3">>, Req0, Opts) -> + Req = case cowboy_req:binding(arg, Req0) of + <<"error">> -> + ct_helper:ignore(cowboy_req, stream_reply, 3), + cowboy_req:stream_reply(200, ok, Req0); + Status -> + cowboy_req:stream_reply(binary_to_integer(Status), + #{<<"content-type">> => <<"text/plain">>}, Req0) + end, + stream_body(Req), + {ok, Req, Opts}; +do(<<"stream_body">>, Req, Opts) -> + %% Call stream_body without initiating streaming. + cowboy_req:stream_body(<<0:800000>>, fin, Req), + {ok, Req, Opts}; +do(<<"push">>, Req, Opts) -> + case cowboy_req:binding(arg, Req) of + <<"method">> -> + cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req, + #{method => <<"HEAD">>}); + <<"origin">> -> + cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req, + #{scheme => <<"ftp">>, host => <<"127.0.0.1">>, port => 21}); + <<"qs">> -> + cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req, + #{qs => <<"server=cowboy&version=2.0">>}); + _ -> + cowboy_req:push("/static/style.css", #{<<"accept">> => <<"text/css">>}, Req), + %% The text/plain mime is not defined by default, so a 406 will be returned. + cowboy_req:push("/static/plain.txt", #{<<"accept">> => <<"text/plain">>}, Req) + end, + {ok, cowboy_req:reply(200, Req), Opts}. + +stream_body(Req) -> + _ = [cowboy_req:stream_body(<<0:800000>>, nofin, Req) || _ <- lists:seq(1,9)], + cowboy_req:stream_body(<<0:800000>>, fin, Req). diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 1b39feb..f0670a9 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -945,7 +945,7 @@ do_body_to_chunks(ChunkSize, Body, Acc) -> false -> ChunkSize end, << Chunk:ChunkSize2/binary, Rest/binary >> = Body, - ChunkSizeBin = list_to_binary(integer_to_list(ChunkSize2, 16)), + ChunkSizeBin = integer_to_binary(ChunkSize2, 16), do_body_to_chunks(ChunkSize, Rest, [<< ChunkSizeBin/binary, "\r\n", Chunk/binary, "\r\n" >>|Acc]). diff --git a/test/http_SUITE_data/http_body_qs.erl b/test/http_SUITE_data/http_body_qs.erl index e0673cf..09ca5e4 100644 --- a/test/http_SUITE_data/http_body_qs.erl +++ b/test/http_SUITE_data/http_body_qs.erl @@ -10,7 +10,7 @@ init(Req, Opts) -> {ok, maybe_echo(Method, HasBody, Req), Opts}. maybe_echo(<<"POST">>, true, Req) -> - case cowboy_req:body_qs(Req) of + case cowboy_req:read_urlencoded_body(Req) of {badlength, Req2} -> echo(badlength, Req2); {ok, PostVals, Req2} -> diff --git a/test/http_SUITE_data/http_loop_stream_recv.erl b/test/http_SUITE_data/http_loop_stream_recv.erl index c006b6d..18b3d29 100644 --- a/test/http_SUITE_data/http_loop_stream_recv.erl +++ b/test/http_SUITE_data/http_loop_stream_recv.erl @@ -15,7 +15,7 @@ info(stream, Req, undefined) -> stream(Req, 1, <<>>). stream(Req, ID, Acc) -> - case cowboy_req:body(Req) of + case cowboy_req:read_body(Req) of {ok, <<>>, Req2} -> {stop, cowboy_req:reply(200, Req2), undefined}; {_, Data, Req2} -> diff --git a/test/http_SUITE_data/http_multipart.erl b/test/http_SUITE_data/http_multipart.erl index 212f569..5a4c6be 100644 --- a/test/http_SUITE_data/http_multipart.erl +++ b/test/http_SUITE_data/http_multipart.erl @@ -9,9 +9,9 @@ init(Req, Opts) -> {ok, cowboy_req:reply(200, #{}, term_to_binary(Result), Req2), Opts}. acc_multipart(Req, Acc) -> - case cowboy_req:part(Req) of + case cowboy_req:read_part(Req) of {ok, Headers, Req2} -> - {ok, Body, Req3} = cowboy_req:part_body(Req2), + {ok, Body, Req3} = cowboy_req:read_part_body(Req2), acc_multipart(Req3, [{Headers, Body}|Acc]); {done, Req2} -> {lists:reverse(Acc), Req2} diff --git a/test/http_SUITE_data/http_multipart_stream.erl b/test/http_SUITE_data/http_multipart_stream.erl index 82662ad..88cf611 100644 --- a/test/http_SUITE_data/http_multipart_stream.erl +++ b/test/http_SUITE_data/http_multipart_stream.erl @@ -9,9 +9,9 @@ init(Req, Opts) -> {ok, cowboy_req:reply(200, Req2), Opts}. multipart(Req) -> - case cowboy_req:part(Req) of + case cowboy_req:read_part(Req) of {ok, [{<<"content-length">>, BinLength}], Req2} -> - Length = list_to_integer(binary_to_list(BinLength)), + Length = binary_to_integer(BinLength), {Length, Req3} = stream_body(Req2, 0), multipart(Req3); {done, Req2} -> @@ -19,7 +19,7 @@ multipart(Req) -> end. stream_body(Req, N) -> - case cowboy_req:part_body(Req) of + case cowboy_req:read_part_body(Req) of {ok, Data, Req2} -> {N + byte_size(Data), Req2}; {more, Data, Req2} -> diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl index 648ebcd..b0aabad 100644 --- a/test/req_SUITE.erl +++ b/test/req_SUITE.erl @@ -34,6 +34,13 @@ groups() -> %% @todo With compression enabled. ]. +init_per_suite(Config) -> + ct_helper:create_static_dir(config(priv_dir, Config) ++ "/static"), + Config. + +end_per_suite(Config) -> + ct_helper:delete_static_dir(config(priv_dir, Config) ++ "/static"). + init_per_group(Name, Config) -> cowboy_test:init_common_groups(Name, Config, ?MODULE). @@ -42,10 +49,20 @@ end_per_group(Name, _) -> %% Routes. -init_dispatch(_) -> +init_dispatch(Config) -> cowboy_router:compile([{"[...]", [ - {"/no/:key", echo_h, []}, + {"/static/[...]", cowboy_static, {dir, config(priv_dir, Config) ++ "/static"}}, + %% @todo Seriously InitialState should be optional. + {"/resp/:key[/:arg]", resp_h, []}, + {"/multipart[/:key]", multipart_h, []}, {"/args/:key/:arg[/:default]", echo_h, []}, + {"/crash/:key/period", echo_h, #{length => infinity, period => 1000, crash => true}}, + {"/no-opts/:key", echo_h, #{crash => true}}, + {"/opts/:key/length", echo_h, #{length => 1000}}, + {"/opts/:key/period", echo_h, #{length => infinity, period => 1000}}, + {"/opts/:key/timeout", echo_h, #{timeout => 1000, crash => true}}, + {"/full/:key", echo_h, []}, + {"/no/:key", echo_h, []}, {"/:key/[...]", echo_h, []} ]}]). @@ -55,15 +72,32 @@ do_body(Method, Path, Config) -> do_body(Method, Path, [], Config). do_body(Method, Path, Headers, Config) -> + do_body(Method, Path, Headers, <<>>, Config). + +do_body(Method, Path, Headers, Body, Config) -> ConnPid = gun_open(Config), - Ref = gun:request(ConnPid, Method, Path, Headers), + Ref = case Body of + <<>> -> gun:request(ConnPid, Method, Path, Headers); + _ -> gun:request(ConnPid, Method, Path, Headers, Body) + end, {response, IsFin, 200, _} = gun:await(ConnPid, Ref), - {ok, Body} = case IsFin of + {ok, RespBody} = case IsFin of nofin -> gun:await_body(ConnPid, Ref); fin -> {ok, <<>>} end, gun:close(ConnPid), - Body. + RespBody. + +do_get(Path, Config) -> + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, Path, []), + {response, IsFin, Status, Headers} = gun:await(ConnPid, Ref), + {ok, RespBody} = case IsFin of + nofin -> gun:await_body(ConnPid, Ref); + fin -> {ok, <<>>} + end, + gun:close(ConnPid), + {Status, Headers, RespBody}. do_get_body(Path, Config) -> do_get_body(Path, [], Config). @@ -71,7 +105,7 @@ do_get_body(Path, Config) -> do_get_body(Path, Headers, Config) -> do_body("GET", Path, Headers, Config). -%% Tests. +%% Tests: Request. binding(Config) -> doc("Value bound from request URI path with/without default."), @@ -109,6 +143,7 @@ host_info(Config) -> <<"[<<\"localhost\">>]">> = do_get_body("/host_info", Config), ok. +%% @todo Actually write the related unit tests. match_cookies(Config) -> doc("Matched request cookies."), <<"#{}">> = do_get_body("/match/cookies", [{<<"cookie">>, "a=b; c=d"}], Config), @@ -119,6 +154,7 @@ match_cookies(Config) -> %% This function is tested more extensively through unit tests. ok. +%% @todo Actually write the related unit tests. match_qs(Config) -> doc("Matched request URI query string."), <<"#{}">> = do_get_body("/match/qs?a=b&c=d", Config), @@ -131,7 +167,7 @@ match_qs(Config) -> method(Config) -> doc("Request method."), <<"GET">> = do_body("GET", "/method", Config), - <<"HEAD">> = do_body("HEAD", "/method", Config), + <<>> = do_body("HEAD", "/method", Config), <<"OPTIONS">> = do_body("OPTIONS", "/method", Config), <<"PATCH">> = do_body("PATCH", "/method", Config), <<"POST">> = do_body("POST", "/method", Config), @@ -147,6 +183,9 @@ parse_cookies(Config) -> = do_get_body("/parse_cookies", [{<<"cookie">>, "cake=strawberry"}], Config), <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">> = do_get_body("/parse_cookies", [{<<"cookie">>, "cake=strawberry; color=blue"}], Config), + <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">> + = do_get_body("/parse_cookies", + [{<<"cookie">>, "cake=strawberry"}, {<<"cookie">>, "color=blue"}], Config), ok. parse_header(Config) -> @@ -252,3 +291,394 @@ version(Config) -> <<"HTTP/1.1">> when Protocol =:= http -> ok; <<"HTTP/2">> when Protocol =:= http2 -> ok end. + +%% Tests: Request body. + +body_length(Config) -> + doc("Request body length."), + <<"0">> = do_get_body("/body_length", Config), + <<"12">> = do_body("POST", "/body_length", [], "hello world!", Config), + ok. + +has_body(Config) -> + doc("Has a request body?"), + <<"false">> = do_get_body("/has_body", Config), + <<"true">> = do_body("POST", "/has_body", [], "hello world!", Config), + ok. + +read_body(Config) -> + doc("Request body."), + <<>> = do_get_body("/read_body", Config), + <<"hello world!">> = do_body("POST", "/read_body", [], "hello world!", Config), + %% We expect to have read *at least* 1000 bytes. + <<0:8000, _/bits>> = do_body("POST", "/opts/read_body/length", [], <<0:8000000>>, Config), + %% We read any length for at most 1 second. + %% + %% The body is sent twice, first with nofin, then wait 2 seconds, then again with fin. + <<0:8000000>> = do_read_body_period("/opts/read_body/period", <<0:8000000>>, Config), + %% The timeout value is set too low on purpose to ensure a crash occurs. + ok = do_read_body_timeout("/opts/read_body/timeout", <<0:8000000>>, Config), + %% 10MB body larger than default length. + <<0:80000000>> = do_body("POST", "/full/read_body", [], <<0:80000000>>, Config), + ok. + +do_read_body_period(Path, Body, Config) -> + ConnPid = gun_open(Config), + Ref = gun:request(ConnPid, "POST", Path, [ + {<<"content-length">>, integer_to_binary(byte_size(Body) * 2)} + ]), + gun:data(ConnPid, Ref, nofin, Body), + timer:sleep(2000), + gun:data(ConnPid, Ref, fin, Body), + {response, nofin, 200, _} = gun:await(ConnPid, Ref), + {ok, RespBody} = gun:await_body(ConnPid, Ref), + gun:close(ConnPid), + RespBody. + +%% We expect a crash. +do_read_body_timeout(Path, Body, Config) -> + ConnPid = gun_open(Config), + Ref = gun:request(ConnPid, "POST", Path, [ + {<<"content-length">>, integer_to_binary(byte_size(Body))} + ]), + {response, _, 500, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +%% @todo Do we really want a key/value list here instead of a map? +read_urlencoded_body(Config) -> + doc("application/x-www-form-urlencoded request body."), + <<"[]">> = do_body("POST", "/read_urlencoded_body", [], <<>>, Config), + <<"[{<<\"abc\">>,true}]">> = do_body("POST", "/read_urlencoded_body", [], "abc", Config), + <<"[{<<\"a\">>,<<\"b\">>},{<<\"c\">>,<<\"d e\">>}]">> + = do_body("POST", "/read_urlencoded_body", [], "a=b&c=d+e", Config), + %% Send a 10MB body, larger than the default length, to ensure a crash occurs. + ok = do_read_urlencoded_body_too_large("/no-opts/read_urlencoded_body", + string:chars($a, 10000000), Config), + %% We read any length for at most 1 second. + %% + %% The body is sent twice, first with nofin, then wait 1.1 second, then again with fin. + %% We expect the handler to crash because read_urlencoded_body expects the full body. + ok = do_read_urlencoded_body_too_long("/crash/read_urlencoded_body/period", <<"abc">>, Config), + %% The timeout value is set too low on purpose to ensure a crash occurs. + ok = do_read_body_timeout("/opts/read_urlencoded_body/timeout", <<"abc">>, Config), + ok. + +%% We expect a crash. +do_read_urlencoded_body_too_large(Path, Body, Config) -> + ConnPid = gun_open(Config), + Ref = gun:request(ConnPid, "POST", Path, [ + {<<"content-length">>, integer_to_binary(iolist_size(Body))} + ]), + gun:data(ConnPid, Ref, fin, Body), + {response, _, 500, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +%% We expect a crash. +do_read_urlencoded_body_too_long(Path, Body, Config) -> + ConnPid = gun_open(Config), + Ref = gun:request(ConnPid, "POST", Path, [ + {<<"content-length">>, integer_to_binary(byte_size(Body) * 2)} + ]), + gun:data(ConnPid, Ref, nofin, Body), + timer:sleep(1100), + gun:data(ConnPid, Ref, fin, Body), + {response, _, 500, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +multipart(Config) -> + doc("Multipart request body."), + do_multipart("/multipart", Config). + +do_multipart(Path, Config) -> + LargeBody = iolist_to_binary(string:chars($a, 10000000)), + ReqBody = [ + "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n" + "--deadbeef\r\nContent-Type: application/octet-stream\r\nX-Custom: value\r\n\r\n", LargeBody, "\r\n" + "--deadbeef--" + ], + RespBody = do_body("POST", Path, [ + {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>} + ], ReqBody, Config), + [ + {[{<<"content-type">>, <<"text/plain">>}], <<"Cowboy is an HTTP server.">>}, + {LargeHeaders, LargeBody} + ] = binary_to_term(RespBody), + %% @todo Multipart header order is currently undefined. + [ + {<<"content-type">>, <<"application/octet-stream">>}, + {<<"x-custom">>, <<"value">>} + ] = lists:sort(LargeHeaders), + ok. + +read_part_skip_body(Config) -> + doc("Multipart request body skipping part bodies."), + LargeBody = iolist_to_binary(string:chars($a, 10000000)), + ReqBody = [ + "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n" + "--deadbeef\r\nContent-Type: application/octet-stream\r\nX-Custom: value\r\n\r\n", LargeBody, "\r\n" + "--deadbeef--" + ], + RespBody = do_body("POST", "/multipart/skip_body", [ + {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>} + ], ReqBody, Config), + [ + [{<<"content-type">>, <<"text/plain">>}], + LargeHeaders + ] = binary_to_term(RespBody), + %% @todo Multipart header order is currently undefined. + [ + {<<"content-type">>, <<"application/octet-stream">>}, + {<<"x-custom">>, <<"value">>} + ] = lists:sort(LargeHeaders), + ok. + +%% @todo When reading a multipart body, length and period +%% only apply to a single read_body call. We may want a +%% separate option to know how many reads we want to do +%% before we give up. + +read_part2(Config) -> + doc("Multipart request body using read_part/2."), + %% Override the length and period values only, making + %% the request process use more read_body calls. + %% + %% We do not try a custom timeout value since this would + %% be the same test as read_body/2. + do_multipart("/multipart/read_part2", Config). + +read_part_body2(Config) -> + doc("Multipart request body using read_part_body/2."), + %% Override the length and period values only, making + %% the request process use more read_body calls. + %% + %% We do not try a custom timeout value since this would + %% be the same test as read_body/2. + do_multipart("/multipart/read_part_body2", Config). + +%% Tests: Response. + +%% @todo We want to crash when calling set_resp_* or related +%% functions after the reply has been sent. + +set_resp_cookie(Config) -> + doc("Response using set_resp_cookie."), + %% Single cookie, no options. + {200, Headers1, _} = do_get("/resp/set_resp_cookie3", Config), + {_, <<"mycookie=myvalue; Version=1">>} + = lists:keyfind(<<"set-cookie">>, 1, Headers1), + %% Single cookie, with options. + {200, Headers2, _} = do_get("/resp/set_resp_cookie4", Config), + {_, <<"mycookie=myvalue; Version=1; Path=/resp/set_resp_cookie4">>} + = lists:keyfind(<<"set-cookie">>, 1, Headers2), + %% Multiple cookies. + {200, Headers3, _} = do_get("/resp/set_resp_cookie3/multiple", Config), + [_, _] = [H || H={<<"set-cookie">>, _} <- Headers3], + %% Overwrite previously set cookie. + {200, Headers4, _} = do_get("/resp/set_resp_cookie3/overwrite", Config), + {_, <<"mycookie=overwrite; Version=1">>} + = lists:keyfind(<<"set-cookie">>, 1, Headers4), + ok. + +set_resp_header(Config) -> + doc("Response using set_resp_header."), + {200, Headers, <<"OK">>} = do_get("/resp/set_resp_header", Config), + true = lists:keymember(<<"content-type">>, 1, Headers), + ok. + +set_resp_body(Config) -> + doc("Response using set_resp_body."), + {200, _, <<"OK">>} = do_get("/resp/set_resp_body", Config), + {200, _, <<"OVERRIDE">>} = do_get("/resp/set_resp_body/override", Config), + {ok, AppFile} = file:read_file(code:where_is_file("cowboy.app")), + {200, _, AppFile} = do_get("/resp/set_resp_body/sendfile", Config), + ok. + +has_resp_header(Config) -> + doc("Has response header?"), + {200, Headers, <<"OK">>} = do_get("/resp/has_resp_header", Config), + true = lists:keymember(<<"content-type">>, 1, Headers), + ok. + +has_resp_body(Config) -> + doc("Has response body?"), + {200, _, <<"OK">>} = do_get("/resp/has_resp_body", Config), + {200, _, <<"OK">>} = do_get("/resp/has_resp_body/sendfile", Config), + ok. + +delete_resp_header(Config) -> + doc("Delete response header."), + {200, Headers, <<"OK">>} = do_get("/resp/delete_resp_header", Config), + false = lists:keymember(<<"content-type">>, 1, Headers), + ok. + +reply2(Config) -> + doc("Response with default headers and no body."), + {200, _, _} = do_get("/resp/reply2/200", Config), + {201, _, _} = do_get("/resp/reply2/201", Config), + {404, _, _} = do_get("/resp/reply2/404", Config), + {200, _, _} = do_get("/resp/reply2/binary", Config), + {500, _, _} = do_get("/resp/reply2/error", Config), + %% @todo We want to crash when reply or stream_reply is called twice. + %% How to test this properly? This isn't enough. + {200, _, _} = do_get("/resp/reply2/twice", Config), + ok. + +reply3(Config) -> + doc("Response with additional headers and no body."), + {200, Headers1, _} = do_get("/resp/reply3/200", Config), + true = lists:keymember(<<"content-type">>, 1, Headers1), + {201, Headers2, _} = do_get("/resp/reply3/201", Config), + true = lists:keymember(<<"content-type">>, 1, Headers2), + {404, Headers3, _} = do_get("/resp/reply3/404", Config), + true = lists:keymember(<<"content-type">>, 1, Headers3), + {500, _, _} = do_get("/resp/reply3/error", Config), + ok. + +reply4(Config) -> + doc("Response with additional headers and body."), + {200, _, <<"OK">>} = do_get("/resp/reply4/200", Config), + {201, _, <<"OK">>} = do_get("/resp/reply4/201", Config), + {404, _, <<"OK">>} = do_get("/resp/reply4/404", Config), + {500, _, _} = do_get("/resp/reply4/error", Config), + ok. + +%% @todo Crash when stream_reply is called twice. + +stream_reply2(Config) -> + doc("Response with default headers and streamed body."), + Body = <<0:8000000>>, + {200, _, Body} = do_get("/resp/stream_reply2/200", Config), + {201, _, Body} = do_get("/resp/stream_reply2/201", Config), + {404, _, Body} = do_get("/resp/stream_reply2/404", Config), + {200, _, Body} = do_get("/resp/stream_reply2/binary", Config), + {500, _, _} = do_get("/resp/stream_reply2/error", Config), + ok. + +stream_reply3(Config) -> + doc("Response with additional headers and streamed body."), + Body = <<0:8000000>>, + {200, Headers1, Body} = do_get("/resp/stream_reply3/200", Config), + true = lists:keymember(<<"content-type">>, 1, Headers1), + {201, Headers2, Body} = do_get("/resp/stream_reply3/201", Config), + true = lists:keymember(<<"content-type">>, 1, Headers2), + {404, Headers3, Body} = do_get("/resp/stream_reply3/404", Config), + true = lists:keymember(<<"content-type">>, 1, Headers3), + {500, _, _} = do_get("/resp/stream_reply3/error", Config), + ok. + +%% @todo Crash when calling stream_body after the fin flag has been set. +%% @todo Crash when calling stream_body after calling reply. +%% @todo Crash when calling stream_body before calling stream_reply. + +%% Tests: Push. + +%% @todo We want to crash when push is called after reply has been initiated. + +push(Config) -> + case config(protocol, Config) of + http -> do_push_http("/resp/push", Config); + http2 -> do_push_http2(Config) + end. + +push_method(Config) -> + case config(protocol, Config) of + http -> do_push_http("/resp/push/method", Config); + http2 -> do_push_http2_method(Config) + end. + + +push_origin(Config) -> + case config(protocol, Config) of + http -> do_push_http("/resp/push/origin", Config); + http2 -> do_push_http2_origin(Config) + end. + +push_qs(Config) -> + case config(protocol, Config) of + http -> do_push_http("/resp/push/qs", Config); + http2 -> do_push_http2_qs(Config) + end. + +do_push_http(Path, Config) -> + doc("Ignore pushed responses when protocol is HTTP/1.1."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, Path, []), + {response, fin, 200, _} = gun:await(ConnPid, Ref), + ok. + +do_push_http2(Config) -> + doc("Pushed responses."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/resp/push", []), + %% We expect two pushed resources. + Origin = iolist_to_binary([ + case config(type, Config) of + tcp -> "http"; + ssl -> "https" + end, + "://localhost:", + integer_to_binary(config(port, Config)) + ]), + OriginLen = byte_size(Origin), + {push, PushCSS, <<"GET">>, <<Origin:OriginLen/binary, "/static/style.css">>, + [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref), + {push, PushTXT, <<"GET">>, <<Origin:OriginLen/binary, "/static/plain.txt">>, + [{<<"accept">>,<<"text/plain">>}]} = gun:await(ConnPid, Ref), + %% Pushed CSS. + {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS), + {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), + {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS), + %% Pushed TXT is 406 because the pushed accept header uses an undefined type. + {response, fin, 406, _} = gun:await(ConnPid, PushTXT), + %% Let's not forget about the response to the client's request. + {response, fin, 200, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +do_push_http2_method(Config) -> + doc("Pushed response with non-GET method."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/resp/push/method", []), + %% Pushed CSS. + {push, PushCSS, <<"HEAD">>, _, [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref), + {response, fin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS), + {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), + %% Let's not forget about the response to the client's request. + {response, fin, 200, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +do_push_http2_origin(Config) -> + doc("Pushed response with custom scheme/host/port."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/resp/push/origin", []), + %% Pushed CSS. + {push, PushCSS, <<"GET">>, <<"ftp://127.0.0.1:21/static/style.css">>, + [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref), + {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS), + {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), + {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS), + %% Let's not forget about the response to the client's request. + {response, fin, 200, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). + +do_push_http2_qs(Config) -> + doc("Pushed response with query string."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/resp/push/qs", []), + %% Pushed CSS. + Origin = iolist_to_binary([ + case config(type, Config) of + tcp -> "http"; + ssl -> "https" + end, + "://localhost:", + integer_to_binary(config(port, Config)) + ]), + OriginLen = byte_size(Origin), + {push, PushCSS, <<"GET">>, <<Origin:OriginLen/binary, "/static/style.css?server=cowboy&version=2.0">>, + [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref), + {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS), + {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS), + {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS), + %% Let's not forget about the response to the client's request. + {response, fin, 200, _} = gun:await(ConnPid, Ref), + gun:close(ConnPid). diff --git a/test/ws_SUITE.erl b/test/ws_SUITE.erl index f15c9fe..b4eebf7 100644 --- a/test/ws_SUITE.erl +++ b/test/ws_SUITE.erl @@ -39,14 +39,14 @@ init_per_group(Name = autobahn, Config) -> _ -> {ok, _} = cowboy:start_clear(Name, 100, [{port, 33080}], #{ env => #{dispatch => init_dispatch()}, - compress => true %% @todo Use a separate option for HTTP and Websocket compression. + websocket_compress => true }), Config end; init_per_group(Name = ws, Config) -> cowboy_test:init_http(Name, #{ env => #{dispatch => init_dispatch()}, - compress => true %% @todo Use a separate option for HTTP and Websocket compression. + websocket_compress => true }, Config). end_per_group(Listener, _Config) -> @@ -78,9 +78,9 @@ init_dispatch() -> {close, 1001, <<"some text!">>}, {text, <<"won't be received">>}]} ]}, + {"/ws_subprotocol", ws_subprotocol, []}, {"/ws_timeout_hibernate", ws_timeout_hibernate, []}, - {"/ws_timeout_cancel", ws_timeout_cancel, []}, - {"/ws_subprotocol", ws_subprotocol, []} + {"/ws_timeout_cancel", ws_timeout_cancel, []} ]} ]). @@ -523,6 +523,35 @@ ws_send_many(Config) -> {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. +ws_subprotocol(Config) -> + {port, Port} = lists:keyfind(port, 1, Config), + {ok, Socket} = gen_tcp:connect("localhost", Port, + [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, [ + "GET /ws_subprotocol HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: Upgrade\r\n" + "Upgrade: websocket\r\n" + "Sec-WebSocket-Origin: http://localhost\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + "Sec-WebSocket-Protocol: foo, bar\r\n" + "\r\n"]), + {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), + {ok, {http_response, {1, 1}, 101, "Switching Protocols"}, Rest} + = erlang:decode_packet(http, Handshake, []), + [Headers, <<>>] = do_decode_headers( + erlang:decode_packet(httph, Rest, []), []), + {'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers), + {'Upgrade', "websocket"} = lists:keyfind('Upgrade', 1, Headers), + {"sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="} + = lists:keyfind("sec-websocket-accept", 1, Headers), + {"sec-websocket-protocol", "foo"} + = lists:keyfind("sec-websocket-protocol", 1, Headers), + {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000), + {error, closed} = gen_tcp:recv(Socket, 0, 6000), + ok. + ws_text_fragments(Config) -> {port, Port} = lists:keyfind(port, 1, Config), {ok, Socket} = gen_tcp:connect("localhost", Port, @@ -664,35 +693,6 @@ ws_timeout_reset(Config) -> {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. -ws_subprotocol(Config) -> - {port, Port} = lists:keyfind(port, 1, Config), - {ok, Socket} = gen_tcp:connect("localhost", Port, - [binary, {active, false}, {packet, raw}]), - ok = gen_tcp:send(Socket, [ - "GET /ws_subprotocol HTTP/1.1\r\n" - "Host: localhost\r\n" - "Connection: Upgrade\r\n" - "Upgrade: websocket\r\n" - "Sec-WebSocket-Origin: http://localhost\r\n" - "Sec-WebSocket-Version: 13\r\n" - "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" - "Sec-WebSocket-Protocol: foo, bar\r\n" - "\r\n"]), - {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), - {ok, {http_response, {1, 1}, 101, "Switching Protocols"}, Rest} - = erlang:decode_packet(http, Handshake, []), - [Headers, <<>>] = do_decode_headers( - erlang:decode_packet(httph, Rest, []), []), - {'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers), - {'Upgrade', "websocket"} = lists:keyfind('Upgrade', 1, Headers), - {"sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="} - = lists:keyfind("sec-websocket-accept", 1, Headers), - {"sec-websocket-protocol", "foo"} - = lists:keyfind("sec-websocket-protocol", 1, Headers), - {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000), - {error, closed} = gen_tcp:recv(Socket, 0, 6000), - ok. - %% Internal. do_decode_headers({ok, http_eoh, Rest}, Acc) -> |