%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 2008-2026. 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.
%%
%% %CopyrightEnd%
%%

%%

-module(ssh_renegotiate_SUITE).

-include_lib("common_test/include/ct.hrl").
-include_lib("kernel/include/inet.hrl").
-include_lib("kernel/include/file.hrl").
-include("ssh_test_lib.hrl").

-export([
         suite/0,
         all/0,
         groups/0,
         init_per_suite/1,
         end_per_suite/1,
         init_per_group/2,
         end_per_group/2,
         init_per_testcase/2,
         end_per_testcase/2
        ]).

-export([
         norekey_limit_client/0,
         norekey_limit_client/1,
         norekey_limit_daemon/0,
         norekey_limit_daemon/1,
         rekey0/0,
         rekey0/1,
         rekey1/0,
         rekey1/1,
         rekey2/0,
         rekey2/1,
         rekey3/0,
         rekey3/1,
         rekey4/0,
         rekey4/1,
         rekey_limit_client/0,
         rekey_limit_client/1,
         rekey_limit_daemon/0,
         rekey_limit_daemon/1,
         rekey_time_limit_client/0,
         rekey_time_limit_client/1,
         rekey_time_limit_daemon/0,
         rekey_time_limit_daemon/1,
         renegotiate1/1,
         renegotiate2/1
        ]).

