diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl index 0628154ad56..76b282bbb99 100644 --- a/lib/ssh/src/ssh_connection.erl +++ b/lib/ssh/src/ssh_connection.erl @@ -1126,21 +1126,31 @@ handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, binary_to_list(SigName)}); handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, - request_type = "subsystem", - want_reply = WantReply, - data = Data}, - #connection{channel_cache = Cache} = Connection, server, _SSH) -> + request_type = "subsystem", + want_reply = WantReply, + data = Data}, + #connection{channel_cache = Cache} = Connection, server, _SSH) -> <> = Data, - #channel{remote_id=RemoteId} = Channel = - ssh_client_channel:cache_lookup(Cache, ChannelId), + #channel{remote_id=RemoteId,user=User} = Channel = + ssh_client_channel:cache_lookup(Cache, ChannelId), Reply = - case start_subsystem(SsName, Connection, Channel, - {subsystem, ChannelId, WantReply, binary_to_list(SsName)}) of - {ok, Pid} -> - erlang:monitor(process, Pid), - ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}), - channel_success_msg(RemoteId); - {error,_Error} -> + case User of + undefined -> + case start_subsystem(SsName, Connection, Channel, + {subsystem, ChannelId, WantReply, binary_to_list(SsName)}) of + {ok, Pid} -> + erlang:monitor(process, Pid), + ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}), + channel_success_msg(RemoteId); + {error,_Error} -> + channel_failure_msg(RemoteId) + end; + _ -> + %% If shell, exec or any subsystem is active on the channel, starting another + %% subsystem must fail. See RFC 4254 6.5 + %% In our case 'env' (RFC 4254 6.6) and 'pty-req' (RFC 4254 6.2) also start cli + %% handler, so if any of them are started, trying to start any subsystem + %% will also fail here. channel_failure_msg(RemoteId) end, {[{connection_reply,Reply}], Connection}; diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl index b9fc642e0db..92f881decba 100644 --- a/lib/ssh/test/ssh_connection_SUITE.erl +++ b/lib/ssh/test/ssh_connection_SUITE.erl @@ -108,6 +108,11 @@ start_shell_sock_daemon_exec_multi/1, start_shell_sock_exec_fun/1, start_subsystem_on_closed_channel/1, + start_subsystem_on_channel_with_subsystem/1, + start_subsystem_on_channel_with_pty_req/1, + start_subsystem_on_channel_with_shell/1, + start_subsystem_on_channel_with_exec/1, + start_subsystem_on_channel_with_env/1, stop_listener/1, trap_exit_connect/1, trap_exit_daemon/1, @@ -187,6 +192,11 @@ all() -> stop_listener, no_sensitive_leak, start_subsystem_on_closed_channel, + start_subsystem_on_channel_with_subsystem, + start_subsystem_on_channel_with_pty_req, + start_subsystem_on_channel_with_shell, + start_subsystem_on_channel_with_exec, + start_subsystem_on_channel_with_env, max_channels_option, handler_down_before_open, replace_options_enable_services @@ -1761,23 +1771,29 @@ no_sensitive_leak(Config) -> {_, _, 0} -> ok; {_, _, Nt0} -> ct:fail("Leak in ~p cases!", [Nt0]) end. - -start_subsystem_on_closed_channel(Config) -> + +start_server_and_connect_client(Config) -> + start_server_and_connect_client(Config, []). +start_server_and_connect_client(Config, DaemonOpts) -> PrivDir = proplists:get_value(priv_dir, Config), UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth file:make_dir(UserDir), SysDir = proplists:get_value(data_dir, Config), {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, - {user_dir, UserDir}, - {password, "morot"}, - {subsystems, [{"echo_n", {ssh_echo_server, [4000000]}}]}]), + {user_dir, UserDir}, + {password, "morot"}, + {subsystems, [{"echo_n", {ssh_echo_server, [4000000]}}]}] ++ DaemonOpts), ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, - {user, "foo"}, - {password, "morot"}, - {user_interaction, false}, - {user_dir, UserDir}]), + {user, "foo"}, + {password, "morot"}, + {user_interaction, false}, + {user_dir, UserDir}]), + + {ConnectionRef, Pid}. +start_subsystem_on_closed_channel(Config) -> + {ConnectionRef, Pid} = start_server_and_connect_client(Config), {ok, ChannelId1} = ssh_connection:session_channel(ConnectionRef, infinity), ok = ssh_connection:close(ConnectionRef, ChannelId1), @@ -1798,6 +1814,49 @@ start_subsystem_on_closed_channel(Config) -> ssh:close(ConnectionRef), ssh:stop_daemon(Pid). +start_server_connect_client_and_get_channel(Config) -> + start_server_connect_client_and_get_channel(Config, []). +start_server_connect_client_and_get_channel(Config, DaemonOpts) -> + {ConnectionRef, Pid} = start_server_and_connect_client(Config, DaemonOpts), + {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity), + {ConnectionRef, ChannelId, Pid}. + +start_subsystem_on_channel_with_subsystem(Config) -> + {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config), + success = ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", 5000), + check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). + +start_subsystem_on_channel_with_pty_req(Config) -> + DaemonOpts = [{shell, fun(_U, _P) -> spawn(fun() -> timer:sleep(infinity) end) end}], + {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config, DaemonOpts), + success = ssh_connection:ptty_alloc(ConnectionRef, ChannelId, []), + check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). + +start_subsystem_on_channel_with_shell(Config) -> + DaemonOpts = [{shell, fun(_U, _P) -> spawn(fun() -> timer:sleep(infinity) end) end}], + {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config, DaemonOpts), + ok = ssh_connection:shell(ConnectionRef, ChannelId), + check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). + +start_subsystem_on_channel_with_exec(Config) -> + DaemonOpts = [{exec, fun ssh_exec_echo/1}], + {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config, DaemonOpts), + success = ssh_connection:exec(ConnectionRef, ChannelId, "testing1.\n", 5000), + check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). + +start_subsystem_on_channel_with_env(Config) -> + DaemonOpts = [{shell, fun(_U, _P) -> spawn(fun() -> timer:sleep(infinity) end) end}], + {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config, DaemonOpts), + %% setenv is not implemented, and will return a failure, but cli handler will be started anyway + failure = ssh_connection:setenv(ConnectionRef, ChannelId, "ENV_TEST", "VALUE", infinity), + check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). + +check_subsystem_start_failure(ConnectionRef, ChannelId, Pid) -> + failure = ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", 5000), + + ssh:close(ConnectionRef), + ssh:stop_daemon(Pid). + %%-------------------------------------------------------------------- max_channels_option(Config) when is_list(Config) -> PrivDir = proplists:get_value(priv_dir, Config),