%%% Copyright 2009 Jack Danger Canty . All rights reserved. %%% %%% Permission is hereby granted, free of charge, to any person obtaining %%% a copy of this software and associated documentation files (the %%% "Software"), to deal in the Software without restriction, including %%% without limitation the rights to use, copy, modify, merge, publish, %%% distribute, sublicense, and/or sell copies of the Software, and to %%% permit persons to whom the Software is furnished to do so, subject to %%% the following conditions: %%% %%% The above copyright notice and this permission notice shall be %%% included in all copies or substantial portions of the Software. %%% %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE %%% LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION %%% OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION %%% WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. %% @doc Facilitates transparent gen_tcp/ssl socket handling -module(smtp_socket). -define(TCP_LISTEN_OPTIONS,[ {active, false}, {backlog, 30}, {ip,{0,0,0,0}}, {keepalive, true}, {packet, line}, {reuseaddr, true}]). -define(TCP_CONNECT_OPTIONS,[ {active, false}, {packet, line}, {ip, {0,0,0,0}}, {port, 0}]). -define(SSL_LISTEN_OPTIONS, [ {active, false}, {backlog, 30}, {certfile, "server.crt"}, {depth, 0}, {keepalive, true}, {keyfile, "server.key"}, {packet, line}, {reuse_sessions, false}, {reuseaddr, true}, {ssl_imp, new}]). -define(SSL_CONNECT_OPTIONS,[ {active, false}, {depth, 0}, {packet, line}, {ip, {0,0,0,0}}, {versions, ['tlsv1', 'tlsv1.1', 'tlsv1.2']}, {port, 0}]). -define(SSL_CONNECT_OPTIONS_KEYS, [ciphers]). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. %% API -export([connect/3, connect/4, connect/5]). -export([listen/2, listen/3, accept/1, accept/2]). -export([send/2, recv/2, recv/3]). -export([controlling_process/2]). -export([peername/1]). -export([close/1, shutdown/2]). -export([active_once/1]). -export([setopts/2]). -export([get_proto/1]). -export([begin_inet_async/1]). -export([handle_inet_async/1, handle_inet_async/2, handle_inet_async/3]). -export([extract_port_from_socket/1]). -export([to_ssl_server/1,to_ssl_server/2,to_ssl_server/3]). -export([to_ssl_client/1,to_ssl_client/2,to_ssl_client/3]). -export([type/1]). -type protocol() :: 'tcp' | 'ssl'. -type address() :: inet:ip_address() | string() | binary(). -type socket() :: ssl:sslsocket() | gen_tcp:socket(). -export_type([socket/0]). %%%----------------------------------------------------------------- %%% API %%%----------------------------------------------------------------- -spec connect(Protocol :: protocol(), Address :: address(), Port :: pos_integer()) -> {ok, socket()} | {error, any()}. connect(Protocol, Address, Port) -> connect(Protocol, Address, Port, [], infinity). -spec connect(Protocol :: protocol(), Address :: address(), Port :: pos_integer(), Options :: list()) -> {ok, socket()} | {error, any()}. connect(Protocol, Address, Port, Opts) -> connect(Protocol, Address, Port, Opts, infinity). -spec connect(Protocol :: protocol(), Address :: address(), Port :: pos_integer(), Options :: list(), Time :: non_neg_integer() | 'infinity') -> {ok, socket()} | {error, any()}. connect(tcp, Address, Port, Opts, Time) -> gen_tcp:connect(Address, Port, tcp_connect_options(Opts), Time); connect(ssl, Address, Port, Opts, Time) -> ssl:connect(Address, Port, ssl_connect_options(Opts), Time). -spec listen(Protocol :: protocol(), Port :: pos_integer()) -> {ok, socket()} | {error, any()}. listen(Protocol, Port) -> listen(Protocol, Port, []). -spec listen(Protocol :: protocol(), Port :: pos_integer(), Options :: list()) -> {ok, socket()} | {error, any()}. listen(ssl, Port, Options) -> ssl:listen(Port, ssl_listen_options(Options)); listen(tcp, Port, Options) -> gen_tcp:listen(Port, tcp_listen_options(Options)). -spec accept(Socket :: socket()) -> {'ok', socket()} | {'error', any()}. accept(Socket) -> accept(Socket, infinity). -spec accept(Socket :: socket(), Timeout :: pos_integer() | 'infinity') -> {'ok', socket()} | {'error', any()}. accept(Socket, Timeout) when is_port(Socket) -> case gen_tcp:accept(Socket, Timeout) of {ok, NewSocket} -> {ok, Opts} = inet:getopts(Socket, [active,keepalive,packet,reuseaddr]), inet:setopts(NewSocket, Opts), {ok, NewSocket}; {error, _} = Error -> Error end; accept(Socket, Timeout) -> case ssl:transport_accept(Socket, Timeout) of {ok, NewSocket} -> ssl_handshake(NewSocket); {error, _} = Error -> Error end. -ifdef(OTP_RELEASE). ssl_handshake(Socket) -> ssl:handshake(Socket). ssl_handshake(Socket, Options, Timeout) -> ssl:handshake(Socket, Options, Timeout). -else. ssl_handshake(Socket) -> case ssl:ssl_accept(Socket) of ok -> {ok, Socket}; {error, _} = Error -> Error end. ssl_handshake(Socket, Options, Timeout) -> case ssl:ssl_accept(Socket, Options, Timeout) of {ok, _} = OK -> OK; {error, _} = Error -> Error end. -endif. -spec send(Socket :: socket(), Data :: binary() | string() | iolist()) -> 'ok' | {'error', any()}. send(Socket, Data) when is_port(Socket) -> gen_tcp:send(Socket, Data); send(Socket, Data) -> ssl:send(Socket, Data). -spec recv(Socket :: socket(), Length :: non_neg_integer()) -> {'ok', any()} | {'error', any()}. recv(Socket, Length) -> recv(Socket, Length, infinity). -spec recv(Socket :: socket(), Length :: non_neg_integer(), Timeout :: non_neg_integer() | 'infinity') -> {'ok', any()} | {'error', any()}. recv(Socket, Length, Timeout) when is_port(Socket) -> gen_tcp:recv(Socket, Length, Timeout); recv(Socket, Length, Timeout) -> ssl:recv(Socket, Length, Timeout). -spec controlling_process(Socket :: socket(), NewOwner :: pid()) -> 'ok' | {'error', any()}. controlling_process(Socket, NewOwner) when is_port(Socket) -> gen_tcp:controlling_process(Socket, NewOwner); controlling_process(Socket, NewOwner) -> ssl:controlling_process(Socket, NewOwner). -spec peername(Socket :: socket()) -> {ok, {inet:ip_address(), non_neg_integer()}} | {'error', any()}. peername(Socket) when is_port(Socket) -> inet:peername(Socket); peername(Socket) -> ssl:peername(Socket). -spec close(Socket :: socket()) -> 'ok'. close(Socket) when is_port(Socket) -> gen_tcp:close(Socket); close(Socket) -> ssl:close(Socket). -spec shutdown(Socket :: socket(), How :: 'read' | 'write' | 'read_write') -> 'ok' | {'error', any()}. shutdown(Socket, How) when is_port(Socket) -> gen_tcp:shutdown(Socket, How); shutdown(Socket, How) -> ssl:shutdown(Socket, How). -spec active_once(Socket :: socket()) -> 'ok' | {'error', any()}. active_once(Socket) when is_port(Socket) -> inet:setopts(Socket, [{active, once}]); active_once(Socket) -> ssl:setopts(Socket, [{active, once}]). -spec setopts(Socket :: socket(), Options :: list()) -> 'ok' | {'error', any()}. setopts(Socket, Options) when is_port(Socket) -> inet:setopts(Socket, Options); setopts(Socket, Options) -> ssl:setopts(Socket, Options). -spec get_proto(Socket :: any()) -> 'tcp' | 'ssl'. get_proto(Socket) when is_port(Socket) -> tcp; get_proto(_Socket) -> ssl. %% @doc {inet_async,...} will be sent to current process when a client connects -spec begin_inet_async(Socket :: socket()) -> any(). begin_inet_async(Socket) when is_port(Socket) -> prim_inet:async_accept(Socket, -1); begin_inet_async(Socket) -> Port = extract_port_from_socket(Socket), begin_inet_async(Port). %% @doc handle the {inet_async,...} message -spec handle_inet_async(Message :: {'inet_async', socket(), any(), {'ok', socket()}}) -> {'ok', socket()}. handle_inet_async({inet_async, ListenSocket, _, {ok,ClientSocket}}) -> handle_inet_async(ListenSocket, ClientSocket, []). -spec handle_inet_async(ListenSocket :: socket(), ClientSocket :: socket()) -> {'ok', socket()}. handle_inet_async(ListenObject, ClientSocket) -> handle_inet_async(ListenObject, ClientSocket, []). -spec handle_inet_async(ListenSocket :: socket(), ClientSocket :: socket(), Options :: list()) -> {'ok', socket()}. handle_inet_async(ListenObject, ClientSocket, Options) -> ListenSocket = extract_port_from_socket(ListenObject), case set_sockopt(ListenSocket, ClientSocket) of ok -> ok; Error -> erlang:error(set_sockopt, Error) end, %% Signal the network driver that we are ready to accept another connection begin_inet_async(ListenSocket), %% If the listening socket is SSL then negotiate the client socket case is_port(ListenObject) of true -> {ok, ClientSocket}; false -> {ok, UpgradedClientSocket} = to_ssl_server(ClientSocket, Options), {ok, UpgradedClientSocket} end. %% @doc Upgrade a TCP connection to SSL -spec to_ssl_server(Socket :: socket()) -> {'ok', ssl:sslsocket()} | {'error', any()}. to_ssl_server(Socket) -> to_ssl_server(Socket, []). -spec to_ssl_server(Socket :: socket(), Options :: list()) -> {'ok', ssl:sslsocket()} | {'error', any()}. to_ssl_server(Socket, Options) -> to_ssl_server(Socket, Options, infinity). -spec to_ssl_server(Socket :: socket(), Options :: list(), Timeout :: non_neg_integer() | 'infinity') -> {'ok', ssl:sslsocket()} | {'error', any()}. to_ssl_server(Socket, Options, Timeout) when is_port(Socket) -> ssl_handshake(Socket, ssl_listen_options(Options), Timeout); to_ssl_server(_Socket, _Options, _Timeout) -> {error, already_ssl}. -spec to_ssl_client(Socket :: socket()) -> {'ok', ssl:sslsocket()} | {'error', 'already_ssl'}. to_ssl_client(Socket) -> to_ssl_client(Socket, []). -spec to_ssl_client(Socket :: socket(), Options :: list()) -> {'ok', ssl:sslsocket()} | {'error', 'already_ssl'}. to_ssl_client(Socket, Options) -> to_ssl_client(Socket, Options, infinity). -spec to_ssl_client(Socket :: socket(), Options :: list(), Timeout :: non_neg_integer() | 'infinity') -> {'ok', ssl:sslsocket()} | {'error', 'already_ssl'}. to_ssl_client(Socket, Options, Timeout) when is_port(Socket) -> ssl:connect(Socket, ssl_connect_options(Options), Timeout); to_ssl_client(_Socket, _Options, _Timeout) -> {error, already_ssl}. -spec type(Socket :: socket()) -> protocol(). type(Socket) when is_port(Socket) -> tcp; type(_Socket) -> ssl. %%%----------------------------------------------------------------- %%% Internal functions (OS_Mon configuration) %%%----------------------------------------------------------------- tcp_listen_options([Format|Options]) when Format =:= list; Format =:= binary -> tcp_listen_options(Options, Format); tcp_listen_options(Options) -> tcp_listen_options(Options, list). tcp_listen_options(Options, Format) -> parse_address([Format|proplist_merge(Options, ?TCP_LISTEN_OPTIONS)]). ssl_listen_options([Format|Options]) when Format =:= list; Format =:= binary -> ssl_listen_options(Options, Format); ssl_listen_options(Options) -> ssl_listen_options(Options, list). ssl_listen_options(Options, Format) -> parse_address([Format|proplist_merge(Options, ?SSL_LISTEN_OPTIONS)]). tcp_connect_options([Format|Options]) when Format =:= list; Format =:= binary -> tcp_connect_options(Options, Format); tcp_connect_options(Options) -> tcp_connect_options(Options, list). tcp_connect_options(Options, Format) -> parse_address([Format|proplist_merge(Options, ?TCP_CONNECT_OPTIONS)]). ssl_connect_options([Format|Options]) when Format =:= list; Format =:= binary -> ssl_connect_options(Options, Format); ssl_connect_options(Options) -> ssl_connect_options(Options, list). ssl_connect_options(Options, Format) -> parse_address([Format|proplist_merge(Options, ?SSL_CONNECT_OPTIONS)]). proplist_merge(PrimaryList, DefaultList) -> {PrimaryTuples, PrimaryOther} = lists:partition(fun(X) -> is_tuple(X) end, PrimaryList), {DefaultTuples, DefaultOther} = lists:partition(fun(X) -> is_tuple(X) end, DefaultList), MergedTuples = lists:ukeymerge(1, lists:keysort(1, PrimaryTuples), lists:keysort(1, DefaultTuples) ), MergedOther = lists:merge(lists:sort(PrimaryOther), lists:sort(DefaultOther)), %% remove all the values that don't belong here [Option || Option = {Key, _} <- MergedTuples, lists:member(Key, ?SSL_CONNECT_OPTIONS_KEYS) or proplists:is_defined(Key, DefaultList)] ++ [Option || Option <- MergedOther, Option == inet6 ]. parse_address(Options) -> case proplists:get_value(ip, Options) of X when is_tuple(X) -> Options; X when is_list(X) -> case inet_parse:address(X) of {error, _} = Error -> erlang:error(Error); {ok, IP} -> proplists:delete(ip, Options) ++ [{ip, IP}] end; _ -> Options end. -spec extract_port_from_socket(Socket :: socket()) -> port(). extract_port_from_socket({sslsocket,_,{SSLPort,_}}) -> SSLPort; extract_port_from_socket(Socket) -> Socket. -spec set_sockopt(ListSock :: port(), CliSocket :: port()) -> 'ok' | any(). set_sockopt(ListenObject, ClientSocket) -> ListenSocket = extract_port_from_socket(ListenObject), true = inet_db:register_socket(ClientSocket, inet_tcp), case prim_inet:getopts(ListenSocket, [active, nodelay, keepalive, delay_send, priority, tos]) of {ok, Opts} -> case prim_inet:setopts(ClientSocket, Opts) of ok -> ok; Error -> smtp_socket:close(ClientSocket), Error end; Error -> smtp_socket:close(ClientSocket), Error end. -ifdef(TEST). -define(TEST_PORT, 7586). connect_test_() -> [ {"listen and connect via tcp", fun() -> Self = self(), Port = ?TEST_PORT + 1, Ref = make_ref(), spawn(fun() -> {ok, ListenSocket} = listen(tcp, Port), ?assert(is_port(ListenSocket)), Self ! {Ref, listen}, {ok, ServerSocket} = accept(ListenSocket), controlling_process(ServerSocket, Self), Self ! {Ref, ListenSocket} end), receive {Ref, listen} -> ok end, {ok, ClientSocket} = connect(tcp, "localhost", Port), receive {Ref, ListenSocket} when is_port(ListenSocket) -> ok end, ?assert(is_port(ClientSocket)), close(ListenSocket) end }, {"listen and connect via ssl", fun() -> Self = self(), Port = ?TEST_PORT + 2, Ref = make_ref(), gen_smtp_application:ensure_all_started(gen_smtp), spawn(fun() -> {ok, ListenSocket} = listen(ssl, Port, [{keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}]), ?assertMatch([sslsocket|_], tuple_to_list(ListenSocket)), Self ! {Ref, listen}, {ok, ServerSocket} = accept(ListenSocket), controlling_process(ServerSocket, Self), Self ! {Ref, ListenSocket} end), receive {Ref, listen} -> ok end, {ok, ClientSocket} = connect(ssl, "localhost", Port, []), receive {Ref, {sslsocket,_,_} = ListenSocket} -> ok end, ?assertMatch([sslsocket|_], tuple_to_list(ClientSocket)), close(ListenSocket) end } ]. evented_connections_test_() -> [ {"current process receives connection to TCP listen sockets", fun() -> Port = ?TEST_PORT + 3, {ok, ListenSocket} = listen(tcp, Port), begin_inet_async(ListenSocket), spawn(fun()-> connect(tcp, "localhost", Port) end), receive {inet_async, ListenSocket, _, {ok,ServerSocket}} -> ok end, {ok, NewServerSocket} = handle_inet_async(ListenSocket, ServerSocket), ?assert(is_port(ServerSocket)), ?assertEqual(ServerSocket, NewServerSocket), %% only true for TCP ?assert(is_port(ListenSocket)), % Stop the async spawn(fun()-> connect(tcp, "localhost", Port) end), receive _Ignored -> ok end, close(NewServerSocket), close(ListenSocket) end }, {"current process receives connection to SSL listen sockets", fun() -> Port = ?TEST_PORT + 4, gen_smtp_application:ensure_all_started(gen_smtp), {ok, ListenSocket} = listen(ssl, Port, [{keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}]), begin_inet_async(ListenSocket), spawn(fun()-> connect(ssl, "localhost", Port) end), receive {inet_async, _ListenPort, _, {ok,ServerSocket}} -> ok end, {ok, NewServerSocket} = handle_inet_async(ListenSocket, ServerSocket, [{keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}]), ?assert(is_port(ServerSocket)), ?assertMatch([sslsocket|_], tuple_to_list(NewServerSocket)), ?assertMatch([sslsocket|_], tuple_to_list(ListenSocket)), %Stop the async spawn(fun()-> connect(ssl, "localhost", Port) end), receive _Ignored -> ok end, close(ListenSocket), close(NewServerSocket), ok end }, %% TODO: figure out if the following passes because %% of an incomplete test case or if this really is %% a magical feature where a single listener %% can respond to either ssl or tcp connections. {"current TCP listener receives SSL connection", fun() -> Port = ?TEST_PORT + 5, gen_smtp_application:ensure_all_started(gen_smtp), {ok, ListenSocket} = listen(tcp, Port), begin_inet_async(ListenSocket), spawn(fun()-> connect(ssl, "localhost", Port) end), ServerSocket = receive {inet_async, _ListenPort, _, {ok,ServerSocket0}} -> ServerSocket0 end, ?assertMatch({ok, ServerSocket}, handle_inet_async(ListenSocket, ServerSocket)), ?assert(is_port(ListenSocket)), ?assert(is_port(ServerSocket)), {ok, NewServerSocket} = to_ssl_server(ServerSocket, [{certfile, "test/fixtures/mx1.example.com-server.crt"}, {keyfile, "test/fixtures/mx1.example.com-server.key"}]), ?assertMatch([sslsocket|_], tuple_to_list(NewServerSocket)), % Stop the async spawn(fun()-> connect(ssl, "localhost", Port) end), receive _Ignored -> ok end, close(ListenSocket), close(NewServerSocket) end } ]. accept_test_() -> [ {"Accept via tcp", fun() -> Port = ?TEST_PORT + 6, {ok, ListenSocket} = listen(tcp, Port, tcp_listen_options([])), ?assert(is_port(ListenSocket)), spawn(fun()-> connect(ssl, "localhost", Port, tcp_connect_options([])) end), {ok, ServerSocket} = accept(ListenSocket), ?assert(is_port(ListenSocket)), close(ServerSocket), close(ListenSocket) end }, {"Accept via ssl", fun() -> Port = ?TEST_PORT + 7, gen_smtp_application:ensure_all_started(gen_smtp), {ok, ListenSocket} = listen(ssl, Port, [{keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}]), ?assertMatch([sslsocket|_], tuple_to_list(ListenSocket)), spawn(fun()->connect(ssl, "localhost", Port) end), accept(ListenSocket), close(ListenSocket) end } ]. type_test_() -> [ {"a tcp socket returns 'tcp'", fun() -> {ok, ListenSocket} = listen(tcp, ?TEST_PORT + 8), ?assertMatch(tcp, type(ListenSocket)), close(ListenSocket) end }, {"an ssl socket returns 'ssl'", fun() -> gen_smtp_application:ensure_all_started(gen_smtp), {ok, ListenSocket} = listen(ssl, ?TEST_PORT + 9, [{keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}]), ?assertMatch(ssl, type(ListenSocket)), close(ListenSocket) end } ]. active_once_test_() -> [ {"socket is set to active:once on tcp", fun() -> {ok, ListenSocket} = listen(tcp, ?TEST_PORT + 10, tcp_listen_options([])), ?assertEqual({ok, [{active,false}]}, inet:getopts(ListenSocket, [active])), active_once(ListenSocket), ?assertEqual({ok, [{active,once}]}, inet:getopts(ListenSocket, [active])), close(ListenSocket) end }, {"socket is set to active:once on ssl", fun() -> {ok, ListenSocket} = listen(ssl, ?TEST_PORT + 11, ssl_listen_options([{keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}])), ?assertEqual({ok, [{active,false}]}, ssl:getopts(ListenSocket, [active])), active_once(ListenSocket), ?assertEqual({ok, [{active,once}]}, ssl:getopts(ListenSocket, [active])), close(ListenSocket) end } ]. option_test_() -> [ {"options removes bogus values", fun() -> ?assertEqual(lists:sort([list|?TCP_LISTEN_OPTIONS]), lists:sort(tcp_listen_options([{notvalid,whatever}]))) end }, {"tcp_listen_options has defaults", fun() -> ?assertEqual(lists:sort([list|?TCP_LISTEN_OPTIONS]), lists:sort(tcp_listen_options([]))) end }, {"tcp_connect_options has defaults", fun() -> ?assertEqual(lists:sort([list|?TCP_CONNECT_OPTIONS]), lists:sort(tcp_connect_options([]))) end }, {"ssl_listen_options has defaults", fun() -> ?assertEqual(lists:sort([list|?SSL_LISTEN_OPTIONS]), lists:sort(ssl_listen_options([]))) end }, {"ssl_connect_options has defaults", fun() -> ?assertEqual(lists:sort([list|?SSL_CONNECT_OPTIONS]), lists:sort(ssl_connect_options([]))) end }, {"tcp_listen_options defaults to list type", fun() -> ?assertEqual(lists:sort([list|?TCP_LISTEN_OPTIONS]), lists:sort(tcp_listen_options([{active,false}]))), ?assertEqual(lists:sort([binary|?TCP_LISTEN_OPTIONS]), lists:sort(tcp_listen_options([binary,{active,false}]))) end }, {"tcp_connect_options defaults to list type", fun() -> ?assertEqual(lists:sort([list|?TCP_CONNECT_OPTIONS]), lists:sort(tcp_connect_options([{active,false}]))), ?assertEqual(lists:sort([binary|?TCP_CONNECT_OPTIONS]), lists:sort(tcp_connect_options([binary,{active,false}]))) end }, {"ssl_listen_options defaults to list type", fun() -> ?assertEqual(lists:sort([list|?SSL_LISTEN_OPTIONS]), lists:sort(ssl_listen_options([{active,false}]))), ?assertEqual(lists:sort([binary|?SSL_LISTEN_OPTIONS]), lists:sort(ssl_listen_options([binary,{active,false}]))) end }, {"ssl_connect_options defaults to list type", fun() -> ?assertEqual(lists:sort([list|?SSL_CONNECT_OPTIONS]), lists:sort(ssl_connect_options([{active,false}]))), ?assertEqual(lists:sort([binary|?SSL_CONNECT_OPTIONS]), lists:sort(ssl_connect_options([binary,{active,false}]))) end }, {"tcp_listen_options merges provided proplist", fun() -> ?assertEqual([list|lists:keysort(1, [{active, true}, {backlog, 30}, {ip,{0,0,0,0}}, {keepalive, true}, {packet, 2}, {reuseaddr, true}])], tcp_listen_options([{active, true},{packet,2}])) end }, {"tcp_connect_options merges provided proplist", fun() -> ?assertEqual(lists:sort([list,{active, true}, {packet, 2}, {ip,{0,0,0,0}}, {port, 0}]), lists:sort(tcp_connect_options([{active, true},{packet,2}]))) end }, {"ssl_listen_options merges provided proplist", fun() -> ?assertEqual([list|lists:keysort(1, [{active, true}, {backlog, 30}, {certfile, "server.crt"}, {depth, 0}, {keepalive, true}, {keyfile, "server.key"}, {packet, 2}, {reuse_sessions, false}, {reuseaddr, true}, {ssl_imp, new}])], ssl_listen_options([{active, true},{packet,2}])), ?assertEqual([list|lists:keysort(1, [{active, false}, {backlog, 30}, {certfile, "../server.crt"}, {depth, 0}, {keepalive, true}, {keyfile, "../server.key"}, {packet, line}, {reuse_sessions, false}, {reuseaddr, true}, {ssl_imp, new}])], ssl_listen_options([{certfile, "../server.crt"}, {keyfile, "../server.key"}])) end }, {"ssl_connect_options merges provided proplist", fun() -> ?assertEqual(lists:sort([list,{active, true}, {depth, 0}, {ip, {0,0,0,0}}, {port, 0}, {packet, 2}, {versions, [tlsv1,'tlsv1.1','tlsv1.2']}]), lists:sort(ssl_connect_options([{active, true},{packet,2}]))) end } ]. ssl_upgrade_test_() -> [ {"TCP connection can be upgraded to ssl", fun() -> Self = self(), Port = ?TEST_PORT + 12, gen_smtp_application:ensure_all_started(gen_smtp), spawn(fun() -> {ok, ListenSocket} = listen(tcp, Port), Self ! listening, {ok, ServerSocket} = accept(ListenSocket), {ok, NewServerSocket} = smtp_socket:to_ssl_server( ServerSocket, [{keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}]), Self ! {sock, NewServerSocket} end), receive listening -> ok end, erlang:yield(), {ok, ClientSocket} = connect(tcp, "localhost", Port), ?assert(is_port(ClientSocket)), {ok, NewClientSocket} = to_ssl_client(ClientSocket), ?assertMatch([sslsocket|_], tuple_to_list(NewClientSocket)), receive {sock, NewServerSocket} -> ok end, ?assertMatch({sslsocket, _, _}, NewServerSocket), close(NewClientSocket), close(NewServerSocket) end }, {"SSL server connection can't be upgraded again", fun() -> Self = self(), Port = ?TEST_PORT + 13, gen_smtp_application:ensure_all_started(gen_smtp), spawn(fun() -> {ok, ListenSocket} = listen(ssl, Port, [{keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}]), Self ! listening, {ok, ServerSocket} = accept(ListenSocket), ?assertMatch({error, already_ssl}, to_ssl_server(ServerSocket)), close(ServerSocket) end), receive listening -> ok end, erlang:yield(), {ok, ClientSocket} = connect(tcp, "localhost", Port), close(ClientSocket) end }, {"SSL client connection can't be upgraded again", fun() -> Self = self(), Port = ?TEST_PORT + 14, gen_smtp_application:ensure_all_started(gen_smtp), spawn(fun() -> {ok, ListenSocket} = listen(ssl, Port, [{keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}]), Self ! listening, {ok, ServerSocket} = accept(ListenSocket), Self ! {sock, ServerSocket} end), receive listening -> ok end, erlang:yield(), {ok, ClientSocket} = connect(ssl, "localhost", Port), receive {sock, ServerSocket} -> ok end, ?assertMatch({error, already_ssl}, to_ssl_client(ClientSocket)), close(ClientSocket), close(ServerSocket) end } ]. -endif.