############################################################################### # Net::Whois::RIPE - implementation of RIPE Whois. # Copyright (C) 2005-2006 Paul Gampe, Kevin Baker # vim:tw=78:ts=4 ############################################################################### package Net::Whois::RIPE; use strict; use Carp; use IO::Socket; use Net::Whois::RIPE::Object; use Net::Whois::RIPE::Object::Template; use Net::Whois::RIPE::Iterator; use constant MAX_RETRY_ATTEMPTS => 3; # number of times to attempt connection use constant SLEEP_INTERVAL => 1; # time interval between attempts use vars qw($VERSION); $VERSION = do { my @r = ( q$Revision: 1.4 $ =~ /\d+/g ); sprintf "%d." . "%02d" x $#r, @r }; # class wide debug flag 0=off,1=on,2=on for IO::Socket my $DEBUG = 0; # couple of regexs that may need attention my $RE_WHOIS = '(?:whois\.apnic\.net)$'; my $RE_RIPE = '(?:ripe|ra|apnic|afrinic|rr\.arin|6bone)\.net$'; # version string to let whois know which client version it is talking to my $VER_FLAG = '-VNWR' . $VERSION; sub new { my $proto = shift; my ( $host, %arg ) = @_; my $class = ref($proto) || $proto; my $debug = exists $arg{Debug} ? $arg{Debug} : 0; unless ($host) { carp "new: no hostname found." if $DEBUG || $debug; return undef; } my $self = bless { # object fields SOCKET => undef, # unconnected TIMEOUT => $arg{Timeout} || 30, # default timeout MAX_READ_SIZE => 0, # no read size limit DEBUG => $debug, # object debug # whois flags FLAG_a => 0, # search all databases FLAG_B => 0, # disable filtering of "notify:", "changed:", "e-mail: FLAG_F => 0, # fast raw output FLAG_g => 0, # used to sync databases. shouldn't be used for general use FLAG_G => 0, # Disables the grouping of objects by relevance FLAG_h => $host, # host to connect to FLAG_i => '', # do an inverse lookup for specified attributes FLAG_k => 0, # for persistant socket connection FLAG_K => 0, # return only primary keys FLAG_L => 0, # find all Less specific matches FLAG_m => 0, # find first level more specific matches FLAG_M => 0, # find all More specific matches FLAG_p => $arg{Port} || 'whois', # port, usually 43 for whois FLAG_r => 0, # turn off recursive lookups FLAG_R => 0, # do not trigger referral mechanism FLAG_s => '', # search databases with source 'source' FLAG_S => 0, # tell server to leave out 'syntactic sugar' FLAG_t => '', # requests template for object of type 'type' FLAG_T => '', # only look for objects of type 'type' FLAG_v => '', # request verbose template for object of type 'type' FLAG_V => $VER_FLAG, # client Version }, $class; # if host matches a server that accepts a # referral IP then add the remote addr to version $self->{FLAG_V} = $VER_FLAG . "," . $ENV{"REMOTE_ADDR"} if $self->{FLAG_h} =~ /$RE_WHOIS/oi and $ENV{"REMOTE_ADDR"}; # connect to server #unless ($self->_connect) { # carp "new: whois connection failure." if $DEBUG || $debug; # return undef; # } return $self; } sub connect { my $self = shift; $self->_connect(); } sub query_iterator { my $self = shift; my $query_key = shift; unless ($query_key) { carp "query: no QUERY_KEY found" if $self->debug; return undef; } # TODO - close the connection pseudo gracefully if the timeout value # expires. allow user to set timeouts? my $sock; unless ( $sock = $self->_connect ) { carp "query: unable to obtain socket" if $self->debug; return undef; } my $string; unless ( $string = $self->_options($query_key) ) { carp "query: unable to parse options" if $self->debug; return undef; } return Net::Whois::RIPE::Iterator->new( $self, $string . "\n\n" ); } sub template { my $self = shift; my $template = shift; unless ($template) { carp "template: no WHOIS OBJECT NAME found" if $self->debug; return wantarray ? () : undef; } $self->{FLAG_t} = 1; my $string; unless ( $string = $self->_options($template) ) { carp "template: unable to parse options" if $self->debug; return undef; } return $self->_query( $string . "\n\n", "Net::Whois::RIPE::Object::Template" ); } sub verbose_template { my $self = shift; my $template = shift; unless ($template) { carp "verbose_template: no WHOIS OBJECT NAME found" if $self->debug; return wantarray ? () : undef; } $self->{FLAG_v} = 1; my $string; unless ( $string = $self->_options($template) ) { carp "verbose_template: unable to parse options" if $self->debug; return undef; } return $self->_query( $string . "\n\n", "Net::Whois::RIPE::Object::Template" ); } sub query { my $self = shift; my $query_key = shift; unless ($query_key) { carp "query: no QUERY_KEY found" if $self->debug; return undef; } my $string; unless ($string = $self->_options($query_key)) { carp "query: unable to parse options" if $self->debug; return undef; } if ($self->{cache}) { my $object = $self->{cache}->get($string); return wantarray ? @$object : $object->[0] if $object; } # TODO - close the connection pseudo gracefully if the timeout value # expires. allow user to set timeouts? my $sock; unless ( $sock = $self->_connect ) { carp "query: unable to obtain socket" if $self->debug; return undef; } my @object = $self->_query($string."\n", "Net::Whois::RIPE::Object"); $self->{cache}->set($string, \@object) if $self->{cache} and @object; return wantarray ? @object : $object[0]; } sub update { my $self = shift; my $message = shift; unless ($message) { carp 'update: no TEXT message found' if $self->debug; return undef; } # pull out login and domain from the changed: line my ( $login, $domain ) = ( $message =~ /changed:\s+(.+)@(.+)\n/ ); unless ( $login and $domain ) { carp "update: cannot find 'changed' attribute" if $self->debug; return undef; } my $string = $self->{FLAG_V} . " -U $login $domain\n" . $message; return $self->_query( $string, "Net::Whois::RIPE::Object" ); } sub _query { my $self = shift; my $string = shift; my $ripe_type = shift; my $sock; my @objects; my $connection_attempts = 0; while ($connection_attempts < MAX_RETRY_ATTEMPTS) { unless ( $sock = $self->_connect ) { carp "_query: unable to obtain socket" if $self->debug; return undef; } unless ( print $sock $string ) { carp "_query: unable to print to socket:\n$string" if $self->debug; return undef; } $sock->flush; my $bytes = 0; my $max = $self->max_read_size; while ( my $t = $ripe_type->new( $sock, $self->{FLAG_k} ) ) { # discards pseudo-records containing only comments next if $self->{FLAG_k} and not $t->attributes and $t->success; if ( $t->size <= 2 ) { return wantarray ? @objects : $objects[0]; } push @objects, $t; $bytes += $t->size; if ( $max and $bytes > $max ) { my $msg = "exceeded maximum read size of " . $max . " bytes." . " results may have been truncated."; $t->push_warn($msg); carp "_query: " . $msg if $self->debug; last; } last if $sock->eof or not wantarray; } # exit the retry loop unless the client has been disconnected last unless not @objects and $sock->eof; carp "_query: disconnected by server " . $self->{FLAG_h} . ", trying again..." if $self->debug; $self->_disconnect; sleep SLEEP_INTERVAL; $connection_attempts++; next; } if ( $sock and $self->{FLAG_k} ) { $sock->flush; $self->{SOCKET}->flush; } else { $self->_disconnect; } return wantarray ? @objects : $objects[0]; } sub max_read_size { my $self = shift; @_ ? $self->{MAX_READ_SIZE} = 0 + shift: $self->{MAX_READ_SIZE}; } sub disconnect { $_[0]->_disconnect; } sub cache { return $_[0]->{cache} if not defined $_[1]; $_[0]->{cache} = $_[1]; } sub search_all { $_[0]->{FLAG_a} = 1 } sub fast_raw { $_[0]->{FLAG_F} = 1 } sub set_persistance { $_[0]->{FLAG_a} = 1 } sub find_less { $_[0]->{FLAG_L} = 1 } sub find_more { $_[0]->{FLAG_m} = 1 } sub find_all_more { $_[0]->{FLAG_M} = 1 } sub no_recursive { $_[0]->{FLAG_r} = 1 } sub no_referral { $_[0]->{FLAG_R} = 1 } sub no_sugar { $_[0]->{FLAG_S} = 1 } sub persistant { $_[0]->{FLAG_k} = 1 } sub no_filtering { $_[0]->{FLAG_B} = 1 } sub no_grouping { $_[0]->{FLAG_G} = 1 } # sync is special and is here for completeness. it # is not expected that it wil be used sub sync { my $self = shift; $self->{FLAG_g} = shift; } sub inverse_lookup { my $self = shift; $self->{FLAG_i} = shift; } sub primary_only { my $self = shift; $self->{FLAG_K} = shift; } sub source { my $self = shift; $self->{FLAG_s} = shift; } sub type { my $self = shift; $self->{FLAG_T} = shift; } sub port { my $self = shift; unless ( $self->{FLAG_p} ) { carp 'port: no port defined!' if $self->debug; return undef; } # trying to change port? not allowed if (@_) { carp "port: cannot switch port." if $self->debug; } return $self->{FLAG_p}; } sub server { my $self = shift; unless ( $self->{FLAG_h} ) { carp 'server: no hostname found' if $self->debug; return undef; } # trying to change servers? not allowed if (@_) { carp "server: cannot switch server." if $self->debug; } return $self->{FLAG_h}; } sub debug { my $self = shift; if (@_) { ref($self) ? $self->{DEBUG} = shift: $DEBUG = shift; } return ref($self) ? ( $DEBUG || $self->{DEBUG} ) : $DEBUG; } sub DESTROY { my $self = shift; carp "Destroying ", ref($self) if $self->debug; if ( $self->{SOCKET} and $self->{FLAG_k} ) { # $sock->flush; $self->{SOCKET}->flush; } else { $self->_disconnect; } } END { carp "All Net::Whois::RIPE objects are going away now." if $DEBUG; } ############################################################################### ## P R I V A T E M E T H O D S ############################################################################### sub _connect { my $self = shift; if ( $self->{SOCKET} and $self->{SOCKET}->connected ) { #carp 'already connected to '.$self->{SOCKET}->peerhost; return $self->{SOCKET}; } my $sock; my $attempt = 0; my $connected = 0; while ( !$connected and $attempt < MAX_RETRY_ATTEMPTS ) { if ($attempt) { carp "_connect: to server " . $self->{FLAG_h} . " failed, trying again..." if $self->debug; sleep SLEEP_INTERVAL; } $attempt++; $connected = 1 if $sock = IO::Socket::INET->new( PeerAddr => $self->server, PeerPort => $self->port, Proto => 'tcp', Timeout => $self->{TIMEOUT} ); carp $@ if $@ and $self->debug > 1; } if ( not $connected ) { carp "Failed to connect to host [" . $self->server . "]" if $self->debug; return undef; } $sock->autoflush; # on by default since IO 1.18, but anyhow return $self->{SOCKET} = $sock; } sub _disconnect { my $self = shift; my $sock = $self->{SOCKET}; return unless $sock and $sock->connected; $sock->flush; # probably not necessary carp "disconnecting from " . $self->{FLAG_h} if $self->debug; $sock->close; $self->{SOCKET} = undef; } sub _options { my $self = shift; my $key = shift; if ( ( !$key ) && ( !$self->{FLAG_t} ) && ( !$self->{FLAG_g} ) && ( !$self->{FLAG_v} ) && ( !( ( $self->{FLAG_g} ) && ( $self->{FLAG_t} ) ) ) ) { carp '_options: no search key or valid option found' if $self->debug; return undef; } if ( !$self->{FLAG_h} ) { carp "_options: no hostname found" if $self->debug; return undef; } if ( $self->{FLAG_L} ) { if ( $self->debug ) { carp "_options: warning -L overrides -m\n" if $self->{FLAG_m}; carp "_options: warning -L overrides -M\n" if $self->{FLAG_M}; } $self->{FLAG_m} = 0; $self->{FLAG_M} = 0; } if ( $self->{FLAG_m} ) { if ( $self->debug ) { carp "_options: warning -m overrides -M\n" if $self->{FLAG_M}; } $self->{FLAG_M} = 0; } my $query = ""; # tell the server what version of RIPE whois we are running, # but only if we are sure that we are talking to an # RIPE whois server if ( ( $self->{FLAG_h} =~ /$RE_RIPE/oi ) || $self->{FLAG_a} || $self->{FLAG_B} || $self->{FLAG_g} || $self->{FLAG_G} || $self->{FLAG_F} || $self->{FLAG_i} || $self->{FLAG_k} || $self->{FLAG_K} || $self->{FLAG_m} || $self->{FLAG_M} || $self->{FLAG_R} || $self->{FLAG_L} || $self->{FLAG_r} || $self->{FLAG_s} || $self->{FLAG_S} || $self->{FLAG_t} || $self->{FLAG_v} || $self->{FLAG_T} ) { $query .= $self->{FLAG_V} . " "; } # XXX -g is an undocumented option: get specified updates: # -g Source:First-Last # get updates with 'Source' # from serial 'First' till 'Last' (you may use 'LAST') $query .= "-a " if ( $self->{FLAG_a} ); $query .= "-B " if ( $self->{FLAG_B} ); $query .= "-F " if ( $self->{FLAG_F} ); $query .= "-g " . $self->{FLAG_g} . " " if ( $self->{FLAG_g} ); $query .= "-G " if ( $self->{FLAG_G} ); $query .= "-i " . $self->{FLAG_i} . " " if ( $self->{FLAG_i} ); $query .= "-k " if ( $self->{FLAG_k} ); $query .= "-K " if ( $self->{FLAG_K} ); $query .= "-L " if ( $self->{FLAG_L} ); $query .= "-m " if ( $self->{FLAG_m} ); $query .= "-M " if ( $self->{FLAG_M} ); $query .= "-r " if ( $self->{FLAG_r} ); $query .= "-R " if ( $self->{FLAG_R} ); $query .= "-S " if ( $self->{FLAG_S} ); $query .= "-s " . $self->{FLAG_s} . " " if ( $self->{FLAG_s} ); $query .= "-T " . $self->{FLAG_T} . " " if ( $self->{FLAG_T} ); $query .= "-t " if ( $self->{FLAG_t} ); $query .= "-v " if ( $self->{FLAG_v} ); $query .= $key; carp "_options: parsed query string: $query" if $self->debug; return $query; } 1; __END__