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