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_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([
34         auth_method_kb_interactive_data_tuple/1,
35         auth_method_kb_interactive_data_fun3/1,
36         auth_method_kb_interactive_data_fun4/1,
37         connectfun_disconnectfun_client/1,
38	 disconnectfun_option_client/1,
39	 disconnectfun_option_server/1,
40	 id_string_no_opt_client/1,
41	 id_string_no_opt_server/1,
42	 id_string_own_string_client/1,
43	 id_string_own_string_client_trail_space/1,
44	 id_string_own_string_server/1,
45	 id_string_own_string_server_trail_space/1,
46	 id_string_random_client/1,
47	 id_string_random_server/1,
48	 max_sessions_sftp_start_channel_parallel/1,
49	 max_sessions_sftp_start_channel_sequential/1,
50	 max_sessions_ssh_connect_parallel/1,
51	 max_sessions_ssh_connect_sequential/1,
52         max_sessions_drops_tcp_connects/1,
53         max_sessions_drops_tcp_connects/0,
54	 server_password_option/1,
55	 server_userpassword_option/1,
56	 server_pwdfun_option/1,
57	 server_pwdfun_4_option/1,
58	 server_keyboard_interactive/1,
59	 server_keyboard_interactive_extra_msg/1,
60	 ssh_connect_arg4_timeout/1,
61	 ssh_connect_negtimeout_parallel/1,
62	 ssh_connect_negtimeout_sequential/1,
63	 ssh_connect_nonegtimeout_connected_parallel/1,
64	 ssh_connect_nonegtimeout_connected_sequential/1,
65	 ssh_connect_timeout/1, connect/4,
66	 ssh_daemon_minimal_remote_max_packet_size_option/1,
67	 ssh_msg_debug_fun_option_client/1,
68	 ssh_msg_debug_fun_option_server/1,
69	 system_dir_option/1,
70	 unexpectedfun_option_client/1,
71	 unexpectedfun_option_server/1,
72	 user_dir_option/1,
73	 user_dir_fun_option/1,
74	 connectfun_disconnectfun_server/1,
75	 hostkey_fingerprint_check/1,
76	 hostkey_fingerprint_check_md5/1,
77	 hostkey_fingerprint_check_sha/1,
78	 hostkey_fingerprint_check_sha256/1,
79	 hostkey_fingerprint_check_sha384/1,
80	 hostkey_fingerprint_check_sha512/1,
81	 hostkey_fingerprint_check_list/1,
82         save_accepted_host_option/1,
83         raw_option/1,
84         config_file/1,
85         config_file_modify_algorithms_order/1
86	]).
87
88%%% Common test callbacks
89-export([suite/0, all/0, groups/0,
90	 init_per_suite/1, end_per_suite/1,
91	 init_per_group/2, end_per_group/2,
92	 init_per_testcase/2, end_per_testcase/2
93	]).
94
95-define(NEWLINE, <<"\r\n">>).
96
97%%--------------------------------------------------------------------
98%% Common Test interface functions -----------------------------------
99%%--------------------------------------------------------------------
100
101suite() ->
102    [{ct_hooks,[ts_install_cth]},
103     {timetrap,{seconds,60}}].
104
105all() ->
106    [connectfun_disconnectfun_server,
107     connectfun_disconnectfun_client,
108     server_password_option,
109     server_userpassword_option,
110     server_pwdfun_option,
111     server_pwdfun_4_option,
112     server_keyboard_interactive,
113     server_keyboard_interactive_extra_msg,
114     auth_method_kb_interactive_data_tuple,
115     auth_method_kb_interactive_data_fun3,
116     auth_method_kb_interactive_data_fun4,
117     {group, dir_options},
118     ssh_connect_timeout,
119     ssh_connect_arg4_timeout,
120     ssh_daemon_minimal_remote_max_packet_size_option,
121     ssh_msg_debug_fun_option_client,
122     ssh_msg_debug_fun_option_server,
123     disconnectfun_option_server,
124     disconnectfun_option_client,
125     unexpectedfun_option_server,
126     unexpectedfun_option_client,
127     hostkey_fingerprint_check,
128     hostkey_fingerprint_check_md5,
129     hostkey_fingerprint_check_sha,
130     hostkey_fingerprint_check_sha256,
131     hostkey_fingerprint_check_sha384,
132     hostkey_fingerprint_check_sha512,
133     hostkey_fingerprint_check_list,
134     id_string_no_opt_client,
135     id_string_own_string_client,
136     id_string_own_string_client_trail_space,
137     id_string_random_client,
138     id_string_no_opt_server,
139     id_string_own_string_server,
140     id_string_own_string_server_trail_space,
141     id_string_random_server,
142     save_accepted_host_option,
143     raw_option,
144     config_file,
145     config_file_modify_algorithms_order,
146     {group, hardening_tests}
147    ].
148
149groups() ->
150    [{hardening_tests, [], [ssh_connect_nonegtimeout_connected_parallel,
151			    ssh_connect_nonegtimeout_connected_sequential,
152			    ssh_connect_negtimeout_parallel,
153			    ssh_connect_negtimeout_sequential,
154			    max_sessions_ssh_connect_parallel,
155			    max_sessions_ssh_connect_sequential,
156			    max_sessions_sftp_start_channel_parallel,
157			    max_sessions_sftp_start_channel_sequential,
158                            max_sessions_drops_tcp_connects
159			   ]},
160     {dir_options, [], [user_dir_option,
161                        user_dir_fun_option,
162			system_dir_option]}
163    ].
164
165
166%%--------------------------------------------------------------------
167init_per_suite(Config) ->
168    ?CHECK_CRYPTO(Config).
169
170end_per_suite(_Config) ->
171    ssh:stop().
172
173%%--------------------------------------------------------------------
174init_per_group(hardening_tests, Config) ->
175    ct:log("Pub keys setup for: ~p",
176           [ssh_test_lib:setup_all_user_host_keys(Config)]),
177    Config;
178init_per_group(dir_options, Config) ->
179    PrivDir = proplists:get_value(priv_dir, Config),
180    %% Make unreadable dir:
181    Dir_unreadable = filename:join(PrivDir, "unread"),
182    ok = file:make_dir(Dir_unreadable),
183    {ok,F1} = file:read_file_info(Dir_unreadable),
184    ok = file:write_file_info(Dir_unreadable,
185			      F1#file_info{mode = F1#file_info.mode band (bnot 8#00444)}),
186    %% Make readable file:
187    File_readable = filename:join(PrivDir, "file"),
188    ok = file:write_file(File_readable, <<>>),
189
190    %% Check:
191    case {file:read_file_info(Dir_unreadable),
192	  file:read_file_info(File_readable)} of
193	{{ok, Id=#file_info{type=directory, access=Md}},
194	 {ok, If=#file_info{type=regular,   access=Mf}}} ->
195	    AccessOK =
196		case {Md,                Mf} of
197		    {read,               _} -> false;
198		    {read_write,         _} -> false;
199		    {_,               read} -> true;
200		    {_,         read_write} -> true;
201		    _ -> false
202		end,
203
204	    case AccessOK of
205		true ->
206		    %% Save:
207		    [{unreadable_dir, Dir_unreadable},
208		     {readable_file, File_readable}
209		     | Config];
210		false ->
211		    ct:log("File#file_info : ~p~n"
212			   "Dir#file_info  : ~p",[If,Id]),
213		    {skip, "File or dir mode settings failed"}
214	    end;
215
216	NotDirFile ->
217	    ct:log("{Dir,File} -> ~p",[NotDirFile]),
218	    {skip, "File/Dir creation failed"}
219    end;
220init_per_group(_, Config) ->
221    Config.
222
223end_per_group(_, Config) ->
224    Config.
225%%--------------------------------------------------------------------
226init_per_testcase(_TestCase, Config) ->
227    ssh:start(),
228    %% Create a clean user_dir
229    UserDir = filename:join(proplists:get_value(priv_dir, Config), nopubkey),
230    ssh_test_lib:del_dirs(UserDir),
231    file:make_dir(UserDir),
232    [{user_dir,UserDir}|Config].
233
234end_per_testcase(_TestCase, _Config) ->
235    ssh:stop(),
236    ok.
237
238%%--------------------------------------------------------------------
239%% Test Cases --------------------------------------------------------
240%%--------------------------------------------------------------------
241
242%%% validate to server that uses the 'password' option
243server_password_option(Config) when is_list(Config) ->
244    UserDir = proplists:get_value(user_dir, Config),
245    SysDir = proplists:get_value(data_dir, Config),
246    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
247					     {user_dir, UserDir},
248					     {password, "morot"}]),
249
250    ConnectionRef =
251	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
252					  {user, "foo"},
253					  {password, "morot"},
254					  {user_interaction, false},
255					  {user_dir, UserDir}]),
256
257    Reason = "Unable to connect using the available authentication methods",
258
259    {error, Reason} =
260	ssh:connect(Host, Port, [{silently_accept_hosts, true},
261                                 {save_accepted_host, false},
262				 {user, "vego"},
263				 {password, "foo"},
264				 {user_interaction, false},
265				 {user_dir, UserDir}]),
266
267    ct:log("Test of wrong password: Error msg: ~p ~n", [Reason]),
268
269    ssh:close(ConnectionRef),
270    ssh:stop_daemon(Pid).
271
272%%--------------------------------------------------------------------
273
274%%% validate to server that uses the 'password' option
275server_userpassword_option(Config) when is_list(Config) ->
276    UserDir = proplists:get_value(user_dir, Config),
277    SysDir = proplists:get_value(data_dir, Config),
278    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
279					     {user_dir, UserDir},
280					     {user_passwords, [{"vego", "morot"}]}]),
281
282    ConnectionRef =
283	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
284					  {user, "vego"},
285					  {password, "morot"},
286					  {user_interaction, false},
287					  {user_dir, UserDir}]),
288    ssh:close(ConnectionRef),
289
290    Reason = "Unable to connect using the available authentication methods",
291
292    {error, Reason} =
293	ssh:connect(Host, Port, [{silently_accept_hosts, true},
294                                 {save_accepted_host, false},
295				 {user, "foo"},
296				 {password, "morot"},
297				 {user_interaction, false},
298				 {user_dir, UserDir}]),
299    {error, Reason} =
300	ssh:connect(Host, Port, [{silently_accept_hosts, true},
301                                 {save_accepted_host, false},
302				 {user, "vego"},
303				 {password, "foo"},
304				 {user_interaction, false},
305				 {user_dir, UserDir}]),
306    ssh:stop_daemon(Pid).
307
308%%--------------------------------------------------------------------
309%%% validate to server that uses the 'pwdfun' option
310server_pwdfun_option(Config) ->
311    UserDir = proplists:get_value(user_dir, Config),
312    SysDir = proplists:get_value(data_dir, Config),
313    CHKPWD = fun("foo",Pwd) -> Pwd=="bar";
314		(_,_) -> false
315	     end,
316    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
317					     {user_dir, UserDir},
318					     {pwdfun,CHKPWD}]),
319    ConnectionRef =
320	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
321					  {user, "foo"},
322					  {password, "bar"},
323					  {user_interaction, false},
324					  {user_dir, UserDir}]),
325    ssh:close(ConnectionRef),
326
327    Reason = "Unable to connect using the available authentication methods",
328
329    {error, Reason} =
330	ssh:connect(Host, Port, [{silently_accept_hosts, true},
331                                 {save_accepted_host, false},
332				 {user, "foo"},
333				 {password, "morot"},
334				 {user_interaction, false},
335				 {user_dir, UserDir}]),
336    {error, Reason} =
337	ssh:connect(Host, Port, [{silently_accept_hosts, true},
338                                 {save_accepted_host, false},
339				 {user, "vego"},
340				 {password, "foo"},
341				 {user_interaction, false},
342				 {user_dir, UserDir}]),
343    ssh:stop_daemon(Pid).
344
345
346%%--------------------------------------------------------------------
347%%% validate to server that uses the 'pwdfun/4' option
348server_pwdfun_4_option(Config) ->
349    UserDir = proplists:get_value(user_dir, Config),
350    SysDir = proplists:get_value(data_dir, Config),
351    PWDFUN = fun("foo",Pwd,{_,_},undefined) -> Pwd=="bar";
352		("fie",Pwd,{_,_},undefined) -> {Pwd=="bar",new_state};
353		("bandit",_,_,_) -> disconnect;
354		(_,_,_,_) -> false
355	     end,
356    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
357					     {user_dir, UserDir},
358					     {pwdfun,PWDFUN}]),
359    ConnectionRef1 =
360	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
361					  {user, "foo"},
362					  {password, "bar"},
363					  {user_interaction, false},
364					  {user_dir, UserDir}]),
365    ssh:close(ConnectionRef1),
366
367    ConnectionRef2 =
368	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
369					  {user, "fie"},
370					  {password, "bar"},
371					  {user_interaction, false},
372					  {user_dir, UserDir}]),
373    ssh:close(ConnectionRef2),
374
375    Reason = "Unable to connect using the available authentication methods",
376
377    {error, Reason} =
378	ssh:connect(Host, Port, [{silently_accept_hosts, true},
379                                 {save_accepted_host, false},
380				 {user, "foo"},
381				 {password, "morot"},
382				 {user_interaction, false},
383				 {user_dir, UserDir}]),
384    {error, Reason} =
385	ssh:connect(Host, Port, [{silently_accept_hosts, true},
386                                 {save_accepted_host, false},
387				 {user, "fie"},
388				 {password, "morot"},
389				 {user_interaction, false},
390				 {user_dir, UserDir}]),
391    {error, Reason} =
392	ssh:connect(Host, Port, [{silently_accept_hosts, true},
393				 {user, "vego"},
394				 {password, "foo"},
395				 {user_interaction, false},
396				 {user_dir, UserDir}]),
397
398    {error, Reason} =
399	ssh:connect(Host, Port, [{silently_accept_hosts, true},
400                                 {save_accepted_host, false},
401				 {user, "bandit"},
402				 {password, "pwd breaking"},
403				 {user_interaction, false},
404				 {user_dir, UserDir}]),
405    ssh:stop_daemon(Pid).
406
407
408%%--------------------------------------------------------------------
409server_keyboard_interactive(Config) ->
410    UserDir = proplists:get_value(user_dir, Config),
411    SysDir = proplists:get_value(data_dir, Config),
412    %% Test that the state works
413    Parent = self(),
414    PWDFUN = fun("foo",P="bar",_,S) -> Parent!{P,S},true;
415		(_,P,_,S=undefined) -> Parent!{P,S},{false,1};
416		(_,P,_,S) -> Parent!{P,S},          {false,S+1}
417	     end,
418    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
419					     {user_dir, UserDir},
420					     {auth_methods,"keyboard-interactive"},
421					     {pwdfun,PWDFUN}]),
422
423    %% Try with passwords "incorrect", "Bad again" and finally "bar"
424    KIFFUN = fun(_Name, _Instr, _PromptInfos) ->
425		     K={k,self()},
426                     Answer =
427                         case get(K) of
428                             undefined ->
429                                 put(K,1),
430                                 ["incorrect"];
431                             2 ->
432                                 put(K,3),
433                                 ["bar"];
434                             S->
435                                 put(K,S+1),
436                                 ["Bad again"]
437                         end,
438                     ct:log("keyboard_interact_fun:~n"
439                            " Name        = ~p~n"
440                            " Instruction = ~p~n"
441                            " Prompts     = ~p~n"
442                            "~nAnswer:~n  ~p~n",
443                            [_Name, _Instr, _PromptInfos, Answer]),
444
445                     Answer
446	     end,
447
448    ConnectionRef2 =
449	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
450					  {user, "foo"},
451					  {keyboard_interact_fun, KIFFUN},
452					  {user_dir, UserDir}]),
453    ssh:close(ConnectionRef2),
454    ssh:stop_daemon(Pid),
455
456    lists:foreach(fun(Expect) ->
457			  receive
458			      Expect -> ok;
459			      Other -> ct:fail("Expect: ~p~nReceived ~p",[Expect,Other])
460			  after
461			      2000 -> ct:fail("Timeout expecting ~p",[Expect])
462			  end
463		  end, [{"incorrect",undefined},
464			{"Bad again",1},
465			{"bar",2}]).
466
467%%--------------------------------------------------------------------
468server_keyboard_interactive_extra_msg(Config) ->
469    UserDir = proplists:get_value(user_dir, Config),
470    SysDir = proplists:get_value(data_dir, Config),
471    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
472					     {user_dir, UserDir},
473					     {auth_methods,"keyboard-interactive"},
474                                             {tstflg, [{one_empty,true}]},
475                                             {user_passwords, [{"foo","bar"}]}
476                                            ]),
477
478    ConnectionRef =
479	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
480					  {user, "foo"},
481					  {password, "bar"},
482					  {user_dir, UserDir}]),
483    ssh:close(ConnectionRef),
484    ssh:stop_daemon(Pid).
485
486%%--------------------------------------------------------------------
487auth_method_kb_interactive_data_tuple(Config) ->
488    T = {"abc1", "def1", "ghi1: ", true},
489    amkid(Config, T, T).
490
491auth_method_kb_interactive_data_fun3(Config) ->
492    T = {"abc2", "def2", "ghi2: ", true},
493    amkid(Config, T,
494          fun(_Peer, _User, _Service) -> T end
495         ).
496
497auth_method_kb_interactive_data_fun4(Config) ->
498    T = {"abc3", "def3", "ghi3: ", true},
499    amkid(Config, T,
500          fun(_Peer, _User, _Service, _State) -> T end
501         ).
502
503amkid(Config, {ExpectName,ExpectInstr,ExpectPrompts,ExpectEcho}, OptVal) ->
504    UserDir = proplists:get_value(user_dir, Config),
505    SysDir = proplists:get_value(data_dir, Config),
506    %% Test that the state works
507    Parent = self(),
508    PWDFUN = fun("foo",P="bar",_,S) -> Parent!{P,S},true;
509		(_,P,_,S=undefined) -> Parent!{P,S},{false,1};
510		(_,P,_,S) -> Parent!{P,S},          {false,S+1}
511	     end,
512    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
513					     {user_dir, UserDir},
514					     {auth_methods,"keyboard-interactive"},
515					     {pwdfun,PWDFUN},
516                                             {auth_method_kb_interactive_data,OptVal}
517                                            ]),
518
519    KIFFUN = fun(Name, Instr, PromptInfos) ->
520		     K={k,self()},
521                     Answer =
522                         case get(K) of
523                             undefined ->
524                                 put(K,1),
525                                 ["incorrect"];
526                             2 ->
527                                 put(K,3),
528                                 ["bar"];
529                             S->
530                                 put(K,S+1),
531                                 ["Bad again"]
532                         end,
533                     ct:log("keyboard_interact_fun:~n"
534                            " Name        = ~p~n"
535                            " Instruction = ~p~n"
536                            " Prompts     = ~p~n"
537                            "~nAnswer:~n  ~p~n",
538                            [Name, Instr, PromptInfos, Answer]),
539                     case {binary_to_list(Name),
540                           binary_to_list(Instr),
541                           [{binary_to_list(PI),Echo} || {PI,Echo} <- PromptInfos]
542                          } of
543                         {ExpectName, ExpectInstr, [{ExpectPrompts,ExpectEcho}]} ->
544                             ct:log("Match!", []),
545                             Answer;
546                         _ ->
547                             ct:log("Not match!~n"
548                                    " ExpectName        = ~p~n"
549                                    " ExpectInstruction = ~p~n"
550                                    " ExpectPrompts     = ~p~n",
551                                    [ExpectName, ExpectInstr, [{ExpectPrompts,ExpectEcho}]]),
552                             ct:fail("no_match")
553                     end
554	     end,
555    ssh_dbg:start(), ssh_dbg:on(authentication), %% Test dbg code
556    ConnectionRef2 =
557	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
558					  {user, "foo"},
559					  {keyboard_interact_fun, KIFFUN},
560					  {user_dir, UserDir}]),
561    ssh_dbg:stop(),
562    ssh:close(ConnectionRef2),
563    ssh:stop_daemon(Pid),
564
565    lists:foreach(fun(Expect) ->
566			  receive
567			      Expect -> ok;
568			      Other -> ct:fail("Expect: ~p~nReceived ~p",[Expect,Other])
569			  after
570			      2000 -> ct:fail("Timeout expecting ~p",[Expect])
571			  end
572		  end, [{"incorrect",undefined},
573			{"Bad again",1},
574			{"bar",2}]).
575
576%%--------------------------------------------------------------------
577system_dir_option(Config) ->
578    DirUnread = proplists:get_value(unreadable_dir,Config),
579    FileRead = proplists:get_value(readable_file,Config),
580
581    case ssh_test_lib:daemon([{system_dir, DirUnread}]) of
582	{error,{eoptions,{{system_dir,DirUnread},eacces}}} ->
583	    ok;
584	{Pid1,_Host1,Port1} when is_pid(Pid1),is_integer(Port1) ->
585	    ssh:stop_daemon(Pid1),
586	    ct:fail("Didn't detect that dir is unreadable", [])
587	end,
588
589    case ssh_test_lib:daemon([{system_dir, FileRead}]) of
590	{error,{eoptions,{{system_dir,FileRead},enotdir}}} ->
591	    ok;
592	{Pid2,_Host2,Port2} when is_pid(Pid2),is_integer(Port2) ->
593	    ssh:stop_daemon(Pid2),
594	    ct:fail("Didn't detect that option is a plain file", [])
595    end.
596
597%%--------------------------------------------------------------------
598user_dir_option(Config) ->
599    DirUnread = proplists:get_value(unreadable_dir,Config),
600    FileRead = proplists:get_value(readable_file,Config),
601    %% Any port will do (beware, implementation knowledge!):
602    Port = 65535,
603
604    case ssh:connect("localhost", Port, [{user_dir, DirUnread},
605                                         {save_accepted_host, false}]) of
606	{error,{eoptions,{{user_dir,DirUnread},eacces}}} ->
607	    ok;
608	{error,econnrefused} ->
609	    ct:fail("Didn't detect that dir is unreadable", [])
610    end,
611
612    case ssh:connect("localhost", Port, [{user_dir, FileRead},
613                                         {save_accepted_host, false}]) of
614	{error,{eoptions,{{user_dir,FileRead},enotdir}}} ->
615	    ok;
616	{error,econnrefused} ->
617	    ct:fail("Didn't detect that option is a plain file", [])
618    end.
619
620%%--------------------------------------------------------------------
621user_dir_fun_option(Config) ->
622    DataDir = proplists:get_value(data_dir, Config),
623    PrivDir = proplists:get_value(priv_dir, Config),
624    SysDir = filename:join(PrivDir,"system"),
625    ssh_test_lib:setup_all_host_keys(DataDir, SysDir),
626    UserDir = filename:join(PrivDir,"user"),
627    ssh_test_lib:setup_all_user_keys(DataDir, UserDir),
628
629    Parent = self(),
630    Ref = make_ref(),
631    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
632					     {user_dir_fun, fun(User) ->
633                                                                    ct:log("user_dir_fun called ~p",[User]),
634                                                                    Parent ! {user,Ref,User},
635                                                                    UserDir
636                                                            end},
637					     {failfun, fun ssh_test_lib:failfun/2}]),
638    _ConnectionRef =
639	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
640					  {user, "foo"},
641					  {user_dir, UserDir},
642                                          {auth_methods,"publickey"},
643					  {user_interaction, false}]),
644    receive
645        {user,Ref,"foo"} ->
646            ssh:stop_daemon(Pid),
647            ok;
648        {user,Ref,What} ->
649            ssh:stop_daemon(Pid),
650            ct:log("Got ~p",[What]),
651            {fail, bad_userid}
652    after 2000 ->
653            ssh:stop_daemon(Pid),
654            {fail,timeout_in_receive}
655    end.
656
657
658%%--------------------------------------------------------------------
659%%% validate client that uses the 'ssh_msg_debug_fun' option
660ssh_msg_debug_fun_option_client(Config) ->
661    UserDir = proplists:get_value(user_dir, Config),
662    SysDir = proplists:get_value(data_dir, Config),
663
664    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
665					     {user_dir, UserDir},
666					     {password, "morot"},
667					     {failfun, fun ssh_test_lib:failfun/2}]),
668    Parent = self(),
669    DbgFun = fun(ConnRef,Displ,Msg,Lang) -> Parent ! {msg_dbg,{ConnRef,Displ,Msg,Lang}} end,
670
671    ConnectionRef =
672	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
673					  {user, "foo"},
674					  {password, "morot"},
675					  {user_dir, UserDir},
676					  {user_interaction, false},
677					  {ssh_msg_debug_fun,DbgFun}]),
678    %% Beware, implementation knowledge:
679    gen_statem:cast(ConnectionRef,{ssh_msg_debug,false,<<"Hello">>,<<>>}),
680    receive
681	{msg_dbg,X={ConnectionRef,false,<<"Hello">>,<<>>}} ->
682	    ct:log("Got expected dbg msg ~p",[X]),
683	    ssh:stop_daemon(Pid);
684	{msg_dbg,X={_,false,<<"Hello">>,<<>>}} ->
685	    ct:log("Got dbg msg but bad ConnectionRef (~p expected) ~p",[ConnectionRef,X]),
686	    ssh:stop_daemon(Pid),
687	    {fail, "Bad ConnectionRef received"};
688	{msg_dbg,X} ->
689	    ct:log("Got bad dbg msg ~p",[X]),
690	    ssh:stop_daemon(Pid),
691	    {fail,"Bad msg received"}
692    after 1000 ->
693	    ssh:stop_daemon(Pid),
694	    {fail,timeout}
695    end.
696
697%%--------------------------------------------------------------------
698connectfun_disconnectfun_server(Config) ->
699    UserDir = proplists:get_value(user_dir, Config),
700    SysDir = proplists:get_value(data_dir, Config),
701
702    Parent = self(),
703    Ref = make_ref(),
704    ConnFun = fun(_,_,_) -> Parent ! {connect,Ref} end,
705    DiscFun = fun(R) -> Parent ! {disconnect,Ref,R} end,
706
707    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
708					     {user_dir, UserDir},
709					     {password, "morot"},
710					     {failfun, fun ssh_test_lib:failfun/2},
711					     {disconnectfun, DiscFun},
712					     {connectfun, ConnFun}]),
713    ConnectionRef =
714	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
715					  {user, "foo"},
716					  {password, "morot"},
717					  {user_dir, UserDir},
718					  {user_interaction, false}]),
719    receive
720	{connect,Ref} ->
721	    ssh:close(ConnectionRef),
722	    receive
723		{disconnect,Ref,R} ->
724		    ct:log("Disconnect result: ~p",[R]),
725		    ssh:stop_daemon(Pid)
726	    after 10000 ->
727		    receive
728			X -> ct:log("received ~p",[X])
729		    after 0 -> ok
730		    end,
731		    {fail, "No disconnectfun action"}
732	    end
733    after 10000 ->
734	    receive
735		X -> ct:log("received ~p",[X])
736	    after 0 -> ok
737	    end,
738	    {fail, "No connectfun action"}
739    end.
740
741%%--------------------------------------------------------------------
742connectfun_disconnectfun_client(Config) ->
743    UserDir = proplists:get_value(user_dir, Config),
744    SysDir = proplists:get_value(data_dir, Config),
745
746    Parent = self(),
747    Ref = make_ref(),
748    DiscFun = fun(R) -> Parent ! {disconnect,Ref,R} end,
749
750    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
751					     {user_dir, UserDir},
752					     {password, "morot"},
753					     {failfun, fun ssh_test_lib:failfun/2}]),
754    _ConnectionRef =
755	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
756					  {user, "foo"},
757					  {password, "morot"},
758					  {user_dir, UserDir},
759					  {disconnectfun, DiscFun},
760					  {user_interaction, false}]),
761    ssh:stop_daemon(Pid),
762    receive
763	{disconnect,Ref,R} ->
764	    ct:log("Disconnect result: ~p",[R])
765    after 2000 ->
766	    {fail, "No disconnectfun action"}
767    end.
768
769%%--------------------------------------------------------------------
770%%% validate client that uses the 'ssh_msg_debug_fun' option
771ssh_msg_debug_fun_option_server(Config) ->
772    UserDir = proplists:get_value(user_dir, Config),
773    SysDir = proplists:get_value(data_dir, Config),
774
775    Parent = self(),
776    DbgFun = fun(ConnRef,Displ,Msg,Lang) -> Parent ! {msg_dbg,{ConnRef,Displ,Msg,Lang}} end,
777    ConnFun = fun(_,_,_) -> Parent ! {connection_pid,self()} end,
778
779    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
780					     {user_dir, UserDir},
781					     {password, "morot"},
782					     {failfun, fun ssh_test_lib:failfun/2},
783					     {connectfun, ConnFun},
784					     {ssh_msg_debug_fun, DbgFun}]),
785    _ConnectionRef =
786	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
787					  {user, "foo"},
788					  {password, "morot"},
789					  {user_dir, UserDir},
790					  {user_interaction, false}]),
791    receive
792	{connection_pid,Server} ->
793	    %% Beware, implementation knowledge:
794	    gen_statem:cast(Server,{ssh_msg_debug,false,<<"Hello">>,<<>>}),
795	    receive
796		{msg_dbg,X={_,false,<<"Hello">>,<<>>}} ->
797		    ct:log("Got expected dbg msg ~p",[X]),
798		    ssh:stop_daemon(Pid);
799		{msg_dbg,X} ->
800		    ct:log("Got bad dbg msg ~p",[X]),
801		    ssh:stop_daemon(Pid),
802		    {fail,"Bad msg received"}
803	    after 3000 ->
804		    ssh:stop_daemon(Pid),
805		    {fail,timeout2}
806	    end
807    after 3000 ->
808	    ssh:stop_daemon(Pid),
809	    {fail,timeout1}
810    end.
811
812%%--------------------------------------------------------------------
813disconnectfun_option_server(Config) ->
814    UserDir = proplists:get_value(user_dir, Config),
815    SysDir = proplists:get_value(data_dir, Config),
816
817    Parent = self(),
818    DisConnFun = fun(Reason) -> Parent ! {disconnect,Reason} end,
819
820    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
821					     {user_dir, UserDir},
822					     {password, "morot"},
823					     {failfun, fun ssh_test_lib:failfun/2},
824					     {disconnectfun, DisConnFun}]),
825    ConnectionRef =
826	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
827					  {user, "foo"},
828					  {password, "morot"},
829					  {user_dir, UserDir},
830					  {user_interaction, false}]),
831    ssh:close(ConnectionRef),
832    receive
833	{disconnect,Reason} ->
834	    ct:log("Server detected disconnect: ~p",[Reason]),
835	    ssh:stop_daemon(Pid),
836	    ok
837    after 5000 ->
838	    receive
839		X -> ct:log("received ~p",[X])
840	    after 0 -> ok
841	    end,
842	    {fail,"Timeout waiting for disconnect"}
843    end.
844
845%%--------------------------------------------------------------------
846disconnectfun_option_client(Config) ->
847    UserDir = proplists:get_value(user_dir, Config),
848    SysDir = proplists:get_value(data_dir, Config),
849
850    Parent = self(),
851    DisConnFun = fun(Reason) -> Parent ! {disconnect,Reason} end,
852
853    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
854					     {user_dir, UserDir},
855					     {password, "morot"},
856					     {failfun, fun ssh_test_lib:failfun/2}]),
857    _ConnectionRef =
858	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
859					  {user, "foo"},
860					  {password, "morot"},
861					  {user_dir, UserDir},
862					  {user_interaction, false},
863					  {disconnectfun, DisConnFun}]),
864    ssh:stop_daemon(Pid),
865    receive
866	{disconnect,Reason} ->
867	    ct:log("Client detected disconnect: ~p",[Reason]),
868	    ok
869    after 3000 ->
870	    receive
871		X -> ct:log("received ~p",[X])
872	    after 0 -> ok
873	    end,
874	    {fail,"Timeout waiting for disconnect"}
875    end.
876
877%%--------------------------------------------------------------------
878unexpectedfun_option_server(Config) ->
879    UserDir = proplists:get_value(user_dir, Config),
880    SysDir = proplists:get_value(data_dir, Config),
881
882    Parent = self(),
883    ConnFun = fun(_,_,_) -> Parent ! {connection_pid,self()} end,
884    UnexpFun = fun(Msg,Peer) ->
885		       Parent ! {unexpected,Msg,Peer,self()},
886		       skip
887	       end,
888
889    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
890					     {user_dir, UserDir},
891					     {password, "morot"},
892					     {failfun, fun ssh_test_lib:failfun/2},
893					     {connectfun, ConnFun},
894					     {unexpectedfun, UnexpFun}]),
895    _ConnectionRef =
896	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
897					  {user, "foo"},
898					  {password, "morot"},
899					  {user_dir, UserDir},
900					  {user_interaction, false}]),
901    receive
902	{connection_pid,Server} ->
903	    %% Beware, implementation knowledge:
904	    Server ! unexpected_message,
905	    receive
906		{unexpected, unexpected_message, {{_,_,_,_},_}, _} -> ok;
907		{unexpected, unexpected_message, Peer, _} -> ct:fail("Bad peer ~p",[Peer]);
908		M = {unexpected, _, _, _} -> ct:fail("Bad msg ~p",[M])
909	    after 3000 ->
910		    ssh:stop_daemon(Pid),
911		    {fail,timeout2}
912	    end
913    after 3000 ->
914	    ssh:stop_daemon(Pid),
915	    {fail,timeout1}
916    end.
917
918%%--------------------------------------------------------------------
919unexpectedfun_option_client(Config) ->
920    UserDir = proplists:get_value(user_dir, Config),
921    SysDir = proplists:get_value(data_dir, Config),
922
923    Parent = self(),
924    UnexpFun = fun(Msg,Peer) ->
925		       Parent ! {unexpected,Msg,Peer,self()},
926		       skip
927	       end,
928
929    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
930					     {user_dir, UserDir},
931					     {password, "morot"},
932					     {failfun, fun ssh_test_lib:failfun/2}]),
933    ConnectionRef =
934	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
935					  {user, "foo"},
936					  {password, "morot"},
937					  {user_dir, UserDir},
938					  {user_interaction, false},
939					  {unexpectedfun, UnexpFun}]),
940    %% Beware, implementation knowledge:
941    ConnectionRef ! unexpected_message,
942
943    receive
944	{unexpected, unexpected_message, {{_,_,_,_},_}, ConnectionRef} ->
945	    ok;
946	{unexpected, unexpected_message, Peer, ConnectionRef} ->
947	    ct:fail("Bad peer ~p",[Peer]);
948	M = {unexpected, _, _, _} ->
949	    ct:fail("Bad msg ~p",[M])
950    after 3000 ->
951	    ssh:stop_daemon(Pid),
952	    {fail,timeout}
953    end.
954
955%%--------------------------------------------------------------------
956hostkey_fingerprint_check(Config) ->
957    do_hostkey_fingerprint_check(Config, old).
958
959hostkey_fingerprint_check_md5(Config) ->
960    do_hostkey_fingerprint_check(Config, md5).
961
962hostkey_fingerprint_check_sha(Config) ->
963    do_hostkey_fingerprint_check(Config, sha).
964
965hostkey_fingerprint_check_sha256(Config) ->
966    do_hostkey_fingerprint_check(Config, sha256).
967
968hostkey_fingerprint_check_sha384(Config) ->
969    do_hostkey_fingerprint_check(Config, sha384).
970
971hostkey_fingerprint_check_sha512(Config) ->
972    do_hostkey_fingerprint_check(Config, sha512).
973
974hostkey_fingerprint_check_list(Config) ->
975    do_hostkey_fingerprint_check(Config, [sha,md5,sha256]).
976
977%%%----
978do_hostkey_fingerprint_check(Config, HashAlg) ->
979    case supported_hash(HashAlg) of
980	true ->
981	    really_do_hostkey_fingerprint_check(Config, HashAlg);
982	false when HashAlg == old ->
983	    {skip,{unsupported_hash,md5}};% Happen to know that ssh:hostkey_fingerprint/1 uses md5...
984	false ->
985	    {skip,{unsupported_hash,HashAlg}}
986    end.
987
988supported_hash(old) ->
989    supported_hash(md5); % Happen to know that ssh:hostkey_fingerprint/1 uses md5...
990supported_hash(HashAlg) ->
991    Hs = if is_atom(HashAlg) -> [HashAlg];
992            is_list(HashAlg) -> HashAlg
993         end,
994    [] == (Hs -- proplists:get_value(hashs, crypto:supports(), [])).
995
996
997really_do_hostkey_fingerprint_check(Config, HashAlg) ->
998    UserDir = proplists:get_value(user_dir, Config),
999    SysDir = proplists:get_value(data_dir, Config),
1000
1001    %% All host key fingerprints.  Trust that public_key has checked the hostkey_fingerprint
1002    %% function since that function is used by the ssh client...
1003    FPs0 = [case HashAlg of
1004	       old -> ssh:hostkey_fingerprint(Key);
1005	       _ -> ssh:hostkey_fingerprint(HashAlg, Key)
1006	   end
1007	   || FileCandidate <- begin
1008				   {ok,KeyFileCands} = file:list_dir(SysDir),
1009				   KeyFileCands
1010			       end,
1011	      nomatch =/= re:run(FileCandidate, ".*\\.pub", []),
1012	      {Key,_Cmnts} <- begin
1013				  {ok,Bin} = file:read_file(filename:join(SysDir, FileCandidate)),
1014				  try ssh_file:decode(Bin, public_key)
1015				  catch
1016				      _:_ -> []
1017				  end
1018			      end],
1019    FPs = if is_atom(HashAlg) -> FPs0;
1020             is_list(HashAlg) -> lists:concat(FPs0)
1021          end,
1022    ct:log("Fingerprints(~p) = ~p",[HashAlg,FPs]),
1023
1024    %% Start daemon with the public keys that we got fingerprints from
1025    {Pid, Host0, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
1026					     {user_dir, UserDir},
1027					     {password, "morot"}]),
1028    Host = ssh_test_lib:ntoa(Host0),
1029    FP_check_fun = fun(PeerName, FP) ->
1030			   ct:log("PeerName = ~p, FP = ~p",[PeerName,FP]),
1031			   HostCheck = ssh_test_lib:match_ip(Host, PeerName),
1032			   FPCheck =
1033                               if is_atom(HashAlg) -> lists:member(FP, FPs);
1034                                  is_list(HashAlg) -> lists:all(fun(FP1) -> lists:member(FP1,FPs) end,
1035                                                                FP)
1036                               end,
1037			   ct:log("check ~p == ~p (~p) and ~n~p~n in ~p (~p)~n",
1038				  [PeerName,Host,HostCheck,FP,FPs,FPCheck]),
1039			   HostCheck and FPCheck
1040		   end,
1041
1042    ssh_test_lib:connect(Host, Port, [{silently_accept_hosts,
1043				       case HashAlg of
1044					   old -> FP_check_fun;
1045					   _ -> {HashAlg, FP_check_fun}
1046				       end},
1047				      {user, "foo"},
1048				      {password, "morot"},
1049				      {user_dir, UserDir},
1050                                      {save_accepted_host, false}, % Ensure no 'known_hosts' disturbs
1051				      {user_interaction, false}]),
1052    ssh:stop_daemon(Pid).
1053
1054%%--------------------------------------------------------------------
1055%%% Test connect_timeout option in ssh:connect/4
1056ssh_connect_timeout(_Config) ->
1057    ConnTimeout = 2000,
1058    {error,{faked_transport,connect,TimeoutToTransport}} =
1059	ssh:connect("localhost", 12345,
1060		    [{transport,{tcp,?MODULE,tcp_closed}},
1061                     {save_accepted_host, false},
1062		     {connect_timeout,ConnTimeout}],
1063		    1000),
1064    case TimeoutToTransport of
1065	ConnTimeout -> ok;
1066	Other ->
1067	    ct:log("connect_timeout is ~p but transport received ~p",[ConnTimeout,Other]),
1068	    {fail,"ssh:connect/4 wrong connect_timeout received in transport"}
1069    end.
1070
1071%% Plugin function for the test above
1072connect(_Host, _Port, _Opts, Timeout) ->
1073    {error, {faked_transport,connect,Timeout}}.
1074
1075%%--------------------------------------------------------------------
1076%%% Test fourth argument in ssh:connect/4
1077ssh_connect_arg4_timeout(_Config) ->
1078    Timeout = 1000,
1079    Parent = self(),
1080    %% start the server
1081    Server = spawn(fun() ->
1082			   {ok,Sl} = gen_tcp:listen(0,[]),
1083			   {ok,{_,Port}} = inet:sockname(Sl),
1084			   Parent ! {port,self(),Port},
1085			   Rsa = gen_tcp:accept(Sl),
1086			   ct:log("Server gen_tcp:accept got ~p",[Rsa]),
1087			   receive after 2*Timeout -> ok end %% let client timeout first
1088		   end),
1089
1090    %% Get listening port
1091    Port = receive
1092	       {port,Server,ServerPort} -> ServerPort
1093	   after
1094	       10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
1095	   end,
1096
1097    %% try to connect with a timeout, but "supervise" it
1098    Client = spawn(fun() ->
1099			   T0 = erlang:monotonic_time(),
1100			   Rc = ssh:connect("localhost",Port,[{save_accepted_host, false}],Timeout),
1101			   ct:log("Client ssh:connect got ~p",[Rc]),
1102			   Parent ! {done,self(),Rc,T0}
1103		   end),
1104
1105    %% Wait for client reaction on the connection try:
1106    receive
1107	{done, Client, {error,timeout}, T0} ->
1108	    Msp = ms_passed(T0),
1109	    exit(Server,hasta_la_vista___baby),
1110	    Low = 0.9*Timeout,
1111	    High =  4.0*Timeout,
1112	    ct:log("Timeout limits: ~.4f - ~.4f ms, timeout "
1113                   "was ~.4f ms, expected ~p ms",[Low,High,Msp,Timeout]),
1114	    if
1115		Low<Msp, Msp<High -> ok;
1116		true -> {fail, "timeout not within limits"}
1117	    end;
1118
1119	{done, Client, {error,Other}, _T0} ->
1120	    ct:log("Error message \"~p\" from the client is unexpected.",[{error,Other}]),
1121	    {fail, "Unexpected error message"};
1122
1123	{done, Client, {ok,_Ref}, _T0} ->
1124	    {fail,"ssh-connected ???"}
1125    after
1126	5000 ->
1127	    exit(Server,hasta_la_vista___baby),
1128	    exit(Client,hasta_la_vista___baby),
1129	    {fail, "Didn't timeout"}
1130    end.
1131
1132%% Help function, elapsed milliseconds since T0
1133ms_passed(T0) ->
1134    %% OTP 18
1135    erlang:convert_time_unit(erlang:monotonic_time() - T0,
1136			     native,
1137			     micro_seconds) / 1000.
1138
1139%%--------------------------------------------------------------------
1140ssh_daemon_minimal_remote_max_packet_size_option(Config) ->
1141    SystemDir = proplists:get_value(data_dir, Config),
1142    UserDir = proplists:get_value(user_dir, Config),
1143
1144    {Server, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},
1145						{user_dir, UserDir},
1146						{user_passwords, [{"vego", "morot"}]},
1147						{failfun, fun ssh_test_lib:failfun/2},
1148						{minimal_remote_max_packet_size, 14}]),
1149    Conn =
1150	ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
1151					  {user_dir, UserDir},
1152					  {user_interaction, false},
1153					  {user, "vego"},
1154					  {password, "morot"}]),
1155
1156    %% Try the limits of the minimal_remote_max_packet_size:
1157    {ok, _ChannelId} = ssh_connection:session_channel(Conn, 100, 14, infinity),
1158    {open_error,_,"Maximum packet size below 14 not supported",_} =
1159	ssh_connection:session_channel(Conn, 100, 13, infinity),
1160
1161    ssh:close(Conn),
1162    ssh:stop_daemon(Server).
1163
1164%%--------------------------------------------------------------------
1165%% This test try every algorithm by connecting to an Erlang server
1166id_string_no_opt_client(Config) ->
1167    {Server, _Host, Port} = fake_daemon(Config),
1168    {error,_} = ssh:connect("localhost", Port, [{save_accepted_host, false}], 1000),
1169    receive
1170	{id,Server,"SSH-2.0-Erlang/"++Vsn} ->
1171	    true = expected_ssh_vsn(Vsn);
1172	{id,Server,Other} ->
1173	    ct:fail("Unexpected id: ~s.",[Other])
1174    after 5000 ->
1175	    {fail,timeout}
1176    end.
1177
1178%%--------------------------------------------------------------------
1179id_string_own_string_client(Config) ->
1180    {Server, _Host, Port} = fake_daemon(Config),
1181    {error,_} = ssh:connect("localhost", Port, [{id_string,"Pelle"},
1182                                                {save_accepted_host, false}
1183                                               ], 1000),
1184    receive
1185	{id,Server,"SSH-2.0-Pelle\r\n"} ->
1186	    ok;
1187	{id,Server,Other} ->
1188	    ct:fail("Unexpected id: ~s.",[Other])
1189    after 5000 ->
1190	    {fail,timeout}
1191    end.
1192
1193%%--------------------------------------------------------------------
1194id_string_own_string_client_trail_space(Config) ->
1195    {Server, _Host, Port} = fake_daemon(Config),
1196    {error,_} = ssh:connect("localhost", Port, [{id_string,"Pelle "},
1197                                                {save_accepted_host, false}], 1000),
1198    receive
1199	{id,Server,"SSH-2.0-Pelle \r\n"} ->
1200	    ok;
1201	{id,Server,Other} ->
1202	    ct:fail("Unexpected id: ~s.",[Other])
1203    after 5000 ->
1204	    {fail,timeout}
1205    end.
1206
1207%%--------------------------------------------------------------------
1208id_string_random_client(Config) ->
1209    {Server, _Host, Port} = fake_daemon(Config),
1210    {error,_} = ssh:connect("localhost", Port, [{id_string,random},
1211                                                {save_accepted_host, false}], 1000),
1212    receive
1213	{id,Server,Id="SSH-2.0-Erlang"++_} ->
1214	    ct:fail("Unexpected id: ~s.",[Id]);
1215	{id,Server,Rnd="SSH-2.0-"++ID} when 4=<length(ID),length(ID)=<7 -> %% Add 2 for CRLF
1216	    ct:log("Got correct ~s",[Rnd]);
1217	{id,Server,Id} ->
1218	    ct:fail("Unexpected id: ~s.",[Id])
1219    after 5000 ->
1220	    {fail,timeout}
1221    end.
1222
1223%%--------------------------------------------------------------------
1224id_string_no_opt_server(Config) ->
1225    {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, []),
1226    {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]),
1227    {ok,"SSH-2.0-Erlang/"++Vsn} = gen_tcp:recv(S1, 0, 2000),
1228    true = expected_ssh_vsn(Vsn).
1229
1230%%--------------------------------------------------------------------
1231id_string_own_string_server(Config) ->
1232    {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, [{id_string,"Olle"}]),
1233    {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]),
1234    {ok,"SSH-2.0-Olle\r\n"} = gen_tcp:recv(S1, 0, 2000).
1235
1236%%--------------------------------------------------------------------
1237id_string_own_string_server_trail_space(Config) ->
1238    {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, [{id_string,"Olle "}]),
1239    {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]),
1240    {ok,"SSH-2.0-Olle \r\n"} = gen_tcp:recv(S1, 0, 2000).
1241
1242%%--------------------------------------------------------------------
1243id_string_random_server(Config) ->
1244    %% Check undocumented format of id_string. First a bad variant:
1245    {error,{eoptions,_}} = ssh:daemon(0, [{id_string,{random,8,6}}]),
1246    %% And then a correct one:
1247    {_Server, Host, Port} = ssh_test_lib:std_daemon(Config, [{id_string,{random,6,8}}]),
1248    {ok,S1}=ssh_test_lib:gen_tcp_connect(Host,Port,[{active,false},{packet,line}]),
1249    {ok,"SSH-2.0-"++Rnd} = gen_tcp:recv(S1, 0, 2000),
1250    case Rnd of
1251	"Erlang"++_ -> ct:log("Id=~p",[Rnd]),
1252		       {fail,got_default_id};
1253	"Olle\r\n" -> {fail,got_previous_tests_value};
1254	_ when 8=<length(Rnd),length(Rnd)=<10 -> %% Add 2 for CRLF
1255	    ct:log("Got correct ~s",[Rnd]);
1256	_ ->
1257            ct:log("Got wrong sized ~s.",[Rnd]),
1258            {fail,got_wrong_size}
1259    end.
1260
1261%%--------------------------------------------------------------------
1262ssh_connect_negtimeout_parallel(Config) -> ssh_connect_negtimeout(Config,true).
1263ssh_connect_negtimeout_sequential(Config) -> ssh_connect_negtimeout(Config,false).
1264
1265ssh_connect_negtimeout(Config, Parallel) ->
1266    process_flag(trap_exit, true),
1267    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
1268    UserDir = proplists:get_value(priv_dir, Config),
1269    NegTimeOut = 2000,				% ms
1270    ct:log("Parallel: ~p",[Parallel]),
1271
1272    {_Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir},
1273                                               {parallel_login, Parallel},
1274                                               {negotiation_timeout, NegTimeOut},
1275                                               {failfun, fun ssh_test_lib:failfun/2}]),
1276
1277    {ok,Socket} = ssh_test_lib:gen_tcp_connect(Host, Port, []),
1278
1279    Factor = 2,
1280    ct:log("And now sleeping ~p*NegTimeOut (~p ms)...", [Factor, round(Factor * NegTimeOut)]),
1281    ct:sleep(round(Factor * NegTimeOut)),
1282
1283    case inet:sockname(Socket) of
1284	{ok,_} ->
1285	    %% Give it another chance...
1286	    ct:log("Sleep more...",[]),
1287	    ct:sleep(round(Factor * NegTimeOut)),
1288	    case inet:sockname(Socket) of
1289		{ok,_} -> ct:fail("Socket not closed");
1290		{error,_} -> ok
1291	    end;
1292	{error,_} -> ok
1293    end.
1294
1295%%--------------------------------------------------------------------
1296%%% Test that ssh connection does not timeout if the connection is established (parallel)
1297ssh_connect_nonegtimeout_connected_parallel(Config) ->
1298    ssh_connect_nonegtimeout_connected(Config, true).
1299
1300%%% Test that ssh connection does not timeout if the connection is established (non-parallel)
1301ssh_connect_nonegtimeout_connected_sequential(Config) ->
1302    ssh_connect_nonegtimeout_connected(Config, false).
1303
1304
1305ssh_connect_nonegtimeout_connected(Config, Parallel) ->
1306    process_flag(trap_exit, true),
1307    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
1308    UserDir = proplists:get_value(priv_dir, Config),
1309    NegTimeOut = 2000,				% ms
1310    ct:log("Parallel: ~p",[Parallel]),
1311
1312    {_Pid, _Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir},{user_dir, UserDir},
1313					       {parallel_login, Parallel},
1314					       {negotiation_timeout, NegTimeOut},
1315					       {failfun, fun ssh_test_lib:failfun/2}]),
1316    ct:log("~p Listen ~p:~p",[_Pid,_Host,Port]),
1317    ct:sleep(500),
1318
1319    IO = ssh_test_lib:start_io_server(),
1320    Shell = ssh_test_lib:start_shell(Port, IO, [{user_dir,UserDir}]),
1321    receive
1322	Error = {'EXIT', _, _} ->
1323	    ct:log("~p",[Error]),
1324	    ct:fail(no_ssh_connection);
1325	ErlShellStart ->
1326	    ct:log("---Erlang shell start: ~p~n", [ErlShellStart]),
1327	    one_shell_op(IO, NegTimeOut),
1328	    one_shell_op(IO, NegTimeOut),
1329
1330	    Factor = 2,
1331	    ct:log("And now sleeping ~p*NegTimeOut (~p ms)...", [Factor, round(Factor * NegTimeOut)]),
1332	    ct:sleep(round(Factor * NegTimeOut)),
1333
1334	    one_shell_op(IO, NegTimeOut)
1335    after
1336	10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
1337    end,
1338    exit(Shell, kill).
1339
1340
1341one_shell_op(IO, TimeOut) ->
1342    ct:log("One shell op: Waiting for prompter"),
1343    receive
1344	ErlPrompt0 -> ct:log("Erlang prompt: ~p~n", [ErlPrompt0])
1345    after TimeOut -> ct:fail("Timeout waiting for promter")
1346    end,
1347
1348    IO ! {input, self(), "2*3*7.\r\n"},
1349    receive
1350	Result0 -> ct:log("Result: ~p~n", [Result0])
1351    after TimeOut ->  ct:fail("Timeout waiting for result")
1352    end.
1353
1354%%--------------------------------------------------------------------
1355max_sessions_ssh_connect_parallel(Config) ->
1356    max_sessions(Config, true, connect_fun(ssh__connect,Config)).
1357max_sessions_ssh_connect_sequential(Config) ->
1358    max_sessions(Config, false, connect_fun(ssh__connect,Config)).
1359
1360max_sessions_sftp_start_channel_parallel(Config) ->
1361    max_sessions(Config, true, connect_fun(ssh_sftp__start_channel, Config)).
1362max_sessions_sftp_start_channel_sequential(Config) ->
1363    max_sessions(Config, false, connect_fun(ssh_sftp__start_channel, Config)).
1364
1365
1366%%%---- helpers:
1367connect_fun(ssh__connect, Config) ->
1368    fun(Host,Port) ->
1369	    ssh_test_lib:connect(Host, Port,
1370				 [{silently_accept_hosts, true},
1371				  {user_dir, proplists:get_value(priv_dir,Config)},
1372				  {user_interaction, false},
1373				  {user, "carni"},
1374				  {password, "meat"}
1375				 ])
1376	    %% ssh_test_lib returns R when ssh:connect returns {ok,R}
1377    end;
1378connect_fun(ssh_sftp__start_channel, _Config) ->
1379    fun(Host,Port) ->
1380	    {ok,_Pid,ConnRef} =
1381		ssh_sftp:start_channel(Host, Port,
1382				       [{silently_accept_hosts, true},
1383                                        {save_accepted_host, false},
1384					{user, "carni"},
1385					{password, "meat"}
1386				       ]),
1387	    ConnRef
1388    end.
1389
1390
1391max_sessions(Config, ParallelLogin, Connect0) when is_function(Connect0,2) ->
1392    Connect = fun(Host,Port) ->
1393		      R = Connect0(Host,Port),
1394		      ct:log("Connect(~p,~p) -> ~p",[Host,Port,R]),
1395		      R
1396	      end,
1397    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
1398    UserDir = proplists:get_value(priv_dir, Config),
1399    MaxSessions = 5,
1400    {Pid, Host, Port} = ssh_test_lib:daemon([
1401					     {system_dir, SystemDir},
1402					     {user_dir, UserDir},
1403					     {user_passwords, [{"carni", "meat"}]},
1404					     {parallel_login, ParallelLogin},
1405					     {max_sessions, MaxSessions}
1406					    ]),
1407    ct:log("~p Listen ~p:~p for max ~p sessions",[Pid,Host,Port,MaxSessions]),
1408    try [Connect(Host,Port) || _ <- lists:seq(1,MaxSessions)]
1409    of
1410	Connections ->
1411	    %% Step 1 ok: could set up max_sessions connections
1412	    ct:log("Connections up: ~p",[Connections]),
1413	    [_|_] = Connections,
1414
1415	    %% N w try one more than alowed:
1416	    ct:pal("Info Report expected here (if not disabled) ...",[]),
1417	    try Connect(Host,Port)
1418	    of
1419		_ConnectionRef1 ->
1420		    ssh:stop_daemon(Pid),
1421		    {fail,"Too many connections accepted"}
1422	    catch
1423		error:{badmatch,{error,"Connection closed"}} ->
1424                    ct:log("Step 2 ok: could not set up too many connections. Good.",[]),
1425		    %% Now stop one connection and try to open one more
1426		    ok = ssh:close(hd(Connections)),
1427		    try_to_connect(Connect, Host, Port, Pid)
1428	    end
1429    catch
1430	error:{badmatch,{error,"Connection closed"}} ->
1431	    ssh:stop_daemon(Pid),
1432	    {fail,"Too few connections accepted"}
1433    end.
1434
1435
1436try_to_connect(Connect, Host, Port, Pid) ->
1437    {ok,Tref} = timer:send_after(30000, timeout_no_connection), % give the supervisors some time...
1438    try_to_connect(Connect, Host, Port, Pid, Tref, 1). % will take max 3300 ms after 11 tries
1439
1440try_to_connect(Connect, Host, Port, Pid, Tref, N) ->
1441     try Connect(Host,Port)
1442     of
1443	 _ConnectionRef1 ->
1444	     timer:cancel(Tref),
1445             ct:log("Step 3 ok: could set up one more connection after killing one. Thats good.",[]),
1446	     ssh:stop_daemon(Pid),
1447	     receive % flush.
1448		 timeout_no_connection -> ok
1449	     after 0 -> ok
1450	     end
1451     catch
1452	 error:{badmatch,{error,"Connection closed"}} ->
1453	     %% Could not set up one more connection. Try again until timeout.
1454	     receive
1455		 timeout_no_connection ->
1456		     ssh:stop_daemon(Pid),
1457		     {fail,"Does not decrease # active sessions"}
1458	     after N*50 -> % retry after this time
1459		     try_to_connect(Connect, Host, Port, Pid, Tref, N+1)
1460	     end
1461     end.
1462
1463%%--------------------------------------------------------------------
1464max_sessions_drops_tcp_connects() ->
1465    [{timetrap,{minutes,20}}].
1466
1467max_sessions_drops_tcp_connects(Config) ->
1468    MaxSessions = 20,
1469    UseSessions = 2, % Must be =< MaxSessions
1470    FloodSessions = 1000,
1471    ParallelLogin = true,
1472    NegTimeOut = 8*1000,
1473    HelloTimeOut = 200,
1474
1475    %% Start a test daemon
1476    SystemDir = filename:join(proplists:get_value(priv_dir, Config), system),
1477    UserDir = proplists:get_value(priv_dir, Config),
1478    {Pid, Host0, Port} =
1479        ssh_test_lib:daemon([
1480                             {system_dir, SystemDir},
1481                             {user_dir, UserDir},
1482                             {user_passwords, [{"carni", "meat"}]},
1483                             {parallel_login, ParallelLogin},
1484                             {hello_timeout, HelloTimeOut},
1485                             {negotiation_timeout, NegTimeOut},
1486                             {max_sessions, MaxSessions}
1487                            ]),
1488    Host = ssh_test_lib:mangle_connect_address(Host0),
1489    ct:log("~p:~p ~p Listen ~p:~p for max ~p sessions. Mangled Host = ~p",
1490           [?MODULE,?LINE,Pid,Host0,Port,MaxSessions,Host]),
1491
1492    %% Log in UseSessions connections
1493    SSHconnect = fun(N) ->
1494                         R = ssh:connect(Host, Port,
1495                                         [{silently_accept_hosts, true},
1496                                          {save_accepted_host, false},
1497                                          {user_dir, proplists:get_value(priv_dir,Config)},
1498                                          {user_interaction, false},
1499                                          {user, "carni"},
1500                                          {password, "meat"}
1501                                         ]),
1502                         ct:log("~p:~p ~p: ssh:connect -> ~p", [?MODULE,?LINE,N,R]),
1503                         R
1504                 end,
1505
1506    L1 = oks([SSHconnect(N) || N <- lists:seq(1,UseSessions)]),
1507    case length(L1) of
1508        UseSessions ->
1509            %% As expected
1510            %% Try gen_tcp:connect
1511            [ct:log("~p:~p ~p: gen_tcp:connect -> ~p",
1512                    [?MODULE,?LINE, N, gen_tcp:connect(Host, Port, [])])
1513             || N <- lists:seq(UseSessions+1, MaxSessions)
1514            ],
1515
1516            ct:log("~p:~p Now try ~p gen_tcp:connect to be rejected", [?MODULE,?LINE,FloodSessions]),
1517            [ct:log("~p:~p ~p: gen_tcp:connect -> ~p",
1518                    [?MODULE,?LINE, N, gen_tcp:connect(Host, Port, [])])
1519             || N <- lists:seq(MaxSessions+1, MaxSessions+1+FloodSessions)
1520            ],
1521
1522            ct:log("~p:~p try ~p ssh:connect", [?MODULE,?LINE, MaxSessions - UseSessions]),
1523            try_ssh_connect(MaxSessions - UseSessions, NegTimeOut, SSHconnect);
1524
1525        Len1 ->
1526            {fail, Len1}
1527    end.
1528
1529try_ssh_connect(N, NegTimeOut, F) when N>0 ->
1530    case F(N) of
1531        {ok,_} ->
1532            try_ssh_connect(N-1, NegTimeOut, F);
1533        {error,_} when N==1 ->
1534            try_ssh_connect(N, NegTimeOut, F);
1535        {error,_} ->
1536            timer:sleep(NegTimeOut),
1537            try_ssh_connect(N, NegTimeOut, F)
1538    end;
1539try_ssh_connect(_N, _NegTimeOut, _F) ->
1540    done.
1541
1542
1543oks(L) -> lists:filter(fun({ok,_}) -> true;
1544                          (_) -> false
1545                       end, L).
1546
1547%%--------------------------------------------------------------------
1548save_accepted_host_option(Config) ->
1549    UserDir = proplists:get_value(user_dir, Config),
1550    KnownHosts = filename:join(UserDir, "known_hosts"),
1551    SysDir = proplists:get_value(data_dir, Config),
1552    {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
1553					     {user_dir, UserDir},
1554					     {user_passwords, [{"vego", "morot"}]}
1555                                            ]),
1556    {error,enoent} = file:read_file(KnownHosts),
1557
1558    {ok,_C1} = ssh:connect(Host, Port, [{silently_accept_hosts, true},
1559                                        {save_accepted_host, false},
1560                                        {user, "vego"},
1561                                        {password, "morot"},
1562                                        {user_interaction, false},
1563                                        {user_dir, UserDir}]),
1564    {error,enoent} = file:read_file(KnownHosts),
1565
1566    {ok,_C2} = ssh:connect(Host, Port, [{silently_accept_hosts, true},
1567                                        {user, "vego"},
1568                                        {password, "morot"},
1569                                        {user_interaction, false},
1570                                        {user_dir, UserDir}]),
1571    {ok,_} = file:read_file(KnownHosts),
1572    ssh:stop_daemon(Pid).
1573
1574%%--------------------------------------------------------------------
1575raw_option(_Config) ->
1576    Opts = [{raw,1,2,3,4}],
1577    #{socket_options := Opts} = ssh_options:handle_options(client, Opts),
1578    #{socket_options := Opts} = ssh_options:handle_options(server, Opts).
1579
1580%%--------------------------------------------------------------------
1581config_file(Config) ->
1582    %% First find common algs:
1583    ServerAlgs = ssh_test_lib:default_algorithms(sshd),
1584    OurAlgs = ssh_transport:supported_algorithms(), % Incl disabled but supported
1585    CommonAlgs = ssh_test_lib:intersection(ServerAlgs, OurAlgs),
1586    ct:log("ServerAlgs =~n~p~n~nOurAlgs =~n~p~n~nCommonAlgs =~n~p",[ServerAlgs,OurAlgs,CommonAlgs]),
1587    Nkex = length(proplists:get_value(kex, CommonAlgs, [])),
1588
1589    %% Adjust for very old ssh daemons that only supports ssh-rsa and ssh-dss:
1590    AdjustClient =
1591        case proplists:get_value(public_key,ServerAlgs,[]) -- ['ssh-rsa','ssh-dss'] of
1592            [] ->
1593                %% Old, let the client support them also:
1594                ct:log("Adjust the client's public_key set", []),
1595                [{public_key, ['ssh-rsa','ssh-dss']}];
1596            [_|_] ->
1597                %% Ok, let the client be un-modified:
1598                []
1599        end,
1600
1601    case {ServerAlgs, ssh_test_lib:some_empty(CommonAlgs)} of
1602        {[],_} ->
1603            {skip, "No server algorithms found"};
1604        {_,true} ->
1605            {fail, "Missing common algorithms"};
1606        _ when Nkex<3 ->
1607            {skip, "Not enough number of common kex"};
1608        _ ->
1609            %% Then find three common kex and one common cipher:
1610            [K1a,K1b,K2a|_] = proplists:get_value(kex, CommonAlgs),
1611            [{_,[Ch1|_]}|_] = proplists:get_value(cipher, CommonAlgs),
1612
1613            %% Make config file:
1614            Contents =
1615                [{ssh, [{preferred_algorithms,
1616                         [{cipher, [Ch1]},
1617                          {kex,    [K1a]}
1618                         ] ++ AdjustClient},
1619                        {client_options,
1620                         [{modify_algorithms,
1621                           [{rm,     [{kex, [K1a]}]},
1622                            {append, [{kex, [K1b]}]}
1623                           ]}
1624                         ]}
1625                       ]}
1626                ],
1627            %% write the file:
1628            PrivDir = proplists:get_value(priv_dir, Config),
1629            ConfFile = filename:join(PrivDir,"c2.config"),
1630            {ok,D} = file:open(ConfFile, [write]),
1631            io:format(D, "~p.~n", [Contents]),
1632            file:close(D),
1633            {ok,Cnfs} = file:read_file(ConfFile),
1634            ct:log("c2.config:~n~s", [Cnfs]),
1635
1636            %% Start the slave node with the configuration just made:
1637            {ok,Node} = start_node(random_node_name(?MODULE), ConfFile),
1638
1639            R0 = rpc:call(Node, ssh, default_algorithms, []),
1640            ct:log("R0 = ~p",[R0]),
1641            R0 = ssh:default_algorithms(),
1642
1643            %% Start ssh on the slave. This should apply the ConfFile:
1644            rpc:call(Node, ssh, start, []),
1645
1646            R1 = rpc:call(Node, ssh, default_algorithms, []),
1647            ct:log("R1 = ~p",[R1]),
1648            [{kex,[K1a]},
1649             {public_key,_},
1650             {cipher,[{_,[Ch1]},
1651                      {_,[Ch1]}]} | _] = R1,
1652
1653            %% First connection. The client_options should be applied:
1654            {ok,C1} = rpc:call(Node, ssh, connect, [loopback, ?SSH_DEFAULT_PORT,
1655                                                    [{silently_accept_hosts, true},
1656                                                     {save_accepted_host, false},
1657                                                     {user_interaction, false}
1658                                                    ]]),
1659            ct:log("C1 = ~n~p", [C1]),
1660            {algorithms,As1} = rpc:call(Node, ssh, connection_info, [C1, algorithms]),
1661            K1b = proplists:get_value(kex, As1),
1662            Ch1 = proplists:get_value(encrypt, As1),
1663            Ch1 = proplists:get_value(decrypt, As1),
1664            {options,Os1} = rpc:call(Node, ssh, connection_info, [C1, options]),
1665            ct:log("C1 algorithms:~n~p~n~noptions:~n~p", [As1,Os1]),
1666
1667            %% Second connection, the Options take precedence:
1668            C2_Opts = [{modify_algorithms,[{rm,[{kex,[K1b]}]}, % N.B.
1669                                           {append, [{kex,[K2a]}]}]},
1670                       {silently_accept_hosts, true},
1671                       {save_accepted_host, false},
1672                       {user_interaction, false}
1673                      ],
1674            {ok,C2} = rpc:call(Node, ssh, connect, [loopback, ?SSH_DEFAULT_PORT, C2_Opts]),
1675            {algorithms,As2} = rpc:call(Node, ssh, connection_info, [C2, algorithms]),
1676            K2a = proplists:get_value(kex, As2),
1677            Ch1 = proplists:get_value(encrypt, As2),
1678            Ch1 = proplists:get_value(decrypt, As2),
1679            {options,Os2} = rpc:call(Node, ssh, connection_info, [C2, options]),
1680            ct:log("C2 opts:~n~p~n~nalgorithms:~n~p~n~noptions:~n~p", [C2_Opts,As2,Os2]),
1681
1682            stop_node_nice(Node)
1683    end.
1684
1685%%%----------------------------------------------------------------
1686config_file_modify_algorithms_order(Config) ->
1687    %% First find common algs:
1688    ServerAlgs = ssh_test_lib:default_algorithms(sshd),
1689    OurAlgs = ssh_transport:supported_algorithms(), % Incl disabled but supported
1690    CommonAlgs = ssh_test_lib:intersection(ServerAlgs, OurAlgs),
1691    ct:log("ServerAlgs =~n~p~n~nOurAlgs =~n~p~n~nCommonAlgs =~n~p",[ServerAlgs,OurAlgs,CommonAlgs]),
1692    Nkex = length(proplists:get_value(kex, CommonAlgs, [])),
1693    case {ServerAlgs, ssh_test_lib:some_empty(CommonAlgs)} of
1694        {[],_} ->
1695            {skip, "No server algorithms found"};
1696        {_,true} ->
1697            {fail, "Missing common algorithms"};
1698        _ when Nkex<3 ->
1699            {skip, "Not enough number of common kex"};
1700        _ ->
1701            %% Then find three common kex and one common cipher:
1702            [K1,K2,K3|_] = proplists:get_value(kex, CommonAlgs),
1703            [{_,[Ch1|_]}|_] = proplists:get_value(cipher, CommonAlgs),
1704
1705            %% Make config file:
1706            Contents =
1707                [{ssh, [{preferred_algorithms,
1708                         [{cipher, [Ch1]},
1709                          {kex,    [K1]}
1710                         ]},
1711                        {server_options,
1712                         [{modify_algorithms,
1713                           [{rm,     [{kex, [K1]}]},
1714                            {append, [{kex, [K2]}]}
1715                           ]}
1716                         ]},
1717                        {client_options,
1718                         [{modify_algorithms,
1719                           [{rm,     [{kex, [K1]}]},
1720                            {append, [{kex, [K3]}]}
1721                           ]}
1722                         ]}
1723                       ]}
1724                ],
1725            %% write the file:
1726            PrivDir = proplists:get_value(priv_dir, Config),
1727            ConfFile = filename:join(PrivDir,"c3.config"),
1728            {ok,D} = file:open(ConfFile, [write]),
1729            io:format(D, "~p.~n", [Contents]),
1730            file:close(D),
1731            {ok,Cnfs} = file:read_file(ConfFile),
1732            ct:log("c3.config:~n~s", [Cnfs]),
1733
1734            %% Start the slave node with the configuration just made:
1735            {ok,Node} = start_node(random_node_name(?MODULE), ConfFile),
1736
1737            R0 = rpc:call(Node, ssh, default_algorithms, []),
1738            ct:log("R0 = ~p",[R0]),
1739            R0 = ssh:default_algorithms(),
1740
1741            %% Start ssh on the slave. This should apply the ConfFile:
1742            ok = rpc:call(Node, ssh, start, []),
1743            R1 = rpc:call(Node, ssh, default_algorithms, []),
1744            ct:log("R1 = ~p",[R1]),
1745            [{kex,[K1]} | _] = R1,
1746
1747            %% Start a daemon
1748            {Server, Host, Port} = rpc:call(Node, ssh_test_lib, std_daemon, [Config, []]),
1749            {ok,ServerInfo} = rpc:call(Node, ssh, daemon_info, [Server]),
1750            ct:log("ServerInfo =~n~p", [ServerInfo]),
1751
1752            %% Test that the server_options env key works:
1753            [K2] = proplists:get_value(kex,
1754                   proplists:get_value(preferred_algorithms,
1755                   proplists:get_value(options, ServerInfo))),
1756
1757            {badrpc, {'EXIT', {{badmatch,ExpectedError}, _}}} =
1758                %% No common kex algorithms expected.
1759                rpc:call(Node, ssh_test_lib, std_connect, [Config, Host, Port, []]),
1760            {error,"Key exchange failed"} = ExpectedError,
1761
1762            C = rpc:call(Node, ssh_test_lib, std_connect,
1763                         [Config, Host, Port,
1764                          [{modify_algorithms,[{append,[{kex,[K2]}]}]}]]),
1765            ConnInfo = rpc:call(Node, ssh, connection_info, [C]),
1766            ct:log("ConnInfo =~n~p", [ConnInfo]),
1767            Algs = proplists:get_value(algorithms, ConnInfo),
1768            ct:log("Algs =~n~p", [Algs]),
1769            ConnOptions = proplists:get_value(options, ConnInfo),
1770            ConnPrefAlgs = proplists:get_value(preferred_algorithms, ConnOptions),
1771
1772            %% And now, are all levels appied in right order:
1773            [K3,K2] = proplists:get_value(kex, ConnPrefAlgs),
1774
1775            stop_node_nice(Node)
1776    end.
1777
1778
1779%%--------------------------------------------------------------------
1780%% Internal functions ------------------------------------------------
1781%%--------------------------------------------------------------------
1782
1783start_node(Name, ConfigFile) ->
1784    Pa = filename:dirname(code:which(?MODULE)),
1785    test_server:start_node(Name, slave, [{args,
1786                                          " -pa " ++ Pa ++
1787                                          " -config " ++ ConfigFile}]).
1788
1789stop_node_nice(Node) when is_atom(Node) ->
1790    test_server:stop_node(Node).
1791
1792random_node_name(BaseName) ->
1793    L = integer_to_list(erlang:unique_integer([positive])),
1794    lists:concat([BaseName,"___",L]).
1795
1796%%%----
1797
1798expected_ssh_vsn(Str) ->
1799    try
1800	{ok,L} = application:get_all_key(ssh),
1801	proplists:get_value(vsn,L,"")++"\r\n"
1802    of
1803	Str -> true;
1804	"\r\n" -> true;
1805	_ -> false
1806    catch
1807	_:_ -> true %% ssh not started so we dont't know
1808    end.
1809
1810
1811fake_daemon(_Config) ->
1812    Parent = self(),
1813    %% start the server
1814    Server = spawn(fun() ->
1815			   {ok,Sl} = gen_tcp:listen(0,[{packet,line}]),
1816			   {ok,{Host,Port}} = inet:sockname(Sl),
1817			   ct:log("fake_daemon listening on ~p:~p~n",[Host,Port]),
1818			   Parent ! {sockname,self(),Host,Port},
1819			   Rsa = gen_tcp:accept(Sl),
1820			   ct:log("Server gen_tcp:accept got ~p",[Rsa]),
1821			   {ok,S} = Rsa,
1822			   receive
1823			       {tcp, S, Id} -> Parent ! {id,self(),Id}
1824			   after
1825			       10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
1826			   end
1827		   end),
1828    %% Get listening host and port
1829    receive
1830	{sockname,Server,ServerHost,ServerPort} -> {Server, ServerHost, ServerPort}
1831    after
1832	10000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE])
1833    end.
1834