%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2008-2021. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %% -module(ssh_to_openssh_SUITE). -include_lib("common_test/include/ct.hrl"). -include("ssh_test_lib.hrl"). -export([ suite/0, all/0, groups/0, init_per_suite/1, end_per_suite/1, init_per_group/2, end_per_group/2, init_per_testcase/2, end_per_testcase/2 ]). -export([ erlang_server_openssh_client_renegotiate/1, erlang_shell_client_openssh_server/1, exec_direct_with_io_in_sshc/1, exec_with_io_in_sshc/1, tunnel_in_erlclient_erlserver/1, tunnel_in_erlclient_openssh_server/1, tunnel_in_non_erlclient_erlserver/1, tunnel_out_erlclient_erlserver/1, tunnel_out_erlclient_openssh_server/1, tunnel_out_non_erlclient_erlserver/1 ]). -define(REKEY_DATA_TMO, 65000). %%-------------------------------------------------------------------- %% Common Test interface functions ----------------------------------- %%-------------------------------------------------------------------- suite() -> [{timetrap,{seconds,60}}]. all() -> case os:find_executable("ssh") of false -> {skip, "openSSH not installed on host"}; _ -> [{group, erlang_client}, {group, erlang_server} ] end. groups() -> [{erlang_client, [], [tunnel_in_erlclient_erlserver, tunnel_out_erlclient_erlserver, {group, tunnel_distro_server}, erlang_shell_client_openssh_server ]}, {tunnel_distro_server, [], [tunnel_in_erlclient_openssh_server, tunnel_out_erlclient_openssh_server]}, {erlang_server, [], [{group, tunnel_distro_client}, erlang_server_openssh_client_renegotiate, exec_with_io_in_sshc, exec_direct_with_io_in_sshc ] }, {tunnel_distro_client, [], [tunnel_in_non_erlclient_erlserver, tunnel_out_non_erlclient_erlserver]} ]. init_per_suite(Config) -> ?CHECK_CRYPTO( case gen_tcp:connect("localhost", ?SSH_DEFAULT_PORT, []) of {error,econnrefused} -> {skip,"No openssh deamon (econnrefused)"}; _ -> ssh_test_lib:openssh_sanity_check( [{ptty_supported, ssh_test_lib:ptty_supported()} | Config] ) end ). end_per_suite(_Config) -> ok. init_per_group(erlang_server, Config) -> Config; init_per_group(G, Config) when G==tunnel_distro_server ; G==tunnel_distro_client -> case no_forwarding(Config) of true -> {skip, "port forwarding disabled in external ssh"}; false -> Config end; init_per_group(erlang_client, Config) -> CommonAlgs = ssh_test_lib:algo_intersection( ssh:default_algorithms(), ssh_test_lib:default_algorithms(sshd)), [{common_algs,CommonAlgs} | Config]; init_per_group(_, Config) -> Config. end_per_group(_, Config) -> Config. init_per_testcase(erlang_server_openssh_client_renegotiate, Config) -> case os:type() of {unix,_} -> ssh:start(), Config; Type -> {skip, io_lib:format("Unsupported test on ~p",[Type])} end; init_per_testcase(_TestCase, Config) -> ssh:start(), Config. end_per_testcase(_TestCase, _Config) -> ssh:stop(), ok. %%-------------------------------------------------------------------- %% Test Cases -------------------------------------------------------- %%-------------------------------------------------------------------- erlang_shell_client_openssh_server(Config) when is_list(Config) -> process_flag(trap_exit, true), IO = ssh_test_lib:start_io_server(), Prev = lists:usort(supervisor:which_children(sshc_sup)), Shell = ssh_test_lib:start_shell(?SSH_DEFAULT_PORT, IO), IO ! {input, self(), "echo Hej\n"}, case proplists:get_value(ptty_supported, Config) of true -> ct:log("~p:~p ptty supported", [?MODULE,?LINE]), receive_data("Hej", undefined), IO ! {input, self(), "exit\n"}, receive_logout(), receive_normal_exit(Shell), %% Check that the connection is closed: ct:log("Expects ~p", [Prev]), ?wait_match(Prev, lists:usort(supervisor:which_children(sshc_sup))); false -> ct:log("~p:~p ptty unsupported", [?MODULE,?LINE]), receive_exit(Shell, fun({{badmatch,failure}, [{ssh,shell,_,_} | _]}) -> true; (_) -> false end) end. %%-------------------------------------------------------------------- %% Test that the server could redirect stdin and stdout from/to an %% OpensSSH client when handling an exec request exec_with_io_in_sshc(Config) when is_list(Config) -> SystemDir = proplists:get_value(data_dir, Config), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, {failfun, fun ssh_test_lib:failfun/2}]), ct:sleep(500), _PrivDir = proplists:get_value(priv_dir, Config), ExecStr = "\"io:read('% ').\"", Cmd = "echo howdy. | " ++ ssh_test_lib:open_sshc_cmd(Host, Port, [" -o UserKnownHostsFile=", "/dev/null", " -o CheckHostIP=no" " -o StrictHostKeyChecking=no" " -q" " -x" % Disable X forwarding ], ExecStr), ct:log("Cmd = ~p~n",[Cmd]), case os:cmd(Cmd) of "% {ok,howdy}" -> ok; "{ok,howdy}% " -> ok; % Could happen if the client sends the piped % input before receiving the prompt ("% "). Other -> ct:fail("Received ~p",[Other]) end, ssh:stop_daemon(Pid). %%-------------------------------------------------------------------- %% Test that the server could redirect stdin and stdout from/to an %% OpensSSH client when handling an direct exec request exec_direct_with_io_in_sshc(Config) when is_list(Config) -> SystemDir = proplists:get_value(data_dir, Config), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, {failfun, fun ssh_test_lib:failfun/2}, {exec,{direct,fun(Cmnd) -> {ok,X} = io:read(Cmnd), {ok,{X,lists:reverse(atom_to_list(X))}} end}} ]), ct:sleep(500), _PrivDir = proplists:get_value(priv_dir, Config), Cmd = "echo ciao. | " ++ ssh_test_lib:open_sshc_cmd(Host, Port, [" -o UserKnownHostsFile=", "/dev/null", " -o CheckHostIP=no" " -o StrictHostKeyChecking=no" " -q" " -x" % Disable X forwarding ], "'? '"), ct:log("Cmd = ~p~n",[Cmd]), case os:cmd(Cmd) of "? {ciao,\"oaic\"}" -> ok; "'? '{ciao,\"oaic\"}" -> ok; % WSL "{ciao,\"oaic\"}? " -> ok; % Could happen if the client sends the piped % input before receiving the prompt ("? "). Other -> ct:fail("Received ~p",[Other]) end, ssh:stop_daemon(Pid). %%-------------------------------------------------------------------- %% Test that the Erlang/OTP server can renegotiate with openSSH erlang_server_openssh_client_renegotiate(Config) -> _PubKeyAlg = ssh_rsa, SystemDir = proplists:get_value(data_dir, Config), PrivDir = proplists:get_value(priv_dir, Config), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SystemDir}, {failfun, fun ssh_test_lib:failfun/2}]), ct:sleep(500), RenegLimitK = 3, DataFile = filename:join(PrivDir, "renegotiate_openssh_client.data"), Data = lists:duplicate(trunc(1.1*RenegLimitK*1024), $a), ok = file:write_file(DataFile, Data), Cmd = ssh_test_lib:open_sshc_cmd(Host, Port, [" -o UserKnownHostsFile=", "/dev/null", " -o CheckHostIP=no" " -o StrictHostKeyChecking=no" " -q" " -x", " -o RekeyLimit=",integer_to_list(RenegLimitK),"K"]), OpenSsh = ssh_test_lib:open_port({spawn, Cmd++" < "++DataFile}), Expect = fun({data,R}) -> try NonAlphaChars = [C || C<-lists:seq(1,255), not lists:member(C,lists:seq($a,$z)), not lists:member(C,lists:seq($A,$Z)) ], Lines = string:tokens(binary_to_list(R), NonAlphaChars), lists:any(fun(L) -> length(L)>1 andalso lists:prefix(L, Data) end, Lines) catch _:_ -> false end; ({exit_status,E}) when E=/=0 -> ct:log("exit_status ~p",[E]), throw({skip,"exit status"}); (_) -> false end, try ssh_test_lib:rcv_expected(Expect, OpenSsh, ?TIMEOUT) of _ -> %% Unfortunately we can't check that there has been a renegotiation, just trust OpenSSH. ssh:stop_daemon(Pid) catch throw:{skip,R} -> {skip,R} end. %%-------------------------------------------------------------------- tunnel_out_non_erlclient_erlserver(Config) -> SystemDir = proplists:get_value(data_dir, Config), _PrivDir = proplists:get_value(priv_dir, Config), {_Pid, Host, Port} = ssh_test_lib:daemon([{tcpip_tunnel_out, true}, {system_dir, SystemDir}, {failfun, fun ssh_test_lib:failfun/2}]), {ToSock, _ToHost, ToPort} = tunneling_listner(), ListenHost = {127,0,0,1}, ListenPort = 2345, Cmd = ssh_test_lib:open_sshc_cmd(Host, Port, [" -o UserKnownHostsFile=", "/dev/null", " -o CheckHostIP=no" " -o StrictHostKeyChecking=no" " -q" " -x", " -R ",integer_to_list(ListenPort),":127.0.0.1:",integer_to_list(ToPort)]), spawn(fun() -> ct:log(["ssh command:\r\n ",Cmd],[]), R = os:cmd(Cmd), ct:log(["ssh returned:\r\n",R],[]) end), ct:sleep(1000), test_tunneling(ToSock, ListenHost, ListenPort). %%-------------------------------------------------------------------- tunnel_in_non_erlclient_erlserver(Config) -> SystemDir = proplists:get_value(data_dir, Config), _UserDir = proplists:get_value(priv_dir, Config), {_Pid, Host, Port} = ssh_test_lib:daemon([{tcpip_tunnel_in, true}, {system_dir, SystemDir}, {failfun, fun ssh_test_lib:failfun/2}]), {ToSock, _ToHost, ToPort} = tunneling_listner(), ListenHost = {127,0,0,1}, ListenPort = 2345, Cmd = ssh_test_lib:open_sshc_cmd(Host, Port, [" -o UserKnownHostsFile=", "/dev/null", " -o CheckHostIP=no" " -o StrictHostKeyChecking=no" " -q" " -x", " -L ",integer_to_list(ListenPort),":127.0.0.1:",integer_to_list(ToPort)]), spawn(fun() -> ct:log(["ssh command:\r\n ",Cmd],[]), R = os:cmd(Cmd), ct:log(["ssh returned:\r\n",R],[]) end), ct:sleep(1000), test_tunneling(ToSock, ListenHost, ListenPort). %%-------------------------------------------------------------------- tunnel_in_erlclient_erlserver(Config) -> SystemDir = proplists:get_value(data_dir, Config), UserDir = proplists:get_value(priv_dir, Config), {_Pid, Host, Port} = ssh_test_lib:daemon([{tcpip_tunnel_in, true}, {system_dir, SystemDir}, {user_dir, UserDir}, {user_passwords, [{"foo", "bar"}]}, {failfun, fun ssh_test_lib:failfun/2}]), C = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user_dir, UserDir}, {user,"foo"},{password,"bar"}, {user_interaction, false}]), {ToSock, ToHost, ToPort} = tunneling_listner(), ListenHost = {127,0,0,1}, {ok,ListenPort} = ssh:tcpip_tunnel_to_server(C, ListenHost,0, ToHost,ToPort, 2000), test_tunneling(ToSock, ListenHost, ListenPort). %%-------------------------------------------------------------------- tunnel_in_erlclient_openssh_server(_Config) -> C = ssh_test_lib:connect(?SSH_DEFAULT_PORT, []), {ToSock, ToHost, ToPort} = tunneling_listner(), ListenHost = {127,0,0,1}, {ok,ListenPort} = ssh:tcpip_tunnel_to_server(C, ListenHost,0, ToHost,ToPort, 5000), test_tunneling(ToSock, ListenHost, ListenPort). %%-------------------------------------------------------------------- tunnel_out_erlclient_erlserver(Config) -> SystemDir = proplists:get_value(data_dir, Config), UserDir = proplists:get_value(priv_dir, Config), {_Pid, Host, Port} = ssh_test_lib:daemon([{tcpip_tunnel_out, true}, {system_dir, SystemDir}, {user_dir, UserDir}, {user_passwords, [{"foo", "bar"}]}, {failfun, fun ssh_test_lib:failfun/2}]), C = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, {user_dir, UserDir}, {user,"foo"},{password,"bar"}, {user_interaction, false}]), {ToSock, ToHost, ToPort} = tunneling_listner(), ListenHost = {127,0,0,1}, {ok,ListenPort} = ssh:tcpip_tunnel_from_server(C, ListenHost,0, ToHost,ToPort, 5000), test_tunneling(ToSock, ListenHost, ListenPort). %%-------------------------------------------------------------------- tunnel_out_erlclient_openssh_server(_Config) -> C = ssh_test_lib:connect(?SSH_DEFAULT_PORT, []), {ToSock, ToHost, ToPort} = tunneling_listner(), ListenHost = {127,0,0,1}, {ok,ListenPort} = ssh:tcpip_tunnel_from_server(C, ListenHost,0, ToHost,ToPort, 5000), test_tunneling(ToSock, ListenHost, ListenPort). %%-------------------------------------------------------------------- %%% Internal functions ----------------------------------------------- %%-------------------------------------------------------------------- tunneling_listner() -> {ok,LSock} = gen_tcp:listen(0, [{active,false}]), {ok, {LHost,LPort}} = inet:sockname(LSock), {LSock, LHost, LPort}. test_tunneling(ListenSocket, Host, Port) -> {ok,Client1} = gen_tcp:connect(Host, Port, [{active,false}]), {ok,Server1} = gen_tcp:accept(ListenSocket), {ok,Client2} = gen_tcp:connect(Host, Port, [{active,false}]), {ok,Server2} = gen_tcp:accept(ListenSocket), send_rcv("Hi!", Client1, Server1), send_rcv("Happy to see you!", Server1, Client1), send_rcv("Hi, you to!", Client2, Server2), send_rcv("Happy to see you also!", Server2, Client2), close_and_check(Client1, Server1), send_rcv("Still there?", Client2, Server2), send_rcv("Yes!", Server2, Client2), close_and_check(Server2, Client2). close_and_check(OneSide, OtherSide) -> ok = gen_tcp:close(OneSide), ok = chk_closed(OtherSide). chk_closed(Sock) -> chk_closed(Sock, 0). chk_closed(Sock, Timeout) -> case gen_tcp:recv(Sock, 0, Timeout) of {error,closed} -> ok; {error,timeout} -> chk_closed(Sock, 2*max(Timeout,250)); Other -> Other end. send_rcv(Txt, From, To) -> ct:log("Send ~p from ~p to ~p", [Txt, From, To]), ok = gen_tcp:send(From, Txt), ct:log("Recv ~p on ~p", [Txt, To]), {ok,Txt} = gen_tcp:recv(To, 0, 5000), ok. %%-------------------------------------------------------------------- receive_data(Data, Conn) -> receive Info when is_binary(Info) -> Lines = string:tokens(binary_to_list(Info), "\r\n "), case lists:member(Data, Lines) of true -> ct:log("~p:~p Expected result ~p found in lines: ~p~n", [?MODULE,?LINE,Data,Lines]), ok; false -> ct:log("~p:~p Extra info: ~p~n", [?MODULE,?LINE,Info]), receive_data(Data, Conn) end; Other -> ct:log("~p:~p Unexpected: ~p",[?MODULE,?LINE,Other]), receive_data(Data, Conn) after 30000 -> {State, _} = case Conn of undefined -> {'??','??'}; _ -> sys:get_state(Conn) end, ct:log("timeout ~p:~p~nExpect ~p~nState = ~p",[?MODULE,?LINE,Data,State]), ct:fail("timeout ~p:~p",[?MODULE,?LINE]) end. receive_logout() -> receive <<"logout">> -> extra_logout(), receive <<"Connection closed">> -> ok after 30000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE]) end; Info -> ct:log("Extra info when logging out: ~p~n", [Info]), receive_logout() after 30000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE]) end. receive_normal_exit(Shell) -> receive_exit(Shell, fun(Reason) -> Reason == normal end). receive_exit(Shell, F) when is_function(F,1) -> receive {'EXIT', Shell, Reason} -> case F(Reason) of true -> ok; false -> ct:fail({unexpected_exit, Reason}) end; <<"\r\n">> -> receive_normal_exit(Shell); Other -> ct:fail({unexpected_msg, Other}) after 30000 -> ct:fail("timeout ~p:~p",[?MODULE,?LINE]) end. extra_logout() -> receive <<"logout">> -> ok after 500 -> ok end. %%%---------------------------------------------------------------- no_forwarding(Config) -> %%% Check if the ssh of the OS has tunneling enabled _UserDir = proplists:get_value(priv_dir, Config), Cmnd = ["ssh " " -o UserKnownHostsFile=", "/dev/null", " -o CheckHostIP=no" " -o StrictHostKeyChecking=no" " -R 0:localhost:4567 localhost exit"], FailRegExp = "Port forwarding is disabled" "|remote port forwarding failed" "|Bad.*specification" "|Bad forwarding port", {Result,TheText} = try Parent = self(), Pid = spawn(fun() -> Parent ! {self(), os:cmd(Cmnd)} end), receive {Pid, Txt} -> case re:run(Txt, FailRegExp) of {match,_} -> {true,Txt}; _ -> {false,Txt} end after 10000 -> ct:log("*** TIMEOUT ***",[]), {true,""} end catch C:E:S -> ct:log("Exception in no_forwarding():~n~p:~p~n~p~n", [C,E,S]), {true, ""} end, ct:log("---- os:cmd(~p) returned:~n~s~n" "~n" "---- Checking with regexp~n" "~p~n" "~n" "---- The function no_forwarding() returns ~p", [Cmnd,TheText, FailRegExp, Result]), Result.