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,232 @@
|
|
|
1
|
+
-module(ssh_channel_stm).
|
|
2
|
+
-behaviour(gen_statem).
|
|
3
|
+
|
|
4
|
+
%% Channel state machine — manages a single SSH channel (shell/exec/subsystem).
|
|
5
|
+
%% State machine: opening → ready → flowing → eof → closing → closed
|
|
6
|
+
%%
|
|
7
|
+
%% Key design (LLD §6.5, E9 fix):
|
|
8
|
+
%% - Data pushes go through coalesce buffer, not direct to gateway
|
|
9
|
+
%% - rpc_send/rpc_close/rpc_window_change are public RPC entry points
|
|
10
|
+
%% - Channel is a child of conn_worker (spawn_link), parent death = channel death
|
|
11
|
+
|
|
12
|
+
-export([
|
|
13
|
+
start_link/4,
|
|
14
|
+
rpc_send/1,
|
|
15
|
+
rpc_close/1,
|
|
16
|
+
rpc_window_change/1,
|
|
17
|
+
feed_data/2,
|
|
18
|
+
notify_eof/2
|
|
19
|
+
]).
|
|
20
|
+
-export([callback_mode/0, init/1]).
|
|
21
|
+
-export([opening/3, ready/3, flowing/3, eof/3, closed/3]).
|
|
22
|
+
|
|
23
|
+
-include("ssh_ipc.hrl").
|
|
24
|
+
|
|
25
|
+
%% ---------- Public API ----------
|
|
26
|
+
|
|
27
|
+
%% @doc Start a channel state machine.
|
|
28
|
+
start_link(ChId, ConnId, SshRef, Opts) ->
|
|
29
|
+
gen_statem:start_link(?MODULE, [ChId, ConnId, SshRef, Opts], []).
|
|
30
|
+
|
|
31
|
+
%% @doc RPC handler for channel.send
|
|
32
|
+
rpc_send(#{<<"id">> := ChId, <<"data">> := B64Data}) ->
|
|
33
|
+
case find_by_ch_id(ChId) of
|
|
34
|
+
{ok, Pid} ->
|
|
35
|
+
{ok, Data} = ssh_codec:decode_b64(B64Data),
|
|
36
|
+
gen_statem:call(Pid, {send, Data});
|
|
37
|
+
{error, not_found} ->
|
|
38
|
+
{error, channel_not_found}
|
|
39
|
+
end.
|
|
40
|
+
|
|
41
|
+
%% @doc RPC handler for channel.close
|
|
42
|
+
rpc_close(#{<<"id">> := ChId}) ->
|
|
43
|
+
case find_by_ch_id(ChId) of
|
|
44
|
+
{ok, Pid} ->
|
|
45
|
+
gen_statem:call(Pid, close);
|
|
46
|
+
{error, not_found} ->
|
|
47
|
+
{error, channel_not_found}
|
|
48
|
+
end.
|
|
49
|
+
|
|
50
|
+
%% @doc RPC handler for channel.window_change
|
|
51
|
+
rpc_window_change(#{<<"id">> := ChId, <<"cols">> := Cols, <<"rows">> := Rows}) ->
|
|
52
|
+
case find_by_ch_id(ChId) of
|
|
53
|
+
{ok, Pid} ->
|
|
54
|
+
gen_statem:call(Pid, {window_change, Cols, Rows});
|
|
55
|
+
{error, not_found} ->
|
|
56
|
+
{error, channel_not_found}
|
|
57
|
+
end.
|
|
58
|
+
|
|
59
|
+
%% @doc Feed data from SSH layer into the channel (called by conn_worker).
|
|
60
|
+
feed_data(Pid, Data) ->
|
|
61
|
+
gen_statem:cast(Pid, {data, Data}).
|
|
62
|
+
|
|
63
|
+
%% @doc Notify channel of EOF from SSH layer.
|
|
64
|
+
notify_eof(Pid, Reason) ->
|
|
65
|
+
gen_statem:cast(Pid, {eof, Reason}).
|
|
66
|
+
|
|
67
|
+
%% ---------- gen_statem callbacks ----------
|
|
68
|
+
|
|
69
|
+
callback_mode() -> state_functions.
|
|
70
|
+
|
|
71
|
+
init([ChId, ConnId, SshRef, Opts]) ->
|
|
72
|
+
Data = #ch{
|
|
73
|
+
id = ChId,
|
|
74
|
+
conn_id = ConnId,
|
|
75
|
+
ssh_ref = SshRef,
|
|
76
|
+
ssh_chan_id = 0, %% assigned after ssh_connection:session_channel
|
|
77
|
+
type = maps:get(type, Opts, shell),
|
|
78
|
+
term_type = maps:get(term_type, Opts, <<"xterm-256color">>),
|
|
79
|
+
cols = maps:get(cols, Opts, 80),
|
|
80
|
+
rows = maps:get(rows, Opts, 24),
|
|
81
|
+
command = maps:get(command, Opts, undefined),
|
|
82
|
+
coalesce_ref = whereis(ssh_ipc_coalesce)
|
|
83
|
+
},
|
|
84
|
+
{ok, opening, Data, [{next_event, internal, open}]}.
|
|
85
|
+
|
|
86
|
+
%% ---------- State: opening ----------
|
|
87
|
+
|
|
88
|
+
opening(internal, open, #ch{ssh_ref = Ref, cols = Cols, rows = Rows,
|
|
89
|
+
term_type = TermType, type = Type,
|
|
90
|
+
command = Cmd} = D) ->
|
|
91
|
+
%% ssh_connection:session_channel returns {ok, ChanId} | {error, Reason}
|
|
92
|
+
case ssh_connection:session_channel(Ref, ?CONNECT_TIMEOUT_MS) of
|
|
93
|
+
{ok, ChanId} ->
|
|
94
|
+
D1 = D#ch{ssh_chan_id = ChanId},
|
|
95
|
+
ok = init_channel_type(Ref, ChanId, Type, TermType, Cols, Rows, Cmd),
|
|
96
|
+
{next_state, ready, D1};
|
|
97
|
+
{error, Reason} ->
|
|
98
|
+
ssh_ipc_gateway:push_event(<<"channel.eof">>, #{
|
|
99
|
+
<<"id">> => D#ch.id,
|
|
100
|
+
<<"reason">> => atom_to_binary(Reason, utf8)
|
|
101
|
+
}),
|
|
102
|
+
{next_state, closed, D}
|
|
103
|
+
end;
|
|
104
|
+
|
|
105
|
+
opening({call, From}, _Msg, _D) ->
|
|
106
|
+
{keep_state_and_data, [{reply, From, {error, not_ready}}]}.
|
|
107
|
+
|
|
108
|
+
%% ---------- State: ready ----------
|
|
109
|
+
|
|
110
|
+
ready({call, From}, {send, Data}, #ch{ssh_ref = Ref, ssh_chan_id = ChanId, conn_id = ConnId} = D) ->
|
|
111
|
+
ok = ssh_connection:send(Ref, ChanId, Data),
|
|
112
|
+
notify_conn_activity(ConnId),
|
|
113
|
+
{keep_state_and_data, [{reply, From, ok}]};
|
|
114
|
+
|
|
115
|
+
ready({call, From}, {window_change, Cols, Rows}, #ch{ssh_ref = Ref, ssh_chan_id = ChanId, term_type = TermType} = D) ->
|
|
116
|
+
case TermType of
|
|
117
|
+
undefined -> ok;
|
|
118
|
+
_ -> ssh_connection:ptty_alloc(Ref, ChanId, [{Cols, Rows, 0, 0, TermType}])
|
|
119
|
+
end,
|
|
120
|
+
ok = ssh_connection:window_change(Ref, ChanId, Cols, Rows),
|
|
121
|
+
{keep_state, D#ch{cols = Cols, rows = Rows}, [{reply, From, ok}]};
|
|
122
|
+
|
|
123
|
+
ready({call, From}, close, #ch{ssh_ref = Ref, ssh_chan_id = ChanId} = D) ->
|
|
124
|
+
ssh_connection:close(Ref, ChanId),
|
|
125
|
+
{next_state, closed, D, [{reply, From, ok}]};
|
|
126
|
+
|
|
127
|
+
ready(cast, {data, Data}, #ch{conn_id = ConnId} = D) ->
|
|
128
|
+
notify_conn_activity(ConnId),
|
|
129
|
+
push_data(D, Data),
|
|
130
|
+
{next_state, flowing, D};
|
|
131
|
+
|
|
132
|
+
ready(cast, {eof, Reason}, #ch{id = Id} = D) ->
|
|
133
|
+
ssh_ipc_gateway:push_event(<<"channel.eof">>, #{
|
|
134
|
+
<<"id">> => Id,
|
|
135
|
+
<<"reason">> => atom_to_binary(Reason, utf8)
|
|
136
|
+
}),
|
|
137
|
+
{next_state, eof, D}.
|
|
138
|
+
|
|
139
|
+
%% ---------- State: flowing ----------
|
|
140
|
+
|
|
141
|
+
flowing({call, From}, {send, Data}, #ch{ssh_ref = Ref, ssh_chan_id = ChanId, conn_id = ConnId} = D) ->
|
|
142
|
+
ok = ssh_connection:send(Ref, ChanId, Data),
|
|
143
|
+
notify_conn_activity(ConnId),
|
|
144
|
+
{keep_state_and_data, [{reply, From, ok}]};
|
|
145
|
+
|
|
146
|
+
flowing(cast, {data, Data}, #ch{conn_id = ConnId} = D) ->
|
|
147
|
+
notify_conn_activity(ConnId),
|
|
148
|
+
push_data(D, Data),
|
|
149
|
+
{next_state, flowing, D};
|
|
150
|
+
|
|
151
|
+
flowing(cast, {eof, Reason}, #ch{id = Id} = D) ->
|
|
152
|
+
ssh_ipc_gateway:push_event(<<"channel.eof">>, #{
|
|
153
|
+
<<"id">> => Id,
|
|
154
|
+
<<"reason">> => atom_to_binary(Reason, utf8)
|
|
155
|
+
}),
|
|
156
|
+
{next_state, eof, D};
|
|
157
|
+
|
|
158
|
+
flowing({call, From}, close, #ch{ssh_ref = Ref, ssh_chan_id = ChanId} = D) ->
|
|
159
|
+
ssh_connection:close(Ref, ChanId),
|
|
160
|
+
{next_state, closed, D, [{reply, From, ok}]};
|
|
161
|
+
|
|
162
|
+
flowing({call, From}, {window_change, Cols, Rows}, #ch{ssh_ref = Ref, ssh_chan_id = ChanId} = D) ->
|
|
163
|
+
ssh_connection:window_change(Ref, ChanId, Cols, Rows),
|
|
164
|
+
{keep_state, D#ch{cols = Cols, rows = Rows}, [{reply, From, ok}]}.
|
|
165
|
+
|
|
166
|
+
%% ---------- State: eof ----------
|
|
167
|
+
|
|
168
|
+
eof({call, From}, close, #ch{ssh_ref = Ref, ssh_chan_id = ChanId} = D) ->
|
|
169
|
+
ssh_connection:close(Ref, ChanId),
|
|
170
|
+
{next_state, closed, D, [{reply, From, ok}]};
|
|
171
|
+
|
|
172
|
+
eof({call, From}, _Msg, _D) ->
|
|
173
|
+
{keep_state_and_data, [{reply, From, {error, eof}}]}.
|
|
174
|
+
|
|
175
|
+
%% ---------- State: closed ----------
|
|
176
|
+
|
|
177
|
+
closed({call, From}, _Msg, _D) ->
|
|
178
|
+
{keep_state_and_data, [{reply, From, {error, closed}}]}.
|
|
179
|
+
|
|
180
|
+
%% ---------- Internal ----------
|
|
181
|
+
|
|
182
|
+
%% @doc Notify keepalive manager that activity occurred on this connection.
|
|
183
|
+
%% This resets the keepalive fail counter and postpones the next keepalive check.
|
|
184
|
+
notify_conn_activity(ConnId) ->
|
|
185
|
+
case find_conn_worker_by_id(ConnId) of
|
|
186
|
+
{ok, Pid} ->
|
|
187
|
+
ssh_keepalive_mgr:notify_activity(Pid);
|
|
188
|
+
{error, _} ->
|
|
189
|
+
ok
|
|
190
|
+
end.
|
|
191
|
+
|
|
192
|
+
%% @doc Find a connection worker by conn_id.
|
|
193
|
+
find_conn_worker_by_id(ConnId) ->
|
|
194
|
+
Match = [Pid || Pid <- ssh_conn_sup:all_workers(),
|
|
195
|
+
catch ssh_conn_worker:conn_id(Pid) =:= ConnId],
|
|
196
|
+
case Match of
|
|
197
|
+
[Pid | _] -> {ok, Pid};
|
|
198
|
+
[] -> {error, not_found}
|
|
199
|
+
end.
|
|
200
|
+
|
|
201
|
+
%% @doc Initialize channel type (shell, exec, subsystem).
|
|
202
|
+
init_channel_type(Ref, ChanId, shell, TermType, Cols, Rows, _Cmd) ->
|
|
203
|
+
%% OTP 17 ptty_alloc expects a list containing one {Cols, Rows, PixW, PixH, TermType} tuple
|
|
204
|
+
ok = ssh_connection:ptty_alloc(Ref, ChanId, [{Cols, Rows, 0, 0, TermType}]),
|
|
205
|
+
ok = ssh_connection:shell(Ref, ChanId);
|
|
206
|
+
init_channel_type(Ref, ChanId, exec, _TermType, _Cols, _Rows, Cmd) when is_binary(Cmd) ->
|
|
207
|
+
ok = ssh_connection:exec(Ref, ChanId, binary_to_list(Cmd), ?CONNECT_TIMEOUT_MS);
|
|
208
|
+
init_channel_type(Ref, ChanId, subsystem, _TermType, _Cols, _Rows, Subsystem) when is_binary(Subsystem) ->
|
|
209
|
+
ok = ssh_connection:subsystem(Ref, ChanId, binary_to_list(Subsystem), ?CONNECT_TIMEOUT_MS);
|
|
210
|
+
init_channel_type(_, _, _, _, _, _, _) ->
|
|
211
|
+
ok.
|
|
212
|
+
|
|
213
|
+
%% @doc Push data through coalesce buffer (E9 fix).
|
|
214
|
+
push_data(#ch{coalesce_ref = CoalesceRef, id = ChId}, Data) ->
|
|
215
|
+
case CoalesceRef of
|
|
216
|
+
undefined ->
|
|
217
|
+
%% Fallback: direct push (degraded mode)
|
|
218
|
+
ssh_ipc_gateway:push_event(<<"channel.data">>, #{
|
|
219
|
+
<<"id">> => ChId,
|
|
220
|
+
<<"data">> => ssh_codec:encode_b64(Data)
|
|
221
|
+
});
|
|
222
|
+
Ref ->
|
|
223
|
+
ssh_ipc_coalesce:enqueue(Ref, ChId, Data)
|
|
224
|
+
end.
|
|
225
|
+
|
|
226
|
+
%% @doc Find a channel process by channel_id.
|
|
227
|
+
%% Uses ETS index (ch_index) maintained by conn_worker on channel open/close.
|
|
228
|
+
find_by_ch_id(ChId) ->
|
|
229
|
+
case ets:lookup(ch_index, ChId) of
|
|
230
|
+
[{_, Pid}] -> {ok, Pid};
|
|
231
|
+
[] -> {error, not_found}
|
|
232
|
+
end.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
-module(ssh_codec).
|
|
2
|
+
%% Shared encoding/decoding utilities: Base64, JSON, fingerprint, ID generation.
|
|
3
|
+
|
|
4
|
+
-export([
|
|
5
|
+
encode_json/1,
|
|
6
|
+
decode_json/1,
|
|
7
|
+
encode_b64/1,
|
|
8
|
+
decode_b64/1,
|
|
9
|
+
unix_ms/0,
|
|
10
|
+
gen_conn_id/0,
|
|
11
|
+
gen_channel_id/1,
|
|
12
|
+
gen_sftp_id/1,
|
|
13
|
+
gen_rule_id/1,
|
|
14
|
+
fingerprint/1
|
|
15
|
+
]).
|
|
16
|
+
|
|
17
|
+
%% @doc Encode a map to JSON binary (via jsx).
|
|
18
|
+
-spec encode_json(map() | list()) -> binary().
|
|
19
|
+
encode_json(Term) ->
|
|
20
|
+
jsx:encode(Term).
|
|
21
|
+
|
|
22
|
+
%% @doc Decode a JSON binary to map.
|
|
23
|
+
-spec decode_json(binary()) -> {ok, map()} | {error, term()}.
|
|
24
|
+
decode_json(Bin) ->
|
|
25
|
+
try jsx:decode(Bin, [return_maps]) of
|
|
26
|
+
Map -> {ok, Map}
|
|
27
|
+
catch
|
|
28
|
+
_:_ -> {error, parse_error}
|
|
29
|
+
end.
|
|
30
|
+
|
|
31
|
+
%% @doc Base64 encode binary data.
|
|
32
|
+
-spec encode_b64(binary()) -> binary().
|
|
33
|
+
encode_b64(Data) ->
|
|
34
|
+
base64:encode(Data).
|
|
35
|
+
|
|
36
|
+
%% @doc Base64 decode a binary or string.
|
|
37
|
+
-spec decode_b64(binary() | string()) -> {ok, binary()} | {error, term()}.
|
|
38
|
+
decode_b64(Data) ->
|
|
39
|
+
try {ok, base64:decode(Data)}
|
|
40
|
+
catch _:_ -> {error, bad_b64} end.
|
|
41
|
+
|
|
42
|
+
%% @doc Current Unix timestamp in milliseconds.
|
|
43
|
+
-spec unix_ms() -> integer().
|
|
44
|
+
unix_ms() ->
|
|
45
|
+
erlang:system_time(millisecond).
|
|
46
|
+
|
|
47
|
+
%% @doc Generate a connection ID: conn_<unix_ms>_<8hex>
|
|
48
|
+
-spec gen_conn_id() -> binary().
|
|
49
|
+
gen_conn_id() ->
|
|
50
|
+
Ts = integer_to_binary(unix_ms()),
|
|
51
|
+
Rand = binary:encode_hex(crypto:strong_rand_bytes(4)),
|
|
52
|
+
<<"conn_", Ts/binary, "_", Rand/binary>>.
|
|
53
|
+
|
|
54
|
+
%% @doc Generate a channel ID within a connection.
|
|
55
|
+
-spec gen_channel_id(binary()) -> binary().
|
|
56
|
+
gen_channel_id(ConnId) ->
|
|
57
|
+
ConnShort = binary:part(ConnId, {0, erlang:min(byte_size(ConnId), 16)}),
|
|
58
|
+
Seq = integer_to_binary(unix_ms() rem 100000),
|
|
59
|
+
<<"ch_", ConnShort/binary, "_", Seq/binary>>.
|
|
60
|
+
|
|
61
|
+
%% @doc Generate an SFTP session ID.
|
|
62
|
+
-spec gen_sftp_id(binary()) -> binary().
|
|
63
|
+
gen_sftp_id(ConnId) ->
|
|
64
|
+
ConnShort = binary:part(ConnId, {0, erlang:min(byte_size(ConnId), 16)}),
|
|
65
|
+
Ts = integer_to_binary(unix_ms()),
|
|
66
|
+
<<"sftp_", ConnShort/binary, "_", Ts/binary>>.
|
|
67
|
+
|
|
68
|
+
%% @doc Generate a port-forward rule ID.
|
|
69
|
+
-spec gen_rule_id(binary()) -> binary().
|
|
70
|
+
gen_rule_id(ConnId) ->
|
|
71
|
+
ConnShort = binary:part(ConnId, {0, erlang:min(byte_size(ConnId), 16)}),
|
|
72
|
+
<<"fwd_", ConnShort/binary>>.
|
|
73
|
+
|
|
74
|
+
%% @doc Compute SHA-256 fingerprint of a public key in OpenSSH format.
|
|
75
|
+
%% Returns <<"SHA256:base64...">>
|
|
76
|
+
%%
|
|
77
|
+
%% In OTP < 24, public_key:ssh_encode/2 was available, but OTP 24+ moved the
|
|
78
|
+
%% encoding into ssh_message:ssh2_pubkey_encode/1. We use ssh:hostkey_fingerprint/2
|
|
79
|
+
%% which wraps that and returns the canonical "SHA256:..." string.
|
|
80
|
+
-spec fingerprint(term()) -> binary().
|
|
81
|
+
fingerprint(PublicKey) ->
|
|
82
|
+
%% ssh:hostkey_fingerprint/2 returns a string() like "SHA256:base64..."
|
|
83
|
+
list_to_binary(ssh:hostkey_fingerprint(sha256, PublicKey)).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-module(ssh_conn_sup).
|
|
2
|
+
-behaviour(supervisor).
|
|
3
|
+
|
|
4
|
+
-export([start_link/0, start_child/1, all_workers/0, rpc_connect/1, rpc_list/1]).
|
|
5
|
+
-export([init/1]).
|
|
6
|
+
|
|
7
|
+
%% @doc Start the connection pool supervisor (simple_one_for_one).
|
|
8
|
+
start_link() ->
|
|
9
|
+
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
|
10
|
+
|
|
11
|
+
%% @doc Start a new connection worker.
|
|
12
|
+
%% Spec is the connect_spec() map from the Ruby side.
|
|
13
|
+
start_child(Spec) ->
|
|
14
|
+
supervisor:start_child(?MODULE, [Spec]).
|
|
15
|
+
|
|
16
|
+
%% @doc List all active connection workers (pid list).
|
|
17
|
+
all_workers() ->
|
|
18
|
+
[Pid || {_, Pid, _, _} <- supervisor:which_children(?MODULE)].
|
|
19
|
+
|
|
20
|
+
%% @doc RPC handler for conn.connect
|
|
21
|
+
rpc_connect(Params) ->
|
|
22
|
+
{ok, Pid} = start_child(Params),
|
|
23
|
+
%% Triggers the connection sequence, returns {ok, ConnId} | {error, Reason}
|
|
24
|
+
ssh_conn_worker:do_connect(Pid).
|
|
25
|
+
|
|
26
|
+
%% @doc RPC handler for conn.list
|
|
27
|
+
rpc_list(_Params) ->
|
|
28
|
+
lists:map(fun(Pid) ->
|
|
29
|
+
#{id => ssh_conn_worker:conn_id(Pid),
|
|
30
|
+
state => ssh_conn_worker:get_state_name(Pid)}
|
|
31
|
+
end, all_workers()).
|
|
32
|
+
|
|
33
|
+
%% @doc simple_one_for_one, intensity 100/60s, temporary workers.
|
|
34
|
+
init([]) ->
|
|
35
|
+
SupFlags = #{
|
|
36
|
+
strategy => simple_one_for_one,
|
|
37
|
+
intensity => 100,
|
|
38
|
+
period => 60
|
|
39
|
+
},
|
|
40
|
+
ChildSpec = #{
|
|
41
|
+
id => ssh_conn_worker,
|
|
42
|
+
start => {ssh_conn_worker, start_link, []},
|
|
43
|
+
restart => temporary,
|
|
44
|
+
shutdown => 10000,
|
|
45
|
+
type => worker,
|
|
46
|
+
modules => [ssh_conn_worker]
|
|
47
|
+
},
|
|
48
|
+
{ok, {SupFlags, [ChildSpec]}}.
|