1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2008-2018. 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_options_SUITE).
24
25%%% This test suite tests different options for the ssh functions
26
27
28-include_lib("common_test/include/ct.hrl").
29-include_lib("kernel/include/file.hrl").
30-include("ssh_test_lib.hrl").
31
32%%% Test cases
33-export([connectfun_disconnectfun_client/1,
34	 disconnectfun_option_client/1,
35	 disconnectfun_option_server/1,
36	 id_string_no_opt_client/1,
37	 id_string_no_opt_server/1,
38	 id_string_own_string_client/1,
39	 id_string_own_string_client_trail_space/1,
40	 id_string_own_string_server/1,
41	 id_string_own_string_server_trail_space/1,
42	 id_string_random_client/1,
43	 id_string_random_server/1,
44	 max_sessions_sftp_start_channel_parallel/1,
45	 max_sessions_sftp_start_channel_sequential/1,
46	 max_sessions_ssh_connect_parallel/1,
47	 max_sessions_ssh_connect_sequential/1,
48	 server_password_option/1,
49	 server_userpassword_option/1,
50	 server_pwdfun_option/1,
51	 server_pwdfun_4_option/1,
52	 server_keyboard_interactive/1,
53	 ssh_connect_arg4_timeout/1,
54	 ssh_connect_negtimeout_parallel/1,
55	 ssh_connect_negtimeout_sequential/1,
56	 ssh_connect_nonegtimeout_connected_parallel/1,
57	 ssh_connect_nonegtimeout_connected_sequential/1,
58	 ssh_connect_timeout/1, connect/4,
59	 ssh_daemon_minimal_remote_max_packet_size_option/1,
60	 ssh_msg_debug_fun_option_client/1,
61	 ssh_msg_debug_fun_option_server/1,
62	 system_dir_option/1,
63	 unexpectedfun_option_client/1,
64	 unexpectedfun_option_server/1,
65	 user_dir_option/1,
66	 connectfun_disconnectfun_server/1,
67	 hostkey_fingerprint_check/1,
68	 hostkey_fingerprint_check_md5/1,
69	 hostkey_fingerprint_check_sha/1,
70	 hostkey_fingerprint_check_sha256/1,
71	 hostkey_fingerprint_check_sha384/1,
72	 hostkey_fingerprint_check_sha512/1,
73	 hostkey_fingerprint_check_list/1,
74         save_accepted_host_option/1
75	]).
76
77%%% Common test callbacks
78-export([suite/0, all/0, groups/0,
79	 init_per_suite/1, end_per_suite/1,
80	 init_per_group/2, end_per_group/2,
81	 init_per_testcase/2, end_per_testcase/2
82	]).
83
84
85-define(NEWLINE, <<"\r\n">>).
86
87%%--------------------------------------------------------------------
88%% Common Test interface functions -----------------------------------
89%%--------------------------------------------------------------------
90
91suite() ->
92    [{ct_hooks,[ts_install_cth]},
93     {timetrap,{seconds,30}}].
94
95all() ->
96    [connectfun_disconnectfun_server,
97     connectfun_disconnectfun_client,
98     server_password_option,
99     server_userpassword_option,
100     server_pwdfun_option,
101     server_pwdfun_4_option,
102     server_keyboard_interactive,
103     {group, dir_options},
104     ssh_connect_timeout,
105     ssh_connect_arg4_timeout,
106     ssh_daemon_minimal_remote_max_packet_size_option,
107     ssh_msg_debug_fun_option_client,
108     ssh_msg_debug_fun_option_server,
109     disconnectfun_option_server,
110     disconnectfun_option_client,
111     unexpectedfun_option_server,
112     unexpectedfun_option_client,
113     hostkey_fingerprint_check,
114     hostkey_fingerprint_check_md5,
115     hostkey_fingerprint_check_sha,
116     hostkey_fingerprint_check_sha256,
117     hostkey_fingerprint_check_sha384,
118     hostkey_fingerprint_check_sha512,
119     hostkey_fingerprint_check_list,
120     id_string_no_opt_client,
121     id_string_own_string_client,
122     id_string_own_string_client_trail_space,
123     id_string_random_client,
124     id_string_no_opt_server,
125     id_string_own_string_server,
126     id_string_own_string_server_trail_space,
127     id_string_random_server,
128     save_accepted_host_option,
129     {group, hardening_tests}
130    ].
131
132groups() ->
133    [{hardening_tests, [], [ssh_connect_nonegtimeout_connected_parallel,
134			    ssh_connect_nonegtimeout_connected_sequential,
135			    ssh_connect_negtimeout_parallel,
136			    ssh_connect_negtimeout_sequential,
137			    max_sessions_ssh_connect_parallel,
138			    max_sessions_ssh_connect_sequential,
139			    max_sessions_sftp_start_channel_parallel,
140			    max_sessions_sftp_start_channel_sequential
141			   ]},
142     {dir_options, [], [user_dir_option,
143			system_dir_option]}
144    ].
145
146
147%%--------------------------------------------------------------------
148init_per_suite(Config) ->
149    ?CHECK_CRYPTO(Config).
150
151end_per_suite(_Config) ->
152    ssh:stop().
153
154%%--------------------------------------------------------------------
155init_per_group(hardening_tests, Config) ->
156    DataDir = proplists:get_value(data_dir, Config),
157    PrivDir = proplists:get_value(priv_dir, Config),
158    ssh_test_lib:setup_dsa(DataDir, PrivDir),
159    ssh_test_lib:setup_rsa(DataDir, PrivDir),
160    Config;
161init_per_group(dir_options, Config) ->
162    PrivDir = proplists:get_value(priv_dir, Config),
163    %% Make unreadable dir:
164    Dir_unreadable = filename:join(PrivDir, "unread"),
165    ok = file:make_dir(Dir_unreadable),
166    {ok,F1} = file:read_file_info(Dir_unreadable),
167    ok = file:write_file_info(Dir_unreadable,
168			      F1#file_info{mode = F1#file_info.mode band (bnot 8#00444)}),
169    %% Make readable file:
170    File_readable = filename:join(PrivDir, "file"),
171    ok = file:write_file(File_readable, <<>>),
172
173    %% Check:
174    case {file:read_file_info(Dir_unreadable),
175	  file:read_file_info(File_readable)} of
176	{{ok, Id=#file_info{type=directory, access=Md}},
177	 {ok, If=#file_info{type=regular,   access=Mf}}} ->
178	    AccessOK =
179		case {Md,                Mf} of
180		    {read,               _} -> false;
181		    {read_write,         _} -> false;
182		    {_,               read} -> true;
183		    {_,         read_write} -> true;
184		    _ -> false
185		end,
186
187	    case AccessOK of
188		true ->
189		    %% Save:
190		    [{unreadable_dir, Dir_unreadable},
191		     {readable_file, File_readable}
192		     | Config];
193		false ->
194		    ct:log("File#file_info : ~p~n"
195			   "Dir#file_info  : ~p",[If,Id]),
196		    {skip, "File or dir mode settings failed"}
197	    end;
198
199	NotDirFile ->
200	    ct:log("{Dir,File} -> ~p",[NotDirFile]),
201	    {skip, "File/Dir creation failed"}
202    end;
203init_per_group(_, Config) ->
204    Config.
205
206end_per_group(_, Config) ->
207    Config.
208%%--------------------------------------------------------------------
209init_per_testcase(_TestCase, Config) ->
210    ssh:start(),
211    %% Create a clean user_dir
212    UserDir = filename:join(proplists:get_value(priv_dir, Config), nopubkey),
213    ssh_test_lib:del_dirs(UserDir),
214    file:make_dir(UserDir),
215    [{user_dir,UserDir}|Config].
216
217end_per_testcase(_TestCase, Config) ->
218    ssh:stop(),
219    ok.
220
221%%--------------------------------------------------------------------
222%% Test Cases --------------------------------------------------------
223%%--------------------------------------------------------------------
224
225%%% validate to server that uses the 'password' option
226server_password_option(Config) when is_list(Config) ->
227    UserDir = proplists:get_value(user_dir, Config),
228    SysDir = proplists:get_value(data_dir, Config),
229    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
230					     {user_dir, UserDir},
231					     {password, "morot"}]),
232
233    ConnectionRef =
234	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
235					  {user, "foo"},
236					  {password, "morot"},
237					  {user_interaction, false},
238					  {user_dir, UserDir}]),
239
240    Reason = "Unable to connect using the available authentication methods",
241
242    {error, Reason} =
243	ssh:connect(Host, Port, [{silently_accept_hosts, true},
244				 {user, "vego"},
245				 {password, "foo"},
246				 {user_interaction, false},
247				 {user_dir, UserDir}]),
248
249    ct:log("Test of wrong password: Error msg: ~p ~n", [Reason]),
250
251    ssh:close(ConnectionRef),
252    ssh:stop_daemon(Pid).
253
254%%--------------------------------------------------------------------
255
256%%% validate to server that uses the 'password' option
257server_userpassword_option(Config) when is_list(Config) ->
258    UserDir = proplists:get_value(user_dir, Config),
259    SysDir = proplists:get_value(data_dir, Config),
260    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
261					     {user_dir, UserDir},
262					     {user_passwords, [{"vego", "morot"}]}]),
263
264    ConnectionRef =
265	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
266					  {user, "vego"},
267					  {password, "morot"},
268					  {user_interaction, false},
269					  {user_dir, UserDir}]),
270    ssh:close(ConnectionRef),
271
272    Reason = "Unable to connect using the available authentication methods",
273
274    {error, Reason} =
275	ssh:connect(Host, Port, [{silently_accept_hosts, true},
276				 {user, "foo"},
277				 {password, "morot"},
278				 {user_interaction, false},
279				 {user_dir, UserDir}]),
280    {error, Reason} =
281	ssh:connect(Host, Port, [{silently_accept_hosts, true},
282				 {user, "vego"},
283				 {password, "foo"},
284				 {user_interaction, false},
285				 {user_dir, UserDir}]),
286    ssh:stop_daemon(Pid).
287
288%%--------------------------------------------------------------------
289%%% validate to server that uses the 'pwdfun' option
290server_pwdfun_option(Config) ->
291    UserDir = proplists:get_value(user_dir, Config),
292    SysDir = proplists:get_value(data_dir, Config),
293    CHKPWD = fun("foo",Pwd) -> Pwd=="bar";
294		(_,_) -> false
295	     end,
296    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
297					     {user_dir, UserDir},
298					     {pwdfun,CHKPWD}]),
299    ConnectionRef =
300	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
301					  {user, "foo"},
302					  {password, "bar"},
303					  {user_interaction, false},
304					  {user_dir, UserDir}]),
305    ssh:close(ConnectionRef),
306
307    Reason = "Unable to connect using the available authentication methods",
308
309    {error, Reason} =
310	ssh:connect(Host, Port, [{silently_accept_hosts, true},
311				 {user, "foo"},
312				 {password, "morot"},
313				 {user_interaction, false},
314				 {user_dir, UserDir}]),
315    {error, Reason} =
316	ssh:connect(Host, Port, [{silently_accept_hosts, true},
317				 {user, "vego"},
318				 {password, "foo"},
319				 {user_interaction, false},
320				 {user_dir, UserDir}]),
321    ssh:stop_daemon(Pid).
322
323
324%%--------------------------------------------------------------------
325%%% validate to server that uses the 'pwdfun/4' option
326server_pwdfun_4_option(Config) ->
327    UserDir = proplists:get_value(user_dir, Config),
328    SysDir = proplists:get_value(data_dir, Config),
329    PWDFUN = fun("foo",Pwd,{_,_},undefined) -> Pwd=="bar";
330		("fie",Pwd,{_,_},undefined) -> {Pwd=="bar",new_state};
331		("bandit",_,_,_) -> disconnect;
332		(_,_,_,_) -> false
333	     end,
334    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
335					     {user_dir, UserDir},
336					     {pwdfun,PWDFUN}]),
337    ConnectionRef1 =
338	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
339					  {user, "foo"},
340					  {password, "bar"},
341					  {user_interaction, false},
342					  {user_dir, UserDir}]),
343    ssh:close(ConnectionRef1),
344
345    ConnectionRef2 =
346	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
347					  {user, "fie"},
348					  {password, "bar"},
349					  {user_interaction, false},
350					  {user_dir, UserDir}]),
351    ssh:close(ConnectionRef2),
352
353    Reason = "Unable to connect using the available authentication methods",
354
355    {error, Reason} =
356	ssh:connect(Host, Port, [{silently_accept_hosts, true},
357				 {user, "foo"},
358				 {password, "morot"},
359				 {user_interaction, false},
360				 {user_dir, UserDir}]),
361    {error, Reason} =
362	ssh:connect(Host, Port, [{silently_accept_hosts, true},
363				 {user, "fie"},
364				 {password, "morot"},
365				 {user_interaction, false},
366				 {user_dir, UserDir}]),
367    {error, Reason} =
368	ssh:connect(Host, Port, [{silently_accept_hosts, true},
369				 {user, "vego"},
370				 {password, "foo"},
371				 {user_interaction, false},
372				 {user_dir, UserDir}]),
373
374    {error, Reason} =
375	ssh:connect(Host, Port, [{silently_accept_hosts, true},
376				 {user, "bandit"},
377				 {password, "pwd breaking"},
378				 {user_interaction, false},
379				 {user_dir, UserDir}]),
380    ssh:stop_daemon(Pid).
381
382
383%%--------------------------------------------------------------------
384server_keyboard_interactive(Config) ->
385    UserDir = proplists:get_value(user_dir, Config),
386    SysDir = proplists:get_value(data_dir, Config),
387    %% Test that the state works
388    Parent = self(),
389    PWDFUN = fun("foo",P="bar",_,S) -> Parent!{P,S},true;
390		(_,P,_,S=undefined) -> Parent!{P,S},{false,1};
391		(_,P,_,S) -> Parent!{P,S},          {false,S+1}
392	     end,
393    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
394					     {user_dir, UserDir},
395					     {auth_methods,"keyboard-interactive"},
396					     {pwdfun,PWDFUN}]),
397
398    %% Try with passwords "incorrect", "Bad again" and finally "bar"
399    KIFFUN = fun(_Name, _Instr, _PromptInfos) ->
400		     K={k,self()},
401                     Answer =
402                         case get(K) of
403                             undefined ->
404                                 put(K,1),
405                                 ["incorrect"];
406                             2 ->
407                                 put(K,3),
408                                 ["bar"];
409                             S->
410                                 put(K,S+1),
411                                 ["Bad again"]
412                         end,
413                     ct:log("keyboard_interact_fun:~n"
414                            " Name        = ~p~n"
415                            " Instruction = ~p~n"
416                            " Prompts     = ~p~n"
417                            "~nAnswer:~n  ~p~n",
418                            [_Name, _Instr, _PromptInfos, Answer]),
419
420                     Answer
421	     end,
422
423    ConnectionRef2 =
424	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
425					  {user, "foo"},
426					  {keyboard_interact_fun, KIFFUN},
427					  {user_dir, UserDir}]),
428    ssh:close(ConnectionRef2),
429    ssh:stop_daemon(Pid),
430
431    lists:foreach(fun(Expect) ->
432			  receive
433			      Expect -> ok;
434			      Other -> ct:fail("Expect: ~p~nReceived ~p",[Expect,Other])
435			  after
436			      2000 -> ct:fail("Timeout expecting ~p",[Expect])
437			  end
438		  end, [{"incorrect",undefined},
439			{"Bad again",1},
440			{"bar",2}]).
441
442%%--------------------------------------------------------------------
443system_dir_option(Config) ->
444    DirUnread = proplists:get_value(unreadable_dir,Config),
445    FileRead = proplists:get_value(readable_file,Config),
446
447    case ssh_test_lib:daemon([{system_dir, DirUnread}]) of
448	{error,{eoptions,{{system_dir,DirUnread},eacces}}} ->
449	    ok;
450	{Pid1,_Host1,Port1} when is_pid(Pid1),is_integer(Port1) ->
451	    ssh:stop_daemon(Pid1),
452	    ct:fail("Didn't detect that dir is unreadable", [])
453	end,
454
455    case ssh_test_lib:daemon([{system_dir, FileRead}]) of
456	{error,{eoptions,{{system_dir,FileRead},enotdir}}} ->
457	    ok;
458	{Pid2,_Host2,Port2} when is_pid(Pid2),is_integer(Port2) ->
459	    ssh:stop_daemon(Pid2),
460	    ct:fail("Didn't detect that option is a plain file", [])
461    end.
462
463
464user_dir_option(Config) ->
465    DirUnread = proplists:get_value(unreadable_dir,Config),
466    FileRead = proplists:get_value(readable_file,Config),
467    %% Any port will do (beware, implementation knowledge!):
468    Port = 65535,
469
470    case ssh:connect("localhost", Port, [{user_dir, DirUnread}]) of
471	{error,{eoptions,{{user_dir,DirUnread},eacces}}} ->
472	    ok;
473	{error,econnrefused} ->
474	    ct:fail("Didn't detect that dir is unreadable", [])
475    end,
476
477    case ssh:connect("localhost", Port, [{user_dir, FileRead}]) of
478	{error,{eoptions,{{user_dir,FileRead},enotdir}}} ->
479	    ok;
480	{error,econnrefused} ->
481	    ct:fail("Didn't detect that option is a plain file", [])
482    end.
483
484%%--------------------------------------------------------------------
485%%% validate client that uses the 'ssh_msg_debug_fun' option
486ssh_msg_debug_fun_option_client(Config) ->
487    UserDir = proplists:get_value(user_dir, Config),
488    SysDir = proplists:get_value(data_dir, Config),
489
490    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
491					     {user_dir, UserDir},
492					     {password, "morot"},
493					     {failfun, fun ssh_test_lib:failfun/2}]),
494    Parent = self(),
495    DbgFun = fun(ConnRef,Displ,Msg,Lang) -> Parent ! {msg_dbg,{ConnRef,Displ,Msg,Lang}} end,
496
497    ConnectionRef =
498	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
499					  {user, "foo"},
500					  {password, "morot"},
501					  {user_dir, UserDir},
502					  {user_interaction, false},
503					  {ssh_msg_debug_fun,DbgFun}]),
504    %% Beware, implementation knowledge:
505    gen_statem:cast(ConnectionRef,{ssh_msg_debug,false,<<"Hello">>,<<>>}),
506    receive
507	{msg_dbg,X={ConnectionRef,false,<<"Hello">>,<<>>}} ->
508	    ct:log("Got expected dbg msg ~p",[X]),
509	    ssh:stop_daemon(Pid);
510	{msg_dbg,X={_,false,<<"Hello">>,<<>>}} ->
511	    ct:log("Got dbg msg but bad ConnectionRef (~p expected) ~p",[ConnectionRef,X]),
512	    ssh:stop_daemon(Pid),
513	    {fail, "Bad ConnectionRef received"};
514	{msg_dbg,X} ->
515	    ct:log("Got bad dbg msg ~p",[X]),
516	    ssh:stop_daemon(Pid),
517	    {fail,"Bad msg received"}
518    after 1000 ->
519	    ssh:stop_daemon(Pid),
520	    {fail,timeout}
521    end.
522
523%%--------------------------------------------------------------------
524connectfun_disconnectfun_server(Config) ->
525    UserDir = proplists:get_value(user_dir, Config),
526    SysDir = proplists:get_value(data_dir, Config),
527
528    Parent = self(),
529    Ref = make_ref(),
530    ConnFun = fun(_,_,_) -> Parent ! {connect,Ref} end,
531    DiscFun = fun(R) -> Parent ! {disconnect,Ref,R} end,
532
533    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
534					     {user_dir, UserDir},
535					     {password, "morot"},
536					     {failfun, fun ssh_test_lib:failfun/2},
537					     {disconnectfun, DiscFun},
538					     {connectfun, ConnFun}]),
539    ConnectionRef =
540	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
541					  {user, "foo"},
542					  {password, "morot"},
543					  {user_dir, UserDir},
544					  {user_interaction, false}]),
545    receive
546	{connect,Ref} ->
547	    ssh:close(ConnectionRef),
548	    receive
549		{disconnect,Ref,R} ->
550		    ct:log("Disconnect result: ~p",[R]),
551		    ssh:stop_daemon(Pid)
552	    after 10000 ->
553		    receive
554			X -> ct:log("received ~p",[X])
555		    after 0 -> ok
556		    end,
557		    {fail, "No disconnectfun action"}
558	    end
559    after 10000 ->
560	    receive
561		X -> ct:log("received ~p",[X])
562	    after 0 -> ok
563	    end,
564	    {fail, "No connectfun action"}
565    end.
566
567%%--------------------------------------------------------------------
568connectfun_disconnectfun_client(Config) ->
569    UserDir = proplists:get_value(user_dir, Config),
570    SysDir = proplists:get_value(data_dir, Config),
571
572    Parent = self(),
573    Ref = make_ref(),
574    DiscFun = fun(R) -> Parent ! {disconnect,Ref,R} end,
575
576    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
577					     {user_dir, UserDir},
578					     {password, "morot"},
579					     {failfun, fun ssh_test_lib:failfun/2}]),
580    _ConnectionRef =
581	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
582					  {user, "foo"},
583					  {password, "morot"},
584					  {user_dir, UserDir},
585					  {disconnectfun, DiscFun},
586					  {user_interaction, false}]),
587    ssh:stop_daemon(Pid),
588    receive
589	{disconnect,Ref,R} ->
590	    ct:log("Disconnect result: ~p",[R])
591    after 2000 ->
592	    {fail, "No disconnectfun action"}
593    end.
594
595%%--------------------------------------------------------------------
596%%% validate client that uses the 'ssh_msg_debug_fun' option
597ssh_msg_debug_fun_option_server(Config) ->
598    UserDir = proplists:get_value(user_dir, Config),
599    SysDir = proplists:get_value(data_dir, Config),
600
601    Parent = self(),
602    DbgFun = fun(ConnRef,Displ,Msg,Lang) -> Parent ! {msg_dbg,{ConnRef,Displ,Msg,Lang}} end,
603    ConnFun = fun(_,_,_) -> Parent ! {connection_pid,self()} end,
604
605    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
606					     {user_dir, UserDir},
607					     {password, "morot"},
608					     {failfun, fun ssh_test_lib:failfun/2},
609					     {connectfun, ConnFun},
610					     {ssh_msg_debug_fun, DbgFun}]),
611    _ConnectionRef =
612	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
613					  {user, "foo"},
614					  {password, "morot"},
615					  {user_dir, UserDir},
616					  {user_interaction, false}]),
617    receive
618	{connection_pid,Server} ->
619	    %% Beware, implementation knowledge:
620	    gen_statem:cast(Server,{ssh_msg_debug,false,<<"Hello">>,<<>>}),
621	    receive
622		{msg_dbg,X={_,false,<<"Hello">>,<<>>}} ->
623		    ct:log("Got expected dbg msg ~p",[X]),
624		    ssh:stop_daemon(Pid);
625		{msg_dbg,X} ->
626		    ct:log("Got bad dbg msg ~p",[X]),
627		    ssh:stop_daemon(Pid),
628		    {fail,"Bad msg received"}
629	    after 3000 ->
630		    ssh:stop_daemon(Pid),
631		    {fail,timeout2}
632	    end
633    after 3000 ->
634	    ssh:stop_daemon(Pid),
635	    {fail,timeout1}
636    end.
637
638%%--------------------------------------------------------------------
639disconnectfun_option_server(Config) ->
640    UserDir = proplists:get_value(user_dir, Config),
641    SysDir = proplists:get_value(data_dir, Config),
642
643    Parent = self(),
644    DisConnFun = fun(Reason) -> Parent ! {disconnect,Reason} end,
645
646    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
647					     {user_dir, UserDir},
648					     {password, "morot"},
649					     {failfun, fun ssh_test_lib:failfun/2},
650					     {disconnectfun, DisConnFun}]),
651    ConnectionRef =
652	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
653					  {user, "foo"},
654					  {password, "morot"},
655					  {user_dir, UserDir},
656					  {user_interaction, false}]),
657    ssh:close(ConnectionRef),
658    receive
659	{disconnect,Reason} ->
660	    ct:log("Server detected disconnect: ~p",[Reason]),
661	    ssh:stop_daemon(Pid),
662	    ok
663    after 5000 ->
664	    receive
665		X -> ct:log("received ~p",[X])
666	    after 0 -> ok
667	    end,
668	    {fail,"Timeout waiting for disconnect"}
669    end.
670
671%%--------------------------------------------------------------------
672disconnectfun_option_client(Config) ->
673    UserDir = proplists:get_value(user_dir, Config),
674    SysDir = proplists:get_value(data_dir, Config),
675
676    Parent = self(),
677    DisConnFun = fun(Reason) -> Parent ! {disconnect,Reason} end,
678
679    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
680					     {user_dir, UserDir},
681					     {password, "morot"},
682					     {failfun, fun ssh_test_lib:failfun/2}]),
683    _ConnectionRef =
684	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
685					  {user, "foo"},
686					  {password, "morot"},
687					  {user_dir, UserDir},
688					  {user_interaction, false},
689					  {disconnectfun, DisConnFun}]),
690    ssh:stop_daemon(Pid),
691    receive
692	{disconnect,Reason} ->
693	    ct:log("Client detected disconnect: ~p",[Reason]),
694	    ok
695    after 3000 ->
696	    receive
697		X -> ct:log("received ~p",[X])
698	    after 0 -> ok
699	    end,
700	    {fail,"Timeout waiting for disconnect"}
701    end.
702
703%%--------------------------------------------------------------------
704unexpectedfun_option_server(Config) ->
705    UserDir = proplists:get_value(user_dir, Config),
706    SysDir = proplists:get_value(data_dir, Config),
707
708    Parent = self(),
709    ConnFun = fun(_,_,_) -> Parent ! {connection_pid,self()} end,
710    UnexpFun = fun(Msg,Peer) ->
711		       Parent ! {unexpected,Msg,Peer,self()},
712		       skip
713	       end,
714
715    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
716					     {user_dir, UserDir},
717					     {password, "morot"},
718					     {failfun, fun ssh_test_lib:failfun/2},
719					     {connectfun, ConnFun},
720					     {unexpectedfun, UnexpFun}]),
721    _ConnectionRef =
722	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
723					  {user, "foo"},
724					  {password, "morot"},
725					  {user_dir, UserDir},
726					  {user_interaction, false}]),
727    receive
728	{connection_pid,Server} ->
729	    %% Beware, implementation knowledge:
730	    Server ! unexpected_message,
731	    receive
732		{unexpected, unexpected_message, {{_,_,_,_},_}, _} -> ok;
733		{unexpected, unexpected_message, Peer, _} -> ct:fail("Bad peer ~p",[Peer]);
734		M = {unexpected, _, _, _} -> ct:fail("Bad msg ~p",[M])
735	    after 3000 ->
736		    ssh:stop_daemon(Pid),
737		    {fail,timeout2}
738	    end
739    after 3000 ->
740	    ssh:stop_daemon(Pid),
741	    {fail,timeout1}
742    end.
743
744%%--------------------------------------------------------------------
745unexpectedfun_option_client(Config) ->
746    UserDir = proplists:get_value(user_dir, Config),
747    SysDir = proplists:get_value(data_dir, Config),
748
749    Parent = self(),
750    UnexpFun = fun(Msg,Peer) ->
751		       Parent ! {unexpected,Msg,Peer,self()},
752		       skip
753	       end,
754
755    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
756					     {user_dir, UserDir},
757					     {password, "morot"},
758					     {failfun, fun ssh_test_lib:failfun/2}]),
759    ConnectionRef =
760	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
761					  {user, "foo"},
762					  {password, "morot"},
763					  {user_dir, UserDir},
764					  {user_interaction, false},
765					  {unexpectedfun, UnexpFun}]),
766    %% Beware, implementation knowledge:
767    ConnectionRef ! unexpected_message,
768
769    receive
770	{unexpected, unexpected_message, {{_,_,_,_},_}, ConnectionRef} ->
771	    ok;
772	{unexpected, unexpected_message, Peer, ConnectionRef} ->
773	    ct:fail("Bad peer ~p",[Peer]);
774	M = {unexpected, _, _, _} ->
775	    ct:fail("Bad msg ~p",[M])
776    after 3000 ->
777	    ssh:stop_daemon(Pid),
778	    {fail,timeout}
779    end.
780
781%%--------------------------------------------------------------------
782hostkey_fingerprint_check(Config) ->
783    do_hostkey_fingerprint_check(Config, old).
784
785hostkey_fingerprint_check_md5(Config) ->
786    do_hostkey_fingerprint_check(Config, md5).
787
788hostkey_fingerprint_check_sha(Config) ->
789    do_hostkey_fingerprint_check(Config, sha).
790
791hostkey_fingerprint_check_sha256(Config) ->
792    do_hostkey_fingerprint_check(Config, sha256).
793
794hostkey_fingerprint_check_sha384(Config) ->
795    do_hostkey_fingerprint_check(Config, sha384).
796
797hostkey_fingerprint_check_sha512(Config) ->
798    do_hostkey_fingerprint_check(Config, sha512).
799
800hostkey_fingerprint_check_list(Config) ->
801    do_hostkey_fingerprint_check(Config, [sha,md5,sha256]).
802
803%%%----
804do_hostkey_fingerprint_check(Config, HashAlg) ->
805    case supported_hash(HashAlg) of
806	true ->
807	    really_do_hostkey_fingerprint_check(Config, HashAlg);
808	false ->
809	    {skip,{unsupported_hash,HashAlg}}
810    end.
811
812supported_hash(old) -> true;
813supported_hash(HashAlg) ->
814    Hs = if is_atom(HashAlg) -> [HashAlg];
815            is_list(HashAlg) -> HashAlg
816         end,
817    [] == (Hs -- proplists:get_value(hashs, crypto:supports(), [])).
818
819
820really_do_hostkey_fingerprint_check(Config, HashAlg) ->
821    UserDir = proplists:get_value(user_dir, Config),
822    SysDir = proplists:get_value(data_dir, Config),
823
824    %% All host key fingerprints.  Trust that public_key has checked the ssh_hostkey_fingerprint
825    %% function since that function is used by the ssh client...
826    FPs0 = [case HashAlg of
827	       old -> public_key:ssh_hostkey_fingerprint(Key);
828	       _ -> public_key:ssh_hostkey_fingerprint(HashAlg, Key)
829	   end
830	   || FileCandidate <- begin
831				   {ok,KeyFileCands} = file:list_dir(SysDir),
832				   KeyFileCands
833			       end,
834	      nomatch =/= re:run(FileCandidate, ".*\\.pub", []),
835	      {Key,_Cmnts} <- begin
836				  {ok,Bin} = file:read_file(filename:join(SysDir, FileCandidate)),
837				  try public_key:ssh_decode(Bin, public_key)
838				  catch
839				      _:_ -> []
840				  end
841			      end],
842    FPs = if is_atom(HashAlg) -> FPs0;
843             is_list(HashAlg) -> lists:concat(FPs0)
844          end,
845    ct:log("Fingerprints(~p) = ~p",[HashAlg,FPs]),
846
847    %% Start daemon with the public keys that we got fingerprints from
848    {Pid, Host0, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
849					     {user_dir, UserDir},
850					     {password, "morot"}]),
851    Host = ssh_test_lib:ntoa(Host0),
852    FP_check_fun = fun(PeerName, FP) ->
853			   ct:log("PeerName = ~p, FP = ~p",[PeerName,FP]),
854			   HostCheck = ssh_test_lib:match_ip(Host, PeerName),
855			   FPCheck =
856                               if is_atom(HashAlg) -> lists:member(FP, FPs);
857                                  is_list(HashAlg) -> lists:all(fun(FP1) -> lists:member(FP1,FPs) end,
858                                                                FP)
859                               end,
860			   ct:log("check ~p == ~p (~p) and ~n~p~n in ~p (~p)~n",
861				  [PeerName,Host,HostCheck,FP,FPs,FPCheck]),
862			   HostCheck and FPCheck
863		   end,
864
865    ssh_test_lib:connect(Host, Port, [{silently_accept_hosts,
866				       case HashAlg of
867					   old -> FP_check_fun;
868					   _ -> {HashAlg, FP_check_fun}
869				       end},
870				      {user, "foo"},
871				      {password, "morot"},
872				      {user_dir, UserDir},
873                                      {save_accepted_host, false}, % Ensure no 'known_hosts' disturbs
874				      {user_interaction, false}]),
875    ssh:stop_daemon(Pid).
876
877%%--------------------------------------------------------------------
878%%% Test connect_timeout option in ssh:connect/4
879ssh_connect_timeout(_Config) ->
880    ConnTimeout = 2000,
881    {error,{faked_transport,connect,TimeoutToTransport}} =
882	ssh:connect("localhost", 12345,
883		    [{transport,{tcp,?MODULE,tcp_closed}},
884		     {connect_timeout,ConnTimeout}],
885		    1000),
886    case TimeoutToTransport of
887	ConnTimeout -> ok;
888	Other ->
889	    ct:log("connect_timeout is ~p but transport received ~p",[ConnTimeout,Other]),
890	    {fail,"ssh:connect/4 wrong connect_timeout received in transport"}
891    end.
892
893%% Plugin function for the test above
894connect(_Host, _Port, _Opts, Timeout) ->
895    {error, {faked_transport,connect,Timeout}}.
896
897%%--------------------------------------------------------------------
898%%% Test fourth argument in ssh:connect/4
899ssh_connect_arg4_timeout(_Config) ->
900    Timeout = 1000,
901    Parent = self(),
902    %% start the server
903    Server = spawn(fun() ->
904			   {ok,Sl} = gen_tcp:listen(0,[]),
905			   {ok,{_,Port}} = inet:sockname(Sl),
906			   Parent ! {port,self(),Port},
907			   Rsa = gen_tcp:accept(Sl),
908			   ct:log("Server gen_tcp:accept got ~p",[Rsa]),
909			   receive after 2*Timeout -> ok end %% let client timeout first
910		   end),
911
912    %% Get listening port
913    Port = receive
914	       {port,Server,ServerPort} -> ServerPort
915	   after
916	       10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
917	   end,
918
919    %% try to connect with a timeout, but "supervise" it
920    Client = spawn(fun() ->
921			   T0 = erlang:monotonic_time(),
922			   Rc = ssh:connect("localhost",Port,[],Timeout),
923			   ct:log("Client ssh:connect got ~p",[Rc]),
924			   Parent ! {done,self(),Rc,T0}
925		   end),
926
927    %% Wait for client reaction on the connection try:
928    receive
929	{done, Client, {error,timeout}, T0} ->
930	    Msp = ms_passed(T0),
931	    exit(Server,hasta_la_vista___baby),
932	    Low = 0.9*Timeout,
933	    High =  2.5*Timeout,
934	    ct:log("Timeout limits: ~.4f - ~.4f ms, timeout "
935                   "was ~.4f ms, expected ~p ms",[Low,High,Msp,Timeout]),
936	    if
937		Low<Msp, Msp<High -> ok;
938		true -> {fail, "timeout not within limits"}
939	    end;
940
941	{done, Client, {error,Other}, _T0} ->
942	    ct:log("Error message \"~p\" from the client is unexpected.",[{error,Other}]),
943	    {fail, "Unexpected error message"};
944
945	{done, Client, {ok,_Ref}, _T0} ->
946	    {fail,"ssh-connected ???"}
947    after
948	5000 ->
949	    exit(Server,hasta_la_vista___baby),
950	    exit(Client,hasta_la_vista___baby),
951	    {fail, "Didn't timeout"}
952    end.
953
954%% Help function, elapsed milliseconds since T0
955ms_passed(T0) ->
956    %% OTP 18
957    erlang:convert_time_unit(erlang:monotonic_time() - T0,
958			     native,
959			     micro_seconds) / 1000.
960
961%%--------------------------------------------------------------------
962ssh_daemon_minimal_remote_max_packet_size_option(Config) ->
963    SystemDir = proplists:get_value(data_dir, Config),
964    UserDir = proplists:get_value(user_dir, Config),
965
966    {Server, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},
967						{user_dir, UserDir},
968						{user_passwords, [{"vego", "morot"}]},
969						{failfun, fun ssh_test_lib:failfun/2},
970						{minimal_remote_max_packet_size, 14}]),
971    Conn =
972	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
973					  {user_dir, UserDir},
974					  {user_interaction, false},
975					  {user, "vego"},
976					  {password, "morot"}]),
977
978    %% Try the limits of the minimal_remote_max_packet_size:
979    {ok, _ChannelId} = ssh_connection:session_channel(Conn, 100, 14, infinity),
980    {open_error,_,"Maximum packet size below 14 not supported",_} =
981	ssh_connection:session_channel(Conn, 100, 13, infinity),
982
983    ssh:close(Conn),
984    ssh:stop_daemon(Server).
985
986%%--------------------------------------------------------------------
987%% This test try every algorithm by connecting to an Erlang server
988id_string_no_opt_client(Config) ->
989    {Server, _Host, Port} = fake_daemon(Config),
990    {error,_} = ssh:connect("localhost", Port, [], 1000),
991    receive
992	{id,Server,"SSH-2.0-Erlang/"++Vsn} ->
993	    true = expected_ssh_vsn(Vsn);
994	{id,Server,Other} ->
995	    ct:fail("Unexpected id: ~s.",[Other])
996    after 5000 ->
997	    {fail,timeout}
998    end.
999
1000%%--------------------------------------------------------------------
1001id_string_own_string_client(Config) ->
1002    {Server, _Host, Port} = fake_daemon(Config),
1003    {error,_} = ssh:connect("localhost", Port, [{id_string,"Pelle"}], 1000),
1004    receive
1005	{id,Server,"SSH-2.0-Pelle\r\n"} ->
1006	    ok;
1007	{id,Server,Other} ->
1008	    ct:fail("Unexpected id: ~s.",[Other])
1009    after 5000 ->
1010	    {fail,timeout}
1011    end.
1012
1013%%--------------------------------------------------------------------
1014id_string_own_string_client_trail_space(Config) ->
1015    {Server, _Host, Port} = fake_daemon(Config),
1016    {error,_} = ssh:connect("localhost", Port, [{id_string,"Pelle "}], 1000),
1017    receive
1018	{id,Server,"SSH-2.0-Pelle \r\n"} ->
1019	    ok;
1020	{id,Server,Other} ->
1021	    ct:fail("Unexpected id: ~s.",[Other])
1022    after 5000 ->
1023	    {fail,timeout}
1024    end.
1025
1026%%--------------------------------------------------------------------
1027id_string_random_client(Config) ->
1028    {Server, _Host, Port} = fake_daemon(Config),
1029    {error,_} = ssh:connect("localhost", Port, [{id_string,random}], 1000),
1030    receive
1031	{id,Server,Id="SSH-2.0-Erlang"++_} ->
1032	    ct:fail("Unexpected id: ~s.",[Id]);
1033	{id,Server,Rnd="SSH-2.0-"++_} ->
1034	    ct:log("Got correct ~s",[Rnd]);
1035	{id,Server,Id} ->
1036	    ct:fail("Unexpected id: ~s.",[Id])
1037    after 5000 ->
1038	    {fail,timeout}
1039    end.
1040
1041%%--------------------------------------------------------------------
1042id_string_no_opt_server(Config) ->
1043    {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, []),
1044    {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]),
1045    {ok,"SSH-2.0-Erlang/"++Vsn} = gen_tcp:recv(S1, 0, 2000),
1046    true = expected_ssh_vsn(Vsn).
1047
1048%%--------------------------------------------------------------------
1049id_string_own_string_server(Config) ->
1050    {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, [{id_string,"Olle"}]),
1051    {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]),
1052    {ok,"SSH-2.0-Olle\r\n"} = gen_tcp:recv(S1, 0, 2000).
1053
1054%%--------------------------------------------------------------------
1055id_string_own_string_server_trail_space(Config) ->
1056    {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, [{id_string,"Olle "}]),
1057    {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]),
1058    {ok,"SSH-2.0-Olle \r\n"} = gen_tcp:recv(S1, 0, 2000).
1059
1060%%--------------------------------------------------------------------
1061id_string_random_server(Config) ->
1062    {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, [{id_string,random}]),
1063    {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]),
1064    {ok,"SSH-2.0-"++Rnd} = gen_tcp:recv(S1, 0, 2000),
1065    case Rnd of
1066	"Erlang"++_ -> ct:log("Id=~p",[Rnd]),
1067		       {fail,got_default_id};
1068	"Olle\r\n" -> {fail,got_previous_tests_value};
1069	_ -> ct:log("Got ~s.",[Rnd])
1070    end.
1071
1072%%--------------------------------------------------------------------
1073ssh_connect_negtimeout_parallel(Config) -> ssh_connect_negtimeout(Config,true).
1074ssh_connect_negtimeout_sequential(Config) -> ssh_connect_negtimeout(Config,false).
1075
1076ssh_connect_negtimeout(Config, Parallel) ->
1077    process_flag(trap_exit, true),
1078    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
1079    UserDir = proplists:get_value(priv_dir, Config),
1080    NegTimeOut = 2000,				% ms
1081    ct:log("Parallel: ~p",[Parallel]),
1082
1083    {_Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir},
1084                                               {parallel_login, Parallel},
1085                                               {negotiation_timeout, NegTimeOut},
1086                                               {failfun, fun ssh_test_lib:failfun/2}]),
1087
1088    {ok,Socket} = ssh_test_lib:gen_tcp_connect(Host, Port, []),
1089
1090    Factor = 2,
1091    ct:log("And now sleeping ~p*NegTimeOut (~p ms)...", [Factor, round(Factor * NegTimeOut)]),
1092    ct:sleep(round(Factor * NegTimeOut)),
1093
1094    case inet:sockname(Socket) of
1095	{ok,_} ->
1096	    %% Give it another chance...
1097	    ct:log("Sleep more...",[]),
1098	    ct:sleep(round(Factor * NegTimeOut)),
1099	    case inet:sockname(Socket) of
1100		{ok,_} -> ct:fail("Socket not closed");
1101		{error,_} -> ok
1102	    end;
1103	{error,_} -> ok
1104    end.
1105
1106%%--------------------------------------------------------------------
1107%%% Test that ssh connection does not timeout if the connection is established (parallel)
1108ssh_connect_nonegtimeout_connected_parallel(Config) ->
1109    ssh_connect_nonegtimeout_connected(Config, true).
1110
1111%%% Test that ssh connection does not timeout if the connection is established (non-parallel)
1112ssh_connect_nonegtimeout_connected_sequential(Config) ->
1113    ssh_connect_nonegtimeout_connected(Config, false).
1114
1115
1116ssh_connect_nonegtimeout_connected(Config, Parallel) ->
1117    process_flag(trap_exit, true),
1118    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
1119    UserDir = proplists:get_value(priv_dir, Config),
1120    NegTimeOut = 2000,				% ms
1121    ct:log("Parallel: ~p",[Parallel]),
1122
1123    {_Pid, _Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir},
1124					       {parallel_login, Parallel},
1125					       {negotiation_timeout, NegTimeOut},
1126					       {failfun, fun ssh_test_lib:failfun/2}]),
1127    ct:log("~p Listen ~p:~p",[_Pid,_Host,Port]),
1128    ct:sleep(500),
1129
1130    IO = ssh_test_lib:start_io_server(),
1131    Shell = ssh_test_lib:start_shell(Port, IO, [{user_dir,UserDir}]),
1132    receive
1133	Error = {'EXIT', _, _} ->
1134	    ct:log("~p",[Error]),
1135	    ct:fail(no_ssh_connection);
1136	ErlShellStart ->
1137	    ct:log("---Erlang shell start: ~p~n", [ErlShellStart]),
1138	    one_shell_op(IO, NegTimeOut),
1139	    one_shell_op(IO, NegTimeOut),
1140
1141	    Factor = 2,
1142	    ct:log("And now sleeping ~p*NegTimeOut (~p ms)...", [Factor, round(Factor * NegTimeOut)]),
1143	    ct:sleep(round(Factor * NegTimeOut)),
1144
1145	    one_shell_op(IO, NegTimeOut)
1146    after
1147	10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
1148    end,
1149    exit(Shell, kill).
1150
1151
1152one_shell_op(IO, TimeOut) ->
1153    ct:log("One shell op: Waiting for prompter"),
1154    receive
1155	ErlPrompt0 -> ct:log("Erlang prompt: ~p~n", [ErlPrompt0])
1156    after TimeOut -> ct:fail("Timeout waiting for promter")
1157    end,
1158
1159    IO ! {input, self(), "2*3*7.\r\n"},
1160    receive
1161	Echo0 -> ct:log("Echo: ~p ~n", [Echo0])
1162    after TimeOut -> ct:fail("Timeout waiting for echo")
1163    end,
1164
1165    receive
1166	?NEWLINE -> ct:log("NEWLINE received", [])
1167    after TimeOut ->
1168	    receive Any1 -> ct:log("Bad NEWLINE: ~p",[Any1])
1169	    after 0 -> ct:fail("Timeout waiting for NEWLINE")
1170	    end
1171    end,
1172
1173    receive
1174	Result0 -> ct:log("Result: ~p~n", [Result0])
1175    after TimeOut ->  ct:fail("Timeout waiting for result")
1176    end.
1177
1178%%--------------------------------------------------------------------
1179max_sessions_ssh_connect_parallel(Config) ->
1180    max_sessions(Config, true, connect_fun(ssh__connect,Config)).
1181max_sessions_ssh_connect_sequential(Config) ->
1182    max_sessions(Config, false, connect_fun(ssh__connect,Config)).
1183
1184max_sessions_sftp_start_channel_parallel(Config) ->
1185    max_sessions(Config, true, connect_fun(ssh_sftp__start_channel, Config)).
1186max_sessions_sftp_start_channel_sequential(Config) ->
1187    max_sessions(Config, false, connect_fun(ssh_sftp__start_channel, Config)).
1188
1189
1190%%%---- helpers:
1191connect_fun(ssh__connect, Config) ->
1192    fun(Host,Port) ->
1193	    ssh_test_lib:connect(Host, Port,
1194				 [{silently_accept_hosts, true},
1195				  {user_dir, proplists:get_value(priv_dir,Config)},
1196				  {user_interaction, false},
1197				  {user, "carni"},
1198				  {password, "meat"}
1199				 ])
1200	    %% ssh_test_lib returns R when ssh:connect returns {ok,R}
1201    end;
1202connect_fun(ssh_sftp__start_channel, _Config) ->
1203    fun(Host,Port) ->
1204	    {ok,_Pid,ConnRef} =
1205		ssh_sftp:start_channel(Host, Port,
1206				       [{silently_accept_hosts, true},
1207					{user, "carni"},
1208					{password, "meat"}
1209				       ]),
1210	    ConnRef
1211    end.
1212
1213
1214max_sessions(Config, ParallelLogin, Connect0) when is_function(Connect0,2) ->
1215    Connect = fun(Host,Port) ->
1216		      R = Connect0(Host,Port),
1217		      ct:log("Connect(~p,~p) -> ~p",[Host,Port,R]),
1218		      R
1219	      end,
1220    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
1221    UserDir = proplists:get_value(priv_dir, Config),
1222    MaxSessions = 5,
1223    {Pid, Host, Port} = ssh_test_lib:daemon([
1224					     {system_dir, SystemDir},
1225					     {user_dir, UserDir},
1226					     {user_passwords, [{"carni", "meat"}]},
1227					     {parallel_login, ParallelLogin},
1228					     {max_sessions, MaxSessions}
1229					    ]),
1230    ct:log("~p Listen ~p:~p for max ~p sessions",[Pid,Host,Port,MaxSessions]),
1231    try [Connect(Host,Port) || _ <- lists:seq(1,MaxSessions)]
1232    of
1233	Connections ->
1234	    %% Step 1 ok: could set up max_sessions connections
1235	    ct:log("Connections up: ~p",[Connections]),
1236	    [_|_] = Connections,
1237
1238	    %% Now try one more than alowed:
1239	    ct:pal("Info Report expected here (if not disabled) ...",[]),
1240	    try Connect(Host,Port)
1241	    of
1242		_ConnectionRef1 ->
1243		    ssh:stop_daemon(Pid),
1244		    {fail,"Too many connections accepted"}
1245	    catch
1246		error:{badmatch,{error,"Connection closed"}} ->
1247                    ct:log("Step 2 ok: could not set up too many connections. Good.",[]),
1248		    %% Now stop one connection and try to open one more
1249		    ok = ssh:close(hd(Connections)),
1250		    try_to_connect(Connect, Host, Port, Pid)
1251	    end
1252    catch
1253	error:{badmatch,{error,"Connection closed"}} ->
1254	    ssh:stop_daemon(Pid),
1255	    {fail,"Too few connections accepted"}
1256    end.
1257
1258
1259try_to_connect(Connect, Host, Port, Pid) ->
1260    {ok,Tref} = timer:send_after(30000, timeout_no_connection), % give the supervisors some time...
1261    try_to_connect(Connect, Host, Port, Pid, Tref, 1). % will take max 3300 ms after 11 tries
1262
1263try_to_connect(Connect, Host, Port, Pid, Tref, N) ->
1264     try Connect(Host,Port)
1265     of
1266	 _ConnectionRef1 ->
1267	     timer:cancel(Tref),
1268             ct:log("Step 3 ok: could set up one more connection after killing one. Thats good.",[]),
1269	     ssh:stop_daemon(Pid),
1270	     receive % flush.
1271		 timeout_no_connection -> ok
1272	     after 0 -> ok
1273	     end
1274     catch
1275	 error:{badmatch,{error,"Connection closed"}} ->
1276	     %% Could not set up one more connection. Try again until timeout.
1277	     receive
1278		 timeout_no_connection ->
1279		     ssh:stop_daemon(Pid),
1280		     {fail,"Does not decrease # active sessions"}
1281	     after N*50 -> % retry after this time
1282		     try_to_connect(Connect, Host, Port, Pid, Tref, N+1)
1283	     end
1284     end.
1285
1286%%--------------------------------------------------------------------
1287save_accepted_host_option(Config) ->
1288    UserDir = proplists:get_value(user_dir, Config),
1289    KnownHosts = filename:join(UserDir, "known_hosts"),
1290    SysDir = proplists:get_value(data_dir, Config),
1291    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
1292					     {user_dir, UserDir},
1293					     {user_passwords, [{"vego", "morot"}]}
1294                                            ]),
1295    {error,enoent} = file:read_file(KnownHosts),
1296
1297    {ok,_C1} = ssh:connect(Host, Port, [{silently_accept_hosts, true},
1298                                        {user, "vego"},
1299                                        {password, "morot"},
1300                                        {user_interaction, false},
1301                                        {save_accepted_host, false},
1302                                        {user_dir, UserDir}]),
1303    {error,enoent} = file:read_file(KnownHosts),
1304
1305    {ok,_C2} = ssh:connect(Host, Port, [{silently_accept_hosts, true},
1306                                        {user, "vego"},
1307                                        {password, "morot"},
1308                                        {user_interaction, false},
1309                                        {user_dir, UserDir}]),
1310    {ok,_} = file:read_file(KnownHosts),
1311    ssh:stop_daemon(Pid).
1312
1313%%--------------------------------------------------------------------
1314%% Internal functions ------------------------------------------------
1315%%--------------------------------------------------------------------
1316
1317expected_ssh_vsn(Str) ->
1318    try
1319	{ok,L} = application:get_all_key(ssh),
1320	proplists:get_value(vsn,L,"")++"\r\n"
1321    of
1322	Str -> true;
1323	"\r\n" -> true;
1324	_ -> false
1325    catch
1326	_:_ -> true %% ssh not started so we dont't know
1327    end.
1328
1329
1330fake_daemon(_Config) ->
1331    Parent = self(),
1332    %% start the server
1333    Server = spawn(fun() ->
1334			   {ok,Sl} = gen_tcp:listen(0,[{packet,line}]),
1335			   {ok,{Host,Port}} = inet:sockname(Sl),
1336			   ct:log("fake_daemon listening on ~p:~p~n",[Host,Port]),
1337			   Parent ! {sockname,self(),Host,Port},
1338			   Rsa = gen_tcp:accept(Sl),
1339			   ct:log("Server gen_tcp:accept got ~p",[Rsa]),
1340			   {ok,S} = Rsa,
1341			   receive
1342			       {tcp, S, Id} -> Parent ! {id,self(),Id}
1343			   after
1344			       10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
1345			   end
1346		   end),
1347    %% Get listening host and port
1348    receive
1349	{sockname,Server,ServerHost,ServerPort} -> {Server, ServerHost, ServerPort}
1350    after
1351	10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
1352    end.
1353