檢視原始碼 範例

以下範例使用工具函式 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 終端客戶端

使用者 otptest 以 bash 作為預設 shell,使用 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.example.com" 上透過 ssh 對 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. 停用一次性執行。
    若要修改上述的精靈啟動範例,以拒絕一次性執行請求,我們將選項 {exec, disabled} 新增到步驟 3
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. 安裝替代評估器。
    使用對處理評估的 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))。