-define(NEWLINE, <<"\r\n">>).
-define(ALIVE, {alive, #{count_max => 10, interval => 10_000}}).
-define(REKEY_DATA_TMO, 1 * 60000). % Should be multiples of 60000

%%--------------------------------------------------------------------
%% Common Test interface functions -----------------------------------
%%--------------------------------------------------------------------

suite() ->
    [{ct_hooks,[ts_install_cth]},
     {timetrap,{seconds,90}}].

all() -> 
    [{group, renegotiate}
    ].

groups() ->
    [{renegotiate, [parallel], [rekey0,
                                rekey1,
                                rekey2,
                                rekey3,
                                rekey4,
                                rekey_limit_client,
                                rekey_limit_daemon,
                                rekey_time_limit_client,
                                rekey_time_limit_daemon,
                                norekey_limit_client,
                                norekey_limit_daemon,
                                renegotiate1,
                                renegotiate2]}
    ].

%%--------------------------------------------------------------------
init_per_suite(Config) ->
    ?CHECK_CRYPTO(begin
                      ssh:start(),
                      ct:log("Pub keys setup for: ~p",
                             [ssh_test_lib:setup_all_user_host_keys(Config)]),
                      [{preferred_algorithms,ssh_transport:supported_algorithms()}
                       | Config]
                  end).

end_per_suite(_Config) ->
    ssh:stop().


init_per_group(_, Config) -> Config.

end_per_group(_, Config) -> Config.

init_per_testcase(_TestCase, Config) ->
    Config.

end_per_testcase(_TestCase, _Config) ->
    ok.

%%----------------------------------------------------------------------------
%%% Idle timeout test
rekey0() -> [{timetrap,{seconds,120 * ssh_test_lib:timetrap_scale()}}].
rekey1() -> [{timetrap,{seconds,120 * ssh_test_lib:timetrap_scale()}}].
rekey2() -> [{timetrap,{seconds,120 * ssh_test_lib:timetrap_scale()}}].
rekey3() -> [{timetrap,{seconds,120 * ssh_test_lib:timetrap_scale()}}].
rekey4() -> [{timetrap,{seconds,120 * ssh_test_lib:timetrap_scale()}}].
    
rekey0(Config) -> ssh_dbg:start(), ssh_dbg:on(renegotiation),
                  R = rekey_chk(Config, 0,                   0),
                  ssh_dbg:stop(),
                  R.
rekey1(Config) -> rekey_chk(Config, infinity,            0).
rekey2(Config) -> rekey_chk(Config, {infinity,infinity}, 0).
rekey3(Config) -> rekey_chk(Config, 0,                   infinity).
rekey4(Config) -> rekey_chk(Config, 0,                   {infinity,infinity}).

rekey_chk(Config, RLdaemon, RLclient) ->
    {Pid, Host, Port} =
        ssh_test_lib:std_daemon(Config,
                                [{subsystems, [ssh_sftpd:subsystem_spec([])]},
                                 {rekey_limit, RLdaemon}, ?ALIVE]),
    ConnectionRef =
        ssh_test_lib:std_connect(Config, Host, Port,
                                 [{rekey_limit, RLclient}, ?ALIVE]),
    Kex1 = ssh_test_lib:get_kex_init(ConnectionRef),

    %% Make both sides send something:
    {ok, _SftpPid} = ssh_sftp:start_channel(ConnectionRef),

    %% Check rekeying
    timer:sleep(?REKEY_DATA_TMO),
    ?wait_match(false, Kex1==ssh_test_lib:get_kex_init(ConnectionRef), [], 2000, 10),

    ssh:close(ConnectionRef),
    ssh:stop_daemon(Pid).

%%--------------------------------------------------------------------
%%% Test rekeying by data volume

rekey_limit_client() -> [{timetrap,{seconds,500}}].
rekey_limit_client(Config) ->
    Limit = 6000,
    UserDir = proplists:get_value(priv_dir, Config),
    DataFile = filename:join(UserDir, "rekey.data"),
    Data = lists:duplicate(Limit+10,1),
    Algs = proplists:get_value(preferred_algorithms, Config),
    {Pid, Host, Port} =
        ssh_test_lib:std_daemon(Config,
                                [{subsystems, [ssh_sftpd:subsystem_spec([])]},
                                 {max_random_length_padding,0},
                                 ?ALIVE,
                                 {preferred_algorithms,Algs}]),

    ConnectionRef =
        ssh_test_lib:std_connect(Config, Host, Port,
                                 [{rekey_limit, Limit},
                                  ?ALIVE,
                                  {max_random_length_padding,0}]),
    {ok, SftpPid} = ssh_sftp:start_channel(ConnectionRef),

    %% Check that it doesn't rekey without data transfer
    Kex1 = ssh_test_lib:get_kex_init(ConnectionRef),
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex1 == ssh_test_lib:get_kex_init(ConnectionRef)),

    %% Check that datatransfer triggers rekeying
    ok = ssh_sftp:write_file(SftpPid, DataFile, Data),
    timer:sleep(?REKEY_DATA_TMO),
    ?wait_match(false, Kex1==(Kex2=ssh_test_lib:get_kex_init(ConnectionRef)), Kex2, 2000, 10),

    %% Check that datatransfer continues to trigger rekeying
    ok = ssh_sftp:write_file(SftpPid, DataFile, Data),
    timer:sleep(?REKEY_DATA_TMO),
    ?wait_match(false, Kex2==(Kex3=ssh_test_lib:get_kex_init(ConnectionRef)), Kex3, 2000, 10),

    %% Check that it doesn't rekey without data transfer
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex3 == ssh_test_lib:get_kex_init(ConnectionRef)),

    %% Check that it doesn't rekey on a small datatransfer
    ok = ssh_sftp:write_file(SftpPid, DataFile, "hi\n"),
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex3 == ssh_test_lib:get_kex_init(ConnectionRef)),

    %% Check that it doesn't rekey without data transfer
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex3 == ssh_test_lib:get_kex_init(ConnectionRef)),

    ssh_sftp:stop_channel(SftpPid),
    ssh:close(ConnectionRef),
    ssh:stop_daemon(Pid).

