查看源代码 示例

以下示例使用实用函数 ssh:start/0 启动所有需要的应用程序 (cryptopublic_keyssh)。所有示例都在 Erlang shell 中或 bash shell 中运行,使用 OpenSSH 来演示如何使用 ssh 应用程序。这些示例以用户 otptest 的身份在本地网络上运行,该用户被授权通过 ssh 登录到主机 ssh.example.com

如果没有其他说明,则假定 otptest 用户在 ssh.example.comauthorized_keys 文件中有一个条目 (允许通过 ssh 登录而无需输入密码)。此外,ssh.example.com 是用户 otptestknown_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_connectionssh_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,3exec_daemon_option()

配置 exec 求值器的两种方法的示例

  1. 禁用一次性执行。
    要修改上面的守护程序启动示例以拒绝一次性执行请求,我们需要在 步骤 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>
  1. 安装备用求值器。
    启动 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() 最多可以接受三个参数(CmdUserClientAddress)。有关详细信息,请参阅 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))。