network-infra-utility 0.3.0 → 0.6.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/GUIDE.md +212 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +70 -0
- data/Rakefile +1 -1
- data/bin/dns-query +836 -0
- data/bin/geo-doc +135 -0
- data/bin/geo-get +1 -1
- data/bin/geo-update +429 -0
- 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 +955 -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 +117 -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 +142 -1
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
-module(ssh_ipc_gateway).
|
|
2
|
+
-behaviour(gen_server).
|
|
3
|
+
|
|
4
|
+
%% IPC Gateway: listens on Unix Socket/TCP, accepts Ruby connections,
|
|
5
|
+
%% authenticates via token, routes JSON-RPC requests, and pushes events.
|
|
6
|
+
%%
|
|
7
|
+
%% Key design:
|
|
8
|
+
%% - Routes are immutable after init
|
|
9
|
+
%% - push_event/2 writes to coalesce buffer or direct push
|
|
10
|
+
%% - push_batch/1 sends already-batched data directly
|
|
11
|
+
%% - synchronous_push/2 blocks for Erlang→Ruby reverse RPC (hostkey.resolve)
|
|
12
|
+
%% - Backpressure: when client send_q exceeds threshold, flow pauses
|
|
13
|
+
|
|
14
|
+
-export([
|
|
15
|
+
start_link/1,
|
|
16
|
+
push_event/2,
|
|
17
|
+
push_batch/1,
|
|
18
|
+
synchronous_push/2
|
|
19
|
+
]).
|
|
20
|
+
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
|
|
21
|
+
|
|
22
|
+
%% Internal RPC handlers
|
|
23
|
+
-export([
|
|
24
|
+
handle_bye/1,
|
|
25
|
+
handle_ping/1,
|
|
26
|
+
handle_stats/1,
|
|
27
|
+
handle_shutdown/1
|
|
28
|
+
]).
|
|
29
|
+
|
|
30
|
+
-include("ssh_ipc.hrl").
|
|
31
|
+
|
|
32
|
+
%% ---------- Public API ----------
|
|
33
|
+
|
|
34
|
+
%% @doc Start the gateway. EndpointToken contains auth token and endpoint file path.
|
|
35
|
+
start_link(#{token := Token} = _EndpointToken) ->
|
|
36
|
+
gen_server:start_link({local, ?MODULE}, ?MODULE, [Token], []).
|
|
37
|
+
|
|
38
|
+
%% @doc Push a single event through coalesce.
|
|
39
|
+
push_event(Method, Params) ->
|
|
40
|
+
gen_server:cast(?MODULE, {push_event, Method, Params}).
|
|
41
|
+
|
|
42
|
+
%% @doc Push an already-batched notification directly.
|
|
43
|
+
push_batch(Params) ->
|
|
44
|
+
gen_server:cast(?MODULE, {push_batch, Params}).
|
|
45
|
+
|
|
46
|
+
%% @doc Synchronous reverse RPC (Erlang→Ruby with id). Blocks until response.
|
|
47
|
+
%% Used by ssh_known_hosts_proxy for hostkey.resolve.
|
|
48
|
+
synchronous_push(Req, TimeoutMs) ->
|
|
49
|
+
gen_server:call(?MODULE, {sync_push, Req, TimeoutMs}, TimeoutMs + 1000).
|
|
50
|
+
|
|
51
|
+
%% ---------- Callbacks ----------
|
|
52
|
+
|
|
53
|
+
init([Token]) ->
|
|
54
|
+
{ok, ListenSock} = start_listener(),
|
|
55
|
+
Routes = build_routes(),
|
|
56
|
+
%% Store auth token in process dictionary for handle_hello/1 access
|
|
57
|
+
put(auth_token, Token),
|
|
58
|
+
%% Start async accept loop
|
|
59
|
+
prim_inet:async_accept(ListenSock, -1),
|
|
60
|
+
{ok, #gw_state{
|
|
61
|
+
listen_sock = ListenSock,
|
|
62
|
+
auth_token = Token,
|
|
63
|
+
routes = Routes
|
|
64
|
+
}}.
|
|
65
|
+
|
|
66
|
+
handle_call({sync_push, Req, TimeoutMs}, From, S) ->
|
|
67
|
+
%% Send a reverse request (with id) to Ruby and wait for response
|
|
68
|
+
case maps:size(S#gw_state.clients) of
|
|
69
|
+
0 ->
|
|
70
|
+
{reply, {error, no_clients}, S};
|
|
71
|
+
_ ->
|
|
72
|
+
ReverseId = generate_reverse_id(),
|
|
73
|
+
Msg = ssh_ipc_proto:frame(
|
|
74
|
+
ssh_ipc_proto:encode_request(ReverseId,
|
|
75
|
+
maps:get(<<"method">>, Req),
|
|
76
|
+
maps:get(<<"params">>, Req))
|
|
77
|
+
),
|
|
78
|
+
case send_to_all_clients(S, Msg) of
|
|
79
|
+
ok ->
|
|
80
|
+
%% Store pending for reverse response matching
|
|
81
|
+
Ref = erlang:make_ref(),
|
|
82
|
+
erlang:send_after(TimeoutMs, self(), {sync_push_timeout, Ref}),
|
|
83
|
+
{noreply, store_reverse_pending(S, ReverseId, From, Ref)};
|
|
84
|
+
{error, no_clients} ->
|
|
85
|
+
{reply, {error, no_clients}, S}
|
|
86
|
+
end
|
|
87
|
+
end;
|
|
88
|
+
|
|
89
|
+
handle_call(_Req, _From, S) ->
|
|
90
|
+
{reply, {error, not_implemented}, S}.
|
|
91
|
+
|
|
92
|
+
handle_cast({push_event, Method, Params}, S) ->
|
|
93
|
+
send_notification(S, Method, Params),
|
|
94
|
+
{noreply, S};
|
|
95
|
+
|
|
96
|
+
handle_cast({push_batch, Params}, S) ->
|
|
97
|
+
send_notification(S, <<"channel.data.batch">>, Params),
|
|
98
|
+
{noreply, S};
|
|
99
|
+
|
|
100
|
+
handle_cast(_Msg, S) ->
|
|
101
|
+
{noreply, S}.
|
|
102
|
+
|
|
103
|
+
handle_info({tcp, Socket, Data}, S) ->
|
|
104
|
+
{Frames, Rest} = ssh_ipc_proto:unframe(Data),
|
|
105
|
+
S1 = lists:foldl(fun(Frame, State) -> handle_frame(Socket, Frame, State) end, S, Frames),
|
|
106
|
+
%% Keep remaining buffer per-socket if partial frame
|
|
107
|
+
case Rest of
|
|
108
|
+
<<>> -> ok;
|
|
109
|
+
_ -> ok %% Partial data stored on next recv — simplification
|
|
110
|
+
end,
|
|
111
|
+
inet:setopts(Socket, [{active, once}]),
|
|
112
|
+
{noreply, S1};
|
|
113
|
+
|
|
114
|
+
handle_info({tcp_closed, Socket}, S) ->
|
|
115
|
+
S1 = remove_client(S, Socket),
|
|
116
|
+
{noreply, S1};
|
|
117
|
+
|
|
118
|
+
handle_info({tcp_error, Socket, _Reason}, S) ->
|
|
119
|
+
S1 = remove_client(S, Socket),
|
|
120
|
+
{noreply, S1};
|
|
121
|
+
|
|
122
|
+
handle_info({sync_push_timeout, Ref}, S) ->
|
|
123
|
+
S1 = handle_reverse_timeout(S, Ref),
|
|
124
|
+
{noreply, S1};
|
|
125
|
+
|
|
126
|
+
handle_info({inet_async, ListenSock, _Ref, {ok, ClientSock}}, #gw_state{listen_sock = ListenSock} = S) ->
|
|
127
|
+
%% Accept new client connection
|
|
128
|
+
inet_db:register_socket(ClientSock, inet_tcp),
|
|
129
|
+
inet:setopts(ClientSock, [{active, once}, {packet, 0}, binary]),
|
|
130
|
+
Ref = make_ref(),
|
|
131
|
+
Client = #client{socket = ClientSock, authed = false},
|
|
132
|
+
Clients = maps:put(Ref, Client, S#gw_state.clients),
|
|
133
|
+
%% Continue accepting
|
|
134
|
+
prim_inet:async_accept(ListenSock, -1),
|
|
135
|
+
{noreply, S#gw_state{clients = Clients}};
|
|
136
|
+
|
|
137
|
+
handle_info({mark_authed, Socket}, S) ->
|
|
138
|
+
%% Mark the client with matching socket as authenticated
|
|
139
|
+
Clients = maps:map(fun(_Ref, #client{socket = Sock} = C) ->
|
|
140
|
+
case Sock =:= Socket of
|
|
141
|
+
true -> C#client{authed = true};
|
|
142
|
+
false -> C
|
|
143
|
+
end
|
|
144
|
+
end, S#gw_state.clients),
|
|
145
|
+
{noreply, S#gw_state{clients = Clients}};
|
|
146
|
+
|
|
147
|
+
handle_info({inet_async, ListenSock, _Ref, {error, _Reason}}, #gw_state{listen_sock = ListenSock} = S) ->
|
|
148
|
+
%% Accept failed, retry
|
|
149
|
+
prim_inet:async_accept(ListenSock, -1),
|
|
150
|
+
{noreply, S};
|
|
151
|
+
|
|
152
|
+
handle_info(_Info, S) ->
|
|
153
|
+
{noreply, S}.
|
|
154
|
+
|
|
155
|
+
terminate(_Reason, #gw_state{listen_sock = ListenSock}) ->
|
|
156
|
+
_ = close_listener(ListenSock),
|
|
157
|
+
ok.
|
|
158
|
+
|
|
159
|
+
%% ---------- Internal: Listener ----------
|
|
160
|
+
|
|
161
|
+
start_listener() ->
|
|
162
|
+
EndpointFile = ssh_infra_sup:generate_endpoint_file(),
|
|
163
|
+
case os:type() of
|
|
164
|
+
{win32, _} ->
|
|
165
|
+
%% On Windows: listen on port 0 (OS assigns free port), then
|
|
166
|
+
%% rewrite endpoint file with the actual port assigned.
|
|
167
|
+
{ok, ListenSock} = gen_tcp:listen(0, [
|
|
168
|
+
{ip, {127, 0, 0, 1}},
|
|
169
|
+
{active, false},
|
|
170
|
+
{packet, 0},
|
|
171
|
+
{reuseaddr, true},
|
|
172
|
+
binary
|
|
173
|
+
]),
|
|
174
|
+
{ok, Port} = inet:port(ListenSock),
|
|
175
|
+
%% Rewrite endpoint file with the actual port
|
|
176
|
+
{ok, EndpointContent} = file:read_file(EndpointFile),
|
|
177
|
+
[_FirstLine, TokenBin] = binary:split(EndpointContent, <<"\n">>),
|
|
178
|
+
NewEndpoint = <<"tcp://127.0.0.1:", (integer_to_binary(Port))/binary>>,
|
|
179
|
+
file:write_file(EndpointFile, <<NewEndpoint/binary, "\n", TokenBin/binary>>),
|
|
180
|
+
{ok, ListenSock};
|
|
181
|
+
_ ->
|
|
182
|
+
Path = read_socket_path_from_file(EndpointFile),
|
|
183
|
+
_ = file:delete(Path),
|
|
184
|
+
%% On Unix, use gen_tcp on loopback as portable fallback
|
|
185
|
+
{ok, ListenSock} = gen_tcp:listen(0, [{ip, {127, 0, 0, 1}}, {active, false}, {packet, 0}, binary]),
|
|
186
|
+
{ok, Port} = inet:port(ListenSock),
|
|
187
|
+
%% Rewrite endpoint file with TCP port for Ruby to pick up
|
|
188
|
+
{ok, Endpoint} = file:read_file(EndpointFile),
|
|
189
|
+
[_, TokenBin] = binary:split(Endpoint, <<"\n">>),
|
|
190
|
+
file:write_file(EndpointFile, <<"tcp://127.0.0.1:", (integer_to_binary(Port))/binary, "\n", TokenBin/binary>>),
|
|
191
|
+
{ok, ListenSock}
|
|
192
|
+
end.
|
|
193
|
+
|
|
194
|
+
select_free_port() ->
|
|
195
|
+
{ok, Sock} = gen_tcp:listen(0, [{ip, {127, 0, 0, 1}}]),
|
|
196
|
+
{ok, Port} = inet:port(Sock),
|
|
197
|
+
gen_tcp:close(Sock),
|
|
198
|
+
{ok, Port}.
|
|
199
|
+
|
|
200
|
+
close_listener(Sock) ->
|
|
201
|
+
_ = (catch gen_tcp:close(Sock)),
|
|
202
|
+
ok.
|
|
203
|
+
|
|
204
|
+
%% ---------- Internal: Routing ----------
|
|
205
|
+
|
|
206
|
+
build_routes() ->
|
|
207
|
+
#{
|
|
208
|
+
<<"bye">> => {?MODULE, handle_bye},
|
|
209
|
+
<<"engine.ping">> => {?MODULE, handle_ping},
|
|
210
|
+
<<"engine.stats">> => {ssh_ipc_gateway, handle_stats},
|
|
211
|
+
<<"engine.shutdown">> => {?MODULE, handle_shutdown},
|
|
212
|
+
<<"conn.connect">> => {ssh_conn_sup, rpc_connect},
|
|
213
|
+
<<"conn.disconnect">> => {ssh_conn_worker, rpc_disconnect},
|
|
214
|
+
<<"conn.list">> => {ssh_conn_sup, rpc_list},
|
|
215
|
+
<<"conn.reconnect">> => {ssh_conn_worker, rpc_reconnect},
|
|
216
|
+
<<"channel.open">> => {ssh_conn_worker, rpc_channel_open},
|
|
217
|
+
<<"channel.send">> => {ssh_channel_stm, rpc_send},
|
|
218
|
+
<<"channel.close">> => {ssh_channel_stm, rpc_close},
|
|
219
|
+
<<"channel.window_change">> => {ssh_channel_stm, rpc_window_change},
|
|
220
|
+
<<"sftp.open">> => {ssh_sftp_sup, rpc_open},
|
|
221
|
+
<<"sftp.list_dir">> => {ssh_sftp_session, rpc_list_dir},
|
|
222
|
+
<<"sftp.download">> => {ssh_sftp_session, rpc_download},
|
|
223
|
+
<<"sftp.upload">> => {ssh_sftp_session, rpc_upload},
|
|
224
|
+
<<"sftp.mkdir">> => {ssh_sftp_session, rpc_mkdir},
|
|
225
|
+
<<"sftp.remove">> => {ssh_sftp_session, rpc_remove},
|
|
226
|
+
<<"sftp.stat">> => {ssh_sftp_session, rpc_stat},
|
|
227
|
+
<<"portfwd.add">> => {ssh_port_fwd, rpc_add},
|
|
228
|
+
<<"portfwd.remove">> => {ssh_port_fwd, rpc_remove},
|
|
229
|
+
<<"portfwd.list">> => {ssh_port_fwd, rpc_list},
|
|
230
|
+
<<"keepalive.set_interval">> => {ssh_keepalive_mgr, rpc_set_interval},
|
|
231
|
+
<<"keepalive.get_interval">> => {ssh_keepalive_mgr, rpc_get_interval},
|
|
232
|
+
<<"keepalive.get_status">> => {ssh_keepalive_mgr, rpc_get_status}
|
|
233
|
+
}.
|
|
234
|
+
|
|
235
|
+
%% ---------- Internal: Frame handling ----------
|
|
236
|
+
|
|
237
|
+
handle_frame(Socket, FrameBin, S) ->
|
|
238
|
+
case ssh_ipc_proto:decode(FrameBin) of
|
|
239
|
+
{ok, {request_or_response, Msg}} ->
|
|
240
|
+
handle_request_or_response(Socket, Msg, S);
|
|
241
|
+
{ok, {notification, Msg}} ->
|
|
242
|
+
handle_push_msg(Msg, S);
|
|
243
|
+
{error, _Reason} ->
|
|
244
|
+
S
|
|
245
|
+
end.
|
|
246
|
+
|
|
247
|
+
handle_request_or_response(Socket, #{<<"id">> := Id} = Msg, S) ->
|
|
248
|
+
case maps:is_key(<<"method">>, Msg) of
|
|
249
|
+
true ->
|
|
250
|
+
%% This is a Ruby→Erlang request
|
|
251
|
+
Method = maps:get(<<"method">>, Msg),
|
|
252
|
+
case Method of
|
|
253
|
+
<<"hello">> ->
|
|
254
|
+
%% Special: hello authenticates the client socket
|
|
255
|
+
%% Pass auth_token explicitly since spawn doesn't inherit process dict
|
|
256
|
+
AuthToken = S#gw_state.auth_token,
|
|
257
|
+
spawn(fun() ->
|
|
258
|
+
Result = try
|
|
259
|
+
RawResult = handle_hello(AuthToken, maps:get(<<"params">>, Msg)),
|
|
260
|
+
normalize_result(RawResult)
|
|
261
|
+
catch
|
|
262
|
+
Class:Reason:Stacktrace ->
|
|
263
|
+
{error, #{class => atom_to_binary(Class, utf8),
|
|
264
|
+
reason => format_reason(Reason),
|
|
265
|
+
stacktrace => format_stacktrace(Stacktrace)}}
|
|
266
|
+
end,
|
|
267
|
+
send_rpc_response(Socket, Id, Result),
|
|
268
|
+
case Result of
|
|
269
|
+
{ok, _} ->
|
|
270
|
+
%% Signal gateway to mark this client as authed
|
|
271
|
+
?MODULE ! {mark_authed, Socket};
|
|
272
|
+
_ ->
|
|
273
|
+
ok
|
|
274
|
+
end
|
|
275
|
+
end),
|
|
276
|
+
S;
|
|
277
|
+
_ ->
|
|
278
|
+
dispatch_rpc(Socket, Id, Msg, S)
|
|
279
|
+
end;
|
|
280
|
+
false ->
|
|
281
|
+
%% This is a response to a reverse RPC (Erlang→Ruby)
|
|
282
|
+
handle_reverse_response(Id, Msg, S)
|
|
283
|
+
end.
|
|
284
|
+
|
|
285
|
+
%% @doc Dispatch a Ruby→Erlang RPC request.
|
|
286
|
+
%% Params is optional: if the client omits it, default to an empty map.
|
|
287
|
+
dispatch_rpc(Socket, Id, #{<<"method">> := Method} = Msg, S) ->
|
|
288
|
+
Params = maps:get(<<"params">>, Msg, #{}),
|
|
289
|
+
case maps:get(Method, S#gw_state.routes, undefined) of
|
|
290
|
+
{Module, Fun} ->
|
|
291
|
+
%% Spawn to avoid blocking gateway
|
|
292
|
+
spawn(fun() ->
|
|
293
|
+
Result = try
|
|
294
|
+
RawResult = erlang:apply(Module, Fun, [Params]),
|
|
295
|
+
normalize_result(RawResult)
|
|
296
|
+
catch
|
|
297
|
+
Class:Reason:Stacktrace ->
|
|
298
|
+
{error, #{class => atom_to_binary(Class, utf8),
|
|
299
|
+
reason => format_reason(Reason),
|
|
300
|
+
stacktrace => format_stacktrace(Stacktrace)}}
|
|
301
|
+
end,
|
|
302
|
+
send_rpc_response(Socket, Id, Result)
|
|
303
|
+
end),
|
|
304
|
+
S;
|
|
305
|
+
undefined ->
|
|
306
|
+
send_rpc_error(Socket, Id, -32601, #{message => <<"Method not found">>}),
|
|
307
|
+
S
|
|
308
|
+
end.
|
|
309
|
+
|
|
310
|
+
%% @doc Handle hello — first message must authenticate with token.
|
|
311
|
+
%% AuthToken is passed explicitly from gateway state (not process dict).
|
|
312
|
+
handle_hello(AuthToken, #{<<"auth_token">> := Token, <<"ver">> := _Ver, <<"client_id">> := _ClientId}) ->
|
|
313
|
+
case Token =:= AuthToken of
|
|
314
|
+
true ->
|
|
315
|
+
{ok, #{capabilities => [<<"coalesce">>], ver => ?PROTOCOL_VER}};
|
|
316
|
+
false ->
|
|
317
|
+
{error, auth_failed}
|
|
318
|
+
end.
|
|
319
|
+
|
|
320
|
+
%% @doc Handle bye — clean up.
|
|
321
|
+
handle_bye(_Params) ->
|
|
322
|
+
{ok, #{ok => true}}.
|
|
323
|
+
|
|
324
|
+
%% @doc Handle engine.ping.
|
|
325
|
+
handle_ping(_Params) ->
|
|
326
|
+
{ok, #{ok => true, timestamp => ssh_codec:unix_ms()}}.
|
|
327
|
+
|
|
328
|
+
%% @doc Handle engine.stats.
|
|
329
|
+
handle_stats(_Params) ->
|
|
330
|
+
{Total, _} = erlang:statistics(runtime),
|
|
331
|
+
{ok, #{
|
|
332
|
+
connections => length(ssh_conn_sup:all_workers()),
|
|
333
|
+
uptime_ms => Total
|
|
334
|
+
}}.
|
|
335
|
+
|
|
336
|
+
%% @doc Handle engine.shutdown.
|
|
337
|
+
handle_shutdown(_Params) ->
|
|
338
|
+
spawn(fun() -> timer:sleep(100), init:stop() end),
|
|
339
|
+
{ok, #{ok => true}}.
|
|
340
|
+
|
|
341
|
+
%% ---------- Internal: Send ----------
|
|
342
|
+
|
|
343
|
+
send_notification(S, Method, Params) ->
|
|
344
|
+
Msg = ssh_ipc_proto:frame(ssh_ipc_proto:encode_push(Method, Params)),
|
|
345
|
+
send_to_all_clients(S, Msg).
|
|
346
|
+
|
|
347
|
+
send_rpc_response(Socket, Id, {ok, Result}) ->
|
|
348
|
+
Msg = ssh_ipc_proto:frame(ssh_ipc_proto:encode_response(Id, Result)),
|
|
349
|
+
gen_tcp:send(Socket, Msg);
|
|
350
|
+
|
|
351
|
+
send_rpc_response(Socket, Id, {error, ErrorObj}) ->
|
|
352
|
+
Code = case is_map(ErrorObj) of
|
|
353
|
+
true -> maps:get(<<"code">>, ErrorObj, -32603);
|
|
354
|
+
false -> -32603
|
|
355
|
+
end,
|
|
356
|
+
Msg = ssh_ipc_proto:frame(ssh_ipc_proto:encode_error(Id, Code, ErrorObj)),
|
|
357
|
+
gen_tcp:send(Socket, Msg).
|
|
358
|
+
|
|
359
|
+
send_rpc_error(Socket, Id, Code, ErrorObj) ->
|
|
360
|
+
Msg = ssh_ipc_proto:frame(ssh_ipc_proto:encode_error(Id, Code, ErrorObj)),
|
|
361
|
+
gen_tcp:send(Socket, Msg).
|
|
362
|
+
|
|
363
|
+
send_to_all_clients(S, Msg) ->
|
|
364
|
+
case maps:size(S#gw_state.clients) of
|
|
365
|
+
0 -> {error, no_clients};
|
|
366
|
+
_ ->
|
|
367
|
+
maps:fold(fun(_Ref, #client{socket = Sock}, _) ->
|
|
368
|
+
gen_tcp:send(Sock, Msg)
|
|
369
|
+
end, ok, S#gw_state.clients),
|
|
370
|
+
ok
|
|
371
|
+
end.
|
|
372
|
+
|
|
373
|
+
%% ---------- Internal: Client management ----------
|
|
374
|
+
|
|
375
|
+
%% @doc Remove a client by socket from the clients map.
|
|
376
|
+
remove_client(S, Socket) ->
|
|
377
|
+
NewClients = maps:filter(fun(_Ref, #client{socket = Sock}) -> Sock =/= Socket end,
|
|
378
|
+
S#gw_state.clients),
|
|
379
|
+
S#gw_state{clients = NewClients}.
|
|
380
|
+
|
|
381
|
+
%% ---------- Internal: Reverse RPC ----------
|
|
382
|
+
|
|
383
|
+
generate_reverse_id() ->
|
|
384
|
+
erlang:unique_integer([positive]).
|
|
385
|
+
|
|
386
|
+
%% @doc Store a pending reverse RPC for response matching.
|
|
387
|
+
%% Uses process dictionary: {reverse_pending, ReverseId} => {From, Ref}
|
|
388
|
+
store_reverse_pending(_S, ReverseId, From, Ref) ->
|
|
389
|
+
put({reverse_pending, ReverseId}, {From, Ref}),
|
|
390
|
+
_S.
|
|
391
|
+
|
|
392
|
+
%% @doc Handle reverse RPC timeout.
|
|
393
|
+
handle_reverse_timeout(S, Ref) ->
|
|
394
|
+
%% Find the pending entry by Ref and reply timeout
|
|
395
|
+
Keys = [K || K <- get_keys_starting_with(reverse_pending), element(2, get(K)) =:= Ref],
|
|
396
|
+
lists:foreach(fun(K) ->
|
|
397
|
+
case get(K) of
|
|
398
|
+
{From, Ref} ->
|
|
399
|
+
gen_server:reply(From, {error, timeout}),
|
|
400
|
+
erase(K)
|
|
401
|
+
end
|
|
402
|
+
end, Keys),
|
|
403
|
+
S.
|
|
404
|
+
|
|
405
|
+
%% @doc Handle reverse RPC response from Ruby.
|
|
406
|
+
handle_reverse_response(Id, Msg, S) ->
|
|
407
|
+
case get({reverse_pending, Id}) of
|
|
408
|
+
{From, _Ref} ->
|
|
409
|
+
Result = case maps:get(<<"result">>, Msg, undefined) of
|
|
410
|
+
undefined -> {error, maps:get(<<"error">>, Msg, #{})};
|
|
411
|
+
R -> {ok, R}
|
|
412
|
+
end,
|
|
413
|
+
gen_server:reply(From, Result),
|
|
414
|
+
erase({reverse_pending, Id}),
|
|
415
|
+
S;
|
|
416
|
+
undefined ->
|
|
417
|
+
S
|
|
418
|
+
end.
|
|
419
|
+
|
|
420
|
+
handle_push_msg(_Msg, S) ->
|
|
421
|
+
S.
|
|
422
|
+
|
|
423
|
+
%% ---------- Internal: Result normalization ----------
|
|
424
|
+
|
|
425
|
+
%% @doc Normalize Erlang results to JSON-encodable maps.
|
|
426
|
+
normalize_result({ok, Result}) when is_map(Result) ->
|
|
427
|
+
{ok, Result};
|
|
428
|
+
normalize_result({ok, true}) ->
|
|
429
|
+
{ok, #{ok => true}};
|
|
430
|
+
normalize_result({ok, Value}) ->
|
|
431
|
+
{ok, #{ok => Value}};
|
|
432
|
+
normalize_result({error, Reason}) ->
|
|
433
|
+
{error, #{message => format_reason(Reason)}};
|
|
434
|
+
normalize_result(ok) ->
|
|
435
|
+
{ok, #{ok => true}};
|
|
436
|
+
normalize_result(Other) ->
|
|
437
|
+
{ok, #{ok => Other}}.
|
|
438
|
+
|
|
439
|
+
format_reason(Reason) when is_atom(Reason) ->
|
|
440
|
+
atom_to_binary(Reason, utf8);
|
|
441
|
+
format_reason(Reason) when is_binary(Reason) ->
|
|
442
|
+
Reason;
|
|
443
|
+
format_reason(Reason) ->
|
|
444
|
+
iolist_to_binary(io_lib:format("~p", [Reason])).
|
|
445
|
+
|
|
446
|
+
format_stacktrace(Stacktrace) ->
|
|
447
|
+
[iolist_to_binary(io_lib:format("~p", [Entry])) || Entry <- Stacktrace].
|
|
448
|
+
|
|
449
|
+
%% ---------- Internal: Helpers ----------
|
|
450
|
+
read_port_from_file(File) ->
|
|
451
|
+
{ok, Content} = file:read_file(File),
|
|
452
|
+
[<<"tcp://127.0.0.1:", Port/binary>> | _] = binary:split(Content, <<"\n">>),
|
|
453
|
+
{ok, binary_to_integer(Port)}.
|
|
454
|
+
|
|
455
|
+
read_socket_path_from_file(File) ->
|
|
456
|
+
{ok, Content} = file:read_file(File),
|
|
457
|
+
[EndpointLine | _] = binary:split(Content, <<"\n">>),
|
|
458
|
+
%% Extract path from "unix:/path" format
|
|
459
|
+
case EndpointLine of
|
|
460
|
+
<<"unix:", Path/binary>> -> Path;
|
|
461
|
+
_ -> <<"/tmp/ssh_core.sock">>
|
|
462
|
+
end.
|
|
463
|
+
|
|
464
|
+
%% @doc Get all process dictionary keys starting with given prefix atom.
|
|
465
|
+
get_keys_starting_with(PrefixAtom) ->
|
|
466
|
+
{dictionary, Dict} = process_info(self(), dictionary),
|
|
467
|
+
[K || K <- Dict, is_tuple(K), element(1, K) =:= PrefixAtom].
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
-module(ssh_ipc_proto).
|
|
2
|
+
%% JSON-RPC 2.0 protocol encode/decode and framing.
|
|
3
|
+
%% Pure module — no network I/O, no state.
|
|
4
|
+
|
|
5
|
+
-export([
|
|
6
|
+
encode_request/3,
|
|
7
|
+
encode_response/2,
|
|
8
|
+
encode_error/3,
|
|
9
|
+
encode_push/2,
|
|
10
|
+
decode/1,
|
|
11
|
+
frame/1,
|
|
12
|
+
unframe/1
|
|
13
|
+
]).
|
|
14
|
+
|
|
15
|
+
%% ---------- Encoding ----------
|
|
16
|
+
|
|
17
|
+
%% @doc Encode a JSON-RPC request.
|
|
18
|
+
-spec encode_request(integer(), binary(), map()) -> binary().
|
|
19
|
+
encode_request(Id, Method, Params) ->
|
|
20
|
+
ssh_codec:encode_json(#{
|
|
21
|
+
<<"jsonrpc">> => <<"2.0">>,
|
|
22
|
+
<<"id">> => Id,
|
|
23
|
+
<<"method">> => Method,
|
|
24
|
+
<<"params">> => Params
|
|
25
|
+
}).
|
|
26
|
+
|
|
27
|
+
%% @doc Encode a successful response.
|
|
28
|
+
-spec encode_response(integer(), map()) -> binary().
|
|
29
|
+
encode_response(Id, Result) ->
|
|
30
|
+
ssh_codec:encode_json(#{
|
|
31
|
+
<<"jsonrpc">> => <<"2.0">>,
|
|
32
|
+
<<"id">> => Id,
|
|
33
|
+
<<"result">> => Result
|
|
34
|
+
}).
|
|
35
|
+
|
|
36
|
+
%% @doc Encode an error response.
|
|
37
|
+
-spec encode_error(integer(), integer(), map()) -> binary().
|
|
38
|
+
encode_error(Id, Code, ErrorObj) ->
|
|
39
|
+
ssh_codec:encode_json(#{
|
|
40
|
+
<<"jsonrpc">> => <<"2.0">>,
|
|
41
|
+
<<"id">> => Id,
|
|
42
|
+
<<"error">> => ErrorObj#{<<"code">> => Code}
|
|
43
|
+
}).
|
|
44
|
+
|
|
45
|
+
%% @doc Encode a push notification (no id).
|
|
46
|
+
-spec encode_push(binary(), map()) -> binary().
|
|
47
|
+
encode_push(Method, Params) ->
|
|
48
|
+
ssh_codec:encode_json(#{
|
|
49
|
+
<<"jsonrpc">> => <<"2.0">>,
|
|
50
|
+
<<"method">> => Method,
|
|
51
|
+
<<"params">> => Params
|
|
52
|
+
}).
|
|
53
|
+
|
|
54
|
+
%% ---------- Decoding ----------
|
|
55
|
+
|
|
56
|
+
%% @doc Decode a single JSON-RPC message (binary, no delimiter).
|
|
57
|
+
%% Returns {ok, Map} | {error, Reason}.
|
|
58
|
+
-spec decode(binary()) -> {ok, map()} | {error, term()}.
|
|
59
|
+
decode(Bin) ->
|
|
60
|
+
case ssh_codec:decode_json(Bin) of
|
|
61
|
+
{ok, Map} when is_map(Map) -> validate_msg(Map);
|
|
62
|
+
{error, _} = E -> E
|
|
63
|
+
end.
|
|
64
|
+
|
|
65
|
+
%% ---------- Framing ----------
|
|
66
|
+
|
|
67
|
+
%% @doc Append newline delimiter to a message.
|
|
68
|
+
-spec frame(binary()) -> binary().
|
|
69
|
+
frame(Msg) ->
|
|
70
|
+
<<Msg/binary, "\n">>.
|
|
71
|
+
|
|
72
|
+
%% @doc Split a buffer on newline delimiters.
|
|
73
|
+
%% Returns {[MessageBinaries], RemainingBuffer}.
|
|
74
|
+
-spec unframe(binary()) -> {[binary()], binary()}.
|
|
75
|
+
unframe(Buffer) ->
|
|
76
|
+
split_frames(Buffer, []).
|
|
77
|
+
|
|
78
|
+
%% ---------- Internal ----------
|
|
79
|
+
|
|
80
|
+
validate_msg(#{<<"jsonrpc">> := <<"2.0">>} = Msg) ->
|
|
81
|
+
case maps:is_key(<<"id">>, Msg) of
|
|
82
|
+
true -> {ok, {request_or_response, Msg}};
|
|
83
|
+
false -> {ok, {notification, Msg}}
|
|
84
|
+
end;
|
|
85
|
+
validate_msg(_) ->
|
|
86
|
+
{error, invalid_jsonrpc}.
|
|
87
|
+
|
|
88
|
+
split_frames(Buffer, Acc) ->
|
|
89
|
+
case binary:match(Buffer, <<"\n">>) of
|
|
90
|
+
{Pos, _} ->
|
|
91
|
+
<<Frame:Pos/binary, _:8, Rest/binary>> = Buffer,
|
|
92
|
+
split_frames(Rest, [Frame | Acc]);
|
|
93
|
+
nomatch ->
|
|
94
|
+
{lists:reverse(Acc), Buffer}
|
|
95
|
+
end.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
-module(ssh_jump_chain).
|
|
2
|
+
|
|
3
|
+
%% Jump chain builder — constructs multi-level ProxyJump SSH connections.
|
|
4
|
+
%%
|
|
5
|
+
%% Key design (LLD §6.7, E3/E6 fixes):
|
|
6
|
+
%% - OTP ssh has no ssh:connect_via/2 — cannot stack SSH on existing channel
|
|
7
|
+
%% - V1.0 uses {sock_fun, Fun} option for tunneled connections
|
|
8
|
+
%% - Each jump opens a direct_tcpip channel on the previous SSH connection
|
|
9
|
+
%% - The channel is wrapped as a socket-like Fun for the next ssh:connect
|
|
10
|
+
%% - V1.0 priority: single-hop jump (1 level); multi-hop is V2.0
|
|
11
|
+
%%
|
|
12
|
+
%% Risk: {sock_fun, Fun} is semi-official in OTP 26. Signature:
|
|
13
|
+
%% Fun(connect, Host, Port, Timeout) -> {ok, Sock} | {error, _}
|
|
14
|
+
%% Fun(close, Sock) -> ok
|
|
15
|
+
%% Fun(send, Sock, Data) -> ok | {error, _}
|
|
16
|
+
%% Fun(recv, Sock, Len, Timeout) -> {ok, Data} | {error, _}
|
|
17
|
+
|
|
18
|
+
-export([
|
|
19
|
+
build/2,
|
|
20
|
+
direct_connect/1,
|
|
21
|
+
tunneled_connect/2,
|
|
22
|
+
make_sock_fun_from_channel/2
|
|
23
|
+
]).
|
|
24
|
+
|
|
25
|
+
-include("ssh_ipc.hrl").
|
|
26
|
+
|
|
27
|
+
%% ---------- Public API ----------
|
|
28
|
+
|
|
29
|
+
%% @doc Build a multi-level jump chain connection.
|
|
30
|
+
%% Jumps = [J1, J2, ...] from nearest to farthest.
|
|
31
|
+
%% Algorithm:
|
|
32
|
+
%% 1. Connect to J1 directly
|
|
33
|
+
%% 2. For J2..Jn: open direct_tcpip on previous SSH ref, wrap as sock_fun
|
|
34
|
+
%% 3. On final jump: direct_tcpip to target, ssh:connect via sock_fun
|
|
35
|
+
%%
|
|
36
|
+
%% Returns {ok, FinalSshRef} | {error, Reason}
|
|
37
|
+
-spec build(map(), [map()]) -> {ok, reference()} | {error, term()}.
|
|
38
|
+
build(TargetSpec, []) ->
|
|
39
|
+
%% No jumps — direct connect
|
|
40
|
+
direct_connect(TargetSpec);
|
|
41
|
+
build(TargetSpec, Jumps) ->
|
|
42
|
+
case lists:foldl(fun(Jump, {ok, PrevRef}) ->
|
|
43
|
+
tunneled_connect(PrevRef, Jump);
|
|
44
|
+
(_, {error, _} = E) -> E
|
|
45
|
+
end, direct_connect(hd(Jumps)), tl(Jumps)) of
|
|
46
|
+
{ok, LastJumpRef} -> tunneled_connect(LastJumpRef, TargetSpec);
|
|
47
|
+
{error, _} = E -> E
|
|
48
|
+
end.
|
|
49
|
+
|
|
50
|
+
%% @doc Direct SSH connection (no tunnel).
|
|
51
|
+
-spec direct_connect(map()) -> {ok, reference()} | {error, term()}.
|
|
52
|
+
direct_connect(Spec) ->
|
|
53
|
+
Host = binary_to_list(maps:get(<<"host">>, Spec)),
|
|
54
|
+
Port = maps:get(<<"port">>, Spec, 22),
|
|
55
|
+
Opts = ssh_conn_worker:build_ssh_options(Spec),
|
|
56
|
+
ssh:connect(Host, Port, Opts, ?CONNECT_TIMEOUT_MS).
|
|
57
|
+
|
|
58
|
+
%% @doc Connect through an existing SSH tunnel (previous jump).
|
|
59
|
+
%% Opens a direct_tcpip channel on PrevRef, then runs ssh:connect
|
|
60
|
+
%% with a custom {sock_fun} that wraps the channel.
|
|
61
|
+
-spec tunneled_connect(reference(), map()) -> {ok, reference()} | {error, term()}.
|
|
62
|
+
tunneled_connect(PrevRef, Spec) ->
|
|
63
|
+
Host = binary_to_list(maps:get(<<"host">>, Spec)),
|
|
64
|
+
Port = maps:get(<<"port">>, Spec, 22),
|
|
65
|
+
%% Open direct_tcpip channel on the previous SSH connection
|
|
66
|
+
case ssh_connection:direct_tcpip(PrevRef, Host, Port, "127.0.0.1", 0, ?CONNECT_TIMEOUT_MS) of
|
|
67
|
+
{ok, Chan} ->
|
|
68
|
+
SockFun = make_sock_fun_from_channel(PrevRef, Chan),
|
|
69
|
+
Opts = ssh_conn_worker:build_ssh_options(Spec) ++ [{sock_fun, SockFun}],
|
|
70
|
+
ssh:connect(Host, Port, Opts, ?CONNECT_TIMEOUT_MS);
|
|
71
|
+
{error, Reason} ->
|
|
72
|
+
{error, {direct_tcpip_failed, Reason}}
|
|
73
|
+
end.
|
|
74
|
+
|
|
75
|
+
%% @doc Wrap an SSH direct_tcpip channel as a sock_fun for ssh:connect.
|
|
76
|
+
%% This is the highest-risk component in V1.0 — the channel must emulate
|
|
77
|
+
%% a TCP socket's connect/close/send/recv semantics.
|
|
78
|
+
-spec make_sock_fun_from_channel(reference(), integer()) -> function().
|
|
79
|
+
make_sock_fun_from_channel(SshRef, ChanId) ->
|
|
80
|
+
%% OTP ssh sock_fun calls different operations with different arities.
|
|
81
|
+
%% We wrap them all through a single 4-arg dispatch:
|
|
82
|
+
%% (connect, Host, Port, Timeout) -> {ok, Sock}
|
|
83
|
+
%% (close, Sock, _, _) -> ok
|
|
84
|
+
%% (send, Sock, Data, _) -> ok | {error,_}
|
|
85
|
+
%% (recv, Sock, Len, Timeout) -> {ok, Data} | {error,_}
|
|
86
|
+
fun
|
|
87
|
+
(connect, _Host, _Port, _Timeout) ->
|
|
88
|
+
{ok, {SshRef, ChanId}};
|
|
89
|
+
(close, _Sock, _A3, _A4) ->
|
|
90
|
+
ssh_connection:close(SshRef, ChanId),
|
|
91
|
+
ok;
|
|
92
|
+
(send, _Sock, Data, _A4) ->
|
|
93
|
+
ssh_connection:send(SshRef, ChanId, Data);
|
|
94
|
+
(recv, _Sock, _Len, Timeout) ->
|
|
95
|
+
receive
|
|
96
|
+
{ssh_cm, SshRef, {data, ChanId, _Type, Data}} ->
|
|
97
|
+
{ok, Data}
|
|
98
|
+
after Timeout ->
|
|
99
|
+
{error, timeout}
|
|
100
|
+
end
|
|
101
|
+
end.
|