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