rekey_limit_daemon() -> [{timetrap,{seconds,500}}].
rekey_limit_daemon(Config) ->
    Limit = 6000,
    UserDir = proplists:get_value(priv_dir, Config),
    DataFile1 = filename:join(UserDir, "rekey1.data"),
    DataFile2 = filename:join(UserDir, "rekey2.data"),
    file:write_file(DataFile1, lists:duplicate(Limit+10,1)),
    file:write_file(DataFile2, "hi\n"),

    Algs = proplists:get_value(preferred_algorithms, Config),
    {Pid, Host, Port} =
        ssh_test_lib:std_daemon(Config, [{subsystems, [ssh_sftpd:subsystem_spec([])]},
                                         {rekey_limit, Limit},
                                         {max_random_length_padding,0},
                                         ?ALIVE,
                                         {preferred_algorithms,Algs}]),
    ConnectionRef =
        ssh_test_lib:std_connect(Config, Host, Port,
                                 [?ALIVE, {max_random_length_padding,0}]),
    {ok, SftpPid} = ssh_sftp:start_channel(ConnectionRef),

    %% Check that it doesn't rekey without data transfer
    Kex1 = ssh_test_lib:get_kex_init(ConnectionRef),
    timer:sleep(?REKEY_DATA_TMO),
    Kex1 = ssh_test_lib:get_kex_init(ConnectionRef),

    %% Check that datatransfer triggers rekeying
    {ok,_} = ssh_sftp:read_file(SftpPid, DataFile1),
    timer:sleep(?REKEY_DATA_TMO),
    ?wait_match(false, Kex1==(Kex2=ssh_test_lib:get_kex_init(ConnectionRef)), Kex2, 2000, 10),

    %% Check that datatransfer continues to trigger rekeying
    {ok,_} = ssh_sftp:read_file(SftpPid, DataFile1),
    timer:sleep(?REKEY_DATA_TMO),
    ?wait_match(false, Kex2==(Kex3=ssh_test_lib:get_kex_init(ConnectionRef)), Kex3, 2000, 10),

    %% Check that it doesn't rekey without data transfer
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex3 == ssh_test_lib:get_kex_init(ConnectionRef)),

    %% Check that it doesn't rekey on a small datatransfer
    {ok,_} = ssh_sftp:read_file(SftpPid, DataFile2),
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex3 == ssh_test_lib:get_kex_init(ConnectionRef)),

    %% Check that it doesn't rekey without data transfer
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex3 == ssh_test_lib:get_kex_init(ConnectionRef)),

    ssh_sftp:stop_channel(SftpPid),
    ssh:close(ConnectionRef),
    ssh:stop_daemon(Pid).


%%--------------------------------------------------------------------
%% Check that datatransfer in the other direction does not trigger re-keying
norekey_limit_client() -> [{timetrap,{seconds,500}}].
norekey_limit_client(Config) ->
    Limit = 6500,
    UserDir = proplists:get_value(priv_dir, Config),
    DataFile = filename:join(UserDir, "rekey3.data"),
    file:write_file(DataFile, lists:duplicate(Limit+10,1)),

    Algs = proplists:get_value(preferred_algorithms, Config),
    {Pid, Host, Port} =
        ssh_test_lib:std_daemon(Config,[{subsystems, [ssh_sftpd:subsystem_spec([])]},
                                        {max_random_length_padding,0},
                                        ?ALIVE,
                                        {preferred_algorithms,Algs}]),

    ConnectionRef =
        ssh_test_lib:std_connect(Config, Host, Port,
                                 [{rekey_limit, Limit},
                                  ?ALIVE,
                                  {max_random_length_padding,0}]),
    {ok, SftpPid} = ssh_sftp:start_channel(ConnectionRef),

    Kex1 = ssh_test_lib:get_kex_init(ConnectionRef),
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex1 == ssh_test_lib:get_kex_init(ConnectionRef)),
    
    {ok,_} = ssh_sftp:read_file(SftpPid, DataFile),
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex1 == ssh_test_lib:get_kex_init(ConnectionRef)),
    
    ssh_sftp:stop_channel(SftpPid),
    ssh:close(ConnectionRef),
    ssh:stop_daemon(Pid).

