%% Copyright (c) 2011-2023, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(gun_tcp).
-export([name/0]).
-export([messages/0]).
-export([domain_lookup/4]).
-export([connect/2]).
-export([send/2]).
-export([setopts/2]).
-export([sockname/1]).
-export([close/1]).
-type lookup_info() :: #{
ip_addresses := [inet:ip_address()],
port := inet:port_number(),
tcp_module := module(),
tcp_opts := [gen_tcp:connect_option()]
}.
-export_type([lookup_info/0]).
name() -> tcp.
messages() -> {tcp, tcp_closed, tcp_error}.
%% The functions domain_lookup/4 and connect/2 are very similar
%% to gen_tcp:connect/4 except the logic is split in order to
%% be able to trigger events between the domain lookup step
%% and the actual connect step.
-spec domain_lookup(inet:ip_address() | inet:hostname(),
inet:port_number(), [gen_tcp:connect_option()], timeout())
-> {ok, lookup_info()} | {error, atom()}.
domain_lookup(Address, Port0, Opts0, Timeout) ->
{Mod, Opts} = inet:tcp_module(Opts0, Address),
Timer = inet:start_timer(Timeout),
try Mod:getaddrs(Address, Timer) of
{ok, IPs} ->
case Mod:getserv(Port0) of
{ok, Port} ->
{ok, #{
ip_addresses => IPs,
port => Port,
tcp_module => Mod,
tcp_opts => Opts ++ [binary, {active, false}, {packet, raw}]
}};
Error ->
maybe_exit(Error)
end;
Error ->
maybe_exit(Error)
after
_ = inet:stop_timer(Timer)
end.
-spec connect(lookup_info(), timeout())
-> {ok, inet:socket()} | {error, atom()}.
connect(#{ip_addresses := IPs, port := Port, tcp_module := Mod, tcp_opts := Opts}, Timeout) ->
Timer = inet:start_timer(Timeout),
Res = try
try_connect(IPs, Port, Opts, Timer, Mod, {error, einval})
after
_ = inet:stop_timer(Timer)
end,
case Res of
{ok, S} -> {ok, S};
Error -> maybe_exit(Error)
end.
try_connect([IP|IPs], Port, Opts, Timer, Mod, _) ->
Timeout = inet:timeout(Timer),
case Mod:connect(IP, Port, Opts, Timeout) of
{ok, S} -> {ok, S};
{error, einval} -> {error, einval};
{error, timeout} -> {error, timeout};
Error -> try_connect(IPs, Port, Opts, Timer, Mod, Error)
end;
try_connect([], _, _, _, _, Error) ->
Error.
maybe_exit({error, einval}) -> exit(badarg);
maybe_exit({error, eaddrnotavail}) -> exit(badarg);
maybe_exit(Error) -> Error.
-spec send(inet:socket(), iodata()) -> ok | {error, atom()}.
send(Socket, Packet) ->
gen_tcp:send(Socket, Packet).
-spec setopts(inet:socket(), list()) -> ok | {error, atom()}.
setopts(Socket, Opts) ->
inet:setopts(Socket, Opts).
-spec sockname(inet:socket())
-> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
sockname(Socket) ->
inet:sockname(Socket).
-spec close(inet:socket()) -> ok.
close(Socket) ->
gen_tcp:close(Socket).