blob: f9d5d157194b8d3c85487c832dd0ccc50907d9aa [file] [log] [blame]
# You may distribute under the terms of either the GNU General Public License
# or the Artistic License (the same terms as Perl itself)
#
# (C) Paul Evans, 2010-2015 -- leonerd@leonerd.org.uk
package IO::Socket::IP;
# $VERSION needs to be set before use base 'IO::Socket'
# - https://rt.cpan.org/Ticket/Display.html?id=92107
BEGIN {
$VERSION = '0.38';
}
use strict;
use warnings;
use base qw( IO::Socket );
use Carp;
use Socket 1.97 qw(
getaddrinfo getnameinfo
sockaddr_family
AF_INET
AI_PASSIVE
IPPROTO_TCP IPPROTO_UDP
IPPROTO_IPV6 IPV6_V6ONLY
NI_DGRAM NI_NUMERICHOST NI_NUMERICSERV NIx_NOHOST NIx_NOSERV
SO_REUSEADDR SO_REUSEPORT SO_BROADCAST SO_ERROR
SOCK_DGRAM SOCK_STREAM
SOL_SOCKET
);
my $AF_INET6 = eval { Socket::AF_INET6() }; # may not be defined
my $AI_ADDRCONFIG = eval { Socket::AI_ADDRCONFIG() } || 0;
use POSIX qw( dup2 );
use Errno qw( EINVAL EINPROGRESS EISCONN ENOTCONN ETIMEDOUT EWOULDBLOCK );
use constant HAVE_MSWIN32 => ( $^O eq "MSWin32" );
# At least one OS (Android) is known not to have getprotobyname()
use constant HAVE_GETPROTOBYNAME => defined eval { getprotobyname( "tcp" ) };
my $IPv6_re = do {
# translation of RFC 3986 3.2.2 ABNF to re
my $IPv4address = do {
my $dec_octet = q<(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])>;
qq<$dec_octet(?: \\. $dec_octet){3}>;
};
my $IPv6address = do {
my $h16 = qq<[0-9A-Fa-f]{1,4}>;
my $ls32 = qq<(?: $h16 : $h16 | $IPv4address)>;
qq<(?:
(?: $h16 : ){6} $ls32
| :: (?: $h16 : ){5} $ls32
| (?: $h16 )? :: (?: $h16 : ){4} $ls32
| (?: (?: $h16 : ){0,1} $h16 )? :: (?: $h16 : ){3} $ls32
| (?: (?: $h16 : ){0,2} $h16 )? :: (?: $h16 : ){2} $ls32
| (?: (?: $h16 : ){0,3} $h16 )? :: $h16 : $ls32
| (?: (?: $h16 : ){0,4} $h16 )? :: $ls32
| (?: (?: $h16 : ){0,5} $h16 )? :: $h16
| (?: (?: $h16 : ){0,6} $h16 )? ::
)>
};
qr<$IPv6address>xo;
};
sub import
{
my $pkg = shift;
my @symbols;
foreach ( @_ ) {
if( $_ eq "-register" ) {
IO::Socket::IP::_ForINET->register_domain( AF_INET );
IO::Socket::IP::_ForINET6->register_domain( $AF_INET6 ) if defined $AF_INET6;
}
else {
push @symbols, $_;
}
}
@_ = ( $pkg, @symbols );
goto &IO::Socket::import;
}
# Convenient capability test function
{
my $can_disable_v6only;
sub CAN_DISABLE_V6ONLY
{
return $can_disable_v6only if defined $can_disable_v6only;
socket my $testsock, Socket::PF_INET6(), SOCK_STREAM, 0 or
die "Cannot socket(PF_INET6) - $!";
if( setsockopt $testsock, IPPROTO_IPV6, IPV6_V6ONLY, 0 ) {
return $can_disable_v6only = 1;
}
elsif( $! == EINVAL ) {
return $can_disable_v6only = 0;
}
else {
die "Cannot setsockopt() - $!";
}
}
}
sub new
{
my $class = shift;
my %arg = (@_ == 1) ? (PeerHost => $_[0]) : @_;
return $class->SUPER::new(%arg);
}
# IO::Socket may call this one; neaten up the arguments from IO::Socket::INET
# before calling our real _configure method
sub configure
{
my $self = shift;
my ( $arg ) = @_;
$arg->{PeerHost} = delete $arg->{PeerAddr}
if exists $arg->{PeerAddr} && !exists $arg->{PeerHost};
$arg->{PeerService} = delete $arg->{PeerPort}
if exists $arg->{PeerPort} && !exists $arg->{PeerService};
$arg->{LocalHost} = delete $arg->{LocalAddr}
if exists $arg->{LocalAddr} && !exists $arg->{LocalHost};
$arg->{LocalService} = delete $arg->{LocalPort}
if exists $arg->{LocalPort} && !exists $arg->{LocalService};
for my $type (qw(Peer Local)) {
my $host = $type . 'Host';
my $service = $type . 'Service';
if( defined $arg->{$host} ) {
( $arg->{$host}, my $s ) = $self->split_addr( $arg->{$host} );
# IO::Socket::INET compat - *Host parsed port always takes precedence
$arg->{$service} = $s if defined $s;
}
}
$self->_io_socket_ip__configure( $arg );
}
# Avoid simply calling it _configure, as some subclasses of IO::Socket::INET on CPAN already take that
sub _io_socket_ip__configure
{
my $self = shift;
my ( $arg ) = @_;
my %hints;
my @localinfos;
my @peerinfos;
my $listenqueue = $arg->{Listen};
if( defined $listenqueue and
( defined $arg->{PeerHost} || defined $arg->{PeerService} || defined $arg->{PeerAddrInfo} ) ) {
croak "Cannot Listen with a peer address";
}
if( defined $arg->{GetAddrInfoFlags} ) {
$hints{flags} = $arg->{GetAddrInfoFlags};
}
else {
$hints{flags} = $AI_ADDRCONFIG;
}
if( defined( my $family = $arg->{Family} ) ) {
$hints{family} = $family;
}
if( defined( my $type = $arg->{Type} ) ) {
$hints{socktype} = $type;
}
if( defined( my $proto = $arg->{Proto} ) ) {
unless( $proto =~ m/^\d+$/ ) {
my $protonum = HAVE_GETPROTOBYNAME
? getprotobyname( $proto )
: eval { Socket->${\"IPPROTO_\U$proto"}() };
defined $protonum or croak "Unrecognised protocol $proto";
$proto = $protonum;
}
$hints{protocol} = $proto;
}
# To maintain compatibility with IO::Socket::INET, imply a default of
# SOCK_STREAM + IPPROTO_TCP if neither hint is given
if( !defined $hints{socktype} and !defined $hints{protocol} ) {
$hints{socktype} = SOCK_STREAM;
$hints{protocol} = IPPROTO_TCP;
}
# Some OSes (NetBSD) don't seem to like just a protocol hint without a
# socktype hint as well. We'll set a couple of common ones
if( !defined $hints{socktype} and defined $hints{protocol} ) {
$hints{socktype} = SOCK_STREAM if $hints{protocol} == IPPROTO_TCP;
$hints{socktype} = SOCK_DGRAM if $hints{protocol} == IPPROTO_UDP;
}
if( my $info = $arg->{LocalAddrInfo} ) {
ref $info eq "ARRAY" or croak "Expected 'LocalAddrInfo' to be an ARRAY ref";
@localinfos = @$info;
}
elsif( defined $arg->{LocalHost} or
defined $arg->{LocalService} or
HAVE_MSWIN32 and $arg->{Listen} ) {
# Either may be undef
my $host = $arg->{LocalHost};
my $service = $arg->{LocalService};
unless ( defined $host or defined $service ) {
$service = 0;
}
local $1; # Placate a taint-related bug; [perl #67962]
defined $service and $service =~ s/\((\d+)\)$// and
my $fallback_port = $1;
my %localhints = %hints;
$localhints{flags} |= AI_PASSIVE;
( my $err, @localinfos ) = getaddrinfo( $host, $service, \%localhints );
if( $err and defined $fallback_port ) {
( $err, @localinfos ) = getaddrinfo( $host, $fallback_port, \%localhints );
}
if( $err ) {
$@ = "$err";
$! = EINVAL;
return;
}
}
if( my $info = $arg->{PeerAddrInfo} ) {
ref $info eq "ARRAY" or croak "Expected 'PeerAddrInfo' to be an ARRAY ref";
@peerinfos = @$info;
}
elsif( defined $arg->{PeerHost} or defined $arg->{PeerService} ) {
defined( my $host = $arg->{PeerHost} ) or
croak "Expected 'PeerHost'";
defined( my $service = $arg->{PeerService} ) or
croak "Expected 'PeerService'";
local $1; # Placate a taint-related bug; [perl #67962]
defined $service and $service =~ s/\((\d+)\)$// and
my $fallback_port = $1;
( my $err, @peerinfos ) = getaddrinfo( $host, $service, \%hints );
if( $err and defined $fallback_port ) {
( $err, @peerinfos ) = getaddrinfo( $host, $fallback_port, \%hints );
}
if( $err ) {
$@ = "$err";
$! = EINVAL;
return;
}
}
my $INT_1 = pack "i", 1;
my @sockopts_enabled;
push @sockopts_enabled, [ SOL_SOCKET, SO_REUSEADDR, $INT_1 ] if $arg->{ReuseAddr};
push @sockopts_enabled, [ SOL_SOCKET, SO_REUSEPORT, $INT_1 ] if $arg->{ReusePort};
push @sockopts_enabled, [ SOL_SOCKET, SO_BROADCAST, $INT_1 ] if $arg->{Broadcast};
if( my $sockopts = $arg->{Sockopts} ) {
ref $sockopts eq "ARRAY" or croak "Expected 'Sockopts' to be an ARRAY ref";
foreach ( @$sockopts ) {
ref $_ eq "ARRAY" or croak "Bad Sockopts item - expected ARRAYref";
@$_ >= 2 and @$_ <= 3 or
croak "Bad Sockopts item - expected 2 or 3 elements";
my ( $level, $optname, $value ) = @$_;
# TODO: consider more sanity checking on argument values
defined $value or $value = $INT_1;
push @sockopts_enabled, [ $level, $optname, $value ];
}
}
my $blocking = $arg->{Blocking};
defined $blocking or $blocking = 1;
my $v6only = $arg->{V6Only};
# IO::Socket::INET defines this key. IO::Socket::IP always implements the
# behaviour it requests, so we can ignore it, unless the caller is for some
# reason asking to disable it.
if( defined $arg->{MultiHomed} and !$arg->{MultiHomed} ) {
croak "Cannot disable the MultiHomed parameter";
}
my @infos;
foreach my $local ( @localinfos ? @localinfos : {} ) {
foreach my $peer ( @peerinfos ? @peerinfos : {} ) {
next if defined $local->{family} and defined $peer->{family} and
$local->{family} != $peer->{family};
next if defined $local->{socktype} and defined $peer->{socktype} and
$local->{socktype} != $peer->{socktype};
next if defined $local->{protocol} and defined $peer->{protocol} and
$local->{protocol} != $peer->{protocol};
my $family = $local->{family} || $peer->{family} or next;
my $socktype = $local->{socktype} || $peer->{socktype} or next;
my $protocol = $local->{protocol} || $peer->{protocol} || 0;
push @infos, {
family => $family,
socktype => $socktype,
protocol => $protocol,
localaddr => $local->{addr},
peeraddr => $peer->{addr},
};
}
}
if( !@infos ) {
# If there was a Family hint then create a plain unbound, unconnected socket
if( defined $hints{family} ) {
@infos = ( {
family => $hints{family},
socktype => $hints{socktype},
protocol => $hints{protocol},
} );
}
# If there wasn't, use getaddrinfo()'s AI_ADDRCONFIG side-effect to guess a
# suitable family first.
else {
( my $err, @infos ) = getaddrinfo( "", "0", \%hints );
if( $err ) {
$@ = "$err";
$! = EINVAL;
return;
}
# We'll take all the @infos anyway, because some OSes (HPUX) are known to
# ignore the AI_ADDRCONFIG hint and return AF_INET6 even if they don't
# support them
}
}
# In the nonblocking case, caller will be calling ->setup multiple times.
# Store configuration in the object for the ->setup method
# Yes, these are messy. Sorry, I can't help that...
${*$self}{io_socket_ip_infos} = \@infos;
${*$self}{io_socket_ip_idx} = -1;
${*$self}{io_socket_ip_sockopts} = \@sockopts_enabled;
${*$self}{io_socket_ip_v6only} = $v6only;
${*$self}{io_socket_ip_listenqueue} = $listenqueue;
${*$self}{io_socket_ip_blocking} = $blocking;
${*$self}{io_socket_ip_errors} = [ undef, undef, undef ];
# ->setup is allowed to return false in nonblocking mode
$self->setup or !$blocking or return undef;
return $self;
}
sub setup
{
my $self = shift;
while(1) {
${*$self}{io_socket_ip_idx}++;
last if ${*$self}{io_socket_ip_idx} >= @{ ${*$self}{io_socket_ip_infos} };
my $info = ${*$self}{io_socket_ip_infos}->[${*$self}{io_socket_ip_idx}];
$self->socket( @{$info}{qw( family socktype protocol )} ) or
( ${*$self}{io_socket_ip_errors}[2] = $!, next );
$self->blocking( 0 ) unless ${*$self}{io_socket_ip_blocking};
foreach my $sockopt ( @{ ${*$self}{io_socket_ip_sockopts} } ) {
my ( $level, $optname, $value ) = @$sockopt;
$self->setsockopt( $level, $optname, $value ) or ( $@ = "$!", return undef );
}
if( defined ${*$self}{io_socket_ip_v6only} and defined $AF_INET6 and $info->{family} == $AF_INET6 ) {
my $v6only = ${*$self}{io_socket_ip_v6only};
$self->setsockopt( IPPROTO_IPV6, IPV6_V6ONLY, pack "i", $v6only ) or ( $@ = "$!", return undef );
}
if( defined( my $addr = $info->{localaddr} ) ) {
$self->bind( $addr ) or
( ${*$self}{io_socket_ip_errors}[1] = $!, next );
}
if( defined( my $listenqueue = ${*$self}{io_socket_ip_listenqueue} ) ) {
$self->listen( $listenqueue ) or ( $@ = "$!", return undef );
}
if( defined( my $addr = $info->{peeraddr} ) ) {
if( $self->connect( $addr ) ) {
$! = 0;
return 1;
}
if( $! == EINPROGRESS or $! == EWOULDBLOCK ) {
${*$self}{io_socket_ip_connect_in_progress} = 1;
return 0;
}
# If connect failed but we have no system error there must be an error
# at the application layer, like a bad certificate with
# IO::Socket::SSL.
# In this case don't continue IP based multi-homing because the problem
# cannot be solved at the IP layer.
return 0 if ! $!;
${*$self}{io_socket_ip_errors}[0] = $!;
next;
}
return 1;
}
# Pick the most appropriate error, stringified
$! = ( grep defined, @{ ${*$self}{io_socket_ip_errors}} )[0];
$@ = "$!";
return undef;
}
sub connect :method
{
my $self = shift;
# It seems that IO::Socket hides EINPROGRESS errors, making them look like
# a success. This is annoying here.
# Instead of putting up with its frankly-irritating intentional breakage of
# useful APIs I'm just going to end-run around it and call core's connect()
# directly
if( @_ ) {
my ( $addr ) = @_;
# Annoyingly IO::Socket's connect() is where the timeout logic is
# implemented, so we'll have to reinvent it here
my $timeout = ${*$self}{'io_socket_timeout'};
return connect( $self, $addr ) unless defined $timeout;
my $was_blocking = $self->blocking( 0 );
my $err = defined connect( $self, $addr ) ? 0 : $!+0;
if( !$err ) {
# All happy
$self->blocking( $was_blocking );
return 1;
}
elsif( not( $err == EINPROGRESS or $err == EWOULDBLOCK ) ) {
# Failed for some other reason
$self->blocking( $was_blocking );
return undef;
}
elsif( !$was_blocking ) {
# We shouldn't block anyway
return undef;
}
my $vec = ''; vec( $vec, $self->fileno, 1 ) = 1;
if( !select( undef, $vec, $vec, $timeout ) ) {
$self->blocking( $was_blocking );
$! = ETIMEDOUT;
return undef;
}
# Hoist the error by connect()ing a second time
$err = $self->getsockopt( SOL_SOCKET, SO_ERROR );
$err = 0 if $err == EISCONN; # Some OSes give EISCONN
$self->blocking( $was_blocking );
$! = $err, return undef if $err;
return 1;
}
return 1 if !${*$self}{io_socket_ip_connect_in_progress};
# See if a connect attempt has just failed with an error
if( my $errno = $self->getsockopt( SOL_SOCKET, SO_ERROR ) ) {
delete ${*$self}{io_socket_ip_connect_in_progress};
${*$self}{io_socket_ip_errors}[0] = $! = $errno;
return $self->setup;
}
# No error, so either connect is still in progress, or has completed
# successfully. We can tell by trying to connect() again; either it will
# succeed or we'll get EISCONN (connected successfully), or EALREADY
# (still in progress). This even works on MSWin32.
my $addr = ${*$self}{io_socket_ip_infos}[${*$self}{io_socket_ip_idx}]{peeraddr};
if( connect( $self, $addr ) or $! == EISCONN ) {
delete ${*$self}{io_socket_ip_connect_in_progress};
$! = 0;
return 1;
}
else {
$! = EINPROGRESS;
return 0;
}
}
sub connected
{
my $self = shift;
return defined $self->fileno &&
!${*$self}{io_socket_ip_connect_in_progress} &&
defined getpeername( $self ); # ->peername caches, we need to detect disconnection
}
sub _get_host_service
{
my $self = shift;
my ( $addr, $flags, $xflags ) = @_;
defined $addr or
$! = ENOTCONN, return;
$flags |= NI_DGRAM if $self->socktype == SOCK_DGRAM;
my ( $err, $host, $service ) = getnameinfo( $addr, $flags, $xflags || 0 );
croak "getnameinfo - $err" if $err;
return ( $host, $service );
}
sub _unpack_sockaddr
{
my ( $addr ) = @_;
my $family = sockaddr_family $addr;
if( $family == AF_INET ) {
return ( Socket::unpack_sockaddr_in( $addr ) )[1];
}
elsif( defined $AF_INET6 and $family == $AF_INET6 ) {
return ( Socket::unpack_sockaddr_in6( $addr ) )[1];
}
else {
croak "Unrecognised address family $family";
}
}
sub sockhost_service
{
my $self = shift;
my ( $numeric ) = @_;
$self->_get_host_service( $self->sockname, $numeric ? NI_NUMERICHOST|NI_NUMERICSERV : 0 );
}
sub sockhost { my $self = shift; scalar +( $self->_get_host_service( $self->sockname, NI_NUMERICHOST, NIx_NOSERV ) )[0] }
sub sockport { my $self = shift; scalar +( $self->_get_host_service( $self->sockname, NI_NUMERICSERV, NIx_NOHOST ) )[1] }
sub sockhostname { my $self = shift; scalar +( $self->_get_host_service( $self->sockname, 0, NIx_NOSERV ) )[0] }
sub sockservice { my $self = shift; scalar +( $self->_get_host_service( $self->sockname, 0, NIx_NOHOST ) )[1] }
sub sockaddr { my $self = shift; _unpack_sockaddr $self->sockname }
sub peerhost_service
{
my $self = shift;
my ( $numeric ) = @_;
$self->_get_host_service( $self->peername, $numeric ? NI_NUMERICHOST|NI_NUMERICSERV : 0 );
}
sub peerhost { my $self = shift; scalar +( $self->_get_host_service( $self->peername, NI_NUMERICHOST, NIx_NOSERV ) )[0] }
sub peerport { my $self = shift; scalar +( $self->_get_host_service( $self->peername, NI_NUMERICSERV, NIx_NOHOST ) )[1] }
sub peerhostname { my $self = shift; scalar +( $self->_get_host_service( $self->peername, 0, NIx_NOSERV ) )[0] }
sub peerservice { my $self = shift; scalar +( $self->_get_host_service( $self->peername, 0, NIx_NOHOST ) )[1] }
sub peeraddr { my $self = shift; _unpack_sockaddr $self->peername }
# This unbelievably dodgy hack works around the bug that IO::Socket doesn't do
# it
# https://rt.cpan.org/Ticket/Display.html?id=61577
sub accept
{
my $self = shift;
my ( $new, $peer ) = $self->SUPER::accept( @_ ) or return;
${*$new}{$_} = ${*$self}{$_} for qw( io_socket_domain io_socket_type io_socket_proto );
return wantarray ? ( $new, $peer )
: $new;
}
# This second unbelievably dodgy hack guarantees that $self->fileno doesn't
# change, which is useful during nonblocking connect
sub socket :method
{
my $self = shift;
return $self->SUPER::socket(@_) if not defined $self->fileno;
# I hate core prototypes sometimes...
socket( my $tmph, $_[0], $_[1], $_[2] ) or return undef;
dup2( $tmph->fileno, $self->fileno ) or die "Unable to dup2 $tmph onto $self - $!";
}
# Versions of IO::Socket before 1.35 may leave socktype undef if from, say, an
# ->fdopen call. In this case we'll apply a fix
BEGIN {
if( eval($IO::Socket::VERSION) < 1.35 ) {
*socktype = sub {
my $self = shift;
my $type = $self->SUPER::socktype;
if( !defined $type ) {
$type = $self->sockopt( Socket::SO_TYPE() );
}
return $type;
};
}
}
sub as_inet
{
my $self = shift;
croak "Cannot downgrade a non-PF_INET socket to IO::Socket::INET" unless $self->sockdomain == AF_INET;
return IO::Socket::INET->new_from_fd( $self->fileno, "r+" );
}
sub split_addr
{
shift;
my ( $addr ) = @_;
local ( $1, $2 ); # Placate a taint-related bug; [perl #67962]
if( $addr =~ m/\A\[($IPv6_re)\](?::([^\s:]*))?\z/ or
$addr =~ m/\A([^\s:]*):([^\s:]*)\z/ ) {
return ( $1, $2 ) if defined $2 and length $2;
return ( $1, undef );
}
return ( $addr, undef );
}
sub join_addr
{
shift;
my ( $host, $port ) = @_;
$host = "[$host]" if $host =~ m/:/;
return join ":", $host, $port if defined $port;
return $host;
}
# Since IO::Socket->new( Domain => ... ) will delete the Domain parameter
# before calling ->configure, we need to keep track of which it was
package # hide from indexer
IO::Socket::IP::_ForINET;
use base qw( IO::Socket::IP );
sub configure
{
# This is evil
my $self = shift;
my ( $arg ) = @_;
bless $self, "IO::Socket::IP";
$self->configure( { %$arg, Family => Socket::AF_INET() } );
}
package # hide from indexer
IO::Socket::IP::_ForINET6;
use base qw( IO::Socket::IP );
sub configure
{
# This is evil
my $self = shift;
my ( $arg ) = @_;
bless $self, "IO::Socket::IP";
$self->configure( { %$arg, Family => Socket::AF_INET6() } );
}
0x55AA;