%% Check that datatransfer in the other direction does not trigger re-keying
norekey_limit_daemon() -> [{timetrap,{seconds,500}}].
norekey_limit_daemon(Config) ->
    Limit = 6000,
    UserDir = proplists:get_value(priv_dir, Config),
    DataFile = filename:join(UserDir, "rekey4.data"),

    Algs = proplists:get_value(preferred_algorithms, Config),
    {Pid, Host, Port} =
        ssh_test_lib:std_daemon(Config,[{subsystems, [ssh_sftpd:subsystem_spec([])]},
                                        {rekey_limit, Limit},
                                        {max_random_length_padding,0},
                                        ?ALIVE,
                                        {preferred_algorithms,Algs}]),

    ConnectionRef =
        ssh_test_lib:std_connect(Config, Host, Port,
                                 [?ALIVE, {max_random_length_padding,0}]),
    {ok, SftpPid} = ssh_sftp:start_channel(ConnectionRef),

    Kex1 = ssh_test_lib:get_kex_init(ConnectionRef),
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex1 == ssh_test_lib:get_kex_init(ConnectionRef)),
    
    ok = ssh_sftp:write_file(SftpPid, DataFile, lists:duplicate(Limit+10,1)),
    timer:sleep(?REKEY_DATA_TMO),
    true = (Kex1 == ssh_test_lib:get_kex_init(ConnectionRef)),
    
    ssh_sftp:stop_channel(SftpPid),
    ssh:close(ConnectionRef),
    ssh:stop_daemon(Pid).

%%--------------------------------------------------------------------
%%% Test rekeying by time

rekey_time_limit_client() -> [{timetrap,{seconds,500}}].
rekey_time_limit_client(Config) ->
    Minutes = ?REKEY_DATA_TMO div 60000,
    GB = 1024*1000*1000,
    Algs = proplists:get_value(preferred_algorithms, Config),
    {Pid, Host, Port} =
        ssh_test_lib:std_daemon(Config,[{subsystems, [ssh_sftpd:subsystem_spec([])]},
                                        {max_random_length_padding,0},
                                        ?ALIVE,
                                        {preferred_algorithms,Algs}]),
    ConnectionRef =
        ssh_test_lib:std_connect(Config, Host, Port,
                                 [{rekey_limit, {Minutes, GB}},
                                  ?ALIVE,
                                  {max_random_length_padding,0}]),
    rekey_time_limit(Pid, ConnectionRef).

rekey_time_limit_daemon() -> [{timetrap,{seconds,500}}].
rekey_time_limit_daemon(Config) ->
    Minutes = ?REKEY_DATA_TMO div 60000,
    GB = 1024*1000*1000,
    Algs = proplists:get_value(preferred_algorithms, Config),
    {Pid, Host, Port} =
        ssh_test_lib:std_daemon(Config,[{subsystems, [ssh_sftpd:subsystem_spec([])]},
                                        {rekey_limit, {Minutes, GB}},
                                        {max_random_length_padding,0},
                                        ?ALIVE,
                                        {preferred_algorithms,Algs}]),
    ConnectionRef =
        ssh_test_lib:std_connect(Config, Host, Port,
                                 [?ALIVE, {max_random_length_padding,0}]),
    rekey_time_limit(Pid, ConnectionRef).


rekey_time_limit(Pid, ConnectionRef) ->
    {ok, SftpPid} = ssh_sftp:start_channel(ConnectionRef),
    Kex1 = ssh_test_lib:get_kex_init(ConnectionRef),

    timer:sleep(5000),
    true = (Kex1 == ssh_test_lib:get_kex_init(ConnectionRef)),

    %% Check that it rekeys when the max time + 30s has passed
    timer:sleep(?REKEY_DATA_TMO + 30*1000),
    ?wait_match(false, Kex1==(Kex2=ssh_test_lib:get_kex_init(ConnectionRef)), Kex2, 2000, 10),

    %% Check that it does not rekey when nothing is transferred
    timer:sleep(?REKEY_DATA_TMO + 30*1000),
    ?wait_match(false, Kex2==ssh_test_lib:get_kex_init(ConnectionRef), [], 2000, 10),

    ssh_sftp:stop_channel(SftpPid),
    ssh:close(ConnectionRef),
    ssh:stop_daemon(Pid).

