|
| 1 | +% |
| 2 | +% This file is part of AtomVM. |
| 3 | +% |
| 4 | +% Copyright 2024 Paul Guyot <pguyot@kallisys.net> |
| 5 | +% |
| 6 | +% Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | +% you may not use this file except in compliance with the License. |
| 8 | +% You may obtain a copy of the License at |
| 9 | +% |
| 10 | +% http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +% |
| 12 | +% Unless required by applicable law or agreed to in writing, software |
| 13 | +% distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | +% See the License for the specific language governing permissions and |
| 16 | +% limitations under the License. |
| 17 | +% |
| 18 | +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later |
| 19 | +% |
| 20 | + |
| 21 | +%%----------------------------------------------------------------------------- |
| 22 | +%% @doc An implementation of the Erlang/OTP erl_epmd interface. |
| 23 | +%% |
| 24 | +%% This module implements a strict subset of the Erlang/OTP erl_epmd |
| 25 | +%% interface. |
| 26 | +%% @end |
| 27 | +%%----------------------------------------------------------------------------- |
| 28 | +-module(erl_epmd). |
| 29 | + |
| 30 | +% API |
| 31 | +-export([ |
| 32 | + start_link/0, |
| 33 | + stop/0, |
| 34 | + port_please/2, |
| 35 | + register_node/2, |
| 36 | + names/1 |
| 37 | +]). |
| 38 | + |
| 39 | +% gen_server |
| 40 | +-behaviour(gen_server). |
| 41 | +-export([ |
| 42 | + init/1, |
| 43 | + handle_call/3, |
| 44 | + handle_cast/2, |
| 45 | + handle_info/2, |
| 46 | + terminate/2, |
| 47 | + code_change/3 |
| 48 | +]). |
| 49 | + |
| 50 | +-record(state, {socket = undefined}). |
| 51 | + |
| 52 | +-define(EPMD_PORT, 4369). |
| 53 | +-define(TIMEOUT, 5000). |
| 54 | + |
| 55 | +-define(NAMES_REQ, 110). |
| 56 | +-define(ALIVE2_X_RESP, 118). |
| 57 | +-define(PORT2_RESP, 119). |
| 58 | +-define(ALIVE2_REQ, 120). |
| 59 | +-define(ALIVE2_RESP, 121). |
| 60 | +-define(PORT_PLEASE2_REQ, 122). |
| 61 | + |
| 62 | +-define(TCP_INET4_PROTOCOL, 0). |
| 63 | +-define(ERLANG_NODE_TYPE, 77). |
| 64 | +-define(VERSION, 6). |
| 65 | + |
| 66 | +-record(receive_port2_resp, { |
| 67 | + port_no :: non_neg_integer(), |
| 68 | + highest_version :: non_neg_integer(), |
| 69 | + lowest_version :: non_neg_integer() |
| 70 | +}). |
| 71 | + |
| 72 | +-record(alive2_resp, { |
| 73 | + creation :: non_neg_integer() |
| 74 | +}). |
| 75 | + |
| 76 | +%% @doc Start EPMD client |
| 77 | +-spec start_link() -> {ok, pid()}. |
| 78 | +start_link() -> |
| 79 | + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). |
| 80 | + |
| 81 | +%% @doc Stop EPMD client- |
| 82 | +-spec stop() -> ok. |
| 83 | +stop() -> |
| 84 | + gen_server:call(?MODULE, stop, infinity). |
| 85 | + |
| 86 | +%% @param Name name of the node to get the port of |
| 87 | +%% @param Host host of the node to get the port of |
| 88 | +%% @doc Get the port and version of a node on a given host. |
| 89 | +%% This function will connect to epmd on the host. |
| 90 | +-spec port_please(Name :: iodata(), Host :: inet:hostname() | inet:ip_address()) -> |
| 91 | + {port, inet:port_number(), non_neg_integer()} | noport. |
| 92 | +port_please(Name, Host) -> |
| 93 | + case inet:getaddr(Host, inet) of |
| 94 | + {ok, IP} -> |
| 95 | + {ok, Socket} = socket:open(inet, stream, tcp), |
| 96 | + case socket:connect(Socket, #{addr => IP, port => ?EPMD_PORT, family => inet}) of |
| 97 | + ok -> |
| 98 | + NameBin = iolist_to_binary(Name), |
| 99 | + Result = |
| 100 | + case send_request(Socket, <<?PORT_PLEASE2_REQ, NameBin/binary>>) of |
| 101 | + {ok, #receive_port2_resp{ |
| 102 | + port_no = PortNo, |
| 103 | + highest_version = HighVersion, |
| 104 | + lowest_version = LowVersion |
| 105 | + }} when HighVersion >= ?VERSION andalso LowVersion =< ?VERSION -> |
| 106 | + {port, PortNo, ?VERSION}; |
| 107 | + {ok, #receive_port2_resp{port_no = PortNo}} -> |
| 108 | + {port, PortNo, 0}; |
| 109 | + {ok, _Unexpected} -> |
| 110 | + noport; |
| 111 | + {error, _} -> |
| 112 | + noport |
| 113 | + end, |
| 114 | + ok = socket:close(Socket), |
| 115 | + Result; |
| 116 | + {error, _} -> |
| 117 | + noport |
| 118 | + end; |
| 119 | + {error, _} -> |
| 120 | + noport |
| 121 | + end. |
| 122 | + |
| 123 | +%% @param Host the host to connect to |
| 124 | +%% @return a list of names and ports of registered nodes |
| 125 | +%% @doc Get the names and ports of all registered nodes |
| 126 | +%% This function will connect to epmd on localhost. |
| 127 | +-spec names(Host :: inet:hostname() | inet:ip_address()) -> |
| 128 | + {ok, [{string(), inet:port_number()}]} | {error, any()}. |
| 129 | +names(Host) -> |
| 130 | + case inet:getaddr(Host, inet) of |
| 131 | + {ok, IP} -> |
| 132 | + {ok, Socket} = socket:open(inet, stream, tcp), |
| 133 | + case socket:connect(Socket, #{addr => IP, port => ?EPMD_PORT, family => inet}) of |
| 134 | + ok -> |
| 135 | + Result = |
| 136 | + case socket:send(Socket, <<1:16, ?NAMES_REQ>>) of |
| 137 | + ok -> |
| 138 | + case socket:recv(Socket, 4, ?TIMEOUT) of |
| 139 | + {ok, <<?EPMD_PORT:32>>} -> |
| 140 | + receive_names_loop(Socket, <<>>, []); |
| 141 | + {ok, Unexpected} -> |
| 142 | + {error, {unexpected, Unexpected}}; |
| 143 | + {error, _} = ErrRecv -> |
| 144 | + ErrRecv |
| 145 | + end; |
| 146 | + {error, _} = ErrSend -> |
| 147 | + ErrSend |
| 148 | + end, |
| 149 | + ok = socket:close(Socket), |
| 150 | + Result; |
| 151 | + {error, _} = ErrConnect -> |
| 152 | + ErrConnect |
| 153 | + end; |
| 154 | + {error, _} = ErrGetAddr -> |
| 155 | + ErrGetAddr |
| 156 | + end. |
| 157 | + |
| 158 | +receive_names_loop(Socket, AccBuffer, AccL) -> |
| 159 | + case binary:split(AccBuffer, <<"\n">>) of |
| 160 | + [AccBuffer] -> |
| 161 | + case socket:recv(Socket, 0, ?TIMEOUT) of |
| 162 | + {error, closed} when AccBuffer =:= <<>> -> {ok, lists:reverse(AccL)}; |
| 163 | + {error, _} = ErrT -> ErrT; |
| 164 | + {ok, Data} -> receive_names_loop(Socket, <<Data/binary, AccBuffer/binary>>, AccL) |
| 165 | + end; |
| 166 | + [<<"name ", RestLine/binary>>, RestBuffer] -> |
| 167 | + case binary:split(RestLine, <<" at port ">>) of |
| 168 | + [NameBin, PortBin] -> |
| 169 | + try binary_to_integer(PortBin) of |
| 170 | + Port -> |
| 171 | + receive_names_loop(Socket, RestBuffer, [ |
| 172 | + {binary_to_list(NameBin), Port} | AccL |
| 173 | + ]) |
| 174 | + catch |
| 175 | + error:badarg -> |
| 176 | + {error, {unexpected, <<"name ", RestLine/binary>>}} |
| 177 | + end; |
| 178 | + [_] -> |
| 179 | + {error, {unexpected, <<"name ", RestLine/binary>>}} |
| 180 | + end; |
| 181 | + [UnexpectedLine, _RestBuffer] -> |
| 182 | + {error, {unexpected, UnexpectedLine}} |
| 183 | + end. |
| 184 | + |
| 185 | +%% @param Name name to register |
| 186 | +%% @param Port port to register |
| 187 | +%% @doc Register to local epmd and get a creation number |
| 188 | +-spec register_node(Name :: iodata(), Port :: inet:port_number()) -> |
| 189 | + {ok, non_neg_integer()} | {error, any()}. |
| 190 | +register_node(Name, Port) -> |
| 191 | + gen_server:call(?MODULE, {register_node, Name, Port}, infinity). |
| 192 | + |
| 193 | +%% @hidden |
| 194 | +init([]) -> |
| 195 | + State = #state{}, |
| 196 | + {ok, State}. |
| 197 | + |
| 198 | +%% @hidden |
| 199 | +handle_call({register_node, _Name, _Port}, _From, #state{socket = Socket} = State) when |
| 200 | + Socket =/= undefined |
| 201 | +-> |
| 202 | + {reply, {error, already_registered}, State}; |
| 203 | +handle_call({register_node, Name, Port}, _From, #state{} = State) -> |
| 204 | + {ok, Socket} = socket:open(inet, stream, tcp), |
| 205 | + case socket:connect(Socket, #{addr => {127, 0, 0, 1}, port => ?EPMD_PORT, family => inet}) of |
| 206 | + ok -> |
| 207 | + NameBin = iolist_to_binary(Name), |
| 208 | + NameLen = byte_size(NameBin), |
| 209 | + Packet = |
| 210 | + <<?ALIVE2_REQ, Port:16, ?ERLANG_NODE_TYPE, ?TCP_INET4_PROTOCOL, ?VERSION:16, |
| 211 | + ?VERSION:16, NameLen:16, NameBin/binary, 0:16>>, |
| 212 | + case send_request(Socket, Packet) of |
| 213 | + {ok, #alive2_resp{creation = Creation}} -> |
| 214 | + {reply, {ok, Creation}, State#state{socket = Socket}}; |
| 215 | + {error, _} = RequestErr -> |
| 216 | + socket:close(Socket), |
| 217 | + {reply, RequestErr, State} |
| 218 | + end; |
| 219 | + {error, _} = ConnectErr -> |
| 220 | + socket:close(Socket), |
| 221 | + {reply, ConnectErr, State} |
| 222 | + end; |
| 223 | +handle_call(stop, _From, State) -> |
| 224 | + {stop, shutdown, ok, State}. |
| 225 | + |
| 226 | +%% @hidden |
| 227 | +handle_cast(_Message, State) -> |
| 228 | + {noreply, State}. |
| 229 | + |
| 230 | +%% @hidden |
| 231 | +handle_info(_Message, State) -> |
| 232 | + {noreply, State}. |
| 233 | + |
| 234 | +%% @hidden |
| 235 | +terminate(_Reason, #state{socket = Socket}) -> |
| 236 | + case Socket of |
| 237 | + undefined -> ok; |
| 238 | + _ -> socket:close(Socket) |
| 239 | + end, |
| 240 | + ok. |
| 241 | + |
| 242 | +%% @hidden |
| 243 | +code_change(_OldVsn, State, _Extra) -> |
| 244 | + {ok, State}. |
| 245 | + |
| 246 | +send_request(Socket, Request) -> |
| 247 | + RequestSize = byte_size(Request), |
| 248 | + case socket:send(Socket, <<(RequestSize):16, Request/binary>>) of |
| 249 | + ok -> |
| 250 | + case socket:recv(Socket, 1, ?TIMEOUT) of |
| 251 | + {ok, <<?PORT2_RESP>>} -> |
| 252 | + receive_port2_resp(Socket); |
| 253 | + {ok, <<?ALIVE2_X_RESP>>} -> |
| 254 | + receive_alive2_x_resp(Socket); |
| 255 | + {ok, <<?ALIVE2_RESP>>} -> |
| 256 | + receive_alive2_resp(Socket); |
| 257 | + {error, _} = ErrorRecv2 -> |
| 258 | + ErrorRecv2 |
| 259 | + end; |
| 260 | + {error, _} = ErrorSend -> |
| 261 | + ErrorSend |
| 262 | + end. |
| 263 | + |
| 264 | +receive_port2_resp(Socket) -> |
| 265 | + case socket:recv(Socket, 1, ?TIMEOUT) of |
| 266 | + {ok, <<0>>} -> |
| 267 | + case socket:recv(Socket, 10, ?TIMEOUT) of |
| 268 | + {ok, |
| 269 | + <<PortNo:16, _NodeType, _Protocol, HighestVersion:16, LowestVersion:16, |
| 270 | + NameLen:16>>} -> |
| 271 | + case socket:recv(Socket, NameLen + 2, ?TIMEOUT) of |
| 272 | + {ok, <<_Name:NameLen/binary, ExtraLen:16>>} -> |
| 273 | + case ExtraLen of |
| 274 | + 0 -> |
| 275 | + {ok, #receive_port2_resp{ |
| 276 | + port_no = PortNo, |
| 277 | + highest_version = HighestVersion, |
| 278 | + lowest_version = LowestVersion |
| 279 | + }}; |
| 280 | + N -> |
| 281 | + case socket:recv(Socket, N, ?TIMEOUT) of |
| 282 | + {ok, _ExtraData} -> |
| 283 | + {ok, #receive_port2_resp{ |
| 284 | + port_no = PortNo, |
| 285 | + highest_version = HighestVersion, |
| 286 | + lowest_version = LowestVersion |
| 287 | + }}; |
| 288 | + {error, _} = ErrT1 -> |
| 289 | + ErrT1 |
| 290 | + end |
| 291 | + end; |
| 292 | + {error, _} = ErrT2 -> |
| 293 | + ErrT2 |
| 294 | + end; |
| 295 | + {error, _} = ErrT3 -> |
| 296 | + ErrT3 |
| 297 | + end; |
| 298 | + {ok, <<N>>} -> |
| 299 | + {error, N}; |
| 300 | + {error, _} = ErrT4 -> |
| 301 | + ErrT4 |
| 302 | + end. |
| 303 | + |
| 304 | +receive_alive2_x_resp(Socket) -> |
| 305 | + case socket:recv(Socket, 5, ?TIMEOUT) of |
| 306 | + {ok, <<0, Creation:32>>} -> {ok, #alive2_resp{creation = Creation}}; |
| 307 | + {ok, <<Err, _:32>>} -> {error, Err}; |
| 308 | + {error, _} = ErrT -> ErrT |
| 309 | + end. |
| 310 | + |
| 311 | +receive_alive2_resp(Socket) -> |
| 312 | + case socket:recv(Socket, 3, ?TIMEOUT) of |
| 313 | + {ok, <<0, Creation:16>>} -> {ok, #alive2_resp{creation = Creation}}; |
| 314 | + {ok, <<Err, _:16>>} -> {error, Err}; |
| 315 | + {error, _} = ErrT -> ErrT |
| 316 | + end. |
0 commit comments