network-infra-utility 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +21 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +70 -0
- data/Rakefile +1 -1
- data/bin/dns-query +834 -0
- data/bin/geo-doc +135 -0
- data/bin/geo-get +1 -1
- data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
- data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
- data/document/Geo/345/221/275/344/273/244/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +441 -0
- data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
- data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
- data/document/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/344/275/277/347/224/250/346/226/271/346/263/225.md +764 -0
- data/network-infra-utility.gemspec +4 -2
- data/network.rb +3 -1
- data/service/geodb/geodb.rb +278 -1
- data/service/ssh/README.md +942 -0
- data/service/ssh/bin/ssh-client +198 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/345/212/237/350/203/275/351/234/200/346/261/202/346/226/207/346/241/243.md +292 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/257/246/347/273/206/350/256/276/350/256/241/346/226/207/346/241/243.md +1521 -0
- data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/275/257/344/273/266/350/256/276/350/256/241/346/226/207/346/241/243.md +2493 -0
- data/service/ssh/ext/ssh_core/bin/ssh_core.cmd +28 -0
- data/service/ssh/ext/ssh_core/config/sys.config +0 -0
- data/service/ssh/ext/ssh_core/config/vm.args +0 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/CHECKSUM +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/LICENSE +21 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/README.md +696 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/VERSION +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/contents.tar.gz +0 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/metadata.config +15 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.config +17 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.lock +1 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.app.src +10 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.erl +506 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.erl +393 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.hrl +18 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_consult.erl +81 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_decoder.erl +1909 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_encoder.erl +116 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_parser.erl +1214 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_json.erl +408 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_term.erl +389 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_verify.erl +121 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx.erl +506 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.erl +393 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.hrl +18 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_consult.erl +81 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_decoder.erl +1909 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_encoder.erl +116 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_parser.erl +1214 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_json.erl +408 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_term.erl +389 -0
- data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_verify.erl +121 -0
- data/service/ssh/ext/ssh_core/rebar.config +24 -0
- data/service/ssh/ext/ssh_core/rebar.lock +1 -0
- data/service/ssh/ext/ssh_core/src/ssh_auth_engine.erl +156 -0
- data/service/ssh/ext/ssh_core/src/ssh_channel_stm.erl +232 -0
- data/service/ssh/ext/ssh_core/src/ssh_codec.erl +83 -0
- data/service/ssh/ext/ssh_core/src/ssh_conn_sup.erl +48 -0
- data/service/ssh/ext/ssh_core/src/ssh_conn_worker.erl +535 -0
- data/service/ssh/ext/ssh_core/src/ssh_core.app.src +36 -0
- data/service/ssh/ext/ssh_core/src/ssh_core_app.erl +11 -0
- data/service/ssh/ext/ssh_core/src/ssh_core_sup.erl +46 -0
- data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +104 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc.hrl +80 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_coalesce.erl +94 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_gateway.erl +467 -0
- data/service/ssh/ext/ssh_core/src/ssh_ipc_proto.erl +95 -0
- data/service/ssh/ext/ssh_core/src/ssh_jump_chain.erl +101 -0
- data/service/ssh/ext/ssh_core/src/ssh_keepalive_mgr.erl +222 -0
- data/service/ssh/ext/ssh_core/src/ssh_known_hosts_proxy.erl +67 -0
- data/service/ssh/ext/ssh_core/src/ssh_port_fwd.erl +225 -0
- data/service/ssh/ext/ssh_core/src/ssh_sftp_session.erl +250 -0
- data/service/ssh/ext/ssh_core/src/ssh_sftp_sup.erl +62 -0
- data/service/ssh/ext/ssh_core_rs/Cargo.lock +2345 -0
- data/service/ssh/ext/ssh_core_rs/Cargo.toml +30 -0
- data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs +34 -0
- data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs.cmd +40 -0
- data/service/ssh/ext/ssh_core_rs/src/channel.rs +296 -0
- data/service/ssh/ext/ssh_core_rs/src/coalesce.rs +143 -0
- data/service/ssh/ext/ssh_core_rs/src/codec.rs +71 -0
- data/service/ssh/ext/ssh_core_rs/src/conn.rs +628 -0
- data/service/ssh/ext/ssh_core_rs/src/gateway.rs +389 -0
- data/service/ssh/ext/ssh_core_rs/src/handler.rs +293 -0
- data/service/ssh/ext/ssh_core_rs/src/keepalive.rs +194 -0
- data/service/ssh/ext/ssh_core_rs/src/main.rs +351 -0
- data/service/ssh/ext/ssh_core_rs/src/portfwd.rs +378 -0
- data/service/ssh/ext/ssh_core_rs/src/proto.rs +198 -0
- data/service/ssh/ext/ssh_core_rs/src/sftp.rs +294 -0
- data/service/ssh/lib/network_infra_utility/ssh/automation/macro_engine.rb +213 -0
- data/service/ssh/lib/network_infra_utility/ssh/client.rb +257 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/schema.rb +90 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/settings.rb +103 -0
- data/service/ssh/lib/network_infra_utility/ssh/config/store.rb +90 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/coalesce.rb +83 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/errors.rb +36 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/router.rb +212 -0
- data/service/ssh/lib/network_infra_utility/ssh/ipc/transport.rb +81 -0
- data/service/ssh/lib/network_infra_utility/ssh/security/host_key.rb +211 -0
- data/service/ssh/lib/network_infra_utility/ssh/security/vault.rb +211 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/history.rb +56 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/manager.rb +92 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/session.rb +109 -0
- data/service/ssh/lib/network_infra_utility/ssh/session/tree.rb +95 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/ansi_parser.rb +435 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/buffer.rb +78 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/emulator.rb +159 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/logger.rb +195 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/screen.rb +212 -0
- data/service/ssh/lib/network_infra_utility/ssh/terminal/theme.rb +127 -0
- data/service/ssh/lib/network_infra_utility/ssh/version.rb +7 -0
- data/service/ssh/lib/network_infra_utility/ssh.rb +44 -0
- data/support/basic/as_num.rb +221 -0
- data/support/basic/mac_address.rb +281 -0
- data/version.rb +1 -1
- metadata +138 -1
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
-module(ssh_conn_worker).
|
|
2
|
+
-behaviour(gen_statem).
|
|
3
|
+
|
|
4
|
+
%% Connection worker — manages a single SSH connection's lifecycle + channels.
|
|
5
|
+
%% State machine: idle → connecting → authenticating → ready → reconnecting/closing → failed/closed
|
|
6
|
+
%%
|
|
7
|
+
%% Key design (LLD §6.3, E1/E2/E5 fixes):
|
|
8
|
+
%% - OTP ssh integrates auth into ssh:connect/2,3
|
|
9
|
+
%% - gen_statem (state_functions mode), not gen_fsm
|
|
10
|
+
%% - Channels are spawned as children via proc_lib:spawn_link
|
|
11
|
+
%% - Reconnect is NOT autonomous — Erlang notifies, Ruby decides
|
|
12
|
+
|
|
13
|
+
-export([
|
|
14
|
+
start_link/1,
|
|
15
|
+
do_connect/1,
|
|
16
|
+
rpc_disconnect/1,
|
|
17
|
+
rpc_channel_open/1,
|
|
18
|
+
rpc_reconnect/1,
|
|
19
|
+
get_state_name/1,
|
|
20
|
+
get_state/1,
|
|
21
|
+
get_ssh_ref/1,
|
|
22
|
+
conn_id/1,
|
|
23
|
+
build_ssh_options/1
|
|
24
|
+
]).
|
|
25
|
+
-export([callback_mode/0, init/1]).
|
|
26
|
+
-export([idle/3, connecting/3, authenticating/3, ready/3,
|
|
27
|
+
reconnecting/3, closing/3, failed/3]).
|
|
28
|
+
|
|
29
|
+
-include("ssh_ipc.hrl").
|
|
30
|
+
|
|
31
|
+
%% ---------- Public API ----------
|
|
32
|
+
|
|
33
|
+
%% @doc Start a connection worker. Spec is the connect_spec map.
|
|
34
|
+
start_link(Spec) ->
|
|
35
|
+
gen_statem:start_link(?MODULE, [Spec], []).
|
|
36
|
+
|
|
37
|
+
%% @doc Trigger the connection sequence.
|
|
38
|
+
%% Uses a generous gen_statem call timeout (connect_timeout + 10s buffer)
|
|
39
|
+
%% to avoid the call timing out before ssh:connect does.
|
|
40
|
+
do_connect(Pid) ->
|
|
41
|
+
%% Get the spec's connect_timeout or default, add 10s buffer for state machine overhead
|
|
42
|
+
case catch ssh_conn_worker:get_state(Pid) of
|
|
43
|
+
#conn{spec = Spec} ->
|
|
44
|
+
Timeout = maps:get(<<"connect_timeout_ms">>, Spec, ?CONNECT_TIMEOUT_MS),
|
|
45
|
+
gen_statem:call(Pid, do_connect, Timeout + 10000);
|
|
46
|
+
_ ->
|
|
47
|
+
gen_statem:call(Pid, do_connect, ?CONNECT_TIMEOUT_MS + 10000)
|
|
48
|
+
end.
|
|
49
|
+
|
|
50
|
+
%% @doc RPC handler for conn.disconnect
|
|
51
|
+
rpc_disconnect(#{<<"id">> := ConnId}) ->
|
|
52
|
+
case find_by_conn_id(ConnId) of
|
|
53
|
+
{ok, Pid} ->
|
|
54
|
+
gen_statem:call(Pid, disconnect),
|
|
55
|
+
#{ok => true};
|
|
56
|
+
{error, not_found} ->
|
|
57
|
+
{error, conn_not_found}
|
|
58
|
+
end.
|
|
59
|
+
|
|
60
|
+
%% @doc RPC handler for conn.reconnect
|
|
61
|
+
rpc_reconnect(#{<<"id">> := ConnId}) ->
|
|
62
|
+
case find_by_conn_id(ConnId) of
|
|
63
|
+
{ok, Pid} ->
|
|
64
|
+
gen_statem:call(Pid, reconnect);
|
|
65
|
+
{error, not_found} ->
|
|
66
|
+
{error, conn_not_found}
|
|
67
|
+
end.
|
|
68
|
+
|
|
69
|
+
%% @doc RPC handler for channel.open
|
|
70
|
+
rpc_channel_open(#{<<"conn_id">> := ConnId} = Params) ->
|
|
71
|
+
case find_by_conn_id(ConnId) of
|
|
72
|
+
{ok, Pid} ->
|
|
73
|
+
gen_statem:call(Pid, {open_channel, Params});
|
|
74
|
+
{error, not_found} ->
|
|
75
|
+
{error, conn_not_found}
|
|
76
|
+
end.
|
|
77
|
+
|
|
78
|
+
%% @doc Get current state name (for conn.list).
|
|
79
|
+
get_state_name(Pid) ->
|
|
80
|
+
gen_statem:call(Pid, get_state_name).
|
|
81
|
+
|
|
82
|
+
%% @doc Get full state record (internal use).
|
|
83
|
+
get_state(Pid) ->
|
|
84
|
+
gen_statem:call(Pid, get_state).
|
|
85
|
+
|
|
86
|
+
%% @doc Get the OTP ssh connection reference.
|
|
87
|
+
get_ssh_ref(Pid) ->
|
|
88
|
+
gen_statem:call(Pid, get_ssh_ref).
|
|
89
|
+
|
|
90
|
+
%% @doc Get conn_id for this worker.
|
|
91
|
+
conn_id(Pid) ->
|
|
92
|
+
gen_statem:call(Pid, conn_id).
|
|
93
|
+
|
|
94
|
+
%% ---------- gen_statem callbacks ----------
|
|
95
|
+
|
|
96
|
+
callback_mode() -> state_functions.
|
|
97
|
+
|
|
98
|
+
init([Spec]) ->
|
|
99
|
+
ConnId = ssh_codec:gen_conn_id(),
|
|
100
|
+
Data = #conn{
|
|
101
|
+
id = ConnId,
|
|
102
|
+
spec = Spec,
|
|
103
|
+
options = build_ssh_options(Spec)
|
|
104
|
+
},
|
|
105
|
+
{ok, idle, Data}.
|
|
106
|
+
|
|
107
|
+
%% ---------- State: idle ----------
|
|
108
|
+
|
|
109
|
+
idle({call, From}, do_connect, #conn{spec = Spec, id = Id} = D) ->
|
|
110
|
+
{next_state, connecting, D, [{next_event, internal, {From, do_connect}}]};
|
|
111
|
+
|
|
112
|
+
idle({call, From}, get_state_name, _D) ->
|
|
113
|
+
{keep_state_and_data, [{reply, From, idle}]};
|
|
114
|
+
|
|
115
|
+
idle({call, From}, conn_id, #conn{id = Id}) ->
|
|
116
|
+
{keep_state_and_data, [{reply, From, Id}]};
|
|
117
|
+
|
|
118
|
+
idle(Type, Msg, D) ->
|
|
119
|
+
handle_common(Type, Msg, D).
|
|
120
|
+
|
|
121
|
+
%% ---------- State: connecting ----------
|
|
122
|
+
|
|
123
|
+
connecting(internal, {From, do_connect}, #conn{spec = Spec, id = Id, options = Opts} = D) ->
|
|
124
|
+
Jumps = maps:get(<<"jumps">>, Spec, []),
|
|
125
|
+
ConnectTimeout = maps:get(<<"connect_timeout_ms">>, Spec, ?CONNECT_TIMEOUT_MS),
|
|
126
|
+
Result = case Jumps of
|
|
127
|
+
[] ->
|
|
128
|
+
Host = binary_to_list(maps:get(<<"host">>, Spec)),
|
|
129
|
+
Port = maps:get(<<"port">>, Spec, 22),
|
|
130
|
+
ssh:connect(Host, Port, Opts, ConnectTimeout);
|
|
131
|
+
_ ->
|
|
132
|
+
%% ProxyJump: jump chain handles the entire connection
|
|
133
|
+
ssh_jump_chain:build(Spec, Jumps)
|
|
134
|
+
end,
|
|
135
|
+
case Result of
|
|
136
|
+
{ok, SshRef} ->
|
|
137
|
+
Fingerprint = get_host_fingerprint(SshRef),
|
|
138
|
+
ssh_ipc_gateway:push_event(<<"conn.ready">>, #{
|
|
139
|
+
<<"conn_id">> => Id,
|
|
140
|
+
<<"fingerprint">> => Fingerprint
|
|
141
|
+
}),
|
|
142
|
+
{next_state, ready, D#conn{ssh_ref = SshRef},
|
|
143
|
+
[{reply, From, {ok, #{conn_id => Id, fingerprint => Fingerprint}}}]};
|
|
144
|
+
{error, Reason} ->
|
|
145
|
+
ssh_ipc_gateway:push_event(<<"conn.failed">>, #{
|
|
146
|
+
<<"conn_id">> => Id,
|
|
147
|
+
<<"reason">> => atom_to_binary(Reason, utf8)
|
|
148
|
+
}),
|
|
149
|
+
{next_state, failed, D#conn{fail_reason = Reason},
|
|
150
|
+
[{reply, From, {error, Reason}}]}
|
|
151
|
+
end;
|
|
152
|
+
|
|
153
|
+
connecting({call, From}, get_state_name, _D) ->
|
|
154
|
+
{keep_state_and_data, [{reply, From, connecting}]};
|
|
155
|
+
|
|
156
|
+
connecting(Type, Msg, D) ->
|
|
157
|
+
handle_common(Type, Msg, D).
|
|
158
|
+
|
|
159
|
+
%% ---------- State: authenticating ----------
|
|
160
|
+
|
|
161
|
+
%% OTP ssh does auth in ssh:connect. This state is a transient
|
|
162
|
+
%% placeholder for keyboard_interactive prompt handling.
|
|
163
|
+
authenticating({call, From}, get_state_name, _D) ->
|
|
164
|
+
{keep_state_and_data, [{reply, From, authenticating}]};
|
|
165
|
+
|
|
166
|
+
authenticating(Type, Msg, D) ->
|
|
167
|
+
handle_common(Type, Msg, D).
|
|
168
|
+
|
|
169
|
+
%% ---------- State: ready ----------
|
|
170
|
+
|
|
171
|
+
ready({call, From}, {open_channel, Params}, #conn{ssh_ref = Ref, id = ConnId, next_ch_seq = Seq} = D) ->
|
|
172
|
+
ChId = ssh_codec:gen_channel_id(ConnId),
|
|
173
|
+
ChType = maps:get(<<"type">>, Params, <<"shell">>),
|
|
174
|
+
Cols = maps:get(<<"cols">>, Params, 80),
|
|
175
|
+
Rows = maps:get(<<"rows">>, Params, 24),
|
|
176
|
+
TermType = maps:get(<<"term">>, Params, <<"xterm-256color">>),
|
|
177
|
+
case ssh_channel_stm:start_link(ChId, ConnId, Ref, #{
|
|
178
|
+
type => binary_to_atom(ChType, utf8),
|
|
179
|
+
cols => Cols,
|
|
180
|
+
rows => Rows,
|
|
181
|
+
term_type => TermType,
|
|
182
|
+
command => maps:get(<<"command">>, Params, undefined)
|
|
183
|
+
}) of
|
|
184
|
+
{ok, ChPid} ->
|
|
185
|
+
%% Register in ETS index for channel lookup by id
|
|
186
|
+
ets:insert(ch_index, {ChId, ChPid}),
|
|
187
|
+
Channels = maps:put(ChId, ChPid, D#conn.channels),
|
|
188
|
+
{keep_state, D#conn{channels = Channels, next_ch_seq = Seq + 1},
|
|
189
|
+
[{reply, From, {ok, #{channel_id => ChId}}}]};
|
|
190
|
+
{error, Reason} ->
|
|
191
|
+
{keep_state_and_data, [{reply, From, {error, Reason}}]}
|
|
192
|
+
end;
|
|
193
|
+
|
|
194
|
+
ready({call, From}, disconnect, #conn{ssh_ref = Ref} = D) ->
|
|
195
|
+
ssh:close(Ref),
|
|
196
|
+
{next_state, closing, D, [{reply, From, #{ok => true}}]};
|
|
197
|
+
|
|
198
|
+
ready({call, From}, get_state_name, _D) ->
|
|
199
|
+
{keep_state_and_data, [{reply, From, ready}]};
|
|
200
|
+
|
|
201
|
+
ready({call, From}, get_ssh_ref, #conn{ssh_ref = Ref}) ->
|
|
202
|
+
{keep_state_and_data, [{reply, From, Ref}]};
|
|
203
|
+
|
|
204
|
+
ready({call, From}, get_state, D) ->
|
|
205
|
+
{keep_state_and_data, [{reply, From, D}]};
|
|
206
|
+
|
|
207
|
+
ready({call, From}, conn_id, #conn{id = Id}) ->
|
|
208
|
+
{keep_state_and_data, [{reply, From, Id}]};
|
|
209
|
+
|
|
210
|
+
ready(info, {ssh_channel_down, ChId}, #conn{channels = Chs} = D) ->
|
|
211
|
+
ets:delete(ch_index, ChId),
|
|
212
|
+
NewChs = maps:remove(ChId, Chs),
|
|
213
|
+
{keep_state, D#conn{channels = NewChs}};
|
|
214
|
+
|
|
215
|
+
ready(info, {ssh_closed, _Reason}, #conn{id = Id} = D) ->
|
|
216
|
+
ssh_ipc_gateway:push_event(<<"conn.closed">>, #{
|
|
217
|
+
<<"conn_id">> => Id,
|
|
218
|
+
<<"reason">> => <<"ssh_closed">>
|
|
219
|
+
}),
|
|
220
|
+
{next_state, failed, D#conn{ssh_ref = undefined}};
|
|
221
|
+
|
|
222
|
+
ready(Type, Msg, D) ->
|
|
223
|
+
handle_common(Type, Msg, D).
|
|
224
|
+
|
|
225
|
+
%% ---------- State: reconnecting ----------
|
|
226
|
+
|
|
227
|
+
reconnecting({call, From}, reconnect, #conn{spec = Spec, id = Id, options = Opts} = D) ->
|
|
228
|
+
Jumps = maps:get(<<"jumps">>, Spec, []),
|
|
229
|
+
ConnectTimeout = maps:get(<<"connect_timeout_ms">>, Spec, ?CONNECT_TIMEOUT_MS),
|
|
230
|
+
Result = case Jumps of
|
|
231
|
+
[] ->
|
|
232
|
+
Host = binary_to_list(maps:get(<<"host">>, Spec)),
|
|
233
|
+
Port = maps:get(<<"port">>, Spec, 22),
|
|
234
|
+
ssh:connect(Host, Port, Opts, ConnectTimeout);
|
|
235
|
+
_ ->
|
|
236
|
+
ssh_jump_chain:build(Spec, Jumps)
|
|
237
|
+
end,
|
|
238
|
+
case Result of
|
|
239
|
+
{ok, SshRef} ->
|
|
240
|
+
ssh_ipc_gateway:push_event(<<"conn.ready">>, #{<<"conn_id">> => Id}),
|
|
241
|
+
{next_state, ready, D#conn{ssh_ref = SshRef, reconnect_count = 0},
|
|
242
|
+
[{reply, From, {ok, #{conn_id => Id}}}]};
|
|
243
|
+
{error, Reason} ->
|
|
244
|
+
{keep_state, D#conn{reconnect_count = D#conn.reconnect_count + 1},
|
|
245
|
+
[{reply, From, {error, Reason}}]}
|
|
246
|
+
end;
|
|
247
|
+
|
|
248
|
+
reconnecting({call, From}, get_state_name, _D) ->
|
|
249
|
+
{keep_state_and_data, [{reply, From, reconnecting}]};
|
|
250
|
+
|
|
251
|
+
reconnecting(Type, Msg, D) ->
|
|
252
|
+
handle_common(Type, Msg, D).
|
|
253
|
+
|
|
254
|
+
%% ---------- State: closing ----------
|
|
255
|
+
|
|
256
|
+
closing({call, From}, _Msg, _D) ->
|
|
257
|
+
{keep_state_and_data, [{reply, From, {error, closing}}]};
|
|
258
|
+
|
|
259
|
+
closing(Type, Msg, D) ->
|
|
260
|
+
handle_common(Type, Msg, D).
|
|
261
|
+
|
|
262
|
+
%% ---------- State: failed ----------
|
|
263
|
+
|
|
264
|
+
failed({call, From}, get_state_name, _D) ->
|
|
265
|
+
{keep_state_and_data, [{reply, From, failed}]};
|
|
266
|
+
|
|
267
|
+
failed({call, From}, conn_id, #conn{id = Id}) ->
|
|
268
|
+
{keep_state_and_data, [{reply, From, Id}]};
|
|
269
|
+
|
|
270
|
+
failed({call, From}, reconnect, D) ->
|
|
271
|
+
{next_state, reconnecting, D, [{next_event, internal, {From, reconnect}}]};
|
|
272
|
+
|
|
273
|
+
failed(Type, Msg, D) ->
|
|
274
|
+
handle_common(Type, Msg, D).
|
|
275
|
+
|
|
276
|
+
%% ---------- Common handler ----------
|
|
277
|
+
|
|
278
|
+
handle_common({call, From}, get_state_name, _D) ->
|
|
279
|
+
{keep_state_and_data, [{reply, From, unknown}]};
|
|
280
|
+
|
|
281
|
+
handle_common({call, From}, conn_id, #conn{id = Id}) ->
|
|
282
|
+
{keep_state_and_data, [{reply, From, Id}]};
|
|
283
|
+
|
|
284
|
+
handle_common(_Type, _Msg, D) ->
|
|
285
|
+
{keep_state, D}.
|
|
286
|
+
|
|
287
|
+
%% ---------- Internal: SSH options ----------
|
|
288
|
+
|
|
289
|
+
%% @doc Build ssh:connect options from the connect spec.
|
|
290
|
+
build_ssh_options(Spec) ->
|
|
291
|
+
ConnectTimeout = maps:get(<<"connect_timeout_ms">>, Spec, ?CONNECT_TIMEOUT_MS),
|
|
292
|
+
Auth = maps:get(<<"auth">>, Spec, #{}),
|
|
293
|
+
[
|
|
294
|
+
{user, binary_to_list(maps:get(<<"user">>, Spec))},
|
|
295
|
+
{silently_accept_hosts, false},
|
|
296
|
+
{key_cb, {ssh_auth_engine, [{auth_map, Auth}]}},
|
|
297
|
+
{user_dir, binary_to_list(maps:get(<<"key_dir">>, Spec, <<".">>))},
|
|
298
|
+
{preferred_algorithms, build_algorithms(maps:get(<<"algorithms">>, Spec, undefined))},
|
|
299
|
+
{connect_timeout, ConnectTimeout},
|
|
300
|
+
{user_interaction, false}
|
|
301
|
+
]
|
|
302
|
+
++ build_auth_options(maps:get(<<"auth">>, Spec, #{}))
|
|
303
|
+
++ build_proxy_options(maps:get(<<"proxy">>, Spec, undefined))
|
|
304
|
+
++ build_jump_options(maps:get(<<"jumps">>, Spec, [])).
|
|
305
|
+
|
|
306
|
+
%% @doc Build preferred_algorithms option.
|
|
307
|
+
%% Returns a keyword list (not a map) for OTP 17 compatibility.
|
|
308
|
+
%% OTP 17 expects cipher and mac as [{client2server,[...]}, {server2client,[...]}] pairs,
|
|
309
|
+
%% while kex and public_key use flat lists.
|
|
310
|
+
%%
|
|
311
|
+
%% If user provides custom algorithms in spec, merge them with defaults:
|
|
312
|
+
%% - User-specified algorithms are prepended (higher priority)
|
|
313
|
+
%% - Default algorithms fill in any categories not specified
|
|
314
|
+
%% - Within a category, user list takes priority, defaults appended for fallback
|
|
315
|
+
%% If no custom algorithms, use defaults.
|
|
316
|
+
-spec build_algorithms(undefined | map()) -> [{atom(), term()}].
|
|
317
|
+
build_algorithms(undefined) ->
|
|
318
|
+
preferred_algs();
|
|
319
|
+
build_algorithms(UserAlgs) when is_map(UserAlgs) ->
|
|
320
|
+
Defaults = preferred_algs(),
|
|
321
|
+
%% Convert Defaults to a map for easy merging
|
|
322
|
+
DefaultMap = #{
|
|
323
|
+
kex => proplists:get_value(kex, Defaults),
|
|
324
|
+
public_key => proplists:get_value(public_key, Defaults),
|
|
325
|
+
cipher => proplists:get_value(cipher, Defaults),
|
|
326
|
+
mac => proplists:get_value(mac, Defaults)
|
|
327
|
+
},
|
|
328
|
+
%% Merge each category: user first, then defaults not already included
|
|
329
|
+
MergedMap = maps:map(fun(Category, DefaultList) ->
|
|
330
|
+
UserList = case maps:get(Category, UserAlgs, undefined) of
|
|
331
|
+
undefined -> [];
|
|
332
|
+
L when is_list(L) -> [normalize_alg(A) || A <- L];
|
|
333
|
+
_ -> []
|
|
334
|
+
end,
|
|
335
|
+
UserList ++ [A || A <- DefaultList, not lists:member(A, UserList)]
|
|
336
|
+
end, DefaultMap),
|
|
337
|
+
%% Convert back to keyword list format
|
|
338
|
+
[
|
|
339
|
+
{kex, maps:get(kex, MergedMap)},
|
|
340
|
+
{public_key, maps:get(public_key, MergedMap)},
|
|
341
|
+
{cipher, maps:get(cipher, MergedMap)},
|
|
342
|
+
{mac, maps:get(mac, MergedMap)}
|
|
343
|
+
].
|
|
344
|
+
|
|
345
|
+
%% @doc Normalize algorithm atom from binary or atom.
|
|
346
|
+
normalize_alg(Bin) when is_binary(Bin) ->
|
|
347
|
+
try binary_to_existing_atom(Bin, utf8) catch _:_ -> binary_to_atom(Bin, utf8) end;
|
|
348
|
+
normalize_alg(Atom) when is_atom(Atom) ->
|
|
349
|
+
Atom.
|
|
350
|
+
|
|
351
|
+
%% @doc Preferred algorithms — meets FR-CONN-001 quantitative targets.
|
|
352
|
+
%% Returns keyword list format for OTP 17 compatibility:
|
|
353
|
+
%% kex, public_key => flat list
|
|
354
|
+
%% cipher, mac => [{client2server,[...]}, {server2client,[...]}] pairs
|
|
355
|
+
preferred_algs() ->
|
|
356
|
+
Ciphers = ['aes256-gcm@openssh.com',
|
|
357
|
+
'aes128-gcm@openssh.com',
|
|
358
|
+
'chacha20-poly1305@openssh.com',
|
|
359
|
+
'aes256-ctr', 'aes192-ctr', 'aes128-ctr'],
|
|
360
|
+
Macs = ['hmac-sha2-256',
|
|
361
|
+
'hmac-sha2-512',
|
|
362
|
+
'hmac-sha2-256-etm@openssh.com',
|
|
363
|
+
'hmac-sha2-512-etm@openssh.com'],
|
|
364
|
+
[
|
|
365
|
+
{kex, ['curve25519-sha256@libssh.org',
|
|
366
|
+
'curve25519-sha256',
|
|
367
|
+
'ecdh-sha2-nistp256',
|
|
368
|
+
'ecdh-sha2-nistp384',
|
|
369
|
+
'ecdh-sha2-nistp521',
|
|
370
|
+
'diffie-hellman-group16-sha512',
|
|
371
|
+
'diffie-hellman-group14-sha256']},
|
|
372
|
+
{public_key, ['ssh-ed25519',
|
|
373
|
+
'rsa-sha2-512',
|
|
374
|
+
'rsa-sha2-256',
|
|
375
|
+
'ecdsa-sha2-nistp256',
|
|
376
|
+
'ecdsa-sha2-nistp384',
|
|
377
|
+
'ecdsa-sha2-nistp521']},
|
|
378
|
+
{cipher, [{client2server, Ciphers}, {server2client, Ciphers}]},
|
|
379
|
+
{mac, [{client2server, Macs}, {server2client, Macs}]}
|
|
380
|
+
].
|
|
381
|
+
|
|
382
|
+
%% @doc Build auth options from the auth map.
|
|
383
|
+
build_auth_options(#{<<"type">> := <<"password">>} = Auth) ->
|
|
384
|
+
[{password, binary_to_list(maps:get(<<"password">>, Auth))}];
|
|
385
|
+
build_auth_options(#{<<"type">> := <<"publickey">>}) ->
|
|
386
|
+
%% Key loading handled by ssh_auth_engine key_cb callbacks
|
|
387
|
+
[];
|
|
388
|
+
build_auth_options(#{<<"type">> := <<"keyboard_interactive">>} = Auth) ->
|
|
389
|
+
[{keyboard_interactive, fun(_Prompts, _Ssh) -> handle_kbi_prompts(Auth) end}];
|
|
390
|
+
build_auth_options(_) ->
|
|
391
|
+
[].
|
|
392
|
+
|
|
393
|
+
%% @doc Handle keyboard-interactive prompts.
|
|
394
|
+
handle_kbi_prompts(Auth) ->
|
|
395
|
+
%% Return responses from auth map
|
|
396
|
+
Responses = maps:get(<<"responses">>, Auth, []),
|
|
397
|
+
{ok, [binary_to_list(R) || R <- Responses]}.
|
|
398
|
+
|
|
399
|
+
%% @doc Build proxy options (SOCKS5/HTTP).
|
|
400
|
+
build_proxy_options(undefined) -> [];
|
|
401
|
+
build_proxy_options(#{<<"type">> := _, <<"host">> := _, <<"port">> := _} = Proxy) ->
|
|
402
|
+
[{sock_fun, make_proxy_sock_fun(Proxy)}].
|
|
403
|
+
|
|
404
|
+
%% @doc Build jump chain options.
|
|
405
|
+
%% Jumps are not handled as ssh options — they require a separate connection
|
|
406
|
+
%% sequence via ssh_jump_chain:build/2, called from connecting/reconnecting states.
|
|
407
|
+
%% Returns [] so build_ssh_options skips them.
|
|
408
|
+
build_jump_options(_Jumps) -> [].
|
|
409
|
+
|
|
410
|
+
%% @doc Make proxy sock_fun — supports SOCKS5 and HTTP CONNECT.
|
|
411
|
+
%% The returned fun must implement the OTP ssh sock_fun contract:
|
|
412
|
+
%% (connect, Host, Port, Timeout) -> {ok, Sock} | {error, _}
|
|
413
|
+
%% (close, Sock, _, _) -> ok
|
|
414
|
+
%% (send, Sock, Data, _) -> ok | {error,_}
|
|
415
|
+
%% (recv, Sock, Len, Timeout) -> {ok, Data} | {error,_}
|
|
416
|
+
make_proxy_sock_fun(Proxy) ->
|
|
417
|
+
ProxyHost = binary_to_list(maps:get(<<"host">>, Proxy)),
|
|
418
|
+
ProxyPort = maps:get(<<"port">>, Proxy),
|
|
419
|
+
ProxyType = binary_to_list(maps:get(<<"type">>, Proxy, <<"socks5">>)),
|
|
420
|
+
Username = case maps:get(<<"username">>, Proxy, undefined) of
|
|
421
|
+
undefined -> undefined;
|
|
422
|
+
UBin -> UBin
|
|
423
|
+
end,
|
|
424
|
+
Password = case maps:get(<<"password">>, Proxy, undefined) of
|
|
425
|
+
undefined -> undefined;
|
|
426
|
+
PBin -> PBin
|
|
427
|
+
end,
|
|
428
|
+
fun
|
|
429
|
+
(connect, TargetHost, TargetPort, Timeout) ->
|
|
430
|
+
case gen_tcp:connect(ProxyHost, ProxyPort, [binary, {active, false}, {packet, raw}], Timeout) of
|
|
431
|
+
{error, _} = E -> E;
|
|
432
|
+
{ok, Sock} ->
|
|
433
|
+
case do_proxy_handshake(Sock, ProxyType, TargetHost, TargetPort, Username, Password, Timeout) of
|
|
434
|
+
ok ->
|
|
435
|
+
%% Switch to active mode for async recv
|
|
436
|
+
inet:setopts(Sock, [{active, true}]),
|
|
437
|
+
{ok, {proxy_sock, Sock}};
|
|
438
|
+
{error, _Reason} ->
|
|
439
|
+
gen_tcp:close(Sock),
|
|
440
|
+
{error, proxy_handshake_failed}
|
|
441
|
+
end
|
|
442
|
+
end;
|
|
443
|
+
(close, {proxy_sock, Sock}, _A3, _A4) ->
|
|
444
|
+
gen_tcp:close(Sock),
|
|
445
|
+
ok;
|
|
446
|
+
(send, {proxy_sock, Sock}, Data, _A4) ->
|
|
447
|
+
gen_tcp:send(Sock, Data);
|
|
448
|
+
(recv, {proxy_sock, Sock}, _Len, _Timeout) ->
|
|
449
|
+
receive
|
|
450
|
+
{tcp, Sock, Data} -> {ok, Data};
|
|
451
|
+
{tcp_closed, Sock} -> {error, closed};
|
|
452
|
+
{tcp_error, Sock, _} -> {error, tcp_error}
|
|
453
|
+
after ?CONNECT_TIMEOUT_MS -> {error, timeout}
|
|
454
|
+
end
|
|
455
|
+
end.
|
|
456
|
+
|
|
457
|
+
%% @doc SOCKS5 handshake — RFC 1928/1929
|
|
458
|
+
do_proxy_handshake(Sock, "socks5", TargetHost, TargetPort, Username, Password, Timeout) ->
|
|
459
|
+
%% Step 1: greeting
|
|
460
|
+
AuthMethod = case {Username, Password} of
|
|
461
|
+
{undefined, _} -> 0; % no auth
|
|
462
|
+
_ -> 2 % username/password
|
|
463
|
+
end,
|
|
464
|
+
ok = gen_tcp:send(Sock, <<5, 1, AuthMethod>>),
|
|
465
|
+
case gen_tcp:recv(Sock, 0, Timeout) of
|
|
466
|
+
{ok, <<5, 0>>} when AuthMethod =:= 0 -> ok; % no auth needed
|
|
467
|
+
{ok, <<5, 2>>} when AuthMethod =:= 2 ->
|
|
468
|
+
%% Step 2: username/password auth (RFC 1929)
|
|
469
|
+
ULen = byte_size(Username),
|
|
470
|
+
PLen = byte_size(Password),
|
|
471
|
+
ok = gen_tcp:send(Sock, <<1, ULen, Username/binary, PLen, Password/binary>>),
|
|
472
|
+
case gen_tcp:recv(Sock, 0, Timeout) of
|
|
473
|
+
{ok, <<1, 0>>} -> ok;
|
|
474
|
+
_ -> {error, socks5_auth_failed}
|
|
475
|
+
end;
|
|
476
|
+
_ ->
|
|
477
|
+
{error, socks5_greeting_failed}
|
|
478
|
+
end,
|
|
479
|
+
%% Step 3: connect request
|
|
480
|
+
TargetHostBin = list_to_binary(TargetHost),
|
|
481
|
+
HostBytes = case inet:parse_address(TargetHost) of
|
|
482
|
+
{ok, {A, B, C, D}} -> <<1, A:8, B:8, C:8, D:8>>;
|
|
483
|
+
_ -> <<3, (byte_size(TargetHostBin)):8, TargetHostBin/binary>>
|
|
484
|
+
end,
|
|
485
|
+
ok = gen_tcp:send(Sock, <<5, 1, 0, HostBytes/binary, TargetPort:16>>),
|
|
486
|
+
case gen_tcp:recv(Sock, 0, Timeout) of
|
|
487
|
+
{ok, <<5, 0, _Rest/binary>>} -> ok;
|
|
488
|
+
{ok, <<5, ReplyCode, _Rest/binary>>} -> {error, {socks5_connect_rejected, ReplyCode}};
|
|
489
|
+
_ -> {error, socks5_connect_failed}
|
|
490
|
+
end;
|
|
491
|
+
|
|
492
|
+
%% @doc HTTP CONNECT handshake — RFC 7231
|
|
493
|
+
do_proxy_handshake(Sock, "http", TargetHost, TargetPort, Username, Password, Timeout) ->
|
|
494
|
+
ConnectLine = io_lib:format("CONNECT ~s:~p HTTP/1.1\r\nHost: ~s:~p\r\n",
|
|
495
|
+
[TargetHost, TargetPort, TargetHost, TargetPort]),
|
|
496
|
+
AuthHeader = case {Username, Password} of
|
|
497
|
+
{undefined, _} -> "";
|
|
498
|
+
_ ->
|
|
499
|
+
Creds = base64:encode(<<Username/binary, ":", Password/binary>>),
|
|
500
|
+
io_lib:format("Proxy-Authorization: Basic ~s\r\n", [Creds])
|
|
501
|
+
end,
|
|
502
|
+
Request = iolist_to_binary([ConnectLine, AuthHeader, "\r\n"]),
|
|
503
|
+
ok = gen_tcp:send(Sock, Request),
|
|
504
|
+
case gen_tcp:recv(Sock, 0, Timeout) of
|
|
505
|
+
{ok, Data} ->
|
|
506
|
+
case binary:match(Data, <<"200">>) of
|
|
507
|
+
nomatch -> {error, http_connect_failed};
|
|
508
|
+
_ -> ok
|
|
509
|
+
end;
|
|
510
|
+
_ -> {error, http_connect_timeout}
|
|
511
|
+
end;
|
|
512
|
+
|
|
513
|
+
do_proxy_handshake(_Sock, Unknown, _Host, _Port, _User, _Pass, _Timeout) ->
|
|
514
|
+
{error, {unsupported_proxy_type, Unknown}}.
|
|
515
|
+
|
|
516
|
+
%% ---------- Internal: Helpers ----------
|
|
517
|
+
|
|
518
|
+
%% @doc Get host fingerprint from a connected ssh_ref.
|
|
519
|
+
%% Extracts the peer public key and computes SHA-256 fingerprint.
|
|
520
|
+
get_host_fingerprint(SshRef) ->
|
|
521
|
+
case ssh_connection:hostkey(SshRef) of
|
|
522
|
+
{ok, Key} ->
|
|
523
|
+
ssh_codec:fingerprint(Key);
|
|
524
|
+
{error, _} ->
|
|
525
|
+
<<"SHA256:unknown">>
|
|
526
|
+
end.
|
|
527
|
+
|
|
528
|
+
%% @doc Find a worker by conn_id (scans conn_sup children).
|
|
529
|
+
find_by_conn_id(ConnId) ->
|
|
530
|
+
Match = [Pid || Pid <- ssh_conn_sup:all_workers(),
|
|
531
|
+
conn_id(Pid) =:= ConnId],
|
|
532
|
+
case Match of
|
|
533
|
+
[Pid | _] -> {ok, Pid};
|
|
534
|
+
[] -> {error, not_found}
|
|
535
|
+
end.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{application, ssh_core, [
|
|
2
|
+
{description, "SSH Core Engine - Erlang/OTP ssh application wrapper"},
|
|
3
|
+
{vsn, "1.0.0"},
|
|
4
|
+
{registered, [ssh_core_sup]},
|
|
5
|
+
{mod, {ssh_core_app, []}},
|
|
6
|
+
{applications, [
|
|
7
|
+
kernel,
|
|
8
|
+
stdlib,
|
|
9
|
+
ssh,
|
|
10
|
+
public_key,
|
|
11
|
+
crypto,
|
|
12
|
+
inets
|
|
13
|
+
]},
|
|
14
|
+
{env, []},
|
|
15
|
+
{modules, [
|
|
16
|
+
ssh_core_app,
|
|
17
|
+
ssh_core_sup,
|
|
18
|
+
ssh_infra_sup,
|
|
19
|
+
ssh_conn_sup,
|
|
20
|
+
ssh_sftp_sup,
|
|
21
|
+
ssh_ipc_gateway,
|
|
22
|
+
ssh_ipc_proto,
|
|
23
|
+
ssh_ipc_coalesce,
|
|
24
|
+
ssh_codec,
|
|
25
|
+
ssh_conn_worker,
|
|
26
|
+
ssh_channel_stm,
|
|
27
|
+
ssh_auth_engine,
|
|
28
|
+
ssh_jump_chain,
|
|
29
|
+
ssh_port_fwd,
|
|
30
|
+
ssh_sftp_session,
|
|
31
|
+
ssh_keepalive_mgr,
|
|
32
|
+
ssh_known_hosts_proxy
|
|
33
|
+
]},
|
|
34
|
+
{licenses, ["AGPL-3.0-or-later"]},
|
|
35
|
+
{links, []}
|
|
36
|
+
]}.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-module(ssh_core_sup).
|
|
2
|
+
-behaviour(supervisor).
|
|
3
|
+
|
|
4
|
+
-export([start_link/0, stop/0]).
|
|
5
|
+
-export([init/1]).
|
|
6
|
+
|
|
7
|
+
%% @doc Start the top-level supervisor.
|
|
8
|
+
start_link() ->
|
|
9
|
+
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
|
10
|
+
|
|
11
|
+
%% @doc Graceful shutdown of the entire application.
|
|
12
|
+
stop() ->
|
|
13
|
+
gen_server:stop(?MODULE).
|
|
14
|
+
|
|
15
|
+
%% @doc Top-level supervisor: one_for_one, intensity 5/60s.
|
|
16
|
+
%% Children:
|
|
17
|
+
%% 1. ssh_infra_sup — infrastructure layer (gateway, keepalive, known_hosts)
|
|
18
|
+
%% 2. ssh_conn_sup — dynamic connection workers (simple_one_for_one)
|
|
19
|
+
%% 3. ssh_sftp_sup — dynamic SFTP sessions (simple_one_for_one)
|
|
20
|
+
init([]) ->
|
|
21
|
+
SupFlags = #{
|
|
22
|
+
strategy => one_for_one,
|
|
23
|
+
intensity => 5,
|
|
24
|
+
period => 60
|
|
25
|
+
},
|
|
26
|
+
Children = [
|
|
27
|
+
#{
|
|
28
|
+
id => ssh_infra_sup,
|
|
29
|
+
start => {ssh_infra_sup, start_link, []},
|
|
30
|
+
restart => permanent,
|
|
31
|
+
type => supervisor
|
|
32
|
+
},
|
|
33
|
+
#{
|
|
34
|
+
id => ssh_conn_sup,
|
|
35
|
+
start => {ssh_conn_sup, start_link, []},
|
|
36
|
+
restart => permanent,
|
|
37
|
+
type => supervisor
|
|
38
|
+
},
|
|
39
|
+
#{
|
|
40
|
+
id => ssh_sftp_sup,
|
|
41
|
+
start => {ssh_sftp_sup, start_link, []},
|
|
42
|
+
restart => permanent,
|
|
43
|
+
type => supervisor
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
{ok, {SupFlags, Children}}.
|