%%--------------------------------------------------------------------

%%% Test rekeying with simultaneous send request

renegotiate1(Config) ->
    UserDir = proplists:get_value(priv_dir, Config),
    DataFile = filename:join(UserDir, "renegotiate1.data"),

    Algs = proplists:get_value(preferred_algorithms, Config),
    {Pid, Host, DPort} =
        ssh_test_lib:std_daemon(Config,[{subsystems, [ssh_sftpd:subsystem_spec([])]},
                                        {max_random_length_padding,0},
                                        ?ALIVE,
                                        {preferred_algorithms,Algs}]),

    {ok,RelayPid,_,RPort} = ssh_relay:start_link({0,0,0,0}, 0, Host, DPort),

    ConnectionRef =
        ssh_test_lib:std_connect(Config, Host, RPort,
                                 [?ALIVE, {max_random_length_padding,0}]),
    {ok, SftpPid} = ssh_sftp:start_channel(ConnectionRef),

    Kex1 = ssh_test_lib:get_kex_init(ConnectionRef),

    {ok, Handle} = ssh_sftp:open(SftpPid, DataFile, [write]),

    ok = ssh_sftp:write(SftpPid, Handle, "hi\n"),

    ssh_relay:hold(RelayPid, rx, 20, 1000),
    ssh_connection_handler:renegotiate(ConnectionRef),
    spawn(fun() -> ok=ssh_sftp:write(SftpPid, Handle, "another hi\n") end),

    timer:sleep(2000),

    Kex2 = ssh_test_lib:get_kex_init(ConnectionRef),

    false = (Kex2 == Kex1),
    
    ssh_relay:stop(RelayPid),
    ssh_sftp:stop_channel(SftpPid),
    ssh:close(ConnectionRef),
    ssh:stop_daemon(Pid).

%%--------------------------------------------------------------------

%%% Test rekeying with inflight messages from peer

renegotiate2(Config) ->
    UserDir = proplists:get_value(priv_dir, Config),
    DataFile = filename:join(UserDir, "renegotiate2.data"),

    Algs = proplists:get_value(preferred_algorithms, Config),
    {Pid, Host, DPort} =
        ssh_test_lib:std_daemon(Config,
                                [{subsystems, [ssh_sftpd:subsystem_spec([])]},
                                 {max_random_length_padding,0},
                                 ?ALIVE,
                                 {preferred_algorithms,Algs}]),

    {ok,RelayPid,_,RPort} = ssh_relay:start_link({0,0,0,0}, 0, Host, DPort),

    ConnectionRef =
        ssh_test_lib:std_connect(Config, Host, RPort,
                                 [?ALIVE, {max_random_length_padding,0}]),
    {ok, SftpPid} = ssh_sftp:start_channel(ConnectionRef),

    Kex1 = ssh_test_lib:get_kex_init(ConnectionRef),

    {ok, Handle} = ssh_sftp:open(SftpPid, DataFile, [write]),

    ok = ssh_sftp:write(SftpPid, Handle, "hi\n"),

    ssh_relay:hold(RelayPid, rx, 20, infinity),
    spawn(fun() -> ok=ssh_sftp:write(SftpPid, Handle, "another hi\n") end),
    %% need a small pause here to ensure ssh_sftp:write is executed
    ct:sleep(10),
    ssh_connection_handler:renegotiate(ConnectionRef),
    ssh_relay:release(RelayPid, rx),

    timer:sleep(2000),

    Kex2 = ssh_test_lib:get_kex_init(ConnectionRef),

    false = (Kex2 == Kex1),

    ssh_relay:stop(RelayPid),
    ssh_sftp:stop_channel(SftpPid),
    ssh:close(ConnectionRef),
    ssh:stop_daemon(Pid).

%%--------------------------------------------------------------------
%% Internal functions ------------------------------------------------
%%--------------------------------------------------------------------
