查看源代码 示例
以下示例使用实用函数 ssh:start/0
启动所有需要的应用程序 (crypto
、public_key
和 ssh
)。所有示例都在 Erlang shell 中或 bash shell 中运行,使用 OpenSSH 来演示如何使用 ssh
应用程序。这些示例以用户 otptest
的身份在本地网络上运行,该用户被授权通过 ssh
登录到主机 ssh.example.com。
如果没有其他说明,则假定 otptest
用户在 ssh.example.com 的 authorized_keys 文件中有一个条目 (允许通过 ssh
登录而无需输入密码)。此外,ssh.example.com 是用户 otptest
的 known_hosts
文件中的已知主机。这意味着可以进行主机验证而无需用户交互。
使用 Erlang ssh 终端客户端
默认 shell 为 bash 的用户 otptest
使用 ssh:shell/1
客户端连接到在名为 ssh.example.com 的主机上运行的 OpenSSH 守护程序。
1> ssh:start().
ok
2> {ok, S} = ssh:shell("ssh.example.com").
otptest@ssh.example.com:> pwd
/home/otptest
otptest@ssh.example.com:> exit
logout
3>
运行 Erlang ssh 守护程序
system_dir
选项必须是包含主机密钥文件的目录,默认值为 /etc/ssh
。有关详细信息,请参阅 ssh 中的“配置”部分。
注意
通常,
/etc/ssh
目录只能由 root 用户读取。
user_dir
选项默认为 ~/.ssh
目录。
步骤 1. 要在没有 root 权限的情况下运行示例,请生成新的密钥和主机密钥
$bash> ssh-keygen -t rsa -f /tmp/ssh_daemon/ssh_host_rsa_key
[...]
$bash> ssh-keygen -t rsa -f /tmp/otptest_user/.ssh/id_rsa
[...]
步骤 2. 创建文件 /tmp/otptest_user/.ssh/authorized_keys
并添加 /tmp/otptest_user/.ssh/id_rsa.pub
的内容。
步骤 3. 启动 Erlang ssh
守护程序
1> ssh:start().
ok
2> {ok, Sshd} = ssh:daemon(8989, [{system_dir, "/tmp/ssh_daemon"},
{user_dir, "/tmp/otptest_user/.ssh"}]).
{ok,<0.54.0>}
3>
步骤 4. 使用 shell 中的 OpenSSH 客户端连接到 Erlang ssh
守护程序
$bash> ssh ssh.example.com -p 8989 -i /tmp/otptest_user/.ssh/id_rsa \
-o UserKnownHostsFile=/tmp/otptest_user/.ssh/known_hosts
The authenticity of host 'ssh.example.com' can't be established.
RSA key fingerprint is 14:81:80:50:b1:1f:57:dd:93:a8:2d:2f:dd:90:ae:a8.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'ssh.example.com' (RSA) to the list of known hosts.
Eshell V5.10 (abort with ^G)
1>
有两种关闭 ssh
守护程序的方法,请参见步骤 5a 和步骤 5b。
步骤 5a. 关闭 Erlang ssh
守护程序,使其停止侦听器,但保留侦听器启动的现有连接处于运行状态
3> ssh:stop_listener(Sshd).
ok
4>
步骤 5b. 关闭 Erlang ssh
守护程序,使其停止侦听器和侦听器启动的所有连接
3> ssh:stop_daemon(Sshd).
ok
4>
一次性执行
Erlang 客户端连接到 OS 标准 ssh 服务器
在以下示例中,Erlang shell 是客户端进程,它将通道回复接收为 Erlang 消息。
通过 ssh
对主机“ssh.example.com”上的 OS 的 ssh 服务器执行一次远程 OS 命令 (“pwd”)
1> ssh:start().
ok
2> {ok, ConnectionRef} = ssh:connect("ssh.example.com", 22, []).
{ok,<0.57.0>}
3> {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity).
{ok,0}
4> success = ssh_connection:exec(ConnectionRef, ChannelId, "pwd", infinity).
5> flush(). % Get all pending messages. NOTE: ordering may vary!
Shell got {ssh_cm,<0.57.0>,{data,0,0,<<"/home/otptest\n">>}}
Shell got {ssh_cm,<0.57.0>,{eof,0}}
Shell got {ssh_cm,<0.57.0>,{exit_status,0,0}}
Shell got {ssh_cm,<0.57.0>,{closed,0}}
ok
6> ssh:connection_info(ConnectionRef, channels).
{channels,[]}
7>
有关通道消息的文档,请参阅 ssh_connection
和 ssh_connection:exec/4
。
要在程序中收集通道消息,请使用 receive...end
而不是 flush/1
5> receive
5> {ssh_cm, ConnectionRef, {data, ChannelId, Type, Result}} when Type == 0 ->
5> {ok,Result}
5> {ssh_cm, ConnectionRef, {data, ChannelId, Type, Result}} when Type == 1 ->
5> {error,Result}
5> end.
{ok,<<"/home/otptest\n">>}
6>
请注意,一次性执行后仅关闭 exec 通道。连接仍然存在,并且可以处理以前打开的通道。也可以打开新通道
% try to open a new channel to check if the ConnectionRef is still open
7> {ok, NewChannelId} = ssh_connection:session_channel(ConnectionRef, infinity).
{ok,1}
8>
要关闭连接,请调用函数 ssh:close(ConnectionRef)
。或者,在打开连接时设置选项 {idle_time, 1}
。这将导致在指定的时间段(本例中为 1 毫秒)内没有打开的通道时自动关闭连接。
OS 标准客户端和 Erlang 守护程序 (服务器)
可以调用 Erlang SSH 守护程序来一次性执行“命令”。 “命令”必须像输入到 erlang shell 中一样,即以句点 (.) 结尾的一系列 Erlang 表达式。在该序列中绑定的变量将在整个表达式序列中保留其绑定。结果返回时,将释放这些绑定。
这是一个合适的表达式序列的示例
A=1, B=2, 3 == (A + B).
如果将其提交到 上面的步骤 3 中启动的 Erlang 守护程序,则其求值为 true
$bash> ssh ssh.example.com -p 8989 "A=1, B=2, 3 == (A + B)."
true
$bash>
相同的示例,但现在使用 Erlang ssh 客户端连接到 Erlang 服务器
1> {ok, ConnectionRef} = ssh:connect("ssh.example.com", 8989, []).
{ok,<0.216.0>}
2> {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity).
{ok,0}
3> success = ssh_connection:exec(ConnectionRef, ChannelId,
"A=1, B=2, 3 == (A + B).",
infinity).
success
4> flush().
Shell got {ssh_cm,<0.216.0>,{data,0,0,<<"true">>}}
Shell got {ssh_cm,<0.216.0>,{exit_status,0,0}}
Shell got {ssh_cm,<0.216.0>,{eof,0}}
Shell got {ssh_cm,<0.216.0>,{closed,0}}
ok
5>
请注意,不支持 Erlang shell 特定的函数和控制序列,例如 h().
。
在 Erlang ssh 守护程序中调用的函数的 I/O
服务器端 stdout 的输出以及函数调用的结果项也会显示出来
$bash> ssh ssh.example.com -p 8989 'io:format("Hello!~n~nHow are ~p?~n",[you]).'
Hello!
How are you?
ok
$bash>
从 stdin 读取也是如此。例如,我们使用 io:read/1
,它会在 stdout 上将参数显示为提示,从 stdin 读取项,并将其作为 ok 元组返回
$bash> ssh ssh.example.com -p 8989 'io:read("write something: ").'
write something: [a,b,c].
{ok,[a,b,c]}
$bash>
相同的示例,但使用 Erlang ssh 客户端
Eshell V10.5.2 (abort with ^G)
1> ssh:start().
ok
2> {ok, ConnectionRef} = ssh:connect(loopback, 8989, []).
{ok,<0.92.0>}
3> {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity).
{ok,0}
4> success = ssh_connection:exec(ConnectionRef, ChannelId,
"io:read(\"write something: \").",
infinity).
success
5> flush().
Shell got {ssh_cm,<0.92.0>,{data,0,0,<<"write something: ">>}}
ok
% All data is sent as binaries with string contents:
6> ok = ssh_connection:send(ConnectionRef, ChannelId, <<"[a,b,c].">>).
ok
7> flush().
ok
%% Nothing is received, because the io:read/1
%% requires the input line to end with a newline.
%% Send a newline (it could have been included in the last send):
8> ssh_connection:send(ConnectionRef, ChannelId, <<"\n">>).
ok
9> flush().
Shell got {ssh_cm,<0.92.0>,{data,0,0,<<"{ok,[a,b,c]}">>}}
Shell got {ssh_cm,<0.92.0>,{exit_status,0,0}}
Shell got {ssh_cm,<0.92.0>,{eof,0}}
Shell got {ssh_cm,<0.92.0>,{closed,0}}
ok
10>
配置服务器(守护程序)的命令执行
每次启动守护程序时,它都会启用上一节中描述的命令一次性执行,除非显式禁用。
通常需要配置其他 exec 求值器来定制输入语言或限制可以调用的函数。有两种方法可以做到这一点,下面将通过示例进行说明。有关详细信息,请参阅 ssh:daemon/2,3 和 exec_daemon_option()。
配置 exec 求值器的两种方法的示例
- 禁用一次性执行。
要修改上面的守护程序启动示例以拒绝一次性执行请求,我们需要在 步骤 3 中添加选项{exec, disabled}
,即
1> ssh:start().
ok
2> {ok, Sshd} = ssh:daemon(8989, [{system_dir, "/tmp/ssh_daemon"},
{user_dir, "/tmp/otptest_user/.ssh"},
{exec, disabled}
]).
{ok,<0.54.0>}
3>
对该守护程序的调用将在 stderr 上返回文本“Prohibited.”(取决于客户端和 OS),并且退出状态为 255
$bash> ssh ssh.example.com -p 8989 "test."
Prohibited.
$bash> echo $?
255
$bash>
Erlang 客户端库还在数据类型 1 上返回文本“Prohibited.”,而不是正常的 0 和退出状态 255
2> {ok, ConnectionRef} = ssh:connect(loopback, 8989, []).
{ok,<0.92.0>}
3> {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity).
{ok,0}
4> success = ssh_connection:exec(ConnectionRef, ChannelId, "test."
success
5> flush().
Shell got {ssh_cm,<0.106.0>,{data,0,1,<<"Prohibited.">>}}
Shell got {ssh_cm,<0.106.0>,{exit_status,0,255}}
Shell got {ssh_cm,<0.106.0>,{eof,0}}
Shell got {ssh_cm,<0.106.0>,{closed,0}}
ok
6>
- 安装备用求值器。
启动 damon 并引用处理求值的fun()
1> ssh:start().
ok
2> MyEvaluator = fun("1") -> {ok, some_value};
("2") -> {ok, some_other_value};
("3") -> {ok, V} = io:read("input erlang term>> "),
{ok, V};
(Err) -> {error,{bad_input,Err}}
end.
3> {ok, Sshd} = ssh:daemon(1234, [{system_dir, "/tmp/ssh_daemon"},
{user_dir, "/tmp/otptest_user/.ssh"},
{exec, {direct,MyEvaluator}}
]).
{ok,<0.275.0>}
4>
并调用它
$bash> ssh localhost -p 1234 1
some_value
$bash> ssh localhost -p 1234 2
some_other_value
# I/O works:
$bash> ssh localhost -p 1234 3
input erlang term>> abc.
abc
# Check that Erlang evaluation is disabled:
$bash> ssh localhost -p 1234 1+ 2.
**Error** {bad_input,"1+ 2."}
$bash>
请注意,空格会被保留,并且末尾不需要点 (.) - 这是默认求值器所要求的。
Erlang 客户端中的错误返回(文本作为数据类型 1 和 exit_status 255)
2> {ok, ConnectionRef} = ssh:connect(loopback, 1234, []).
{ok,<0.92.0>}
3> {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity).
{ok,0}
4> success = ssh_connection:exec(ConnectionRef, ChannelId, "1+ 2.").
success
5> flush().
Shell got {ssh_cm,<0.106.0>,{data,0,1,<<"**Error** {bad_input,\"1+ 2.\"}">>}}
Shell got {ssh_cm,<0.106.0>,{exit_status,0,255}}
Shell got {ssh_cm,<0.106.0>,{eof,0}}
Shell got {ssh_cm,<0.106.0>,{closed,0}}
ok
6>
exec 选项中的 fun()
最多可以接受三个参数(Cmd
、User
和 ClientAddress
)。有关详细信息,请参阅 exec_daemon_option()。
注意
存在一种过时、不建议使用且未记录的安装备用求值器的方法。
它仍然有效,但例如缺少 I/O 功能。正是由于这种兼容性,我们需要
{direct,...}
构造。
SFTP 服务器
使用 SFTP 子系统启动 Erlang ssh
守护程序
1> ssh:start().
ok
2> ssh:daemon(8989, [{system_dir, "/tmp/ssh_daemon"},
{user_dir, "/tmp/otptest_user/.ssh"},
{subsystems, [ssh_sftpd:subsystem_spec(
[{cwd, "/tmp/sftp/example"}])
]}]).
{ok,<0.54.0>}
3>
运行 OpenSSH SFTP 客户端
$bash> sftp -oPort=8989 -o IdentityFile=/tmp/otptest_user/.ssh/id_rsa \
-o UserKnownHostsFile=/tmp/otptest_user/.ssh/known_hosts ssh.example.com
Connecting to ssh.example.com...
sftp> pwd
Remote working directory: /tmp/sftp/example
sftp>
SFTP 客户端
使用 Erlang SFTP 客户端获取文件
1> ssh:start().
ok
2> {ok, ChannelPid, Connection} = ssh_sftp:start_channel("ssh.example.com", []).
{ok,<0.57.0>,<0.51.0>}
3> ssh_sftp:read_file(ChannelPid, "/home/otptest/test.txt").
{ok,<<"This is a test file\n">>}
使用 TAR 压缩的 SFTP 客户端
基本示例
这是写入然后读取 tar 文件的示例
{ok,HandleWrite} = ssh_sftp:open_tar(ChannelPid, ?tar_file_name, [write]),
ok = erl_tar:add(HandleWrite, .... ),
ok = erl_tar:add(HandleWrite, .... ),
...
ok = erl_tar:add(HandleWrite, .... ),
ok = erl_tar:close(HandleWrite),
%% And for reading
{ok,HandleRead} = ssh_sftp:open_tar(ChannelPid, ?tar_file_name, [read]),
{ok,NameValueList} = erl_tar:extract(HandleRead,[memory]),
ok = erl_tar:close(HandleRead),
加密示例
可以通过以下方式使用加密和解密来扩展之前的基本示例
%% First three parameters depending on which crypto type we select:
Key = <<"This is a 256 bit key. abcdefghi">>,
Ivec0 = crypto:strong_rand_bytes(16),
DataSize = 1024, % DataSize rem 16 = 0 for aes_cbc
%% Initialization of the CryptoState, in this case it is the Ivector.
InitFun = fun() -> {ok, Ivec0, DataSize} end,
%% How to encrypt:
EncryptFun =
fun(PlainBin,Ivec) ->
EncryptedBin = crypto:block_encrypt(aes_cbc256, Key, Ivec, PlainBin),
{ok, EncryptedBin, crypto:next_iv(aes_cbc,EncryptedBin)}
end,
%% What to do with the very last block:
CloseFun =
fun(PlainBin, Ivec) ->
EncryptedBin = crypto:block_encrypt(aes_cbc256, Key, Ivec,
pad(16,PlainBin) %% Last chunk
),
{ok, EncryptedBin}
end,
Cw = {InitFun,EncryptFun,CloseFun},
{ok,HandleWrite} = ssh_sftp:open_tar(ChannelPid, ?tar_file_name, [write,{crypto,Cw}]),
ok = erl_tar:add(HandleWrite, .... ),
ok = erl_tar:add(HandleWrite, .... ),
...
ok = erl_tar:add(HandleWrite, .... ),
ok = erl_tar:close(HandleWrite),
%% And for decryption (in this crypto example we could use the same InitFun
%% as for encryption):
DecryptFun =
fun(EncryptedBin,Ivec) ->
PlainBin = crypto:block_decrypt(aes_cbc256, Key, Ivec, EncryptedBin),
{ok, PlainBin, crypto:next_iv(aes_cbc,EncryptedBin)}
end,
Cr = {InitFun,DecryptFun},
{ok,HandleRead} = ssh_sftp:open_tar(ChannelPid, ?tar_file_name, [read,{crypto,Cw}]),
{ok,NameValueList} = erl_tar:extract(HandleRead,[memory]),
ok = erl_tar:close(HandleRead),
创建子系统
一个小的 ssh
子系统(可以回显 N 个字节)可以按以下示例所示实现
-module(ssh_echo_server).
-behaviour(ssh_server_channel). % replaces ssh_daemon_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 ->
<<SendData:N/binary, _/binary>> = 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(standard_error, " ~p~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.
该子系统可以使用生成的密钥在主机 ssh.example.com 上运行,如运行 Erlang ssh 守护程序部分中所述
1> ssh:start().
ok
2> ssh:daemon(8989, [{system_dir, "/tmp/ssh_daemon"},
{user_dir, "/tmp/otptest_user/.ssh"}
{subsystems, [{"echo_n", {ssh_echo_server, [10]}}]}]).
{ok,<0.54.0>}
3>
1> ssh:start().
ok
2> {ok, ConnectionRef} = ssh:connect("ssh.example.com", 8989,
[{user_dir, "/tmp/otptest_user/.ssh"}]).
{ok,<0.57.0>}
3> {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity).
4> success = ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", infinity).
5> ok = ssh_connection:send(ConnectionRef, ChannelId, "0123456789", infinity).
6> flush().
{ssh_msg, <0.57.0>, {data, 0, 1, "0123456789"}}
{ssh_msg, <0.57.0>, {eof, 0}}
{ssh_msg, <0.57.0>, {closed, 0}}
7> {error, closed} = ssh_connection:send(ConnectionRef, ChannelId, "10", infinity).
另请参阅 ssh_client_channel
(替换 ssh_channel(3))。