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