From eaead6bf3d330fa909ca5feb33fb211d71891c20 Mon Sep 17 00:00:00 2001 From: Daniel Goertzen Date: Wed, 5 Sep 2012 08:25:18 -0500 Subject: ssh: add test suite for testing channel send/receive behavior. --- lib/ssh/test/Makefile | 3 +- lib/ssh/test/ssh_connection_SUITE.erl | 386 ++++++++++++++++++++++++++++++++++ 2 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 lib/ssh/test/ssh_connection_SUITE.erl (limited to 'lib/ssh') diff --git a/lib/ssh/test/Makefile b/lib/ssh/test/Makefile index 25072688ad..4be49da435 100644 --- a/lib/ssh/test/Makefile +++ b/lib/ssh/test/Makefile @@ -36,7 +36,8 @@ MODULES= \ ssh_to_openssh_SUITE \ ssh_sftp_SUITE \ ssh_sftpd_SUITE \ - ssh_sftpd_erlclient_SUITE + ssh_sftpd_erlclient_SUITE \ + ssh_connection_SUITE HRL_FILES_NEEDED_IN_TEST= \ $(ERL_TOP)/lib/ssh/src/ssh.hrl \ diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl new file mode 100644 index 0000000000..5cd892def8 --- /dev/null +++ b/lib/ssh/test/ssh_connection_SUITE.erl @@ -0,0 +1,386 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +-module(ssh_connection_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include("test_server_line.hrl"). + +%% Note: This directive should only be used in test suites. +-compile(export_all). + +-define(SSH_DEFAULT_PORT, 22). +-define(EXEC_TIMEOUT, 10000). + +%% Test server callback functions +%%-------------------------------------------------------------------- +%% Function: init_per_suite(Config) -> Config +%% Config - [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Description: Initialization before the whole suite +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + case catch crypto:start() of + ok -> + case gen_tcp:connect("localhost", 22, []) of + {error,econnrefused} -> + {skip,"No openssh deamon"}; + _ -> + Config + end; + _Else -> + {skip,"Could not start crypto!"} + end. + +%%-------------------------------------------------------------------- +%% Function: end_per_suite(Config) -> _ +%% Config - [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Description: Cleanup after the whole suite +%%-------------------------------------------------------------------- +end_per_suite(_Config) -> + crypto:stop(), + ok. + +%%-------------------------------------------------------------------- +%% Function: init_per_testcase(TestCase, Config) -> Config +%% Case - atom() +%% Name of the test case that is about to be run. +%% Config - [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% +%% Description: Initialization before each test case +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%% Description: Initialization before each test case +%%-------------------------------------------------------------------- +init_per_testcase(_TestCase, Config) -> + ssh:start(), + Config. + +%%-------------------------------------------------------------------- +%% Function: end_per_testcase(TestCase, Config) -> _ +%% Case - atom() +%% Name of the test case that is about to be run. +%% Config - [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Description: Cleanup after each test case +%%-------------------------------------------------------------------- +end_per_testcase(_TestCase, _Config) -> + ssh:stop(), + ok. + +%%-------------------------------------------------------------------- +%% Function: all(Clause) -> TestCases +%% Clause - atom() - suite | doc +%% TestCases - [Case] +%% Case - atom() +%% Name of a test case. +%% Description: Returns a list of all test cases in this test suite +%%-------------------------------------------------------------------- +all() -> + case os:find_executable("ssh") of + false -> + {skip, "openSSH not installed on host"}; + _ -> + [{group, erlang_client} + ] + end. + +groups() -> + [{erlang_client, [], [ + simple_exec, + small_cat, + big_cat, + send_after_exit, + interrupted_send + ]}]. + +init_per_group(erlang_server, Config) -> + DataDir = ?config(data_dir, Config), + UserDir = ?config(priv_dir, Config), + ssh_test_lib:setup_dsa_known_host(DataDir, UserDir), + Config; +init_per_group(_, Config) -> + Config. + +end_per_group(erlang_server, Config) -> + UserDir = ?config(priv_dir, Config), + ssh_test_lib:clean_dsa(UserDir), + Config; +end_per_group(_, Config) -> + Config. + +%% TEST cases starts here. +%-------------------------------------------------------------------- +simple_exec(doc) -> + ["Simple openssh connectivity test for ssh_connection:exec"]; + +simple_exec(suite) -> + []; + +simple_exec(Config) when is_list(Config) -> + ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, + {user_interaction, false}]), + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "echo testing", infinity), + + %% receive response to input + receive + {ssh_cm, ConnectionRef, {data, ChannelId0, 0, <<"testing\n">>}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + + %% receive close messages + receive + {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end. + + +%-------------------------------------------------------------------- +small_cat(doc) -> + ["Use 'cat' to echo small data block back to us."]; + +small_cat(suite) -> + []; + +small_cat(Config) when is_list(Config) -> + ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, + {user_interaction, false}]), + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "cat", infinity), + + Data = <<"I like spaghetti squash">>, + ok = ssh_connection:send(ConnectionRef, ChannelId0, Data), + ok = ssh_connection:send_eof(ConnectionRef, ChannelId0), + + %% receive response to input + receive + {ssh_cm, ConnectionRef, {data, ChannelId0, 0, Data}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + + %% receive close messages + receive + {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end. + +%-------------------------------------------------------------------- +big_cat(doc) -> + ["Use 'cat' to echo large data block back to us."]; + +big_cat(suite) -> + []; + +big_cat(Config) when is_list(Config) -> + ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, + {user_interaction, false}]), + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "cat", infinity), + + %% build 10MB binary + Data = << <> || X <- lists:seq(1,2500000)>>, + + %% pre-adjust receive window so the other end doesn't block + ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(Data)), + + test_server:format("sending ~p byte binary~n",[size(Data)]), + ok = ssh_connection:send(ConnectionRef, ChannelId0, Data, 10000), + %timer:sleep(3000), + ok = ssh_connection:send_eof(ConnectionRef, ChannelId0), + + %% collect echoed data until eof + case big_cat_rx(ConnectionRef, ChannelId0) of + {ok, Data} -> ok; + {ok, Other} -> + case size(Data) =:= size(Other) of + true -> + test_server:format("received and sent data are same size but do not match~n",[]); + false -> + test_server:format("sent ~p but only received ~p~n",[size(Data), size(Other)]) + end, + test_server:fail(receive_data_mismatch); + Else -> + test_server:fail(Else) + end, + + %% receive close messages (eof already consumed) + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end. + +big_cat_rx(ConnectionRef, ChannelId) -> + big_cat_rx(ConnectionRef, ChannelId, []). + +big_cat_rx(ConnectionRef, ChannelId, Acc) -> + receive + {ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} -> + %% ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)), % window was pre-adjusted, don't adjust again here + big_cat_rx(ConnectionRef, ChannelId, [Data | Acc]); + {ssh_cm, ConnectionRef, {eof, ChannelId}} -> + {ok, iolist_to_binary(lists:reverse(Acc))} + after ?EXEC_TIMEOUT -> timeout + end. + +%-------------------------------------------------------------------- +send_after_exit(doc) -> + ["Send channel data after the channel has been closed."]; + +send_after_exit(suite) -> + []; + +send_after_exit(Config) when is_list(Config) -> + ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, + {user_interaction, false}]), + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + + %% Shell command "false" will exit immediately + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "false", infinity), + + timer:sleep(2000), %% Allow incoming eof/close/exit_status ssh messages to be processed + + Data = <<"I like spaghetti squash">>, + case ssh_connection:send(ConnectionRef, ChannelId0, Data, 2000) of + {error, closed} -> ok; + ok -> test_server:fail({expected,{error,closed}}); + {error, timeout} -> test_server:fail({expected,{error,closed}}); + Else -> test_server:fail(Else) + end, + + %% receive close messages + receive + {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, _}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end. + +%-------------------------------------------------------------------- +interrupted_send(doc) -> + ["Use 'head' to cause a channel exit partway through a large send."]; + +interrupted_send(suite) -> + []; + +interrupted_send(Config) when is_list(Config) -> + ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, + {user_interaction, false}]), + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "head -c 4000000", infinity), + + %% build 10MB binary + Data = << <> || X <- lists:seq(1,2500000)>>, + + %% expect remote end to send us 4MB back + <> = Data, + + %% pre-adjust receive window so the other end doesn't block + ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(Data)), + + test_server:format("sending ~p byte binary~n",[size(Data)]), + + case ssh_connection:send(ConnectionRef, ChannelId0, Data, 10000) of + {error, closed} -> ok; + ok -> test_server:fail({expected,{error,closed}}); + {error, timeout} -> test_server:fail({expected,{error,closed}}); + SendElse -> test_server:fail(SendElse) + end, + + case ssh_connection:send_eof(ConnectionRef, ChannelId0) of + {error, closed} -> ok; + ok -> test_server:fail({expected,{error,closed}}); + EofElse -> test_server:fail(EofElse) + end, + + %% collect echoed data until eof + case interrupted_send_rx(ConnectionRef, ChannelId0) of + {ok, ExpectedData} -> ok; + {ok, Other} -> + case size(ExpectedData) =:= size(Other) of + true -> + test_server:format("received expected number of bytes, but bytes do not match~n",[]); + false -> + test_server:format("expected ~p but only received ~p~n",[size(ExpectedData), size(Other)]) + end, + test_server:fail(receive_data_mismatch); + RxElse -> + test_server:fail(RxElse) + end, + + %% receive close messages (eof already consumed) + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end. + +interrupted_send_rx(ConnectionRef, ChannelId) -> + interrupted_send_rx(ConnectionRef, ChannelId, []). + +interrupted_send_rx(ConnectionRef, ChannelId, Acc) -> + receive + {ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} -> + %% ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)), % window was pre-adjusted, don't adjust again here + interrupted_send_rx(ConnectionRef, ChannelId, [Data | Acc]); + {ssh_cm, ConnectionRef, {eof, ChannelId}} -> + {ok, iolist_to_binary(lists:reverse(Acc))} + after ?EXEC_TIMEOUT -> timeout + end. -- cgit v1.2.3 From 79d51c19399a5666eea74118d36812ade5a5b757 Mon Sep 17 00:00:00 2001 From: Daniel Goertzen Date: Wed, 5 Sep 2012 13:30:11 -0500 Subject: ssh: Ensure that all data is sent before ssh:send returns. --- lib/ssh/src/ssh_connection.erl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'lib/ssh') diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl index e3b8ebfb79..33c8e2aca7 100644 --- a/lib/ssh/src/ssh_connection.erl +++ b/lib/ssh/src/ssh_connection.erl @@ -319,7 +319,7 @@ channel_data(ChannelId, DataType, Data, case ssh_channel:cache_lookup(Cache, ChannelId) of #channel{remote_id = Id} = Channel0 -> - {SendList, Channel} = update_send_window(Channel0, DataType, + {SendList, Channel} = update_send_window(Channel0#channel{flow_control = From}, DataType, Data, Connection), Replies = lists:map(fun({SendDataType, SendData}) -> @@ -329,7 +329,7 @@ channel_data(ChannelId, DataType, Data, SendData)} end, SendList), FlowCtrlMsgs = flow_control(Replies, - Channel#channel{flow_control = From}, + Channel, Cache), {{replies, Replies ++ FlowCtrlMsgs}, Connection}; undefined -> @@ -1126,13 +1126,13 @@ flow_control(Channel, Cache) -> flow_control([], Channel, Cache) -> ssh_channel:cache_update(Cache, Channel), []; -flow_control([_|_], #channel{flow_control = From} = Channel, Cache) -> - case From of - undefined -> - []; - _ -> - [{flow_control, Cache, Channel, From, ok}] - end. + +flow_control([_|_], #channel{flow_control = From, + send_buf = []} = Channel, Cache) when From =/= undefined -> + [{flow_control, Cache, Channel, From, ok}]; +flow_control(_,_,_) -> + []. + encode_pty_opts(Opts) -> Bin = list_to_binary(encode_pty_opts2(Opts)), -- cgit v1.2.3 From 0fa25d095f7760efeb17585f961f9092e6ec1fc9 Mon Sep 17 00:00:00 2001 From: Daniel Goertzen Date: Thu, 6 Sep 2012 10:39:30 -0500 Subject: ssh: ssh_connection:channel_data() and send_eof() now return {error, closed} for closed or invalid channels. ssh_connection:handle_msg(#ssh_msg_channel_close...) will now cause any send() that was in progress to immediately return {error,closed}. --- lib/ssh/doc/src/ssh_connection.xml | 4 ++-- lib/ssh/src/ssh_connection.erl | 31 +++++++++++++++++++++---------- lib/ssh/src/ssh_connection_manager.erl | 28 +++++++++++++++------------- 3 files changed, 38 insertions(+), 25 deletions(-) (limited to 'lib/ssh') diff --git a/lib/ssh/doc/src/ssh_connection.xml b/lib/ssh/doc/src/ssh_connection.xml index 9942306b93..a9ae13d556 100644 --- a/lib/ssh/doc/src/ssh_connection.xml +++ b/lib/ssh/doc/src/ssh_connection.xml @@ -196,7 +196,7 @@ send(ConnectionRef, ChannelId, Data, Timeout) -> send(ConnectionRef, ChannelId, Type, Data) -> send(ConnectionRef, ChannelId, Type, Data, TimeOut) -> - ok | {error, timeout} + ok | {error, timeout} | {error, closed} Sends channel data ConnectionRef = ssh_connection_ref() @@ -212,7 +212,7 @@ - send_eof(ConnectionRef, ChannelId) -> ok + send_eof(ConnectionRef, ChannelId) -> ok | {error, closed} Sends eof on the channel ChannelId. ConnectionRef = ssh_connection_ref() diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl index 33c8e2aca7..5372d48986 100644 --- a/lib/ssh/src/ssh_connection.erl +++ b/lib/ssh/src/ssh_connection.erl @@ -318,7 +318,7 @@ channel_data(ChannelId, DataType, Data, From) -> case ssh_channel:cache_lookup(Cache, ChannelId) of - #channel{remote_id = Id} = Channel0 -> + #channel{remote_id = Id, sent_close = false} = Channel0 -> {SendList, Channel} = update_send_window(Channel0#channel{flow_control = From}, DataType, Data, Connection), Replies = @@ -332,7 +332,8 @@ channel_data(ChannelId, DataType, Data, Channel, Cache), {{replies, Replies ++ FlowCtrlMsgs}, Connection}; - undefined -> + _ -> + gen_server:reply(From, {error, closed}), {noreply, Connection} end. @@ -386,20 +387,30 @@ handle_msg(#ssh_msg_channel_close{recipient_channel = ChannelId}, ConnectionPid, _) -> case ssh_channel:cache_lookup(Cache, ChannelId) of - #channel{sent_close = Closed, remote_id = RemoteId} = Channel -> + #channel{sent_close = Closed, remote_id = RemoteId, flow_control = FlowControl} = Channel -> ssh_channel:cache_delete(Cache, ChannelId), {CloseMsg, Connection} = reply_msg(Channel, Connection0, {closed, ChannelId}), + + ConnReplyMsgs = case Closed of - true -> - {{replies, [CloseMsg]}, Connection}; + true -> []; false -> RemoteCloseMsg = channel_close_msg(RemoteId), - {{replies, - [{connection_reply, - ConnectionPid, RemoteCloseMsg}, - CloseMsg]}, Connection} - end; + [{connection_reply, ConnectionPid, RemoteCloseMsg}] + end, + + %% if there was a send() in progress, make it fail + SendReplyMsgs = + case FlowControl of + undefined -> []; + From -> + [{flow_control, From, {error, closed}}] + end, + + Replies = ConnReplyMsgs ++ [CloseMsg] ++ SendReplyMsgs, + {{replies, Replies}, Connection}; + undefined -> {{replies, []}, Connection0} end; diff --git a/lib/ssh/src/ssh_connection_manager.erl b/lib/ssh/src/ssh_connection_manager.erl index e53cd4f4f7..af521b77e4 100644 --- a/lib/ssh/src/ssh_connection_manager.erl +++ b/lib/ssh/src/ssh_connection_manager.erl @@ -163,7 +163,7 @@ send(ConnectionManager, ChannelId, Type, Data, Timeout) -> call(ConnectionManager, {data, ChannelId, Type, Data}, Timeout). send_eof(ConnectionManager, ChannelId) -> - cast(ConnectionManager, {eof, ChannelId}). + call(ConnectionManager, {eof, ChannelId}). %%==================================================================== %% gen_server callbacks @@ -295,6 +295,18 @@ handle_call({data, ChannelId, Type, Data}, From, channel_data(ChannelId, Type, Data, Connection0, ConnectionPid, From, State); +handle_call({eof, ChannelId}, _From, + #state{connection = Pid, connection_state = + #connection{channel_cache = Cache}} = State) -> + case ssh_channel:cache_lookup(Cache, ChannelId) of + #channel{remote_id = Id, sent_close = false} -> + send_msg({connection_reply, Pid, + ssh_connection:channel_eof_msg(Id)}), + {reply, ok, State}; + _ -> + {reply, {error,closed}, State} + end; + handle_call({connection_info, Options}, From, #state{connection = Connection} = State) -> ssh_connection_handler:connection_info(Connection, From, Options), @@ -453,18 +465,6 @@ handle_cast({adjust_window, ChannelId, Bytes}, end, {noreply, State}; -handle_cast({eof, ChannelId}, - #state{connection = Pid, connection_state = - #connection{channel_cache = Cache}} = State) -> - case ssh_channel:cache_lookup(Cache, ChannelId) of - #channel{remote_id = Id} -> - send_msg({connection_reply, Pid, - ssh_connection:channel_eof_msg(Id)}), - {noreply, State}; - undefined -> - {noreply, State} - end; - handle_cast({success, ChannelId}, #state{connection = Pid} = State) -> Msg = ssh_connection:channel_success_msg(ChannelId), send_msg({connection_reply, Pid, Msg}), @@ -614,6 +614,8 @@ do_send_msg({connection_reply, Pid, Data}) -> ssh_connection_handler:send(Pid, Msg); do_send_msg({flow_control, Cache, Channel, From, Msg}) -> ssh_channel:cache_update(Cache, Channel#channel{flow_control = undefined}), + gen_server:reply(From, Msg); +do_send_msg({flow_control, From, Msg}) -> gen_server:reply(From, Msg). handle_request(ChannelPid, ChannelId, Type, Data, WantReply, From, -- cgit v1.2.3 From 9e7e31c9011e1a63957b48b794b0c72669c081ae Mon Sep 17 00:00:00 2001 From: Ingela Anderton Andin Date: Wed, 3 Oct 2012 17:39:53 +0200 Subject: ssh: Make test case platform independant The test case interrupted_send did not work on all platforms. Also ct-ify the test suite --- lib/ssh/src/ssh_connection.erl | 14 +- lib/ssh/test/Makefile | 3 +- lib/ssh/test/ssh_connection_SUITE.erl | 504 +++++++++------------ .../ssh_connection_SUITE_data/ssh_host_rsa_key | 15 + lib/ssh/test/ssh_echo_server.erl | 71 +++ 5 files changed, 310 insertions(+), 297 deletions(-) create mode 100644 lib/ssh/test/ssh_connection_SUITE_data/ssh_host_rsa_key create mode 100644 lib/ssh/test/ssh_echo_server.erl (limited to 'lib/ssh') diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl index 5372d48986..240d7f70d1 100644 --- a/lib/ssh/src/ssh_connection.erl +++ b/lib/ssh/src/ssh_connection.erl @@ -318,22 +318,22 @@ channel_data(ChannelId, DataType, Data, From) -> case ssh_channel:cache_lookup(Cache, ChannelId) of - #channel{remote_id = Id, sent_close = false} = Channel0 -> + #channel{remote_id = Id, sent_close = false} = Channel0 -> {SendList, Channel} = update_send_window(Channel0#channel{flow_control = From}, DataType, Data, Connection), Replies = lists:map(fun({SendDataType, SendData}) -> - {connection_reply, ConnectionPid, - channel_data_msg(Id, - SendDataType, - SendData)} + {connection_reply, ConnectionPid, + channel_data_msg(Id, + SendDataType, + SendData)} end, SendList), FlowCtrlMsgs = flow_control(Replies, Channel, Cache), {{replies, Replies ++ FlowCtrlMsgs}, Connection}; - _ -> - gen_server:reply(From, {error, closed}), + _ -> + gen_server:reply(From, {error, closed}), {noreply, Connection} end. diff --git a/lib/ssh/test/Makefile b/lib/ssh/test/Makefile index 4be49da435..f5db31baee 100644 --- a/lib/ssh/test/Makefile +++ b/lib/ssh/test/Makefile @@ -37,7 +37,8 @@ MODULES= \ ssh_sftp_SUITE \ ssh_sftpd_SUITE \ ssh_sftpd_erlclient_SUITE \ - ssh_connection_SUITE + ssh_connection_SUITE \ + ssh_echo_server HRL_FILES_NEEDED_IN_TEST= \ $(ERL_TOP)/lib/ssh/src/ssh.hrl \ diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl index 5cd892def8..43a899f974 100644 --- a/lib/ssh/test/ssh_connection_SUITE.erl +++ b/lib/ssh/test/ssh_connection_SUITE.erl @@ -21,125 +21,69 @@ -module(ssh_connection_SUITE). -include_lib("common_test/include/ct.hrl"). --include("test_server_line.hrl"). -%% Note: This directive should only be used in test suites. -compile(export_all). -define(SSH_DEFAULT_PORT, 22). -define(EXEC_TIMEOUT, 10000). -%% Test server callback functions %%-------------------------------------------------------------------- -%% Function: init_per_suite(Config) -> Config -%% Config - [tuple()] -%% A list of key/value pairs, holding the test case configuration. -%% Description: Initialization before the whole suite -%% -%% Note: This function is free to add any key/value pairs to the Config -%% variable, but should NOT alter/remove any existing entries. +suite() -> + [{ct_hooks,[ts_install_cth]}]. + +all() -> + [ + {group, erlang_client}, + interrupted_send + ]. +groups() -> + [{erlang_client, [], [simple_exec, + small_cat, + big_cat, + send_after_exit + ]}]. + %%-------------------------------------------------------------------- + init_per_suite(Config) -> case catch crypto:start() of ok -> - case gen_tcp:connect("localhost", 22, []) of - {error,econnrefused} -> - {skip,"No openssh deamon"}; - _ -> - Config - end; + Config; _Else -> - {skip,"Could not start crypto!"} + {skip, "Crypto could not be started!"} end. -%%-------------------------------------------------------------------- -%% Function: end_per_suite(Config) -> _ -%% Config - [tuple()] -%% A list of key/value pairs, holding the test case configuration. -%% Description: Cleanup after the whole suite -%%-------------------------------------------------------------------- end_per_suite(_Config) -> crypto:stop(), ok. - %%-------------------------------------------------------------------- -%% Function: init_per_testcase(TestCase, Config) -> Config -%% Case - atom() -%% Name of the test case that is about to be run. -%% Config - [tuple()] -%% A list of key/value pairs, holding the test case configuration. -%% -%% Description: Initialization before each test case -%% -%% Note: This function is free to add any key/value pairs to the Config -%% variable, but should NOT alter/remove any existing entries. -%% Description: Initialization before each test case +init_per_group(erlang_client, Config) -> + case gen_tcp:connect("localhost", 22, []) of + {error,econnrefused} -> + {skip,"No openssh deamon"}; + _ -> + Config + end; +init_per_group(_, Config) -> + Config. + +end_per_group(_, Config) -> + Config. + %%-------------------------------------------------------------------- init_per_testcase(_TestCase, Config) -> ssh:start(), Config. -%%-------------------------------------------------------------------- -%% Function: end_per_testcase(TestCase, Config) -> _ -%% Case - atom() -%% Name of the test case that is about to be run. -%% Config - [tuple()] -%% A list of key/value pairs, holding the test case configuration. -%% Description: Cleanup after each test case -%%-------------------------------------------------------------------- -end_per_testcase(_TestCase, _Config) -> +end_per_testcase(_Config) -> ssh:stop(), ok. +%%% TEST cases starts here. %%-------------------------------------------------------------------- -%% Function: all(Clause) -> TestCases -%% Clause - atom() - suite | doc -%% TestCases - [Case] -%% Case - atom() -%% Name of a test case. -%% Description: Returns a list of all test cases in this test suite -%%-------------------------------------------------------------------- -all() -> - case os:find_executable("ssh") of - false -> - {skip, "openSSH not installed on host"}; - _ -> - [{group, erlang_client} - ] - end. - -groups() -> - [{erlang_client, [], [ - simple_exec, - small_cat, - big_cat, - send_after_exit, - interrupted_send - ]}]. - -init_per_group(erlang_server, Config) -> - DataDir = ?config(data_dir, Config), - UserDir = ?config(priv_dir, Config), - ssh_test_lib:setup_dsa_known_host(DataDir, UserDir), - Config; -init_per_group(_, Config) -> - Config. - -end_per_group(erlang_server, Config) -> - UserDir = ?config(priv_dir, Config), - ssh_test_lib:clean_dsa(UserDir), - Config; -end_per_group(_, Config) -> - Config. - -%% TEST cases starts here. -%-------------------------------------------------------------------- simple_exec(doc) -> ["Simple openssh connectivity test for ssh_connection:exec"]; -simple_exec(suite) -> - []; - simple_exec(Config) when is_list(Config) -> ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, {user_interaction, false}]), @@ -147,34 +91,30 @@ simple_exec(Config) when is_list(Config) -> success = ssh_connection:exec(ConnectionRef, ChannelId0, "echo testing", infinity), - %% receive response to input - receive - {ssh_cm, ConnectionRef, {data, ChannelId0, 0, <<"testing\n">>}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end, - - %% receive close messages - receive - {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end, - receive - {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end, - receive - {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end. - - -%-------------------------------------------------------------------- + %% receive response to input + receive + {ssh_cm, ConnectionRef, {data, ChannelId0, 0, <<"testing\n">>}} -> + ok + end, + + %% receive close messages + receive + {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> + ok + end, + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> + ok + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> + ok + end. + +%%-------------------------------------------------------------------- small_cat(doc) -> ["Use 'cat' to echo small data block back to us."]; -small_cat(suite) -> - []; - small_cat(Config) when is_list(Config) -> ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, {user_interaction, false}]), @@ -182,37 +122,34 @@ small_cat(Config) when is_list(Config) -> success = ssh_connection:exec(ConnectionRef, ChannelId0, "cat", infinity), - Data = <<"I like spaghetti squash">>, - ok = ssh_connection:send(ConnectionRef, ChannelId0, Data), - ok = ssh_connection:send_eof(ConnectionRef, ChannelId0), - - %% receive response to input - receive - {ssh_cm, ConnectionRef, {data, ChannelId0, 0, Data}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end, - - %% receive close messages - receive - {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end, - receive - {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end, - receive - {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end. - -%-------------------------------------------------------------------- + Data = <<"I like spaghetti squash">>, + ok = ssh_connection:send(ConnectionRef, ChannelId0, Data), + ok = ssh_connection:send_eof(ConnectionRef, ChannelId0), + + %% receive response to input + receive + {ssh_cm, ConnectionRef, {data, ChannelId0, 0, Data}} -> + ok + end, + + %% receive close messages + receive + {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> + ok + end, + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> + ok + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> + ok + end. + +%%-------------------------------------------------------------------- big_cat(doc) -> ["Use 'cat' to echo large data block back to us."]; -big_cat(suite) -> - []; - big_cat(Config) when is_list(Config) -> ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, {user_interaction, false}]), @@ -220,167 +157,156 @@ big_cat(Config) when is_list(Config) -> success = ssh_connection:exec(ConnectionRef, ChannelId0, "cat", infinity), - %% build 10MB binary - Data = << <> || X <- lists:seq(1,2500000)>>, - - %% pre-adjust receive window so the other end doesn't block - ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(Data)), - - test_server:format("sending ~p byte binary~n",[size(Data)]), - ok = ssh_connection:send(ConnectionRef, ChannelId0, Data, 10000), - %timer:sleep(3000), - ok = ssh_connection:send_eof(ConnectionRef, ChannelId0), - - %% collect echoed data until eof - case big_cat_rx(ConnectionRef, ChannelId0) of - {ok, Data} -> ok; - {ok, Other} -> - case size(Data) =:= size(Other) of - true -> - test_server:format("received and sent data are same size but do not match~n",[]); - false -> - test_server:format("sent ~p but only received ~p~n",[size(Data), size(Other)]) - end, - test_server:fail(receive_data_mismatch); - Else -> - test_server:fail(Else) - end, - - %% receive close messages (eof already consumed) - receive - {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end, - receive - {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end. + %% build 10MB binary + Data = << <> || X <- lists:seq(1,2500000)>>, + + %% pre-adjust receive window so the other end doesn't block + ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(Data)), + + test_server:format("sending ~p byte binary~n",[size(Data)]), + ok = ssh_connection:send(ConnectionRef, ChannelId0, Data, 10000), + ok = ssh_connection:send_eof(ConnectionRef, ChannelId0), + + %% collect echoed data until eof + case big_cat_rx(ConnectionRef, ChannelId0) of + {ok, Data} -> + ok; + {ok, Other} -> + case size(Data) =:= size(Other) of + true -> + test_server:format("received and sent data are same" + "size but do not match~n",[]); + false -> + test_server:format("sent ~p but only received ~p~n", + [size(Data), size(Other)]) + end, + ct:fail(receive_data_mismatch); + Else -> + ct:fail(Else) + end, + + %% receive close messages (eof already consumed) + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> + ok + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> + ok + end. big_cat_rx(ConnectionRef, ChannelId) -> - big_cat_rx(ConnectionRef, ChannelId, []). + big_cat_rx(ConnectionRef, ChannelId, []). big_cat_rx(ConnectionRef, ChannelId, Acc) -> - receive - {ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} -> - %% ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)), % window was pre-adjusted, don't adjust again here - big_cat_rx(ConnectionRef, ChannelId, [Data | Acc]); - {ssh_cm, ConnectionRef, {eof, ChannelId}} -> - {ok, iolist_to_binary(lists:reverse(Acc))} - after ?EXEC_TIMEOUT -> timeout - end. - -%-------------------------------------------------------------------- + receive + {ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} -> + %% ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)), + %% window was pre-adjusted, don't adjust again here + big_cat_rx(ConnectionRef, ChannelId, [Data | Acc]); + {ssh_cm, ConnectionRef, {eof, ChannelId}} -> + {ok, iolist_to_binary(lists:reverse(Acc))} + after ?EXEC_TIMEOUT -> + timeout + end. + +%%-------------------------------------------------------------------- send_after_exit(doc) -> ["Send channel data after the channel has been closed."]; -send_after_exit(suite) -> - []; - send_after_exit(Config) when is_list(Config) -> ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, {user_interaction, false}]), {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), - %% Shell command "false" will exit immediately - success = ssh_connection:exec(ConnectionRef, ChannelId0, + %% Shell command "false" will exit immediately + success = ssh_connection:exec(ConnectionRef, ChannelId0, "false", infinity), - timer:sleep(2000), %% Allow incoming eof/close/exit_status ssh messages to be processed - - Data = <<"I like spaghetti squash">>, - case ssh_connection:send(ConnectionRef, ChannelId0, Data, 2000) of - {error, closed} -> ok; - ok -> test_server:fail({expected,{error,closed}}); - {error, timeout} -> test_server:fail({expected,{error,closed}}); - Else -> test_server:fail(Else) - end, - - %% receive close messages - receive - {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end, - receive - {ssh_cm, ConnectionRef, {exit_status, ChannelId0, _}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end, - receive - {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end. - -%-------------------------------------------------------------------- -interrupted_send(doc) -> - ["Use 'head' to cause a channel exit partway through a large send."]; + timer:sleep(2000), %% Allow incoming eof/close/exit_status ssh messages to be processed -interrupted_send(suite) -> - []; + Data = <<"I like spaghetti squash">>, + case ssh_connection:send(ConnectionRef, ChannelId0, Data, 2000) of + {error, closed} -> ok; + ok -> + ct:fail({expected,{error,closed}}); + {error, timeout} -> + ct:fail({expected,{error,closed}}); + Else -> + ct:fail(Else) + end, + + %% receive close messages + receive + {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> + ok + end, + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, _}} -> + ok + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> + ok + end. +%%-------------------------------------------------------------------- +interrupted_send(doc) -> + ["Use a subsystem that echos n char and then sends eof to cause a channel exit partway through a large send."]; interrupted_send(Config) when is_list(Config) -> - ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, - {user_interaction, false}]), - {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), - success = ssh_connection:exec(ConnectionRef, ChannelId0, - "head -c 4000000", infinity), - - %% build 10MB binary - Data = << <> || X <- lists:seq(1,2500000)>>, - - %% expect remote end to send us 4MB back - <> = Data, - - %% pre-adjust receive window so the other end doesn't block - ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(Data)), - - test_server:format("sending ~p byte binary~n",[size(Data)]), - - case ssh_connection:send(ConnectionRef, ChannelId0, Data, 10000) of - {error, closed} -> ok; - ok -> test_server:fail({expected,{error,closed}}); - {error, timeout} -> test_server:fail({expected,{error,closed}}); - SendElse -> test_server:fail(SendElse) - end, - - case ssh_connection:send_eof(ConnectionRef, ChannelId0) of - {error, closed} -> ok; - ok -> test_server:fail({expected,{error,closed}}); - EofElse -> test_server:fail(EofElse) - end, - - %% collect echoed data until eof - case interrupted_send_rx(ConnectionRef, ChannelId0) of - {ok, ExpectedData} -> ok; - {ok, Other} -> - case size(ExpectedData) =:= size(Other) of - true -> - test_server:format("received expected number of bytes, but bytes do not match~n",[]); - false -> - test_server:format("expected ~p but only received ~p~n",[size(ExpectedData), size(Other)]) - end, - test_server:fail(receive_data_mismatch); - RxElse -> - test_server:fail(RxElse) - end, - - %% receive close messages (eof already consumed) - receive - {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end, - receive - {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok - after ?EXEC_TIMEOUT -> test_server:fail() - end. - -interrupted_send_rx(ConnectionRef, ChannelId) -> - interrupted_send_rx(ConnectionRef, ChannelId, []). - -interrupted_send_rx(ConnectionRef, ChannelId, Acc) -> - receive - {ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} -> - %% ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)), % window was pre-adjusted, don't adjust again here - interrupted_send_rx(ConnectionRef, ChannelId, [Data | Acc]); - {ssh_cm, ConnectionRef, {eof, ChannelId}} -> - {ok, iolist_to_binary(lists:reverse(Acc))} - after ?EXEC_TIMEOUT -> timeout - end. + PrivDir = ?config(priv_dir, Config), + UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth + file:make_dir(UserDir), + SysDir = ?config(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]}}]}]), + + ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, + {user, "foo"}, + {password, "morot"}, + {user_interaction, false}, + {user_dir, UserDir}]), + + {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity), + + success = ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", infinity), + + %% build 10MB binary + Data = << <> || X <- lists:seq(1,2500000)>>, + + %% expect remote end to send us 4MB back + <> = Data, + + %% pre-adjust receive window so the other end doesn't block + ssh_connection:adjust_window(ConnectionRef, ChannelId, size(ExpectedData) + 1), + + case ssh_connection:send(ConnectionRef, ChannelId, Data, 10000) of + {error, closed} -> + ok; + Msg -> + ct:fail({expected,{error,closed}, got, Msg}) + end, + receive_data(ExpectedData, ConnectionRef, ChannelId), + ssh:close(ConnectionRef), + ssh:stop_daemon(Pid). + + +%% Internal funtions ------------------------------------------------------------------ + +receive_data(ExpectedData, ConnectionRef, ChannelId) -> + ExpectedData = collect_data(ConnectionRef, ChannelId). + +collect_data(ConnectionRef, ChannelId) -> + collect_data(ConnectionRef, ChannelId, []). + +collect_data(ConnectionRef, ChannelId, Acc) -> + receive + {ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} -> + collect_data(ConnectionRef, ChannelId, [Data | Acc]); + {ssh_cm, ConnectionRef, {eof, ChannelId}} -> + iolist_to_binary(lists:reverse(Acc)) + after 5000 -> + timeout + end. diff --git a/lib/ssh/test/ssh_connection_SUITE_data/ssh_host_rsa_key b/lib/ssh/test/ssh_connection_SUITE_data/ssh_host_rsa_key new file mode 100644 index 0000000000..6ae7ee023d --- /dev/null +++ b/lib/ssh/test/ssh_connection_SUITE_data/ssh_host_rsa_key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDCZX+4FBDwZIh9y/Uxee1VJnEXlowpz2yDKwj8semM4q843337 +zbNfxHmladB1lpz2NqyxI175xMIJuDxogyZdsOxGnFAzAnthR4dqL/RWRWzjaxSB +6IAO9SPYVVlrpZ+1hsjLW79fwXK/yc8VdhRuWTeQiRgYY2ek8+OKbOqz4QIDAQAB +AoGANmvJzJO5hkLuvyDZHKfAnGTtpifcR1wtSa9DjdKUyn8vhKF0mIimnbnYQEmW +NUUb3gXCZLi9PvkpRSVRrASDOZwcjoU/Kvww163vBUVb2cOZfFhyn6o2Sk88Tt++ +udH3hdjpf9i7jTtUkUe+QYPsia+wgvvrmn4QrahLAH86+kECQQDx5gFeXTME3cnW +WMpFz3PPumduzjqgqMMWEccX4FtQkMX/gyGa5UC7OHFyh0N/gSWvPbRHa8A6YgIt +n8DO+fh5AkEAzbqX4DOn8NY6xJIi42q7l/2jIA0RkB6P7YugW5NblhqBZ0XDnpA5 +sMt+rz+K07u9XZtxgh1xi7mNfwY6lEAMqQJBAJBEauCKmRj35Z6OyeQku59SPsnY ++SJEREVvSNw2lH9SOKQQ4wPsYlTGbvKtNVZgAcen91L5MmYfeckYE/fdIZECQQCt +64zxsTnM1I8iFxj/gP/OYlJBikrKt8udWmjaghzvLMEw+T2DExJyb9ZNeT53+UMB +m6O+B/4xzU/djvp+0hbhAkAemIt+rA5kTmYlFndhpvzkSSM8a2EXsO4XIPgGWCTT +tQKS/tTly0ADMjN/TVy11+9d6zcqadNVuHXHGtR4W0GR +-----END RSA PRIVATE KEY----- diff --git a/lib/ssh/test/ssh_echo_server.erl b/lib/ssh/test/ssh_echo_server.erl new file mode 100644 index 0000000000..739aabe6fb --- /dev/null +++ b/lib/ssh/test/ssh_echo_server.erl @@ -0,0 +1,71 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: Example ssh server +-module(ssh_echo_server). +-behaviour(ssh_channel). +-record(state, { + n, + id, + cm + }). +-export([init/1, handle_msg/2, handle_ssh_msg/2, terminate/2]). + +init([N]) -> + {ok, #state{n = N}}. + +handle_msg({ssh_channel_up, ChannelId, ConnectionManager}, State) -> + {ok, State#state{id = ChannelId, + cm = ConnectionManager}}. + +handle_ssh_msg({ssh_cm, CM, {data, ChannelId, 0, Data}}, #state{n = N} = State) -> + M = N - size(Data), + case M > 0 of + true -> + ssh_connection:send(CM, ChannelId, Data), + {ok, State#state{n = M}}; + false -> + <> = Data, + ssh_connection:send(CM, ChannelId, SendData), + ssh_connection:send_eof(CM, ChannelId), + {stop, ChannelId, State} + end; +handle_ssh_msg({ssh_cm, _ConnectionManager, + {data, _ChannelId, 1, Data}}, State) -> + error_logger:format("ssh: STDERR: ~s\n", [binary_to_list(Data)]), + {ok, State}; + +handle_ssh_msg({ssh_cm, _ConnectionManager, {eof, _ChannelId}}, State) -> + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {signal, _, _}}, State) -> + %% Ignore signals according to RFC 4254 section 6.9. + {ok, State}; + +handle_ssh_msg({ssh_cm, _, {exit_signal, ChannelId, _, _Error, _}}, + State) -> + {stop, ChannelId, State}; + +handle_ssh_msg({ssh_cm, _, {exit_status, ChannelId, _Status}}, State) -> + {stop, ChannelId, State}. + +terminate(_Reason, _State) -> + ok. -- cgit v1.2.3