%%%----------------------------------------------------------------------
%%% File    : fast_tls.erl
%%% Author  : Alexey Shchepin <alexey@process-one.net>
%%% Purpose : Interface to openssl
%%% Created : 24 Jul 2004 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
%%% Copyright (C) 2002-2024 ProcessOne, SARL. All Rights Reserved.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.
%%%
%%%----------------------------------------------------------------------

-module(fast_tls).
-on_load(load_nif/0).

-author('alexey@process-one.net').

-export([open_nif/10, loop_nif/4, get_peer_certificate_nif/1,
         get_verify_result_nif/1, invalidate_nif/1,
         get_negotiated_cipher_nif/1, set_fips_mode_nif/1,
         get_fips_mode_nif/0, get_tls_cb_exporter_nif/1,
         p12_to_pem_nif/2]).

-export([tcp_to_tls/2,
         tls_to_tcp/1, send/2, recv/2, recv/3, recv_data/2,
         setopts/2, sockname/1, peername/1,
         controlling_process/2, close/1,
         get_peer_certificate/1, get_peer_certificate/2,
         get_verify_result/1, get_cert_verify_string/2,
         add_certfile/2, get_certfile/1, delete_certfile/1,
         clear_cache/0, get_negotiated_cipher/1,
         get_tls_last_message/2, set_fips_mode/1, get_fips_mode/0,
         get_tls_cb_exporter/1, p12_to_pem/2, finish_handshake/2]).

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-include_lib("public_key/include/public_key.hrl").

-define(SET_CERTIFICATE_FILE_ACCEPT, 1).

-define(SET_CERTIFICATE_FILE_CONNECT, 2).

-define(SET_ENCRYPTED_INPUT, 3).

-define(SET_DECRYPTED_OUTPUT, 4).

-define(GET_ENCRYPTED_OUTPUT, 5).

-define(GET_DECRYPTED_INPUT, 6).

-define(GET_PEER_CERTIFICATE, 7).

-define(GET_VERIFY_RESULT, 8).

