檢視原始碼 範例
以下範例使用工具函式 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 終端客戶端
使用者 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_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 評估器的兩種方法範例
- 停用一次性執行。
若要修改上述的精靈啟動範例,以拒絕一次性執行請求,我們將選項{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>
- 安裝替代評估器。
使用對處理評估的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))。