1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2008-2020. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21%%
22
23-module(ssh_compat_SUITE).
24
25-include_lib("common_test/include/ct.hrl").
26-include("ssh_transport.hrl"). % #ssh_msg_kexinit{}
27-include_lib("kernel/include/inet.hrl"). % #hostent{}
28-include_lib("kernel/include/file.hrl"). % #file_info{}
29-include("ssh_test_lib.hrl").
30
31-export([
32         suite/0,
33         all/0,
34         groups/0,
35         init_per_suite/1,
36         end_per_suite/1,
37         init_per_group/2,
38         end_per_group/2
39        ]).
40
41-export([
42         all_algorithms_sftp_exec_reneg_otp_is_client/1,
43         check_docker_present/1,
44         login_otp_is_client/1,
45         login_otp_is_server/1,
46         renegotiation_otp_is_server/1,
47         send_recv_big_with_renegotiate_otp_is_client/1
48        ]).
49
50-define(USER,"sshtester").
51-define(PASSWD, "foobar").
52-define(BAD_PASSWD, "NOT-"?PASSWD).
53-define(DOCKER_PFX, "ssh_compat_suite-ssh").
54
55%%--------------------------------------------------------------------
56%% Common Test interface functions -----------------------------------
57%%--------------------------------------------------------------------
58
59suite() ->
60    [{timetrap,{seconds,90}}].
61
62all() ->
63%%    [check_docker_present] ++
64    [{group,G} || G <- ssh_image_versions()].
65
66groups() ->
67    [{otp_client, [], [login_otp_is_client,
68                       all_algorithms_sftp_exec_reneg_otp_is_client,
69                       send_recv_big_with_renegotiate_otp_is_client
70                      ]},
71     {otp_server, [], [login_otp_is_server,
72                       renegotiation_otp_is_server
73                      ]} |
74     [{G, [], [{group,otp_client}, {group,otp_server}]} || G <- ssh_image_versions()]
75    ].
76
77
78ssh_image_versions() ->
79    try
80        %% Find all useful containers in such a way that undefined command, too low
81        %% priviliges, no containers and containers found give meaningful result:
82        L0 = ["REPOSITORY"++_|_] = string:tokens(os:cmd("docker images"), "\r\n"),
83        [["REPOSITORY","TAG"|_]|L1] = [string:tokens(E, " ") || E<-L0],
84        [list_to_atom(V) || [?DOCKER_PFX,V|_] <- L1]
85    of
86        Vs ->
87            lists:sort(Vs)
88    catch
89        error:{badmatch,_} ->
90            []
91    end.
92
93%%--------------------------------------------------------------------
94init_per_suite(Config) ->
95    ?CHECK_CRYPTO(
96       case os:find_executable("docker") of
97           false ->
98               {skip, "No docker"};
99           _ ->
100               ssh:start(),
101               ct:log("Crypto info: ~p",[crypto:info_lib()]),
102               Config
103       end).
104
105end_per_suite(_Config) ->
106    %% Remove all containers that are not running:
107%%%    os:cmd("docker rm $(docker ps -aq -f status=exited)"),
108    %% Remove dangling images:
109%%%    os:cmd("docker rmi $(docker images -f dangling=true -q)"),
110    catch ssh:stop(),
111    ok.
112
113
114init_per_group(otp_server, Config) ->
115    case proplists:get_value(common_remote_client_algs, Config) of
116        undefined ->
117            SSHver = proplists:get_value(ssh_version, Config, ""),
118            {skip,"No "++SSHver++ " client found in docker"};
119        _ ->
120            Config
121    end;
122
123init_per_group(otp_client, Config) ->
124    Config;
125
126init_per_group(G, Config0) ->
127    case lists:member(G, ssh_image_versions()) of
128	true ->
129            %% This group is for one of the images
130            Vssh = atom_to_list(G),
131            Cmnt = io_lib:format("+++ ~s +++",[Vssh]),
132            ct:comment("~s",[Cmnt]),
133            try start_docker(G) of
134                {ok,ID} ->
135                    ct:log("==> ~p started",[G]),
136                    %% Find the algorithms that both client and server supports:
137                    {IP,Port} = ip_port([{id,ID}]),
138                    ct:log("Try contact ~p:~p",[IP,Port]),
139                    Config1 = [{id,ID},
140                               {ssh_version,Vssh}
141                               | Config0],
142                    try common_algs(Config1, IP, Port) of
143                        {ok, ServerHello, RemoteServerCommon, ClientHello, RemoteClientCommon} ->
144                            case chk_hellos([ServerHello,ClientHello], Cmnt) of
145                                Cmnt ->
146                                    ok;
147                                NewCmnt ->
148                                    ct:comment("~s",[NewCmnt])
149                            end,
150                            AuthMethods =
151                                %% This should be obtained by quering the peer, but that
152                                %% is a bit hard. It is possible with ssh_protocol_SUITE
153                                %% techniques, but it can wait.
154                                case Vssh of
155                                    "dropbear" ++ _ ->
156                                        [password, publickey];
157                                    _ ->
158                                        [password, 'keyboard-interactive', publickey]
159                                end,
160                            [{common_remote_server_algs,RemoteServerCommon},
161                             {common_remote_client_algs,RemoteClientCommon},
162                             {common_authmethods,AuthMethods}
163                             |Config1];
164                        Other ->
165                            ct:log("Error in init_per_group: ~p",[Other]),
166                            stop_docker(ID),
167                            {fail, "Can't contact docker sshd"}
168                    catch
169                        Class:Exc:ST ->
170                            ct:log("common_algs: ~p:~p~n~p",[Class,Exc,ST]),
171                            stop_docker(ID),
172                            {fail, "Failed during setup"}
173                    end
174            catch
175                cant_start_docker ->
176                    {skip, "Can't start docker"};
177
178                C:E:ST ->
179                    ct:log("No ~p~n~p:~p~n~p",[G,C,E,ST]),
180                    {skip, "Can't start docker"}
181            end;
182
183	false ->
184	    Config0
185    end.
186
187end_per_group(G, Config) ->
188    case lists:member(G, ssh_image_versions()) of
189        true ->
190            catch stop_docker(proplists:get_value(id,Config));
191        false ->
192            ok
193    end.
194
195%%--------------------------------------------------------------------
196%% Test Cases --------------------------------------------------------
197%%--------------------------------------------------------------------
198check_docker_present(_Config) ->
199    ct:log("This testcase is just to show in Monitor that we have a test host with docker installed",[]),
200    {fail, "Test is OK: just showing docker is available"}.
201
202%%--------------------------------------------------------------------
203login_otp_is_client(Config) ->
204    {IP,Port} = ip_port(Config),
205    PublicKeyAlgs = [A || {public_key,A} <- proplists:get_value(common_remote_server_algs, Config)],
206    CommonAuths =
207        [{AuthMethod,Alg} || AuthMethod <- proplists:get_value(common_authmethods, Config),
208                             Alg <- case AuthMethod of
209                                        publickey ->
210                                            PublicKeyAlgs;
211                                        _ ->
212                                            [' ']
213                                    end
214        ],
215
216    chk_all_algos(?FUNCTION_NAME, CommonAuths, Config,
217                  fun(AuthMethod,Alg) ->
218                          {Opts,Dir} =
219                              case AuthMethod of
220                                  publickey ->
221                                      {[{pref_public_key_algs, [Alg]}],
222                                       setup_remote_auth_keys_and_local_priv(Alg, Config)};
223                                  _ ->
224                                      {[{password,?PASSWD}], new_dir(Config)}
225                              end,
226                          ssh:connect(IP, Port, [{auth_methods, atom_to_list(AuthMethod)},
227                                                 {preferred_algorithms, ssh_transport:supported_algorithms()},
228                                                 {user,?USER},
229                                                 {user_dir, Dir},
230                                                 {silently_accept_hosts,true},
231                                                 {user_interaction,false}
232                                                 | Opts
233                                                ])
234                  end).
235
236
237%%--------------------------------------------------------------------
238login_otp_is_server(Config) ->
239    PublicKeyAlgs = [A || {public_key,A} <- proplists:get_value(common_remote_client_algs, Config)],
240    CommonAuths =
241        [{AuthMethod,Alg} || AuthMethod <- proplists:get_value(common_authmethods, Config),
242                             Alg <- case AuthMethod of
243                                        publickey ->
244                                            PublicKeyAlgs;
245                                        _ ->
246                                            [' ']
247                                    end
248        ],
249    SysDir = setup_local_hostdir(hd(PublicKeyAlgs), Config),
250    chk_all_algos(?FUNCTION_NAME, CommonAuths, Config,
251                  fun(AuthMethod,Alg) ->
252                          {Opts,UsrDir} =
253                              case AuthMethod of
254                                  publickey ->
255                                      {[{user_passwords, [{?USER,?BAD_PASSWD}]},
256                                        {pref_public_key_algs, [Alg]}
257                                       ],
258                                       setup_remote_priv_and_local_auth_keys(Alg, Config)
259                                      };
260                                  _ ->
261                                      {[{user_passwords, [{?USER,?PASSWD}]}],
262                                       new_dir(Config)
263                                      }
264                              end,
265                          {Server, Host, HostPort} =
266                              ssh_test_lib:daemon(0,
267                                                  [{auth_methods, atom_to_list(AuthMethod)},
268                                                   {preferred_algorithms, ssh_transport:supported_algorithms()},
269                                                   {system_dir, SysDir},
270                                                   {user_dir, UsrDir},
271                                                   {failfun, fun ssh_test_lib:failfun/2}
272                                                   | Opts
273                                                  ]),
274                          R = exec_from_docker(Config, Host, HostPort,
275                                               "'lists:concat([\"Answer=\",1+3]).\r\n'",
276                                               [<<"Answer=4">>],
277                                               ""),
278                          ssh:stop_daemon(Server),
279                          R
280                  end).
281
282%%--------------------------------------------------------------------
283all_algorithms_sftp_exec_reneg_otp_is_client(Config) ->
284    CommonAlgs = proplists:get_value(common_remote_server_algs, Config),
285    {IP,Port} = ip_port(Config),
286    chk_all_algos(?FUNCTION_NAME, CommonAlgs, Config,
287                  fun(Tag, Alg) ->
288                          PrefAlgs =
289                              [{T,L} || {T,L} <- ssh_transport:supported_algorithms(),
290                                        T =/= Tag],
291                          ConnRes =
292                              ssh:connect(IP, Port,
293                                          [{user,?USER},
294                                           {password,?PASSWD},
295                                           {auth_methods, "password"},
296                                           {user_dir, new_dir(Config)},
297                                           {preferred_algorithms, [{Tag,[Alg]} | PrefAlgs]},
298                                           {silently_accept_hosts,true},
299                                           {user_interaction,false}
300                                          ])  ,
301                          test_erl_client_reneg(ConnRes, % Seems that max 10 channels may be open in sshd
302                                                [{exec,1},
303                                                 {sftp,5},
304                                                 {no_subsyst,1},
305                                                 {setenv, 1},
306                                                 {sftp_async,1}
307                                                ])
308                  end).
309
310%%--------------------------------------------------------------------
311renegotiation_otp_is_server(Config) ->
312    PublicKeyAlgs = [A || {public_key,A} <- proplists:get_value(common_remote_client_algs, Config, [])],
313    UserDir = setup_remote_priv_and_local_auth_keys(hd(PublicKeyAlgs), Config),
314    SftpRootDir = new_dir(Config),
315    ct:log("Rootdir = ~p",[SftpRootDir]),
316    Parent = self(),
317    Ref = make_ref(),
318    {Server, Host, Port} =
319        ssh_test_lib:daemon(0,
320                            [{system_dir, setup_local_hostdir(Config)},
321                             {user_dir, UserDir},
322                             {user_passwords, [{?USER,?PASSWD}]},
323                             {failfun, fun ssh_test_lib:failfun/2},
324                             {connectfun,
325                              fun(_,_,_) ->
326                                      HostConnRef = self(),
327                                      reneg_tester(Parent, Ref, HostConnRef),
328                                      ct:log("Connected ~p",[HostConnRef]),
329                                      timer:sleep(1100), % Just a bit more than  1 s
330                                      ok
331                              end},
332                             {subsystems,
333                              [ssh_sftpd:subsystem_spec([{cwd,SftpRootDir},
334                                                         {root,SftpRootDir}])
335                              ]}
336                            ]),
337    case sftp_tests_erl_server(Config, Host, Port, SftpRootDir, UserDir, Ref) of
338        ok ->
339            ssh:stop_daemon(Server);
340        {error,Error} ->
341            ssh:stop_daemon(Server),
342            ct:log("Error: ~p", [Error]),
343            {fail, Error}
344    end.
345
346
347reneg_tester(Parent, Ref, HostConnRef) ->
348    spawn(fun() ->
349                  reneg_tester_loop(Parent, Ref, HostConnRef, renegotiate_test(init,HostConnRef))
350          end).
351
352reneg_tester_loop(Parent, Ref, HostConnRef, Kex1) ->
353    case ssh_test_lib:get_kex_init(HostConnRef) of
354        Kex1 ->
355            timer:sleep(500),
356            reneg_tester_loop(Parent, Ref, HostConnRef, Kex1);
357        _OtherKex ->
358            ct:log("Kex is changed.", []),
359            Parent ! {kex_changed, Ref}
360    end.
361
362
363%%--------------------------------------------------------------------
364send_recv_big_with_renegotiate_otp_is_client(Config) ->
365    %% Connect to the remote openssh server:
366    {IP,Port} = ip_port(Config),
367    {ok,C} = ssh:connect(IP, Port, [{user,?USER},
368                                    {password,?PASSWD},
369                                    {user_dir, setup_remote_auth_keys_and_local_priv('ssh-rsa', Config)},
370                                    {silently_accept_hosts,true},
371                                    {preferred_algorithms, ssh_transport:supported_algorithms()},
372                                    {user_interaction,false}
373                                   ]),
374
375    %% Open a channel and exec the Linux 'cat' command at the openssh side.
376    %% This 'cat' will read stdin and write to stdout until an eof is read from stdin.
377    {ok, Ch1} = ssh_connection:session_channel(C, infinity),
378    success = ssh_connection:exec(C, Ch1, "cat", infinity),
379
380    %% Build big binary
381    HalfSizeBytes = 100*1000*1000,
382    Data = << <<X:32>> || X <- lists:seq(1, HalfSizeBytes div 4)>>,
383
384    %% Send the data. Must spawn a process to avoid deadlock. The client will block
385    %% until all is sent through the send window. But the server will stop receiveing
386    %% when the servers send-window towards the client is full.
387    %% Since the client can't receive before the server has received all but 655k from the client
388    %% ssh_connection:send/4 is blocking...
389    spawn_link(
390      fun() ->
391              ct:comment("Sending ~p Mbytes with renegotiation in the middle",[2*byte_size(Data)/1000000]),
392              %% ct:log("sending first ~p bytes",[byte_size(Data)]),
393              ok = ssh_connection:send(C, Ch1, Data, 10000),
394              %% ct:log("Init renegotiation test",[]),
395              Kex1 = renegotiate_test(init, C),
396              %% ct:log("sending next ~p bytes",[byte_size(Data)]),
397              ok = ssh_connection:send(C, Ch1, Data, 10000),
398              %% ct:log("Finnish renegotiation test",[]),
399              renegotiate_test(Kex1, C),
400              %% ct:log("sending eof",[]),
401              ok = ssh_connection:send_eof(C, Ch1)
402              %%, ct:log("READY, sent ~p bytes",[2*byte_size(Data)])
403      end),
404
405    {eof,ReceivedData} =
406        loop_until(fun({eof,_}) -> true;
407                      (_      ) -> false
408                   end,
409                   fun(Acc) ->
410                           %%ct:log("Get more ~p",[ ExpectedSize-byte_size(Acc) ]),
411                           receive
412                               {ssh_cm, C, {eof,Ch}} when Ch==Ch1 ->
413                                   %% ct:log("eof received",[]),
414                                   {eof,Acc};
415
416                               {ssh_cm, C, {data,Ch,0,B}} when Ch==Ch1,
417                                                               is_binary(B) ->
418                                   %% ct:log("(1) Received ~p bytes (total ~p), missing ~p bytes",
419                                   %%        [byte_size(B),
420                                   %%         byte_size(B)+byte_size(Acc),
421                                   %%         2*byte_size(Data)-(byte_size(B)+byte_size(Acc))]),
422                                   ssh_connection:adjust_window(C, Ch1, byte_size(B)),
423                                   <<Acc/binary, B/binary>>
424                           end
425                   end,
426                   <<>>),
427
428    ExpectedData = <<Data/binary, Data/binary>>,
429    case ReceivedData of
430        ExpectedData ->
431            %% ct:log("Correct data returned",[]),
432            %% receive close messages
433            loop_until(fun(Left) -> %% ct:log("Expect: ~p",[Left]),
434                                    Left == []
435                       end,
436                       fun([Next|Rest]) ->
437                               receive
438                                  {ssh_cm,C,Next} -> Rest
439                               end
440                       end,
441                       [%% Already received: {eof, Ch1},
442                        {exit_status,Ch1,0},
443                        {closed,Ch1}]
444                      ),
445            ok;
446        _ when is_binary(ReceivedData) ->
447            ct:fail("~p bytes echoed but ~p expected", [byte_size(ReceivedData), 2*byte_size(Data)])
448    end.
449
450%%--------------------------------------------------------------------
451%% Utilities ---------------------------------------------------------
452%%--------------------------------------------------------------------
453
454%%--------------------------------------------------------------------
455%%
456%% A practical meta function
457%%
458loop_until(CondFun, DoFun, Acc) ->
459    case CondFun(Acc) of
460        true ->
461            Acc;
462        false ->
463            loop_until(CondFun, DoFun, DoFun(Acc))
464    end.
465
466%%--------------------------------------------------------------------
467%%
468%% Exec the Command in the docker.  Add the arguments ExtraSshArg in the
469%% ssh command.
470%%
471%% If Expects is returned, then return 'ok', else return {fail,Msg}.
472%%
473exec_from_docker(Config, HostIP, HostPort, Command, Expects, ExtraSshArg) when is_binary(hd(Expects)),
474                                                                               is_list(Config) ->
475    {DockerIP,DockerPort} = ip_port(Config),
476    {ok,C} = ssh:connect(DockerIP, DockerPort,
477                         [{user,?USER},
478                          {password,?PASSWD},
479                          {user_dir, new_dir(Config)},
480                          {preferred_algorithms, ssh_transport:supported_algorithms()},
481                          {silently_accept_hosts,true},
482                          {user_interaction,false}
483                         ]),
484    R = exec_from_docker(C, HostIP, HostPort, Command, Expects, ExtraSshArg, Config),
485    ssh:close(C),
486    R.
487
488exec_from_docker(C, DestIP, DestPort, Command, Expects, ExtraSshArg, Config) when is_binary(hd(Expects)) ->
489    ExecCommand =
490        lists:concat(
491          ["sshpass -p ",?PASSWD," "
492           | case proplists:get_value(ssh_version,Config) of
493                 "dropbear" ++ _ ->
494                     ["dbclient -y -y -p ",DestPort," ",ExtraSshArg," ",iptoa(DestIP)," "];
495
496                 _ -> %% OpenSSH or compatible
497                     ["/buildroot/ssh/bin/ssh -o 'CheckHostIP=no' -o 'StrictHostKeyChecking=no' ",
498                      ExtraSshArg," -p ",DestPort," ",iptoa(DestIP)," "]
499             end]) ++ Command,
500
501    case exec(C, ExecCommand) of
502        {ok,{ExitStatus,Result}} = R when ExitStatus == 0 ->
503            case binary:match(Result, Expects) of
504                nomatch ->
505                    ct:log("Result of~n    ~s~nis~n    ~p",[ExecCommand,R]),
506                    {fail, "Bad answer"};
507                _ ->
508                    ok
509            end;
510        {ok,_} = R ->
511            ct:log("Result of~n    ~s~nis~n    ~p",[ExecCommand,R]),
512            {fail, "Exit status =/= 0"};
513        R ->
514            ct:log("Result of~n    ~s~nis~n    ~p",[ExecCommand,R]),
515            {fail, "Couldn't login to host"}
516    end.
517
518
519exec(C, Cmd) ->
520    %% ct:log("~s",[Cmd]),
521    {ok,Ch} = ssh_connection:session_channel(C, 10000),
522    success = ssh_connection:exec(C, Ch, Cmd, 10000),
523    result_of_exec(C, Ch).
524
525
526result_of_exec(C, Ch) ->
527    result_of_exec(C, Ch, undefined, <<>>).
528
529result_of_exec(C, Ch, ExitStatus, Acc) ->
530    receive
531        {ssh_cm,C,{closed,Ch}} ->
532            %%ct:log("CHAN ~p got *closed*",[Ch]),
533            {ok, {ExitStatus, Acc}};
534
535        {ssh_cm,C,{exit_status,Ch,ExStat}} when ExitStatus == undefined ->
536            %%ct:log("CHAN ~p got *exit status ~p*",[Ch,ExStat]),
537            result_of_exec(C, Ch, ExStat, Acc);
538
539        {ssh_cm,C,{data,Ch,_,Data}=_X} when ExitStatus == undefined ->
540            %%ct:log("CHAN ~p got ~p",[Ch,_X]),
541            result_of_exec(C, Ch, ExitStatus, <<Acc/binary, Data/binary>>);
542
543        _Other ->
544            %%ct:log("OTHER: ~p",[_Other]),
545            result_of_exec(C, Ch, ExitStatus, Acc)
546
547    after 5000 ->
548            ct:log("NO MORE, received so far:~n~s",[Acc]),
549            {error, timeout}
550    end.
551
552
553%%--------------------------------------------------------------------
554%%
555%% Loop through all {Tag,Alg} pairs in CommonAlgs, call DoTestFun(Tag,Alg) which
556%% returns one of {ok,C}, ok, or Other.
557%%
558%% The chk_all_algos returns 'ok' or {fail,FaledAlgosList}
559%%
560
561chk_all_algos(FunctionName, CommonAlgs, Config, DoTestFun) when is_function(DoTestFun,2) ->
562    ct:comment("~p algorithms",[length(CommonAlgs)]),
563    %% Check each algorithm
564    Nmax = length(CommonAlgs),
565    {_N,Failed} =
566        lists:foldl(
567          fun({Tag,Alg}, {N,FailedAlgos}) ->
568                  ct:log("Try ~p  ~p/~p",[{Tag,Alg},N,Nmax]),
569                  case DoTestFun(Tag,Alg) of
570                      {ok,C} ->
571                          ssh:close(C),
572                          {N+1,FailedAlgos};
573                      ok ->
574                          {N+1,FailedAlgos};
575                      Other ->
576                          ct:log("FAILED! ~p ~p: ~p",[Tag,Alg,Other]),
577                          {N+1, [{Alg,Other}|FailedAlgos]}
578                  end
579          end, {1,[]}, CommonAlgs),
580    ct:pal("~s", [format_result_table_use_all_algos(FunctionName, Config, CommonAlgs, Failed)]),
581    case Failed of
582        [] ->
583            ok;
584        _ ->
585            {fail, Failed}
586    end.
587
588
589
590%%--------------------------------------------------------------------
591%%
592%% Functions to set up local and remote host's and user's keys and directories
593%%
594
595setup_local_hostdir(Config) ->
596    KeyAlgs = lists:usort(
597                [A || From <- [common_remote_client_algs,
598                               common_remote_server_algs],
599                      {public_key,A} <- proplists:get_value(From, Config, [])]),
600    setup_local_hostdirs(KeyAlgs, new_dir(Config), Config).
601
602
603setup_local_hostdirs(KeyAlgs, HostDir, Config) ->
604    lists:foreach(
605      fun(KeyAlg) ->
606              setup_local_hostdir(KeyAlg, HostDir, Config)
607      end, KeyAlgs),
608    HostDir.
609
610
611setup_local_hostdir(KeyAlg, Config) ->
612    setup_local_hostdir(KeyAlg, new_dir(Config), Config).
613setup_local_hostdir(KeyAlg, HostDir, Config) ->
614    {ok, {Priv,Publ}} = host_priv_pub_keys(Config, KeyAlg),
615    %% Local private and public key
616    DstFile = filename:join(HostDir, dst_filename(host,KeyAlg)),
617    ok = file:write_file(DstFile,         Priv),
618    ok = file:write_file(DstFile++".pub", Publ),
619    HostDir.
620
621
622setup_remote_auth_keys_and_local_priv(KeyAlg, Config) ->
623    {IP,Port} = ip_port(Config),
624    setup_remote_auth_keys_and_local_priv(KeyAlg, IP, Port, new_dir(Config), Config).
625
626setup_remote_auth_keys_and_local_priv(KeyAlg, IP, Port, UserDir, Config) ->
627    {ok, {Priv,Publ}} = user_priv_pub_keys(Config, KeyAlg),
628    %% Local private and public keys
629    DstFile = filename:join(UserDir, dst_filename(user,KeyAlg)),
630    ok = file:write_file(DstFile,         Priv),
631    ok = file:write_file(DstFile++".pub", Publ),
632    %% Remote auth_methods with public key
633    {ok,Ch,Cc} = ssh_sftp:start_channel(IP, Port, [{user,     ?USER  },
634                                                   {password, ?PASSWD   },
635                                                   {auth_methods, "password"},
636                                                   {silently_accept_hosts,true},
637                                                   {preferred_algorithms, ssh_transport:supported_algorithms()},
638                                                   {user_interaction,false}
639                                                  ]),
640    _ = ssh_sftp:make_dir(Ch, ".ssh"),
641    ok = ssh_sftp:write_file(Ch, ".ssh/authorized_keys", Publ),
642    ok = ssh_sftp:write_file_info(Ch, ".ssh/authorized_keys",  #file_info{mode=8#700}),
643    ok = ssh_sftp:write_file_info(Ch, ".ssh",  #file_info{mode=8#700}),
644    ok = ssh_sftp:stop_channel(Ch),
645    ok = ssh:close(Cc),
646    UserDir.
647
648
649setup_remote_priv_and_local_auth_keys(KeyAlg, Config) ->
650    {IP,Port} = ip_port(Config),
651    setup_remote_priv_and_local_auth_keys(KeyAlg, IP, Port, new_dir(Config), Config).
652
653setup_remote_priv_and_local_auth_keys(KeyAlg, IP, Port, UserDir, Config) ->
654    {ok, {Priv,Publ}} = user_priv_pub_keys(Config, KeyAlg),
655    %% Local auth_methods with public key
656    AuthKeyFile = filename:join(UserDir, "authorized_keys"),
657    ok = file:write_file(AuthKeyFile, Publ),
658    %% Remote private and public key
659    {ok,Ch,Cc} = ssh_sftp:start_channel(IP, Port, [{user,     ?USER  },
660                                                   {password, ?PASSWD   },
661                                                   {auth_methods, "password"},
662                                                   {preferred_algorithms, ssh_transport:supported_algorithms()},
663                                                   {silently_accept_hosts,true},
664                                                   {user_interaction,false}
665                                                  ]),
666    rm_id_in_remote_dir(Ch, ".ssh"),
667    _ = ssh_sftp:make_dir(Ch, ".ssh"),
668    DstFile = filename:join(".ssh", dst_filename(user,KeyAlg)),
669    ok = ssh_sftp:write_file(Ch, DstFile, Priv),
670    ok = ssh_sftp:write_file_info(Ch, DstFile,  #file_info{mode=8#700}),
671    ok = ssh_sftp:write_file(Ch, DstFile++".pub", Publ),
672    ok = ssh_sftp:write_file_info(Ch, ".ssh",  #file_info{mode=8#700}),
673    ok = ssh_sftp:stop_channel(Ch),
674    ok = ssh:close(Cc),
675    UserDir.
676
677rm_id_in_remote_dir(Ch, Dir) ->
678    case ssh_sftp:list_dir(Ch, Dir) of
679        {error,_Error} ->
680            ok;
681        {ok,FileNames} ->
682            lists:foreach(fun("id_"++_ = F) ->
683                                  ok = ssh_sftp:delete(Ch, filename:join(Dir,F));
684                             (_) ->
685                                  leave
686                          end, FileNames)
687    end.
688
689user_priv_pub_keys(Config, KeyAlg) -> priv_pub_keys("users_keys", user, Config, KeyAlg).
690host_priv_pub_keys(Config, KeyAlg) -> priv_pub_keys("host_keys",  host, Config, KeyAlg).
691
692priv_pub_keys(KeySubDir, Type, Config, KeyAlg) ->
693    KeyDir = filename:join(proplists:get_value(data_dir,Config), KeySubDir),
694    {ok,Priv} = file:read_file(filename:join(KeyDir,src_filename(Type,KeyAlg))),
695    {ok,Publ} = file:read_file(filename:join(KeyDir,src_filename(Type,KeyAlg)++".pub")),
696    {ok, {Priv,Publ}}.
697
698
699%%%---------------- The default filenames
700src_filename(user, 'ssh-rsa'            ) -> "id_rsa";
701src_filename(user, 'rsa-sha2-256'       ) -> "id_rsa";
702src_filename(user, 'rsa-sha2-512'       ) -> "id_rsa";
703src_filename(user, 'ssh-dss'            ) -> "id_dsa";
704src_filename(user, 'ssh-ed25519'        ) -> "id_ed25519";
705src_filename(user, 'ssh-ed448'          ) -> "id_ed448";
706src_filename(user, 'ecdsa-sha2-nistp256') -> "id_ecdsa256";
707src_filename(user, 'ecdsa-sha2-nistp384') -> "id_ecdsa384";
708src_filename(user, 'ecdsa-sha2-nistp521') -> "id_ecdsa521";
709src_filename(host, 'ssh-rsa'            ) -> "ssh_host_rsa_key";
710src_filename(host, 'rsa-sha2-256'       ) -> "ssh_host_rsa_key";
711src_filename(host, 'rsa-sha2-512'       ) -> "ssh_host_rsa_key";
712src_filename(host, 'ssh-dss'            ) -> "ssh_host_dsa_key";
713src_filename(host, 'ssh-ed25519'        ) -> "ssh_host_ed25519_key";
714src_filename(host, 'ssh-ed448'          ) -> "ssh_host_ed448_key";
715src_filename(host, 'ecdsa-sha2-nistp256') -> "ssh_host_ecdsa_key256";
716src_filename(host, 'ecdsa-sha2-nistp384') -> "ssh_host_ecdsa_key384";
717src_filename(host, 'ecdsa-sha2-nistp521') -> "ssh_host_ecdsa_key521".
718
719dst_filename(user, 'ssh-rsa'            ) -> "id_rsa";
720dst_filename(user, 'rsa-sha2-256'       ) -> "id_rsa";
721dst_filename(user, 'rsa-sha2-512'       ) -> "id_rsa";
722dst_filename(user, 'ssh-dss'            ) -> "id_dsa";
723dst_filename(user, 'ssh-ed25519'        ) -> "id_ed25519";
724dst_filename(user, 'ssh-ed448'          ) -> "id_ed448";
725dst_filename(user, 'ecdsa-sha2-nistp256') -> "id_ecdsa";
726dst_filename(user, 'ecdsa-sha2-nistp384') -> "id_ecdsa";
727dst_filename(user, 'ecdsa-sha2-nistp521') -> "id_ecdsa";
728dst_filename(host, 'ssh-rsa'            ) -> "ssh_host_rsa_key";
729dst_filename(host, 'rsa-sha2-256'       ) -> "ssh_host_rsa_key";
730dst_filename(host, 'rsa-sha2-512'       ) -> "ssh_host_rsa_key";
731dst_filename(host, 'ssh-dss'            ) -> "ssh_host_dsa_key";
732dst_filename(host, 'ssh-ed25519'        ) -> "ssh_host_ed25519_key";
733dst_filename(host, 'ssh-ed448'          ) -> "ssh_host_ed448_key";
734dst_filename(host, 'ecdsa-sha2-nistp256') -> "ssh_host_ecdsa_key";
735dst_filename(host, 'ecdsa-sha2-nistp384') -> "ssh_host_ecdsa_key";
736dst_filename(host, 'ecdsa-sha2-nistp521') -> "ssh_host_ecdsa_key".
737
738
739%%--------------------------------------------------------------------
740%%
741%% Format the result table for chk_all_algos/4
742%%
743format_result_table_use_all_algos(FunctionName, Config, CommonAlgs, Failed) ->
744    %% Write a nice table with the result
745    AlgHead = 'Algorithm',
746    AlgWidth = lists:max([length(atom_to_list(A)) || {_,A} <- CommonAlgs]),
747    {ResultTable,_} =
748        lists:mapfoldl(
749          fun({T,A}, Tprev) ->
750                  Tag = case T of
751                            Tprev -> "";
752                            _ -> io_lib:format('~s~n',[T])
753                        end,
754                  {io_lib:format('~s     ~*s ~s~n',
755                                 [Tag, -AlgWidth, A,
756                                  case proplists:get_value(A,Failed) of
757                                      undefined -> "(ok)";
758                                      Err -> io_lib:format("<<<< FAIL <<<< ~p",[Err])
759                                  end]),
760                   T}
761          end, undefined, CommonAlgs),
762
763    Vssh = proplists:get_value(ssh_version,Config,""),
764    io_lib:format("~nResults of ~p, Peer version: ~s~n~n"
765                  "Tag  ~*s Result~n"
766                  "=====~*..=s=======~n~s"
767                 ,[FunctionName, Vssh,
768                   -AlgWidth, AlgHead,
769                   AlgWidth, "", ResultTable]).
770
771%%--------------------------------------------------------------------
772%%
773%% Docker handling: start_docker/1 and stop_docker/1
774%%
775start_docker(Ver) ->
776    Cmnd = lists:concat(["docker run -itd --rm -p 1234 ",?DOCKER_PFX,":",Ver]),
777    Id0 = os:cmd(Cmnd),
778    ct:log("Ver = ~p, Cmnd ~p~n-> ~p",[Ver,Cmnd,Id0]),
779    case is_docker_sha(Id0) of
780        true ->
781            Id = hd(string:tokens(Id0, "\n")),
782            IP = ip(Id),
783            Port = 1234,
784            {ok, {Ver,{IP,Port},Id}};
785        false ->
786            throw(cant_start_docker)
787    end.
788
789
790stop_docker({_Ver,_,Id}) ->
791    Cmnd = lists:concat(["docker kill ",Id]),
792    os:cmd(Cmnd).
793
794is_docker_sha(L) ->
795    lists:all(fun(C) when $a =< C,C =< $z -> true;
796                 (C) when $0 =< C,C =< $9 -> true;
797                 ($\n) -> true;
798                 (_) -> false
799              end, L).
800
801%%--------------------------------------------------------------------
802%%
803%% Misc docker info functions
804
805ip_port(Config) ->
806    {_Ver,{IP,Port},_} = proplists:get_value(id,Config),
807    {IP,Port}.
808
809ip(Id) ->
810    Cmnd = lists:concat(["docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ",
811			 Id]),
812    IPstr0 = os:cmd(Cmnd),
813    ct:log("Cmnd ~p~n-> ~p",[Cmnd,IPstr0]),
814    IPstr = hd(string:tokens(IPstr0, "\n")),
815    {ok,IP} = inet:parse_address(IPstr),
816    IP.
817
818%%--------------------------------------------------------------------
819%%
820%%  Normalize the host returned from ssh_test_lib
821
822iptoa({0,0,0,0}) -> inet_parse:ntoa(host_ip());
823iptoa(IP) -> inet_parse:ntoa(IP).
824
825host_ip() ->
826    {ok,Name} = inet:gethostname(),
827    {ok,IP} = inet:ip(Name),
828    IP.
829
830%%--------------------------------------------------------------------
831%%
832%% Create a new fresh directory or clear an existing one
833%%
834
835new_dir(Config) ->
836    PrivDir = proplists:get_value(priv_dir, Config),
837    SubDirName = integer_to_list(erlang:system_time()),
838    Dir = filename:join(PrivDir, SubDirName),
839    case file:read_file_info(Dir) of
840        {error,enoent} ->
841            ok = file:make_dir(Dir),
842            Dir;
843        _ ->
844            timer:sleep(25),
845            new_dir(Config)
846    end.
847
848%%--------------------------------------------------------------------
849%%
850%% Find the intersection of algoritms for otp ssh and the docker ssh.
851%% Returns {ok, ServerHello, Server, ClientHello, Client} where Server are the algorithms common
852%% with the docker server and analogous for Client.
853%%
854%% Client may be undefined if no usable client is found.
855%%
856%% Both Server and Client are lists of {Tag,AlgName}.
857%%
858
859common_algs(Config, IP, Port) ->
860    case remote_server_algs(IP, Port) of
861        {ok, {ServerHello, RemoteServerKexInit}} ->
862            RemoteServerAlgs = kexint_msg2default_algorithms(RemoteServerKexInit),
863            Server = find_common_algs(RemoteServerAlgs,
864                                      use_algorithms(ServerHello)),
865            ct:log("Remote server:~n~p~n~p",[ServerHello, RemoteServerAlgs]),
866            case remote_client_algs(Config) of
867                {ok,{ClientHello,RemoteClientKexInit}} ->
868                    RemoteClientAlgs = kexint_msg2default_algorithms(RemoteClientKexInit),
869                    Client = find_common_algs(RemoteClientAlgs,
870                                              use_algorithms(ClientHello)),
871                    ct:log("Remote client:~n~p~n~p",[ClientHello, RemoteClientAlgs]),
872                    {ok, ServerHello, Server, ClientHello, Client};
873                {error,_} =TO ->
874                    ct:log("Remote client algs can't be found: ~p",[TO]),
875                    {ok, ServerHello, Server, undefined, undefined};
876                Other ->
877                    Other
878            end;
879        Other ->
880            Other
881    end.
882
883
884chk_hellos(Hs, Str) ->
885    lists:foldl(
886      fun(H, Acc) ->
887              try binary:split(H, <<"-">>, [global])
888              of
889                  %% [<<"SSH">>,<<"2.0">>|_] ->
890                  %%     Acc;
891                  [<<"SSH">>,OldVer = <<"1.",_/binary>>|_] ->
892                      io_lib:format("~s, Old SSH ver ~s",[Acc,OldVer]);
893                  _ ->
894                      Acc
895              catch
896                  _:_ ->
897                      Acc
898              end
899      end, Str, Hs).
900
901
902find_common_algs(Remote, Local) ->
903    [{T,V} || {T,Vs} <- ssh_test_lib:extract_algos(
904                          ssh_test_lib:intersection(Remote,
905                                                    Local)),
906              V <- Vs].
907
908
909use_algorithms(RemoteHelloBin) ->
910    MyAlgos = ssh:chk_algos_opts(
911                [{modify_algorithms,
912                  [{append, alg_diff()}
913                  ]}
914                ]),
915    ssh_transport:adjust_algs_for_peer_version(binary_to_list(RemoteHelloBin)++"\r\n",
916                                               MyAlgos).
917
918
919alg_diff() ->
920    alg_diff(ssh:default_algorithms(), ssh_transport:supported_algorithms()).
921
922alg_diff(L1, L2) when is_atom(hd(L1)) ; is_atom(hd(L2))  ->
923    (L2--L1)--['AEAD_AES_256_GCM','AEAD_AES_128_GCM'];
924alg_diff(L1, L2) ->
925    [{T, Diff} || {{T,EL1},{T,EL2}} <- lists:zip(L1,L2),
926                  Diff <- [alg_diff(EL1,EL2)],
927                  Diff =/= []
928    ].
929
930
931kexint_msg2default_algorithms(#ssh_msg_kexinit{kex_algorithms = Kex,
932                                               server_host_key_algorithms = PubKey,
933                                               encryption_algorithms_client_to_server = CipherC2S,
934                                               encryption_algorithms_server_to_client = CipherS2C,
935                                               mac_algorithms_client_to_server = MacC2S,
936                                               mac_algorithms_server_to_client = MacS2C,
937                                               compression_algorithms_client_to_server = CompC2S,
938                                               compression_algorithms_server_to_client = CompS2C
939                                              }) ->
940    [{kex,         ssh_test_lib:to_atoms(Kex)},
941     {public_key,  ssh_test_lib:to_atoms(PubKey)},
942     {cipher,      [{client2server,ssh_test_lib:to_atoms(CipherC2S)},
943                    {server2client,ssh_test_lib:to_atoms(CipherS2C)}]},
944     {mac,         [{client2server,ssh_test_lib:to_atoms(MacC2S)},
945                    {server2client,ssh_test_lib:to_atoms(MacS2C)}]},
946     {compression, [{client2server,ssh_test_lib:to_atoms(CompC2S)},
947                    {server2client,ssh_test_lib:to_atoms(CompS2C)}]}].
948
949
950%%--------------------------------------------------------------------
951%%
952%% Find the algorithms supported by the remote server
953%%
954%% Connect with tcp to the server, send a hello and read the returned
955%% server hello and kexinit message.
956%%
957remote_server_algs(IP, Port) ->
958    case try_gen_tcp_connect(IP, Port, 5) of
959        {ok,S} ->
960            ok = gen_tcp:send(S, "SSH-2.0-CheckAlgs\r\n"),
961            receive_hello(S);
962        {error,Error} ->
963            {error,Error}
964    end.
965
966try_gen_tcp_connect(IP, Port, N) when N>0 ->
967    case gen_tcp:connect(IP, Port, [binary]) of
968        {ok,S} ->
969            {ok,S};
970        {error,_Error} when N>1 ->
971            receive after 1000 -> ok end,
972            try_gen_tcp_connect(IP, Port, N-1);
973        {error,Error} ->
974            {error,Error}
975    end;
976try_gen_tcp_connect(_, _, _) ->
977    {error, "No contact"}.
978
979
980%%--------------------------------------------------------------------
981%%
982%% Find the algorithms supported by the remote client
983%%
984%% Set up a fake ssh server and make the remote client connect to it. Use
985%% hello message and the kexinit message.
986%%
987remote_client_algs(Config) ->
988    Parent = self(),
989    Ref = make_ref(),
990    spawn(
991      fun() ->
992              {ok,Sl} = gen_tcp:listen(0, [binary]),
993              {ok,{IP,Port}} = inet:sockname(Sl),
994              Parent ! {addr,Ref,IP,Port},
995              {ok,S} = gen_tcp:accept(Sl),
996              ok = gen_tcp:send(S, "SSH-2.0-CheckAlgs\r\n"),
997              Parent ! {Ref,receive_hello(S)}
998      end),
999    receive
1000        {addr,Ref,IP,Port} ->
1001            spawn(fun() ->
1002                          exec_from_docker(Config, IP, Port,
1003                                           "howdy.\r\n",
1004                                           [<<"howdy">>],
1005                                           "")
1006                  end),
1007            receive
1008                {Ref, Result} ->
1009                    Result
1010            after 5000 ->
1011                    {error, {timeout,2}}
1012            end
1013    after 5000 ->
1014            {error, {timeout,1}}
1015    end.
1016
1017
1018%%% Receive a few packets from the remote server or client and find what is supported:
1019
1020receive_hello(S) ->
1021    try
1022        receive_hello(S, <<>>)
1023    of
1024        Result ->
1025            Result
1026    catch
1027        Class:Error:ST ->
1028            {error, {Class,Error,ST}}
1029    end.
1030
1031
1032receive_hello(S, Ack) ->
1033    %% The Ack is to collect bytes until the full message is received
1034    receive
1035        {tcp, S, Bin0} when is_binary(Bin0) ->
1036            case binary:split(<<Ack/binary, Bin0/binary>>, [<<"\r\n">>,<<"\r">>,<<"\n">>]) of
1037                [Hello = <<"SSH-2.0-",_/binary>>, NextPacket] ->
1038                    %% ct:log("Got 2.0 hello (~p), ~p bytes to next msg",[Hello,size(NextPacket)]),
1039                    {ok, {Hello, receive_kexinit(S, NextPacket)}};
1040
1041                [Hello = <<"SSH-1.99-",_/binary>>, NextPacket] ->
1042                    %% ct:log("Got 1.99 hello (~p), ~p bytes to next msg",[Hello,size(NextPacket)]),
1043                    {ok, {Hello, receive_kexinit(S, NextPacket)}};
1044
1045                [Bin] when size(Bin) < 256 ->
1046                    %% ct:log("Got part of hello (~p chars):~n~s~n~s",[size(Bin),Bin,
1047                    %%                                                 [io_lib:format('~2.16.0b ',[C])
1048                    %%                                                  || C <- binary_to_list(Bin0)
1049                    %%                                                 ]
1050                    %%                                                ]),
1051                    receive_hello(S, Bin0);
1052
1053                _ ->
1054                    ct:log("Bad hello string (line ~p, ~p chars):~n~s~n~s",[?LINE,size(Bin0),Bin0,
1055                                                                  [io_lib:format('~2.16.0b ',[C])
1056                                                                   || C <- binary_to_list(Bin0)
1057                                                                  ]
1058                                                                 ]),
1059                    ct:fail("Bad hello string received")
1060            end;
1061        Other ->
1062            ct:log("Bad hello string (line ~p):~n~p",[?LINE,Other]),
1063            ct:fail("Bad hello string received")
1064
1065    after 10000 ->
1066            ct:log("Timeout waiting for hello!~n~s",[Ack]),
1067            throw(timeout)
1068    end.
1069
1070
1071receive_kexinit(_S, <<PacketLen:32, PaddingLen:8, PayloadAndPadding/binary>>)
1072  when PacketLen < 5000, % heuristic max len to stop huge attempts if packet decodeing get out of sync
1073       size(PayloadAndPadding) >= (PacketLen-1) % Need more bytes?
1074       ->
1075    ct:log("Has all ~p packet bytes",[PacketLen]),
1076    PayloadLen = PacketLen - PaddingLen - 1,
1077    <<Payload:PayloadLen/binary, _Padding:PaddingLen/binary>> = PayloadAndPadding,
1078    ssh_message:decode(Payload);
1079
1080receive_kexinit(S, Ack) ->
1081    ct:log("Has ~p bytes, need more",[size(Ack)]),
1082    receive
1083        {tcp, S, Bin0} when is_binary(Bin0) ->
1084            receive_kexinit(S, <<Ack/binary, Bin0/binary>>);
1085        Other ->
1086            ct:log("Bad hello string (line ~p):~n~p",[?LINE,Other]),
1087            ct:fail("Bad hello string received")
1088
1089    after 10000 ->
1090            ct:log("Timeout waiting for kexinit!~n~s",[Ack]),
1091            throw(timeout)
1092    end.
1093
1094%%%----------------------------------------------------------------
1095%%% Test of sftp from the OpenSSH client side
1096%%%
1097
1098sftp_tests_erl_server(Config, ServerIP, ServerPort, ServerRootDir, UserDir, Ref) ->
1099    try
1100        Cmnds = prepare_local_directory(ServerRootDir),
1101        case call_sftp_in_docker(Config, ServerIP, ServerPort, Cmnds, UserDir, Ref) of
1102            ok ->
1103                check_local_directory(ServerRootDir);
1104            {error,Error} ->
1105                {error,Error}
1106        end
1107    catch
1108        Class:Excep:ST ->
1109            {error, {Class,Excep,ST}}
1110    end.
1111
1112
1113prepare_local_directory(ServerRootDir) ->
1114    file:write_file(filename:join(ServerRootDir,"tst1"),
1115                    <<"Some test text">>
1116                   ),
1117    ["get tst1",
1118     "put tst1 tst2",
1119     "put tst1 tst3",
1120     "rename tst1 ex_tst1",
1121     "rm tst3",
1122     "mkdir mydir",
1123     "cd mydir",
1124     "put tst1 file_1",
1125     "put tst1 unreadable_file",
1126     "chmod 222 unreadable_file",
1127     "exit"].
1128
1129
1130check_local_directory(ServerRootDir) ->
1131    TimesToTry = 3,  % sleep 0.5, 1, 2 and then 4 secs (7.5s in total)
1132    check_local_directory(ServerRootDir, 500, TimesToTry-1).
1133
1134check_local_directory(ServerRootDir, SleepTime, N) ->
1135    case do_check_local_directory(ServerRootDir) of
1136        {error,_Error} when N>0 ->
1137            %% Could be that the erlang side is faster and the docker's operations
1138            %% are not yet finalized.
1139            %% Sleep for a while and retry a few times:
1140            timer:sleep(SleepTime),
1141            check_local_directory(ServerRootDir, 2*SleepTime, N-1);
1142        Other ->
1143            Other
1144    end.
1145
1146do_check_local_directory(ServerRootDir) ->
1147    case lists:sort(ok(file:list_dir(ServerRootDir)) -- [".",".."]) of
1148        ["ex_tst1","mydir","tst2"] ->
1149            {ok,Expect} = file:read_file(filename:join(ServerRootDir,"ex_tst1")),
1150            case file:read_file(filename:join(ServerRootDir,"tst2")) of
1151                {ok,Expect} ->
1152                    case lists:sort(ok(file:list_dir(filename:join(ServerRootDir,"mydir"))) -- [".",".."]) of
1153                        ["file_1","unreadable_file"] ->
1154                            case file:read_file(filename:join([ServerRootDir,"mydir","file_1"])) of
1155                                {ok,Expect} ->
1156                                    case file:read_file(filename:join([ServerRootDir,"mydir","unreadable_file"])) of
1157                                        {error,_} ->
1158                                            ok;
1159                                        {ok,_} ->
1160                                            {error, {could_read_unreadable,"mydir/unreadable_file"}}
1161                                    end;
1162                                {ok,Other} ->
1163                                    ct:log("file_1:~n~s~nExpected:~n~s",[Other,Expect]),
1164                                    {error, {bad_contents_in_file,"mydir/file_1"}}
1165                            end;
1166                        Other ->
1167                            ct:log("Directory ~s~n~p",[filename:join(ServerRootDir,"mydir"),Other]),
1168                            {error,{bad_dir_contents,"mydir"}}
1169                    end;
1170                {ok,Other} ->
1171                    ct:log("tst2:~n~s~nExpected:~n~s",[Other,Expect]),
1172                    {error, {bad_contents_in_file,"tst2"}}
1173            end;
1174        ["tst1"] ->
1175            {error,{missing_file,"tst2"}};
1176        Other ->
1177            ct:log("Directory ~s~n~p",[ServerRootDir,Other]),
1178            {error,{bad_dir_contents,"/"}}
1179    end.
1180
1181
1182call_sftp_in_docker(Config, ServerIP, ServerPort, Cmnds, UserDir, Ref) ->
1183    {DockerIP,DockerPort} = ip_port(Config),
1184    ct:log("Going to connect ~p:~p", [DockerIP, DockerPort]),
1185    {ok,C} = ssh:connect(DockerIP, DockerPort,
1186                         [{user,?USER},
1187                          {password,?PASSWD},
1188                          {user_dir, UserDir},
1189                          {preferred_algorithms, ssh_transport:supported_algorithms()},
1190                          {silently_accept_hosts,true},
1191                          {user_interaction,false}
1192                         ]),
1193
1194    %% Make commands for "expect" in the docker:
1195    PreExpectCmnds = ["spawn /buildroot/ssh/bin/sftp -oPort="++integer_to_list(ServerPort)++
1196                          " -oCheckHostIP=no -oStrictHostKeyChecking=no " ++
1197                          iptoa(ServerIP)++"\n"
1198                     ],
1199    PostExpectCmnds= [],
1200    ExpectCmnds =
1201        PreExpectCmnds ++
1202        ["expect \"sftp>\" {sleep 1; send \""++Cmnd++"\n\"}\n" || Cmnd <- Cmnds] ++
1203        PostExpectCmnds,
1204
1205    %% Make an commands file in the docker
1206    {ok,Ch} = ssh_sftp:start_channel(C, [{timeout,10000}]),
1207    ok = ssh_sftp:write_file(Ch, "commands", erlang:iolist_to_binary(ExpectCmnds)),
1208    ok = ssh_sftp:stop_channel(Ch),
1209
1210    %% Call expect in the docker
1211    {ok, Ch1} = ssh_connection:session_channel(C, infinity),
1212    ct:log("Going to execute 'expect commands' in docker", []),
1213    success = ssh_connection:exec(C, Ch1, "expect commands", infinity),
1214    ct:log("'expect commands' in docker executed!", []),
1215    case recv_log_msgs(C, Ch1) of
1216        ok ->
1217            receive
1218                {kex_changed, Ref} ->
1219                    %% success
1220                    ct:log("Kex changed",[]),
1221                    ssh:close(C),
1222                    ok
1223            after
1224                30000 ->
1225                    %% failure
1226                    ct:log("Kex NOT changed",[]),
1227                    ssh:close(C),
1228                    {error,"Kex NOT changed"}
1229            end;
1230
1231        {error,Error} ->
1232            ssh:close(C),
1233            {error,Error}
1234    end.
1235
1236
1237recv_log_msgs(C, Ch) ->
1238    receive
1239        {ssh_cm,C,{closed,Ch}} ->
1240            %% ct:log("Channel closed ~p",[{closed,1}]),
1241            ok;
1242        {ssh_cm,C,{data,Ch,1,Msg}} ->
1243            ct:log("*** ERROR from docker:~n~s",[Msg]),
1244            recv_log_msgs(C, Ch);
1245        {ssh_cm,C,_Msg} ->
1246            %% ct:log("Got ~p",[_Msg]),
1247            recv_log_msgs(C, Ch)
1248    after
1249        30000 ->
1250            %% failure
1251            ct:log("Exec Channel ~p in ~p NOT closed properly", [Ch,C]),
1252            ssh:close(C),
1253            {error,"Exec channel NOT closed"}
1254    end.
1255
1256%%%----------------------------------------------------------------
1257%%%----------------------------------------------------------------
1258%%%
1259%%% Tests from the Erlang client side
1260%%%
1261%%%----------------------------------------------------------------
1262%%%----------------------------------------------------------------
1263test_erl_client_reneg({ok,C}, Spec) ->
1264    %% Start the test processes on the connection C:
1265    Parent = self(),
1266    Pids = [spawn(
1267              fun() ->
1268                      Parent ! {self(), TestType, Id, one_test_erl_client(TestType,Id,C)}
1269              end
1270             )
1271            || {TestType,N} <- Spec,
1272               Id <- lists:seq(1,N)],
1273
1274    Kex1 = renegotiate_test(init, C),
1275
1276    %% Collect the results:
1277    case lists:filter(
1278           fun(R) -> R=/=ok end,
1279           [receive
1280                {Pid,_TestType,_Id,ok} ->
1281                    %% ct:log("Test ~p:~p passed!", [_TestType,_Id]),
1282                    ok;
1283                {Pid,TestType,Id,OtherResult} ->
1284                    ct:log("~p:~p ~p ~p~n~p",[?MODULE,?LINE,TestType,Id,OtherResult]),
1285                    {error,TestType,Id}
1286            end || Pid <- Pids])
1287    of
1288        [] ->
1289            renegotiate_test(Kex1, C),
1290            {ok,C};
1291        Other ->
1292            renegotiate_test(Kex1, C),
1293            Other
1294    end;
1295
1296test_erl_client_reneg(Error, _) ->
1297    Error.
1298
1299
1300one_test_erl_client(exec, Id, C) ->
1301    {ok, Ch} = ssh_connection:session_channel(C, infinity),
1302    success = ssh_connection:exec(C, Ch, "echo Hi there", 5000),
1303    case loop_until(fun({eof,_}) -> true;
1304                       (_      ) -> false
1305                    end,
1306                    fun(Acc) ->
1307                            receive
1308                                {ssh_cm, C, {eof,Ch}} ->
1309                                    {eof,Acc};
1310                                {ssh_cm, C, {data,Ch,0,B}} when is_binary(B) ->
1311                                    <<Acc/binary, B/binary>>
1312                            end
1313                    end,
1314                    <<>>) of
1315        {eof,<<"Hi there\n">>} ->
1316            ok;
1317        Other ->
1318            ct:log("exec Got other ~p", [Other]),
1319            {error, {exec,Id,bad_msg,Other,undefined}}
1320    end;
1321
1322one_test_erl_client(no_subsyst, Id, C) ->
1323    {ok, Ch} = ssh_connection:session_channel(C, infinity),
1324    case ssh_connection:subsystem(C, Ch, "foo", infinity) of
1325        failure ->
1326            ok;
1327        Other ->
1328            ct:log("no_subsyst Got other ~p", [Other]),
1329            {error, {no_subsyst,Id,bad_ret,Other,undefined}}
1330    end;
1331
1332one_test_erl_client(setenv, Id, C) ->
1333    {ok, Ch} = ssh_connection:session_channel(C, infinity),
1334    Var = "ENV_TEST",
1335    Value = lists:concat(["env_test_",Id,"_",erlang:system_time()]),
1336    Env = case ssh_connection:setenv(C, Ch, Var, Value, infinity) of
1337	      success -> binary_to_list(Value++"\n");
1338	      failure -> <<"\n">>
1339	  end,
1340    success = ssh_connection:exec(C, Ch, "echo $"++Var, 5000),
1341    case loop_until(fun({eof,_}) -> true;
1342                       (_      ) -> false
1343                    end,
1344                    fun(Acc) ->
1345                            receive
1346                                {ssh_cm, C, {eof,Ch}} ->
1347                                    {eof,Acc};
1348                                {ssh_cm, C, {data,Ch,0,B}} when is_binary(B) ->
1349                                    <<Acc/binary, B/binary>>
1350                            end
1351                    end,
1352                    <<>>) of
1353        {eof,Env} ->
1354            ok;
1355        Other ->
1356            ct:log("setenv Got other ~p", [Other]),
1357            {error, {setenv,Id,bad_msg,Other,undefined}}
1358    end;
1359
1360one_test_erl_client(SFTP, Id, C) when SFTP==sftp ; SFTP==sftp_async ->
1361    try
1362        {ok,Ch} = ssh_sftp:start_channel(C, [{timeout,10000}]),
1363        %% A new fresh name of a new file tree:
1364        RootDir = lists:concat(["r_",Id,"_",erlang:system_time()]),
1365        %% Check that it does not exist:
1366        false = lists:member(RootDir, ok(ssh_sftp:list_dir(Ch, "."))),
1367        %% Create it:
1368        ok = ssh_sftp:make_dir(Ch, RootDir),
1369        {ok, #file_info{type=directory, access=read_write}} = ssh_sftp:read_file_info(Ch, RootDir),
1370        R = do_sftp_tests_erl_client(SFTP, C, Ch, Id, RootDir),
1371        catch ssh_sftp:stop_channel(Ch),
1372        R
1373    catch
1374        Class:Error:ST ->
1375            {error, {SFTP,Id,Class,Error,ST}}
1376    end.
1377
1378
1379
1380do_sftp_tests_erl_client(sftp_async, _C, Ch, _Id, RootDir) ->
1381    FileName1 = "boring_name",
1382    F1 = filename:join(RootDir, FileName1),
1383    %% Open a new handle and start writing:
1384    {ok,Handle1} = ssh_sftp:open(Ch, F1, [write,binary]),
1385    {async,Aref1} = ssh_sftp:awrite(Ch, Handle1, <<0:250000/unsigned-unit:8>>),
1386    wait_for_async_result(Aref1);
1387
1388do_sftp_tests_erl_client(sftp, _C, Ch, _Id, RootDir) ->
1389    FileName0 = "f0",
1390    F0 = filename:join(RootDir, FileName0),
1391
1392    %% Create and write a file:
1393    ok = ssh_sftp:write_file(Ch,
1394                             F0 = filename:join(RootDir, FileName0),
1395                             Data0 = mkbin(1234,240)),
1396    {ok,Data0} = ssh_sftp:read_file(Ch, F0),
1397    {ok, #file_info{type=regular, access=read_write, size=1234}} = ssh_sftp:read_file_info(Ch, F0),
1398
1399    %% Re-write:
1400    {ok,Handle0} = ssh_sftp:open(Ch, F0, [write,read,binary]),
1401    ok = ssh_sftp:pwrite(Ch, Handle0, 16, Data0_1=mkbin(10,255)),
1402
1403    <<B1:16/binary, _:10/binary, B2:(1234-26)/binary>> = Data0,
1404    FileContents = <<B1:16/binary, Data0_1:10/binary, B2:(1234-26)/binary>>,
1405
1406    <<_:1/binary, Part:25/binary, _/binary>> = FileContents,
1407    {ok, Part} = ssh_sftp:pread(Ch, Handle0, 1, 25),
1408
1409    %% Check:
1410    {ok, FileContents} = ssh_sftp:pread(Ch, Handle0, 0, 1234),
1411    ok = ssh_sftp:close(Ch, Handle0),
1412
1413    %% Check in another way:
1414    {ok, FileContents} = ssh_sftp:read_file(Ch, F0),
1415
1416    %% Remove write access rights and check that it can't be written:
1417    ok = ssh_sftp:write_file_info(Ch, F0, #file_info{mode=8#400}), %read}),
1418    {ok, #file_info{type=regular, access=read}} = ssh_sftp:read_file_info(Ch, F0),
1419    {error,permission_denied} = ssh_sftp:write_file(Ch, F0, mkbin(10,14)),
1420
1421    %% Test deletion of file and dir:
1422    [FileName0] = ok(ssh_sftp:list_dir(Ch, RootDir)) -- [".", ".."],
1423    ok = ssh_sftp:delete(Ch, F0),
1424    [] = ok(ssh_sftp:list_dir(Ch, RootDir)) -- [".", ".."],
1425    ok = ssh_sftp:del_dir(Ch, RootDir),
1426    false = lists:member(RootDir, ok(ssh_sftp:list_dir(Ch, "."))),
1427    ok.
1428
1429
1430wait_for_async_result(Aref) ->
1431    receive
1432        {async_reply, Aref, Result} ->
1433            Result
1434    after
1435        60000 ->
1436            timeout
1437    end.
1438
1439
1440mkbin(Size, Byte) ->
1441    list_to_binary(lists:duplicate(Size,Byte)).
1442
1443ok({ok,X}) -> X.
1444
1445%%%----------------------------------------------------------------
1446renegotiate_test(init, ConnectionRef) ->
1447    Kex1 = ssh_test_lib:get_kex_init(ConnectionRef),
1448    ssh_connection_handler:renegotiate(ConnectionRef),
1449    %%ct:log("Renegotiate test initiated!",[]),
1450    Kex1;
1451
1452renegotiate_test(Kex1, ConnectionRef) ->
1453    case ssh_test_lib:get_kex_init(ConnectionRef) of
1454        Kex1 ->
1455            ct:log("Renegotiate test failed, Kex1 == Kex2!",[]),
1456            error(renegotiate_failed);
1457        _ ->
1458            %% ct:log("Renegotiate test passed!",[]),
1459            ok
1460    end.
1461