-define(VERIFY_NONE, 16#10000).

-define(COMPRESSION_NONE, 16#100000).

-define(PRINT(Format, Args), io:format(Format, Args)).

-record(tlssock, {tcpsock :: inet:socket(),
                  tlsport :: port()}).

-type tls_socket() :: #tlssock{}.

-type cert() :: #'Certificate'{} | #'OTPCertificate'{}.

-export_type([tls_socket/0]).

open_nif(_Flags, _CertFile, _KeyFile, _Ciphers, _ProtocolOpts, _DH, _DHFile, _CAFile, _SNI, _ALPN) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

loop_nif(_Port, _ToSend, _Received, _ReceiveBytes) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

get_peer_certificate_nif(_Port) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

get_verify_result_nif(_Port) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

add_certfile_nif(_Domain, _File) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

get_certfile_nif(_Domain) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

delete_certfile_nif(_Domain) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

invalidate_nif(_Port) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

clear_cache_nif() ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

get_negotiated_cipher_nif(_Port) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

tls_get_peer_finished_nif(_Port) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

tls_get_finished_nif(_Port) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

get_tls_cb_exporter_nif(_Port) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

set_fips_mode_nif(_Flag) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

get_fips_mode_nif() ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

p12_to_pem_nif(_P12Data, _Pass) ->
    erlang:nif_error({nif_not_loaded, ?MODULE}).

%%% --------------------------------------------------------
%%% The call-back functions.
%%% --------------------------------------------------------

-spec tcp_to_tls(inet:socket(), [atom() | {atom(), any()}]) ->
    {ok, tls_socket()} | {'error', 'no_certfile' | binary()}.
tcp_to_tls(TCPSocket, Options) ->
    Command = case lists:member(connect, Options) of
                  true -> ?SET_CERTIFICATE_FILE_CONNECT;
                  false -> ?SET_CERTIFICATE_FILE_ACCEPT
              end,
    CertFile = proplists:get_value(certfile, Options, ""),
    if CertFile /= [] orelse Command == ?SET_CERTIFICATE_FILE_CONNECT ->
        Flags1 = case lists:member(verify_none, Options) of
                     true -> ?VERIFY_NONE;
                     false -> 0
                 end,
        Flags2 = case lists:member(compression_none, Options) of
                     true -> ?COMPRESSION_NONE;
                     false -> 0
                 end,
        Flags = Flags1 bor Flags2,
        Ciphers =
        case lists:keysearch(ciphers, 1, Options) of
            {value, {ciphers, C}} ->
                iolist_to_binary(C);
            false ->
                <<>>
        end,
        ProtocolOpts = case lists:keysearch(protocol_options, 1, Options) of
                           {value, {protocol_options, P}} ->
                               iolist_to_binary(P);
                           false ->
                               <<>>
                       end,
        DH = case lists:keysearch(dh, 1, Options) of
                     {value, {dh, Der}} ->
                         iolist_to_binary(Der);
                     false ->
                         <<>>
                 end,
        DHFile = case lists:keysearch(dhfile, 1, Options) of
                     {value, {dhfile, D}} ->
                         iolist_to_binary(D);
                     false ->
                         <<>>
                 end,
        CAFile = case lists:keysearch(cafile, 1, Options) of
                     {value, {cafile, CA}} ->
                         iolist_to_binary(CA);
                     false ->
                         <<>>
                 end,
        KeyFile = case lists:keysearch(keyfile, 1, Options) of
                      {value, {keyfile, KF}} when CertFile /= "" ->
                          iolist_to_binary(KF);
                      _ ->
                          <<>>
                  end,
        ServerName = case lists:keysearch(sni, 1, Options) of
                         {value, {sni, SNI}} ->
                             iolist_to_binary(SNI);
                         false ->
                             <<>>
                     end,
        ALPN = case lists:keysearch(alpn, 1, Options) of
                   {value, {alpn, ProtoList}} ->
                       encode_alpn(ProtoList);
                   false ->
                       <<>>
               end,
        case open_nif(Command bor Flags, CertFile, KeyFile, Ciphers,
                      ProtocolOpts, DH, DHFile, CAFile, ServerName, ALPN) of
            {ok, Port} ->
                {ok, #tlssock{tcpsock = TCPSocket, tlsport = Port}};
            Err = {error, _} ->
                Err
        end;
        true -> {error, no_certfile}
    end.

-spec tls_to_tcp(tls_socket()) -> inet:socket().
tls_to_tcp(#tlssock{tcpsock = TCPSocket, tlsport = Port}) ->
    invalidate_nif(Port),
    TCPSocket.

-spec recv(tls_socket(), non_neg_integer()) ->
    {error, inet:posix()} | {error, binary()} | {ok, binary()}.
recv(Socket, Length) -> recv(Socket, Length, infinity).

-spec recv(tls_socket(), non_neg_integer(), timeout()) ->
    {error, inet:posix()} | {error, binary()} | {ok, binary()}.
recv(TLSSock, Length, Timeout) ->
    recv_and_loop(TLSSock, <<>>, <<>>, <<>>,
                  case Length of 0 -> -1; _ -> Length end, Timeout).

-spec recv_data(tls_socket(), binary()) ->
    {error, inet:posix() | binary()} | {ok, binary()}.
recv_data(TLSSock, Packet) ->
    loop(TLSSock, <<>>, Packet, <<>>, -1).

-spec loop(tls_socket(), binary(), binary(), binary(), integer()) ->
    {error, inet:posix() | binary()} | {ok, binary()}.
loop(#tlssock{tcpsock = TCPSocket,
              tlsport = Port} = Socket,
     ToSend, Received, DecBuf, Length) ->
    try loop_nif(Port, ToSend, Received, Length) of
        {error, _} = Err ->
            Err;
        {Tag, <<>>, Decrypted} when Tag == ok; Tag == init ->
            {ok, <<DecBuf/binary, Decrypted/binary>>};
        {Tag, ToWrite, Decrypted} when Tag == ok; Tag == init ->
            case gen_tcp:send(TCPSocket, ToWrite) of
                ok ->
                    loop(Socket, <<>>, <<>>, <<DecBuf/binary, Decrypted/binary>>,
                         Length - byte_size(Decrypted));
                {error, _} = Err ->
                    Err
            end;
        {{error, _} = Err, ToWrite, _} ->
            _ = gen_tcp:send(TCPSocket, ToWrite),
            Err
    catch error:badarg ->
        {error, einval}
    end.

-spec recv_and_loop(tls_socket(), binary(), binary(), binary(), integer(), timeout()) ->
    {error, inet:posix() | binary()} | {ok, binary()}.
recv_and_loop(#tlssock{tcpsock = TCPSocket} = Socket,
              ToSend, Received, DecBuf, Length, Timeout) ->
    case loop(Socket, ToSend, Received, DecBuf, Length) of
        {error, _} = Err ->
            Err;
        {ok, Decrypted} ->
            case size(Decrypted) - size(DecBuf) of
                V when V == 0 ->
                    case gen_tcp:recv(TCPSocket, 0, Timeout) of
                        {ok, Received2} ->
                            recv_and_loop(Socket, <<>>, Received2, Decrypted,
                                          Length - V, Timeout);
                        {error, _} = Err ->
                            Err
                    end;
                V when Length > V ->
                    recv_and_loop(Socket, <<>>, <<>>, Decrypted,
                                  Length - V, Timeout);
                _ when Length < 0 ->
                    {ok, Decrypted};
                V when Length == V ->
                    {ok, Decrypted};
                _ ->
                    {error, too_much_data_received}
            end
    end.

-spec finish_handshake(tls_socket(), timeout()) ->
    {error, inet:posix() | binary()} | ok.
finish_handshake(#tlssock{tcpsock = TCPSocket, tlsport = Port}, Timeout) ->
    OurLoop = fun OurLoop(Received) ->
        try loop_nif(Port, <<>>, Received, 0) of
            {error, _} = Err ->
                Err;
            {Tag, ToWrite, <<>>} when Tag == ok; Tag == init ->
                case gen_tcp:send(TCPSocket, ToWrite) of
                    ok when Tag == init ->
                        case gen_tcp:recv(TCPSocket, 0, Timeout) of
                            {ok, Received2} ->
                                OurLoop(Received2);
                            {error, _} = Err ->
                                Err
                        end;
                    ok ->
                        ok;
                    {error, _} = Err ->
                        Err
                end;
            {Tag, _ToWrite, _Data} when Tag == ok; Tag == init ->
                {error, too_much_data_received};
            {{error, _} = Err, ToWrite, _} ->
                _ = gen_tcp:send(TCPSocket, ToWrite),
                Err
        catch error:badarg ->
            {error, einval}
        end
    end,
    OurLoop(<<>>).

-spec send(tls_socket(), binary()) ->
    ok | {error, inet:posix() | binary() | timeout}.
send(Socket, Packet) ->
    case loop(Socket, Packet, <<>>, <<>>, 0) of
        {ok, <<>>} ->
            ok;
        {ok, Data} ->
            Data;
        Other ->
            Other
    end.

-spec setopts(tls_socket(), list()) ->
    ok | {error, inet:posix()}.
setopts(#tlssock{tcpsock = TCPSocket}, Opts) ->
    inet:setopts(TCPSocket, Opts).

-spec sockname(tls_socket()) ->
    {ok, {inet:ip_address(), inet:port_number()}} | {error, inet:posix()}.
sockname(#tlssock{tcpsock = TCPSocket}) ->
    inet:sockname(TCPSocket).

peername(#tlssock{tcpsock = TCPSocket}) ->
    inet:peername(TCPSocket).

controlling_process(#tlssock{tcpsock = TCPSocket},
                    Pid) ->
    gen_tcp:controlling_process(TCPSocket, Pid).

close(#tlssock{tcpsock = TCPSocket, tlsport = Port}) ->
    invalidate_nif(Port),
    gen_tcp:close(TCPSocket).

-spec get_peer_certificate(tls_socket()) -> {ok, cert()} | error.
get_peer_certificate(TLSSock) ->
    get_peer_certificate(TLSSock, plain).

-spec get_peer_certificate(tls_socket(), otp|plain) -> {ok, cert()} | error;
			  (tls_socket(), der) -> {ok, binary()} | error.
get_peer_certificate(#tlssock{tlsport = Port}, Type) ->
    case catch get_peer_certificate_nif(Port) of
        {'EXIT', {badarg, _}} ->
            error;
	{ok, BCert} when Type == der ->
	    {ok, BCert};
        {ok, BCert} ->
            try public_key:pkix_decode_cert(BCert, Type) of
                Cert -> {ok, Cert}
            catch _:_ ->
                error
            end;
        {error, _} -> error
    end.

-spec get_negotiated_cipher(tls_socket()) -> error | {ok, binary()}.
get_negotiated_cipher(#tlssock{tlsport = Port}) ->
    case catch get_negotiated_cipher_nif(Port) of
        Val when is_binary(Val) ->
            {ok, Val};
        _ ->
            error
    end.

-spec get_tls_last_message(peer | self, tls_socket()) -> {ok, binary()} | {error, term()}.
get_tls_last_message(peer, #tlssock{tlsport = Port}) ->
    tls_get_peer_finished_nif(Port);
get_tls_last_message(self, #tlssock{tlsport = Port}) ->
    tls_get_finished_nif(Port).

-spec get_tls_cb_exporter(tls_socket()) -> {ok, binary()} | {error, term()}.
get_tls_cb_exporter(#tlssock{tlsport = Port}) ->
    get_tls_cb_exporter_nif(Port).

-spec get_verify_result(tls_socket()) -> byte().
get_verify_result(#tlssock{tlsport = Port}) ->
    {ok, Res} = get_verify_result_nif(Port),
    Res.

-spec get_cert_verify_string(number(), cert() | binary()) -> binary().
get_cert_verify_string(CertVerifyRes, Cert) ->
    IsSelfsigned = cert_is_self_signed(Cert),
    case {CertVerifyRes, IsSelfsigned} of
	{21, true} -> <<"self-signed certificate">>;
	_ -> cert_verify_code(CertVerifyRes)
    end.

-spec cert_is_self_signed(cert() | binary()) -> boolean().
cert_is_self_signed(#'Certificate'{} = Cert) ->
    BCert = public_key:pkix_encode('Certificate', Cert, plain),
    cert_is_self_signed(BCert);
cert_is_self_signed(Cert) ->
    public_key:pkix_is_self_signed(Cert).

-spec add_certfile(iodata(), iodata()) -> ok.
add_certfile(Domain, File) ->
    add_certfile_nif(Domain, File).

%% @doc This function is intended for tests only
-spec get_certfile(iodata()) -> {ok, binary()} | error.
get_certfile(Domain) ->
    get_certfile_nif(Domain).

%% @doc Returns true if element is deleted, false otherwise
-spec delete_certfile(iodata()) -> boolean().
delete_certfile(Domain) ->
    delete_certfile_nif(Domain).

%% @doc Clears cached SSL_CTX structures
%% You MUST call this function if you change content
%% of your CA, DH or certificate files
-spec clear_cache() -> ok.
clear_cache() ->
    clear_cache_nif().

%% @doc Enables/disables FIPS mode
-spec set_fips_mode(boolean()) -> ok | {error, binary()}.
set_fips_mode(true) ->
    set_fips_mode_nif(1);
set_fips_mode(false) ->
    set_fips_mode_nif(0).

%% @doc Checks whether FIPS mode is enabled or not
-spec get_fips_mode() -> boolean().
get_fips_mode() ->
    get_fips_mode_nif().

%% @doc Converts PKCS12 certificate to PEM encoded private key and certificate
-spec p12_to_pem(binary(), binary()) -> {ok, {binary(), binary()}} | {error, atom() | binary()}.
p12_to_pem(P12Data, Pass) ->
    p12_to_pem_nif(P12Data, <<Pass/binary, 0>>).

cert_verify_code(0)  -> <<"ok">>;
cert_verify_code(2) ->
    <<"unable to get issuer certificate">>;
cert_verify_code(3) ->
    <<"unable to get certificate CRL">>;
cert_verify_code(4) ->
    <<"unable to decrypt certificate's signature">>;
cert_verify_code(5) ->
    <<"unable to decrypt CRL's signature">>;
cert_verify_code(6) ->
    <<"unable to decode issuer public key">>;
cert_verify_code(7) ->
    <<"certificate signature failure">>;
cert_verify_code(8)  -> <<"CRL signature failure">>;
cert_verify_code(9) ->
    <<"certificate is not yet valid">>;
cert_verify_code(10) -> <<"certificate has expired">>;
cert_verify_code(11) -> <<"CRL is not yet valid">>;
cert_verify_code(12) -> <<"CRL has expired">>;
cert_verify_code(13) ->
    <<"format error in certificate's notBefore "
      "field">>;
cert_verify_code(14) ->
    <<"format error in certificate's notAfter "
      "field">>;
cert_verify_code(15) ->
    <<"format error in CRL's lastUpdate field">>;
cert_verify_code(16) ->
    <<"format error in CRL's nextUpdate field">>;
cert_verify_code(17) -> <<"out of memory">>;
cert_verify_code(18) -> <<"self signed certificate">>;
cert_verify_code(19) ->
    <<"self signed certificate in certificate "
      "chain">>;
cert_verify_code(20) ->
    <<"unable to get local issuer certificate">>;
cert_verify_code(21) ->
    <<"unable to verify the first certificate">>;
cert_verify_code(22) ->
    <<"certificate chain too long">>;
cert_verify_code(23) -> <<"certificate revoked">>;
cert_verify_code(24) -> <<"invalid CA certificate">>;
cert_verify_code(25) ->
    <<"path length constraint exceeded">>;
cert_verify_code(26) ->
    <<"unsupported certificate purpose">>;
cert_verify_code(27) -> <<"certificate not trusted">>;
cert_verify_code(28) -> <<"certificate rejected">>;
cert_verify_code(29) -> <<"subject issuer mismatch">>;
cert_verify_code(30) ->
    <<"authority and subject key identifier "
      "mismatch">>;
cert_verify_code(31) ->
    <<"authority and issuer serial number mismatch">>;
cert_verify_code(32) ->
    <<"key usage does not include certificate "
      "signing">>;
cert_verify_code(50) ->
    <<"application verification failure">>;
cert_verify_code(X) ->
    <<"Unknown OpenSSL error code: ", (integer_to_binary(X))/binary>>.

encode_alpn(ProtoList) ->
    [<<(size(Proto)), Proto/binary>> || Proto <- ProtoList, Proto /= <<>>].

load_nif() ->
    case os:getenv("COVERALLS") of
        "true" -> ok;
        _ -> load_nif2()
    end.
load_nif2() ->
    SOPath = p1_nif_utils:get_so_path(fast_tls, [fast_tls], "fast_tls"),
    load_nif(SOPath).

load_nif(SOPath) ->
    case erlang:load_nif(SOPath, 0) of
        ok ->
            set_fips_mode();
        {error, {reload, _}} -> % We don't support upgrade in this module so let's not crash
            ok;
        {error, {upgrade, _}} -> % We don't support upgrade in this module so let's not crash
            ok;
        {error, ErrorDesc} = Err ->
            error_logger:error_msg("failed to load TLS NIF: ~s~n",
                                   [erl_ddll:format_error(ErrorDesc)]),
            Err
    end.

-spec set_fips_mode() -> ok | {error, term()}.
set_fips_mode() ->
    case application:get_env(?MODULE, fips_mode) of
        {ok, Flag} when Flag == true orelse Flag == false ->
            case set_fips_mode(Flag) of
                ok -> ok;
                {error, Reason} = Err ->
                    error_logger:error_msg("~s", [Reason]),
                    Err
            end;
        _ ->
            ok
    end.

-ifdef(TEST).

%% DER encoded DH parameters (2048 bits)
-define(DH, <<48,130,1,8,2,130,1,1,0,146,218,14,246,
              227,231,225,122,39,149,89,115,73,249,41,
              239,197,168,101,114,1,121,135,30,206,
              169,127,254,204,228,53,170,194,229,198,
              217,154,137,237,225,70,196,242,42,25,16,
              129,6,212,231,247,91,254,86,232,151,113,
              176,135,120,61,177,224,162,198,69,68,
              120,113,192,110,70,64,129,180,122,160,
              155,34,145,186,59,199,202,64,186,246,36,
              145,192,24,51,9,255,128,224,249,184,241,
              108,19,170,54,148,113,249,232,106,118,
              228,15,95,90,29,67,140,245,210,158,147,
              244,254,16,109,40,49,179,160,209,228,
              204,21,57,197,168,138,78,22,197,141,183,
              50,31,96,32,138,187,94,99,132,191,8,26,
              98,43,229,49,132,164,146,145,161,232,
              205,44,233,41,18,207,47,11,112,31,201,
              163,151,71,179,128,78,129,38,32,133,165,
              189,187,144,150,186,223,215,91,85,192,
              214,31,143,66,194,29,184,23,60,134,74,
              143,253,33,171,29,79,136,25,197,125,66,
              177,186,76,206,61,138,152,91,88,86,231,
              196,100,76,1,197,196,160,88,120,161,185,
              212,240,103,92,198,221,189,102,246,17,
              77,78,187,121,152,68,227,2,1,2>>).

transmission_with_client_certificate_test() ->
    transmission_test_with_opts([certificate()], [certificate()]).

transmission_without_client_certificate_test() ->
    transmission_test_with_opts([certificate()], []).

transmission_without_server_cert_fails_test() ->
    TestPid = self(),
    {ok, ListenSocket} = gen_tcp:listen(0, [binary, {packet, 0}, {active, false},
                                            {reuseaddr, true}, {nodelay, true}]),
    {ok, Port} = inet:port(ListenSocket),
    _ListenerPid = spawn(fun() -> {ok, Socket} = gen_tcp:accept(ListenSocket),
                                 Res = tcp_to_tls(Socket, []),
                                 TestPid ! {listener_tcp_to_tls, Res}
                        end),
    {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, Port, [binary, {packet, 0}, {active, false},
                                                          {reuseaddr, true}, {nodelay, true}]),
    {ok, TLSSock} = tcp_to_tls(Socket, [connect]),
    close(TLSSock),
    receive
        {listener_tcp_to_tls, Res} ->
            ?assertEqual({error, no_certfile}, Res)
    end.


transmission_test_with_opts(ListenerOpts, SenderOpts) ->
    {LPid, Port} = setup_listener(ListenerOpts),
    SPid = setup_sender(Port, SenderOpts),
    SPid ! {stop, self()},
    FC = receive
             {result, Res, FinishedFromClient} ->
                 ?assertEqual(ok, Res),
                 FinishedFromClient
         end,
    LPid ! {stop, self()},
    FL = receive
             {received, Msg, FinishedFromListener} ->
                 ?assertEqual(<<"abcdefghi">>, Msg),
                 FinishedFromListener
         end,
    ?assertEqual(FC, FL),
    receive
        {certfile, Cert} ->
            case lists:keymember(certfile, 1, SenderOpts) of
                true -> ?assertNotEqual(error, Cert);
                false -> ?assertEqual(error, Cert)
            end
    end.

not_compatible_protocol_options_test() ->
    {LPid, Port} = setup_listener([certificate(), {protocol_options, <<"no_sslv2|no_sslv3|no_tlsv1|no_tlsv1_1|no_tlsv1_2|no_tlsv1_3">>}]),
    SPid = setup_sender(Port, [{protocol_options, <<"no_sslv2|no_sslv3|no_tlsv1|no_tlsv1_1|no_tlsv1_2">>}]),
    SPid ! {stop, self()},
    receive
        {result, _Res, _} -> ok
    end,
    LPid ! {stop, self()},
    receive
        {received, {error, _, _} = Msg, _} ->
            ?assertMatch({error, _, <<>>}, Msg);
        {received, Msg, _} ->
            ?assertMatch(<<>>, Msg)
    end.

setup_listener(Opts) ->
    {ok, ListenSocket} = gen_tcp:listen(0,
                                        [binary, {packet, 0}, {active, false},
                                         {reuseaddr, true}, {nodelay, true}]),
    Pid = spawn(fun() ->
        {ok, Socket} = gen_tcp:accept(ListenSocket),
        {ok, TLSSock} = tcp_to_tls(Socket, [{dh, ?DH}|Opts]),
        listener_loop(TLSSock, <<>>)
                end),
    {ok, Port} = inet:port(ListenSocket),
    {Pid, Port}.

listener_loop(TLSSock, Msg) ->
    Finished = get_tls_last_message(peer, TLSSock),
    case recv(TLSSock, 1, 1000) of
        {error, timeout} ->
            receive
                {stop, Pid} ->
                    Pid ! {received, Msg, Finished},
                    Cert = get_peer_certificate(TLSSock),
                    Pid ! {certfile, Cert}
            after 0 ->
                listener_loop(TLSSock, Msg)
            end;
        {error, closed} ->
            receive
                {stop, Pid} ->
                    Pid ! {received, Msg, Finished},
                    Cert = get_peer_certificate(TLSSock),
                    Pid ! {certfile, Cert}
            end;
        {error, Err} ->
            receive
                {stop, Pid} ->
                    Pid ! {received, {error, Err, Msg}, Finished}
            end;
        {ok, Data} ->
            listener_loop(TLSSock, <<Msg/binary, Data/binary>>)
    end.

setup_sender(Port, Opts) ->
    {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, Port, [
        binary, {packet, 0}, {active, false},
        {reuseaddr, true}, {nodelay, true}]),
    spawn(fun() ->
        {ok, TLSSock} = tcp_to_tls(Socket, [connect | Opts]),
        sender_loop(TLSSock)
          end).

sender_loop(TLSSock) ->
    {Res, Finished} = try
              recv(TLSSock, 0, 100),
              F = get_tls_last_message(self, TLSSock),
              ok = send(TLSSock, <<"abc">>),
              recv(TLSSock, 0, 100),
              ok = send(TLSSock, <<"def">>),
              recv(TLSSock, 0, 100),
              ok = send(TLSSock, <<"ghi">>),
              recv(TLSSock, 0, 100),
              close(TLSSock),
              {ok, F}
          catch
              _:Err ->
                  close(TLSSock),
                  {Err, <<>>}
          end,
    receive
        {stop, Pid} ->
            Pid ! {result, Res, Finished}
    end.

-ifdef(REBAR3).
certificate() ->
    {certfile, <<"tests/cert.pem">>}.
-else.
certificate() ->
    {certfile, <<"../tests/cert.pem">>}.
-endif.

-endif.
