package WhoisNGNGNG;
use strict;

=head1 NAME

  WhoisNGNGNG.pm - yet another whois parsing module

=head1 SYNOPSIS

  use WhoisNGNGNG;
  $WhoisNGNGNG::WARN_NOT_FOUND = 0;
  my $domain_name = 'slashdot.org';
  my ($whois_response, $whois_server, $whois_request) = WhoisNGNGNG::get_whois_raw($domain_name);
  my $whois_obj = WhoisNGNGNG::parse_whois($domain_name, $whois_response, $whois_server);

=head1 DESCRIPTION

=head2 Contacts

The contact objects are any of $w->{$c}, where $c is one of RegistrantContact, AdministrativeContact, TechnicalContact, BillingContact, ZoneContact 

The contact objects themselves have members that match the contact subfields in .ORG ("Name", etc.),
except that we use: State, PostalCode, Fax.
Also we add NexusCategory, ApplicationPurpose for .us.

=head2 Registrar

The 'RegistrarContact' hash has these possible keys: Name, ID, HomePage, Email, IcannID

The IcannID is a string like "R39-LROR" (as used by ICANN for accredited registrars).

The "ID" is anything the whois record calls the Registrar ID. 

=head2 Dates

The dates are ExpirationDate, CreatedDate, UpdatedDate, TransferedDate.
They are strings in the same format as the record.

=head2 Name Servers

The name servers are an array reference in "NameServers". They are names only, no ip.

=head2 Domain Status

The domain status is in "DomainStatus". It is an array of whatever strings are in the registry, with zero, one, or more entries.




=head1 IMPLEMENTATION

Unlike most of the other Perl whois parsers, I did not base this on maintaining
a set of registry-specific parsers, each of which would be a collection of patterns.
That would be cleaner, but it isn't what I did.
Rather, I have a single generic parser that (with some registry-specific tweaks) will parse
any whois output, based on an assumption of it consisting of single-line fields,
multi-line fields, and ignorable content. 

=head1 TODO

Optionally use Net::Whois::Raw, which is better at eliminating blurbs and detecting no match.

Extend pattern matching for personal information suppressed by registrar.

Do better at trimming trailing white space.

XML output, perhaps matching either RIPE or EPP.

More control over warnings (missing fields, unexpected fields, etc.).

Support country domains (ccTLD's) properly.
See list at http://www.iana.org/cctld/cctld-whois.htm

All gTLDs: http://www.iana.org/gtld/gtld.htm
    (plus not listed as of dec 2004: .mobi .jobs .travel .post )

All gTLD registrars: http://www.icann.org/registrars/accredited-list.html

Some gTLDs are "sponsored" (sTLDs), which include .aero, .coop, .museum.
The newer unsponsored TLDs are .biz, .info, .name, and .pro.
See http://www.icann.org/tlds/

Investigate third-level domains, for example: *.co.uk *.com.tw *.kids.us

Country vs. CountryCode. 
FaxExt and PhoneExt

Use a general purpose postal address parser instead.


=head1 Whois Protocol

=head2 Basic Whois Protocol

The Whois protocol is defined in RFC 954. It is extremely simple; send a line of ascii
text to port 43 and you get human readable information back.
The kinds of search expressions vary, as does the response format.

Note that in addition to "Referral Whois" (RWhois, which is a protocol extension), 
there is also loose convention for a "content redirect" -- a response
can indicate that another whois server is appropriate, by sending back a line
that matches the regexp "Whois Server: (.*)".

You might not get a unique record back. For example,
if you specify: 

  whois -h whois.internic.net microsoft.com
  whois -h whois.internic.net google.com

you will get multiple matches and no data.

If you specify "=microsoft.com" or "=google.com", you will get
more data about each of those matches, showing what whois server to go to for each
of them.

If your search string is instead "domain microsoft.com" you
will get the exact match. This can be done with bsd command-line
whois as:
  whois -h whois.internic.net "domain microsoft.com"
Note that the quotes are required, as well as the use of -h (bsd whois
will follow content referrals if no -h, and will incorrectly do this,
appending the result of "No match for DOMAIN" at the end.) 

Note that some registrars deliberately vary their response format in successive
requests, to foil automatic parsers. bulkregister and alldomains both appear to do 
this. Not that it can't be defeated....

=head2 Related Protocol Standards 

Whois:

  RFC 954, "NICNAME/WHOIS", October 1985

RWhois: 

  http://www.rwhois.net/
  RFC 1714, "Referral Whois Protocol (RWhois)", November 1994
  RFC 2167, "Referral Whois (RWhois) Protocol V1.5", June 1997

Whois++:

  RFC1834, "Whois and Network Information Lookup Service Whois++", August 1995
  RFC1835, "Architecture of the WHOIS++ service", August 1995
  RFC1913, "Architecture of the Whois++ Index Service", February 1996
  RFC1914, "How to Interact with a Whois++ Mesh", February 1996
  RFC2957, "The application/whoispp-query Content-Type", October 2000
  RFC2958, "The application/whoispp-response Content-type", October 2000

RPSL:

 RFC2622, "Routing Policy Specification Language (RPSL)", June 1999

SWIP (Shared Whois Project):

 RFC2050, "INTERNET REGISTRY IP ALLOCATION GUIDELINES", November 1996
 http://www.arin.net/library/guidelines/swip.html

CRISP:

  http://www.ietf.org/html.charters/crisp-charter.html

Whois in XML:
 
  "Whois Export and Exchange Format." http://xml.coverpages.org/whois-IETF.html


=head1 Whois Client Implementations

The situation is quite dismal. None of the existing Perl modules seem to work properly.
Net::Whois::Raw is valuable as a good fetching mechanism, but it does not parse.

=head2 Command-Line

These commands below do not parse, just fetch.

=head3 bsd whois

This is the command-line whois client that exists on the bsd unixes,
including mac osx.

If -i or -a and so on are not specified, it prepends the tld (such as "COM") to whois-servers.net.
If that does not exist, it tries whois.crsnic.net

=head3 jwhois

See http://www.gnu.org/software/jwhois/jwhois.html

This is the whois client that RedHat linux uses.
It implements RFC954 and RFC2167 and content redirects.
It also supports "HTTP using an external browser", whatever that means.
It supports RIPE through extra command-line switches.  

Of course it is not compatible with bsd whois. Just a few command-line options
are in common; fortunately one of them is -h.

It starts by default with whois.internic.net.

=head3 MD whois

See http://www.linux.it/~md/software/ and http://freshmeat.net/projects/whois/

It is used in debian, mandrake, suse, PLD.
GPL, in C.

=head3 BW Whois

See http://whois.bw.org/ . By Bill Weinman. In Perl, Perl license.

Does not parse (except for trimming the disclaimer); just a command-line client. 
Does whois and rwhois. Defaults to whois.crsnic.net.
Follows content redirects. Supports arin search and handle search.
Supports a CGI mode.

 
=head2 web-based whois

There are many such services, such as:

   http://www.nsiregistry.com/cgi-bin/whois
   http://www.internic.net/whois.html

=head2 Commercial Libraries and Services

=head3 Registry Fusion http://www.hexillion.com/whois/pricing.htm

A commercial product that converts to XML.
Does not support ccTLDs; it says it will do so "in the near future".

=head2 Open Source Libraries

=head3 http://whois-parser.sourceforge.net/

Java, LGPL. 
Started in 2002; no releases since then.

Uses a set of regular expressions to parse a large number .COM registrars; see http://whois-parser.sourceforge.net/registrar-support.html

=head3 http://phpwhois.com/ ( http://sourceforge.net/projects/phpwhois/ )

PHP, GPL. Started 1999. Still active CVS in 2004.

Probably has the best collection of per-registry handlers of any package listed here.
Supports many different .COM registries, as well as IP responses from ARIN.
Supports some ccTLDs.

=head3 http://freshmeat.net/projects/rwhois.py/

Python. Unknown license. Started in 2003, last release in 2003.

Has about a dozen parsers, for .COM, .INT, .COOP, .BIZ, .INFO.

=head3 http://wp-whois-proxy.sourceforge.net/

Perl GPL. A rewrite of the geektools whois proxy. Does not parse, but does content referrals.

=head3 Net::Whois

Years 1997-1999. Perl.

Connects to whois.networksolutions.com for COM (not authoritative) using the port named 'whois' (not in /etc/services on mac osx). 

Does not follow content redirects, and is generally quite weak.

=head3 Net::WhoisNG

Perl, Perl license. Started in 2004.

It does a direct socket connection, assuming PeerPort is 'whois' (failing on OSX).
It starts with com.whois-servers.net.
It attempts to follow content redirects.

It does extra work to parse dates.
It supports .info and .biz, but does not support the wide spectrum of TLDs, nor is
it comprehensive in its .COM support.


=head3 Net::ParseWhois

Perl, Perl license. Started in 2000. Last release in 2001.

It does a direct socket connection to retrieve whois data.
It has a hard-coded PeerPort => 'whois', which won't work on mac osx which has
an /etc/services that calls port 43 'nicname'.

It connects to whois.nsiregistry.com for .COM, .NET, and .ORG.
It attempts to follow content referrals (see Net/ParseWhois/Domain.pm).

It supports only about a half-dozen registries.


=head3 Net::XWhois

Perl, Perl license. Initially 1998 by Vipul Ved Prakash. Some activity in 2002 from Rob Woodard. Last release in 2002.

Hardcodes whois port to 43. Starts with whois.internic.net.

Seems to have looked at quite a few registrars.
Has good support for RPSL and country domains.

But it doesn't seem to work at all out of the box.

=head3 Gandi::WhoisExtract

See http://open.gandi.net/download/WhoisExtract/
2001-2003. BSD license.


=head3 Net::Whois::Raw

Perl. Years 200-2004.

Does direct socket to 43. Starts with com.whois-servers.net for COM.
Extensive list of patterns for "not found", for content referral, and for blurbs to ignore.
Does not attempt to do other parsing.

=head2 Whois Libraries, Non-Domain Whois

The four Regional Internet Registries (RIRs) are:

  http://arin.net/     whois.arin.net       America
  http://apnic.net/    whois.apnic.net      Asia and Oceania
  http://ripe.net/     whois.ripe.net       Europe, the Middle East, northern Africa,  and parts of Asia
  http://lacnic.net/   whois.lacnic.net     Latin America and Caribbean

An emerging one is:

  http://www.afrinic.net/                   Africa

These whois servers generally do not take domain searches, but other records; see for example: http://www.arin.net/whois/index.html

The RIPE whois server historically held European ccTLD data, and still does hold some; see: 

   http://www.ripe.net/ripe/maillists/archives/db-wg/2000/msg00184.html
   http://www.ripe.net/ripe/maillists/archives/db-wg/2004/msg00482.html

The whois.arin.net server supports rwhois (v1.0 and v1.5) in addition to whois.

=head3 Net::Whois::ARIN

Specifically for ARIN lookups (for IP networks, etc.).

=head2 Net::Whois::RIPE

Specifically for RIPE lookups (as used in Europe).

=head2 Net::IRR

Specifically for the Internet Route Registry Daemon.
The route registry differs from both address allocation and domain allocation.

=head1 Whois Servers

The format is dependent on the TLD, and in the case of .COM, .NET and .EDU, is also dependent on the registrar.
(In the discussion below, we mention only .COM, but we mean all three.)

The situation of the .COM registry is particularly messy. There are several
places to consider starting (which might result in a content referral).

  whois.internic.net          # used by jwhois for COM. will not match for .ORG etc. 
  whois.networksolutions.com  # may say "No match" if not registered there. used by macos whois with -i.
  whois.nsiregistry.com       # root registry. either an answer or a content referral.
  com.whois-servers.net       # a CNAME for whois.verisign-grs.com. whois-servers.net is run by centergate.com. 
  whois.crsnic.net            # a possible fallback for any TLD. run by DotRegistrar.

A directory of ccTLD whois servers is available at: http://www.iana.org/cctld/cctld-whois.htm

=head1 Failure Responses

If there is nothing found, the responses report the failure as follows, according to TLD:

  .COM
     No match for "ASDFLJFDLJKSD.COM".
  .NET
     No match for domain "OFDSIUFDSSFD.NET". 
  .ORG
     NOT FOUND
  .INFO
     NOT FOUND
  .BIZ
     Not found: ljkdfsljkdfs.biz
  .EDU
     No Match

Note that the .ORG registry (PIR) instituted rate limiting (4 queries per minute) on WHOIS Port 43 on 20 August, 2005.
It may respond with:

   [Querying org.whois-servers.net]
   [org.whois-servers.net]
   WHOIS LIMIT EXCEEDED - SEE WWW.PIR.ORG/WHOIS FOR DETAILS


This is just a partial list; Net::Whois::Raw has a more comprehensive coverage. 

=head1 Whois Formats

=head2 .ORG format

Example: slashdot.org

This is served (always?) from whois.publicinterestregistry.net
and it seems to have a consistent simple format.

After a prolog it has "field:value" format including these fields:

  Name Server
  Expiration Date
  Sponsoring Registrar
  Registrant Name
  Admin Name
  Tech Name

There are no spaces on either side of the colon.

The contact fields are:

   ID
   Name
   Organization
   Street1
   Street2
   Street3
   City
   State/Province
   Postal Code
   Country
   Phone
   Phone Ext.
   FAX
   FAX Ext.
   Email


=head2 .INFO format

Example: great.info

Similar to .ORG format. Operated by whois.afilias.info

=head2 .BIZ format

Example: great.biz

Registry is operated by whois.neulevel.biz.
Only single-line fields, which include:

  Domain Name
  Sponsoring Registrar
  Registrant Name
  Registrant Organization
  Administrative Contact Name
  Administrative Contact Organization
  Billing Contact Name
  Technical Contact Name
  Name Server
  Domain Expiration Date

Its contact subfields are:

  ID
  Name
  Organization
  Address1
  City
  State/Province
  Postal Code
  Country
  Country Code
  Phone Number
  Email

=head2 .COM format, whois.networksolutions.com

Example: networksolutions.com

The single-line fields include:

  Domain Name
  Domain status

The multi-line fields include:

  Registrant
  Administrative Contact, Technical Contact
  Domain servers in listed order

Special parsing is required for:

  Record expires on 01-Jun-2014
  Data in Network Solutions' WHOIS database is provided by Network Solutions for information

Name servers are names and ip.

=head2 .COM format, whois.alldomains.com

Example: google.com

The single-line fields include:

  Domain Name
  Registrar Name
  Expires on..............

The multi-line fields include:

  Registrant
  Administrative Contact
  Technical Contact, Zone Contact
  Domain servers in listed order

There may be spaces before the field name, and after the colon.

The domain servers are both name and ip (whereas .ORG gives only name).

It sometimes uses tabs in its output.

The contact addresses are usually field-per-line, such as:

    Registrant:
        Google Inc.
        (DOM-258879)
        2400 E. Bayshore Pkwy
        Mountain View
        CA
        94043
        US

    Administrative Contact:
        DNS Admin
        (NIC-1340142)
        Google Inc.
        2400 E. Bayshore Pkwy
        Mountain View
        CA
        94043
        US
        dns-admin@google.com
        +1.6503300100
        Fax- +1.6506181499

=head2 .COM format, whois.opensrs.net

Example: hotmail.com

Single-line fields include:

  Registrar of Record
  Domain name

Multi-line fields include:

  Registrant
  Administrative Contact
  Technical Contact
  Registration Service Provider
  Domain servers in listed order

and this special parsing:

 Record expires on 08-Oct-2005.

The domain servers are name and ip.

=head2 .COM format, whois.melbourneit.com

Uses ... instead of colons, for example:

  Domain Name.......... melbourneit.com
  Creation Date........ 1999-04-05
  Registration Date.... 2000-05-23
  Expiry Date.......... 2013-04-05

It has single-line fields only, some of which are repeated.

=head2 .COM format, whois.easydns.com 

Example: easydns.com

Similar to opensrs.net. But no "Registration Service Provider" multi-line field, and 
has an extra prelude section with extra single-line fields:

  Domain Name
  Registrar
  Name Server
  Expiration Date

Note that this means that name servers are listed twice, both as single-line
and multi-line fields.

=head2 .COM format, whois.enom.com

Example: enom.com, namecheap.com
 
The single-line fields include:

  Registration Service Provided By
  Domain name
  Expiration date

and the multi-line fields include:

  Registrant Contact
  Administrative Contact
  Technical Contact
  Billing Contact
  Name Servers

and special parsing for:

  consent from us.  The registrar of record is eNom.  We reserve the right

The domain servers are name only.

This is at least the case with "Version 6.3 4/3/2002".

=head1 Whois formats, non-domain searches

=head2 whois.arin.net

Used for whois queries of ip addresses (or if "whois -h whois.arin.net ...")

A net block report ("n") is in "field: value" format, with fields such as:

  OrgName
  OrgTechHandle
  OrgNOCHandle
  TechName
  NetRange
  NetName

It may instead be a succession of autonomous systems, which are in title/indent form:

   Abovenet Communications, Inc ABOVENET (NET-64-124-0-0-1)
                                  64.124.0.0 - 64.125.255.255
   OpenHosting, Inc. MFN-D252-64-124-102-0-26 (NET-64-124-102-0-1)
                                  64.124.102.0 - 64.124.102.63

=head2 whois.ripe.net

Similar to whois.arin.net, but uses lowercase field names like:

  netname
  descr
  tech-c

This is apparently in compliance with RPSL (RFC2622)

Several country registries use this format as well, such as:

  .cz Czech Republic
  .fr France
  .it Italy
  .hu Hungary

A somewhat different format is used for:

  .sk Slovakia  

which has fields like: Tech-name Nameservers

=head1 Whois Update

For whois update, there are two kinds to consider: registrant to registrars, and registrars to registries.

=head2 Registrant-Registrar Update

There are no standards for registrant-registrar update. Some registrars
that support a programmatic API (vs. just going to their website) are:

  Enom http://www.enom.com/resellers/InterfaceInfo.asp
     PDQ (used by aqdomains)
     php-API (used by registerfly)
     asp-API (used by namecheap)
     RegistryRocket http://access.enom.com
     (See http://www.webhostingtalk.com/showthread.php?threadid=58308 )
  OpenSRS http://resellers.tucows.com/opensrs/documentation and http://sourceforge.net/projects/opensrs-php/
     perl and php
  BulkRegister http://www2.bulkregister.com/resellerAPI.php
  GoDaddy http://www.wildwestdomains.com/api.aspx
  Directi http://manage.directi.com/kb/servlet/KBServlet/cat116.html

Note that actually none of these APIs seem open to direct customers; only to resellers.

=head2 Registrar-Registry Update

For registrar-registry update there are two protocols, RPP (Registry-Registrar Protocol) and EPP (Extensible Provisioning Protocol).

RPP is specified using ABNF, and is 7-bit text. It is specified in RFC 2832. It is what has been
used historically by NSI/Verisign for the com/net/org registries.
Note that NSI uses a "thin" registry model, where the central registry holds only domain
and name server, while the sponsoring registrar holds the contact data -- which is why
each registrar must run its own whois server.

EPP is an IETF effort, using XML. 
EPP is intended to replace RPP. It is specified in RFC 3730, 3731, 3732 3733, 3734, and 3735.
A good overview may be found at: http://epp-rtk.sourceforge.net/epp-howto.html
Because it is a relatively new standard, there are interoperability problems: 
   http://www.ripe.net/ripe/maillists/archives/dns-wg/2003/msg00121.html

Registries run on EPP include: .BIZ .INFO .NAME .ORG .US and .CN .
The EPP registries are "thick", holding both name server and contact info. 
In such cases, the registrars need not run whois servers.

Note that, again, EPP is generally only useful to registrars, not end customer registrants.
And it is not entirely suited as a whois query replacement either: 
  http://lists.verisignlabs.com/pipermail/ietf-not43/2002-August/000220.html


Software for RPP:

 Net::RRP
    Perl; appears unmaintained.
 Verisign's own LGPL software at
    http://www.verisign.com/products-services/naming-and-directory-services/naming-services/com-net-registry/page_001047.html 

Software for EPP:

 http://epptt.sourceforge.net/
   Perl, LGPL
 http://epp-rtk.sourceforge.net/
   C++ and Java, LGPL
 http://eppinterpreter.sourceforge.net/
   Java and Python, Apache license
 http://sourceforge.net/projects/aepps/
   Perl and C, Apache license
    a mod_epp module for Apache 
 http://www.isc.org/index.pl?/sw/openreg/
   Perl, BSD-style license

These do both RPP and EPP:

 Gandi::IRI http://open.gandi.net/
    Perl, GPL
    by Patrick Mevzek while at gandi.net
 Net::DRI http://www.dotandco.com/services/software/Net-DRI/index.en
    Perl, GPL
    also by Patrick Mevzek, his next generation.

=head1 Whois Locks and Status

A mess. See for example: http://www.webhostingtalk.com/archive/thread/346207-1.html

=head1 AUTHOR

Copyright 2004, Mark D. Anderson, mda@discerning.com.

This is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=cut

use Data::Dumper;

################################################################
my @CONTACT_FIELDS = qw(ID Name Organization Street1 Street2 Street3 City State PostalCode Country Phone PhoneExt Fax FaxExt Email);
my %CONTACT_FIELD_MAPPING = 
(
 (map {$_ => $_} @CONTACT_FIELDS),
 'State/Province' => 'State',
 'Postal Code' => 'PostalCode',
 'Phone Ext.' => 'PhoneExt',
 'FAX' => 'Fax',
 'FAX Ext.' => 'FaxExt',
 'Fax Number' => 'Fax',
 Address1 => 'Street1',
 Address2 => 'Street2',
 Address3 => 'Street3',
 'Country Code' => 'CountryCode',
 'Phone Number' => 'Phone',
 # .us
 'Nexus Category' => 'NexusCategory',
 'Application Purpose' => 'ApplicationPurpose',
 # .jp
 'Postal code' => 'PostalCode',
 'Web Page' => 'WebPage',
 'Name2' => 'Organization', # probably not right
);
my @CONTACTS = qw(Registrant Administrative Technical Billing Zone);
my @CONTACTS_FULL = map {$_ . 'Contact'} @CONTACTS;
my %CONTACT_MAPPING =
(
 Registrant => 'RegistrantContact',
 Organisation => 'RegistrantContact', # melbourneit
 Admin => 'AdministrativeContact',
 Tech => 'TechnicalContact',
 Billing => 'BillingContact'
);
for(@CONTACTS) {$CONTACT_MAPPING{"$_ Contact"} = $_ . 'Contact';}

# the tld's where we should qualify our search request with "domain $dn".
# this screws up with some whois servers, such as .name, .uk, etc.
my %TLD_DOMAIN_QUALIFY = map {$_=>1} qw(com net edu org);

################################################################
use vars qw($DEBUG_LEVEL $WARN_NOT_FOUND);
$DEBUG_LEVEL = $ENV{DEBUG} ? 3 : 1;
$WARN_NOT_FOUND = 1;

sub debug {
    print "DEBUG: ", @_, "\n" if $DEBUG_LEVEL > 2;
}
sub info {
    print "INFO: ", @_, "\n" if $DEBUG_LEVEL > 1;
}
sub warning {
    print "WARNING: ", @_, "\n" if $DEBUG_LEVEL > 0;
}

################################################################

#### public functions ####

# returns ($whois_response, $whois_server, $whois_request)
sub get_whois_raw {
    my ($dn) = @_;

    my $cmd;
    my $host; 
    my $search;
    my $tld = $dn; $tld =~ s/.*\.//; $tld = lc($tld);
    die "no tld in '$dn'" unless $tld;
    if (1) {
	$host = "$tld.whois-servers.net";
    }
    else {
	$host = 'whois.internic.net';
    }
    $search = $TLD_DOMAIN_QUALIFY{$tld} ? "\"domain $dn\"" : $dn;
    $search .= '/e' if $dn =~ m/\.jp$/i; # english only if jp
    my $whois;
    if ($tld eq 'org') {
	debug("fetching .org data with curl");
	my @lines = `curl -s http://www.pir.org/Search/WhoIsSearchResults.aspx?txtWhoIsSearch=$dn | grep strLine`;
	my @newlines = ();
	for (@lines) {s/.*strLine=(.*)-->/$1/; push(@newlines, $_);}
	$whois = join('', @newlines);
    }
    else {
	$cmd = "whois -h $host $search";
	debug("calling: $cmd");
	$whois = `$cmd`;
    }
    debug("whois $dn done");
    if ($whois =~ /Whois Server: (.*)/) {
	$host = $1;
	# don't do "domain $dn" here because many whois servers don't support that.
        $search = $dn;
	$search .= '/e' if $dn =~ m/\.jp$/i; # english only if jp
	$cmd = "whois -h $host $search";
	info("following content referral to $host with command: $cmd");
	$whois = `$cmd`;
	# debug "got whois response:$whois";
    }
    return ($whois, $host, $search);
}

sub get_whois {
    my ($dn) = @_;
    my ($whois_response, $whois_server, $whois_request) = get_whois_raw($dn);
    return parse_whois($dn, $whois_response, $whois_server);
}

sub parse_whois {
    my ($dn, $whois_response, $whois_server) = @_;

    warning("no whois_server argument specified, so some special rules may not apply"), $whois_server = '' unless $whois_server;

    if (!$whois_response) {
	warning("no whois response for domain '$dn'");
	return undef;
    }
    if ($whois_response =~ m/^no match/mi || $whois_response =~ m/^not found/mi) {
	warning("domain '$dn' not found") if $WARN_NOT_FOUND;
	return undef;
    }
    my @lines = split(/\r?\n/, $whois_response);

    # $hash will hold what parsed out, but $obj later will be what returned
    my $hash = {};

    # 0 means not in a multiline. 1 means waiting for non-blank line. 2 means seen some values.
    my $multiline_state = 0;
    my $multiline_field;
    my $multiline_values;
    my $multiline_seen = 0;
    my $multiline_indent;
    my $is_jp_registrant = 0;
    my $is_melbourneit = ($whois_server =~ m/melbourneit/i);
    my $is_jp = ($dn =~ m/\.jp$/i);
    my $is_bulkregister = ($whois_server =~ m/bulkregister/i);
    for(@lines) {
	# whois.alldomains.com uses tabs; see google.com
	s/\t/        /g;

	# whois.melbourneit.com uses "..." instead of colon
	s/^([^\.]+)\.+ ?/$1:/ if $is_melbourneit;

	# whois.bulkregister.com sometimes uses " -  " instead of colon.
	# other times it seems to omit all colons, or use "->" instead of a colon.
	if ($is_bulkregister) {
	    if (m/ \-/) {
		debug("bulkregister line before: '$_'");
		s/ \-  /:/;
		s/ \- *$/:/;
		debug("bulkregister line after: '$_'");
	    }
	    else {
		s/Contact\-?$/Contact:/;
		s/->/:/;
	    }
	}

	# .jp domains use [] instead of colon, and after "Contact Information" everything is about the registrant
	if ($is_jp) {
	    s/\[(.*?)\] */$1:/;
	    s/^(.)/Registrant $1/ if $is_jp_registrant;
	    $is_jp_registrant = 1, next if m/Contact Information:/;
	}

	# .edu has this confusing "Contacts:" line
	if (m/^Contacts:\s*$/ && $dn =~ m/\.edu$/i) {
	    next;
	}
	# .uk has a confusing "Relevant Dates:" line
	if (m/^ *Relevant Dates: *$/) {
	    next;
	}

	# blank line
	if (m/^\s*$/) {
	    if ($multiline_state == 2) {
		debug "finishing '$multiline_field' with blank line";
		$hash->{$multiline_field} = $multiline_values;
		$multiline_state = 0;
		$multiline_seen = 1;
	    }
	    next;
	}

	# in multiline
	if ($multiline_state > 0) {
	    # sigh, sometimes there is zero indent, so then check for a colon 
	    if (m/^( *)(.*)/ && (length($1) > $multiline_indent || (length($1) == $multiline_indent && !m/:/)) ) {
		push(@$multiline_values, $2);
		debug "continuing '$multiline_field' with value '$2'";
		$multiline_state = 2;
		next;
	    }
	    else {
		debug "ending '$multiline_field' because line starts with not enough spaces: $_";
		$hash->{$multiline_field} = $multiline_values if scalar(@$multiline_values);
		$multiline_state = 0;
		# fall through; this is starting something else
	    }
	}

	# false matches to fields
	my $false_field = (m/use this data to:/i || m/terms of use:/i || m/terms of use please see/ || 
			  m/this data to:/ || m/use this data:/ || 
			   m/NOTE:/ || m/NOTICE:/ || m/please note:/i || 
			   m/type *: *help/ || m/Disclaimer:/ ||
			   m/, go to/ || m/Register your domain name at/ ||
			   m/^[^:]*http:/ ||
			   0
			  );
			   
	# check for start of multiline
	my $multiline_ok = !$is_melbourneit && !$is_jp_registrant;
	# don't do if melbourneit; it only has single-line fields, and in the Address case, we need the repeated fields for position
	if (!$false_field && $multiline_ok && m/^( *)(.*?) *: *$/) {
	    $multiline_indent = length($1);
	    $multiline_field = $2;
	    $multiline_values = [];
	    $multiline_state = 1;
	    debug "starting multi-line field '$multiline_field'";
	    next;
	}

	# special patterns for dates
	if ((m/Record expires on ([^\.]*)/ || m/Expires on\.+ *: *([^\.]*)/) && $1 !~ m/date/i) {
	    $hash->{'Expiration Date'} = $1;
	    debug "setting 'Expiration Date' to $1";
	    next;
	}
	die "why not matching: $_" if m/expires on[^:]/i && ! m/expires on date/i;
	if (m/Record created on ([^\.]*)/ || m/Created on\.+ *: *([^\.]*)/ ) {
	    $hash->{'Creation Date'} = $1;
	    debug "setting 'Creation Date' to $1";
	    next;
	}
	# networksolutions has "Database last updated on 12-Dec-2004 17:51:02 EST." but that is on all its records, for whole database
	# also .biz has ">>>> Whois database was last updated on:Mon Dec 13 00:01:11 GMT 2004 <<<<"
	# also .uk has "WHOIS database last updated at 00:45:01 13-Dec-2004"
	if (m/Database last updated on ([^\.]*)/ || m/Whois database was last updated on: *(.*?) *<*$/ ||
	    m/WHOIS database last updated at (.*)/
	   ) {
	    $hash->{'Database last updated on'} = $1;
	    next;
	}
	if (m/Record last updated on ([^\.]*)/ || m/Record last updated on\.+ *: *([^\.]*)/ || m/Record last updated (\d[^\.]*)/ ||
	    m/Record updated on (\d[^\.]*)/ ||
	   0) {
	    $hash->{'Last Updated On'} = $1;
	    debug "setting 'Last Update On' to $1";
	    next;
	}

	# special patterns for registrar
	if (0 && m/registrar of record is enom/i && !$hash->{'Sponsoring Registrar'}) {
	    $hash->{'Sponsoring Registrar'} = 'enom';
	    debug "setting registrar to enom";
	    next;
	}
	# .uk
	if (m/\(c\) Nominet UK/ && !$hash->{'Sponsoring Registrar'}) {
	    $hash->{'Sponsoring Registrar'} = 'Nominet UK';
	    next;
	}
	# .edu
	if (m,available at: *http://whois.educause.net,) {
	    $hash->{'Sponsoring Registrar'} = 'EDUCAUSE';
	    $hash->{'Registrar Homepage'} = 'http://whois.educause.net';
	    next;
	}

	# other special patterns
	# bulkregister.com
	if ($is_bulkregister && m/TransferGuard LOCK Status => (.*)/) {
	    $hash->{'Domain Status'} = $1;
	    next;
	}

	# bulkregister doesn't have any field indicating the registrant; it just the first non-blurb data
	if ($is_bulkregister && !$hash->{Registrant} && $multiline_state == 0 && m/you agree to abide/) {
	    $multiline_state = 1;
	    $multiline_field = 'Registrant';
	    $multiline_indent = 0;
	    next;
	}

	# single-line
	if (!$false_field && m/^ *(.*?) *: *(.*?) *$/) {
	    my ($field,$value) = ($1,$2);
	    my $v = $hash->{$field};
	    if ($v) {
		unless (ref $v) {
		    $v = $hash->{$field} = [$v];
		}
		push(@$v, $value);
		debug "field '$field' adding value '$value'";
	    }
	    else {
		$hash->{$field} = $value;
		debug "field '$field' setting value '$value'";
	    }
	    next;
	}

	# otherwise don't know how to do; warn if indented
	if ($_ =~ m/^ .*[a-z0-9A-Z]/) {
	    warning("ignoring indented line: $_");
	}
	else {
	    debug "ignoring line: $_"; 
	}
    }
    if ($multiline_state == 2) {
	debug "finishing '$multiline_field' with end of data";
	$hash->{$multiline_field} = $multiline_values;
	$multiline_state = 0;
    }
    # .ORG might end with an empty "Name Server:" so don't worry about that
    die "still waiting to finish the multiline field '$multiline_field'" if $multiline_state > 0 && $multiline_seen;

    # the object we will return
    my $obj = {};

    # the list of parsed fields that we have used
    my @used = ();

    # split compound contacts like "Technical Contact, Zone Contact" or "Administrative Contact, Technical Contact" or "Administrative, Technical Contact"
    for my $f (keys %$hash) {
	next unless $f =~ m/Contact\,/ || ($f =~ m/Contact/ && $f =~ m/Administrative, /);
	my $v = $hash->{$f};
	my @fs = split(/, */, $f);
	# we alter $hash; don't put them into $obj
	for (@fs) {$_ = $_ . ' Contact' unless m/Contact/; $hash->{$_} = $v;}
	push(@used, $f);
    }

    # fix up jp which has a [Registrant] before Contact Information, as well as a [Name] field afterwards.
    if ($is_jp) {
	my $v = $hash->{Registrant};
	if ($v) {
	    delete $hash->{Registrant};
	}
	$hash->{'Registrant Name2'} = $hash->{'Registrant Name'} if $hash->{'Registrant Name'} ;
	$hash->{'Registrant Name'} = $v;
    }

    # if contacts are in multi-line fields, process them
    for my $c (qw(Registrant Administrative Technical Billing Zone)) {
	my $v = $hash->{"$c Contact"};
	if ($v) {
	    push(@used, "$c Contact");
	}
	elsif ($c eq 'Registrant') {
	    $v = $hash->{Registrant};
	    push(@used, 'Registrant') if $v;
	}
	next unless $v;
	$obj->{$c . 'Contact'} = new_contact_from_lines($v);
    }

    # melbourneit has multiple single-line fields called Address, which are the parts of the address
    if ($whois_server =~ m/melbourneit/) {
	for my $c (qw(Organisation Admin Tech)) {
	    my $v = $hash->{"$c Address"};
	    next unless $v;
	    push(@used,"$c Address");
	    $hash->{"$c Street1"} = $v->[0];
	    $hash->{"$c Street2"} = $v->[1];
	    $hash->{"$c City"} = $v->[2];
	    $hash->{"$c Postal Code"} = $v->[3];
	    $hash->{"$c State"} = $v->[4];
	    $hash->{"$c Country"} = $v->[5];
	}
    }

    # .name uses 'Address' instead of 'Street'
    elsif ($dn =~ m/\.name$/i) {
	for my $c (qw(Registrant Admin Tech Billing)) {
	    my $v = $hash->{"$c Address"};
	    next unless $v;
	    # push(@used,"$c Address");
	    delete $hash->{"$c Address"};
	    $hash->{"$c Street1"} = $v;
	}
    }

    # .uk uses "Registrant's Address" as a multiline field
    elsif ($dn =~ m/\.uk$/i) {
	my $v = $hash->{"Registrant's Address"};
	if ($v) {
	    push(@used,"Registrant's Address");
	    $hash->{"Registrant Street1"} = $v->[0];
	    $hash->{"Registrant City"} = $v->[1];
	    $hash->{"Registrant State"} = $v->[2];
	    $hash->{"Registrant Postal Code"} = $v->[3];
	    $hash->{"Registrant Country"} = $v->[4];
	}
    }

    # if have only have single line fields for contact, process those
    for my $prefix (('Registrant', 'Organisation', 'Administrative Contact', 'Admin', 'Technical Contact', 'Tech', 'Billing Contact', 'Billing')) {
	my $contact_key = $CONTACT_MAPPING{$prefix} || die "no prefix=$prefix in mapping";
	while(my($k,$v) = each %$hash) {
	    next if ref($v) && !$is_jp; # this is only for the single-line field case, except for .jp Email which seems to be ok
	    next unless $k =~ m/$prefix (.*)/;
	    my $field = $1;
	    # don't want to match twice
	    next if $field =~ m/^Contact/;
	    push(@used, $k);
	    my $mapped_field = $CONTACT_FIELD_MAPPING{$field};
	    if (!$mapped_field) {
		warning("found field '$k' (prefix '$prefix') which has an unknown contact field '$field' with value '$v'");
		$mapped_field = $field;
	    }
	    $obj->{$contact_key} ||= {};
	    $obj->{$contact_key}->{$mapped_field} = $v;
	}
    }

    # figure out some other fields, under various names
    my $FIELDS = {
	'RegistrarName' => ['Sponsoring Registrar', 'Registrar', 'Registrar Name', 'Registrar of Record', 'Registration Service Provided By', 'Created by Registrar', 'Registered through', 'Registrar Name....', 'Created by Registrar', 'Registration and WHOIS Service Provided By'],
        'RegistrarID' => ['Sponsoring Registrar IANA ID', 'Sponsoring Registrar ID'],
	'RegistrarHomePage' => ['Registrar Homepage', 'Visit'],
	'RegistrarEmail' => ['Contact'],

	'WhoisServer' => ['Registrar Whois', 'Registrar Whois...'],  
	'WhoisDatabaseUpdatedDate' => ['Database last updated on'],

	'ExpirationDate' => ['Expiration Date', 'Expiration date', 'Domain Expiration Date', 'Expires on', 'Expiry Date', 'Expires On', 'Record will expire on', 
			     'Record expires on', 'Renewal Date', 'Record expires on date', 'Record expiring on'],
	# melbourneit uses "Registration Date" to mean "Updated Date"?
	'UpdatedDate' => ['Last Updated On', 'Last Updated on', 'Domain record last updated', 'Updated Date', 'Domain Last Updated Date', 'Record updated date', 
			  'Updated On', 'Registration Date', 'Record update date', 'Last Updated', 'Last updated', 'Updated'],
	'CreatedDate' => ['Created On', 'Created on', 'Domain record activated', 'Creation Date', 'Creation date', 'Domain Registration Date', 
			  'Record created date', 'Record create date'],
        'TransferedDate' => ['Last Transfer Date'], # .biz

	'NameServers' => ['Name Server', 'Domain servers in listed order', 'Name Servers', 'DNS Servers', 'Name servers listed in order', 'Registrant Name Server'],

        'DomainStatus' => ['Domain Status', 'Domain status', 'Status', 'Registration Status'],
        'DomainID' => ['Domain ID'],
        'DomainName' => ['Domain Name', 'Domain name'],

	# at least .biz (see great.biz) reports the registrar that created the record, and the registrar that last updated it
	# also .us (see great.us)
	'CreatedByRegistrar' => ['Created by Registrar'],
	'UpdatedByRegistrar' => ['Last Updated by Registrar'],
    };
    while(my($f,$altfs) = each %$FIELDS) {
	for(@$altfs) {$obj->{$f} = $hash->{$_}, push(@used, $_), last if $hash->{$_}}
    }

    # check out WhoisServer
    my $ws = $obj->{WhoisServer};
    if ($whois_server) {
	if ($ws && lc($whois_server) ne lc($ws)) {
	    warning("declared whois server '$ws' does not match one we used '$whois_server'");
	}
	$obj->{WhoisServer} = $whois_server if $whois_server && !$ws;
    }

    # construct RegistrarContact using RegistrarName, RegistrarID, RegistrarHomePage
	# hotmail.com (whois.opensrs.net) has this:
	#   DBMS VeriSign, dbms-support@verisign.com
	#   800-579-2848 x4
	#   Please contact DBMS VeriSign for domain updates, DNS/Nameserver
	#   changes, and general domain support questions.
        # but opensrs doesn't always have this, and anyway it is not the real registrar.
    my $registrar_lines = $hash->{'Registration Service Provider'};
    if ($registrar_lines) {
	push(@used, 'Registration Service Provider');
    }
    my $registrar = {}; #  Lines => $registrar_lines};
    for my $f (qw(Name ID HomePage Email)) {
	my $v = $obj->{'Registrar' . $f};
	next unless $v;
	delete $obj->{'Registrar' . $f};
	$registrar->{$f} = $v;
    }
    # $registrar->{Phone} = $registrar_lines->[1];
    # maybe split Registrar Name into Name and IcannID
    my $registrar_name = $registrar->{Name};
    if ($registrar_name && $registrar_name =~ m/(.*) \((.*-LROR)\)$/) {
	$registrar->{Name} = $1;
	$registrar->{IcannID} = $2;
    }
    if ($registrar_name && $registrar_name =~ m/^\S+\-LROR$/) {
	$registrar->{IcannID} = $registrar_name; # leave as Name too
    }
    $obj->{'RegistrarContact'} = $registrar;

    # .uk has a "Registrant's Agent" field which seems to be the intermediating registrar
    if ($dn =~ m/\.uk$/i) {
	my $v = $hash->{"Registrant's Agent"};
	if ($v) {
	    push(@used, "Registrant's Agent");
	    $obj->{'RegistrarContact'}->{Agent} = $v; 
	}
    }


    # TODO: deal with "Same as above" as used in edu

    # deal with "CONTACT NOT AUTHORITATIVE ..."
    for my $c (@CONTACTS) {
	my $v = $obj->{$c . 'Contact'};
	next unless $v;
	die "bad value for $c: ", Dumper($v) unless ref($v) eq 'HASH';
	next unless $v->{Name} && $v->{Name} =~ m/NOT AUTHORITATIVE/i;
	my $newv = {Name => '(suppressed by registrar)'};
	$newv->{ID} = $v->{ID} if $v->{ID}; # only thing trustworthy
	$obj->{$c . 'Contact'} = $newv;
    }

    # TODO: sanity check that some mandatory fields are present
    if (1) {
	my $found_bad = 0;
	# google.com has no DomainStatus
	# .edu has no ExpirationDate
	for my $f (qw(CreatedDate NameServers)) {
          warning("domain '$dn' has no value for '$f'"), $found_bad = 1 unless $obj->{$f};
        }
	warning("whois response: $whois_response") if $found_bad;
    }

    # make sure NameServers is an array, and remove ip addresses
    my $nslines = $obj->{'NameServers'} || [];
    if ($nslines) {
	my $v = ref($nslines) ? $nslines : [$nslines];
	my @nv = ();
	for (@$v) {
	    s/ *\d+\.\d+\.\d+\.\d+\.? *//;
	    push(@nv, $_);
	}
	$obj->{NameServers} = [@nv];
    }

    # make sure DomainStatus is an array
    $obj->{DomainStatus} = [] unless $obj->{DomainStatus};
    $obj->{DomainStatus} = [$obj->{DomainStatus}] unless ref($obj->{DomainStatus});


    # warn about any parsed content we didn't use
    my %used = map {$_=>1} @used;
    my @extra = grep {! $used{$_}} keys %$hash;
    if (@extra) {
	for(@extra) {
	    my $v = $hash->{$_};
	    warning("Unused parsed field: $_:", (ref($v) ? Dumper($v) : $v));
	}
    }

    debug("parsed $dn as: ", Dumper($obj));
    return bless $obj;
}

#### public methods ####

sub contact_name {
    my ($w, $contact_type) = @_;
    my $contact = $w->{$contact_type . 'Contact'};
    return '' unless $contact;
    return $contact->{Name} || '';
}
    
sub name_servers {
    my ($w) = @_;
    return $w->{NameServers};
}

sub whois_server {
    my ($w) = @_;
    return $w->{WhoisServer};
}

#### internal functions ####

sub parse_city_state_zip {
    my ($city_state_zip) = @_;
    return ($city_state_zip =~ m/^(.*), *([a-zA-Z ]*) +(\S.*)$/);
}

# product hash of fields similar to .ORG format.
# This doesn't work well, and would be better done with a heuristic postal address
# parser. Things can be very free form; see for example ucsd.edu and google.com.
sub new_contact_from_lines {
    my ($lines_array) = @_;

    my ($name, $handle, $org, $email, $street, $city, $state, $zip, $country, $phone, $fax);
    $lines_array = [$lines_array] unless ref($lines_array);
    my @lines = @$lines_array;
    my $name_and_email = shift @lines;
    ($name, $email) = ($name_and_email =~ m/^(.*?) *(\S+\@\S*)$/);
    $name = $name_and_email unless $name;

    if (0) {
    }

    # enom (namecheap, etc.) puts phones before address: name org phone fax street
    elsif (@lines > 2 && $lines[2] =~ m/Fax: (.*)/ ) {
	$org = shift @lines;
	$phone = shift @lines;
	$fax = $1;
	shift @lines;
	my $city_state_zip;
	($street, $city_state_zip, $country) = @lines;
	($city, $state, $zip) = parse_city_state_zip($city_state_zip) if $city_state_zip;
	if ($org =~ m/^(.*) \((.*\@.*)\)$/) {
	    ($org, $email) = ($1, $2);
	}

    }

    # next might be a handle (see google.com), as with alldomains
    elsif (@lines && $lines[0] =~ m/\((.*)\)/) {
	shift @lines;
	$handle = $1;
	# registrant has no org or email or phones in alldomains
	if (@lines < 6) {
	    ($street, $city, $state, $zip, $country) = @lines;
	}
	else {
	    ($org, $street, $city, $state, $zip, $country, $email, $phone, $fax) = @lines;
	    $fax =~ s/Fax\- //;
	}
    }

    else {

	# organization follows if 4th field is a country
	if (@lines > 3 && length($lines[3]) == 2) {
	    $org = shift @lines;
	}

	my ($city_state_zip, $phones, @rest);
	($street, $city_state_zip, $country, $phones, @rest) = @lines;
	($city, $state, $zip) = parse_city_state_zip($city_state_zip) if $city_state_zip;
	$state =~ s/\s+$// if $state;
	if ($phones) {
	    ($phone, $fax) = ($phones =~ m/^(.*\S) *fax: (.*)$/i);
	    $phone = $phones unless $phone; 
	}

	$phone = $1 if $phone && $phone =~ m/Phone: (.*)/;
	$email = $1 if @rest && $rest[0] =~ m/Email: (.*)/;
	$email = $1, $phone = undef if !$email && $phone && $phone =~ m/Email: (.*)/;
    }

    my $contact = {
	Lines => $lines_array,
	Name => $name,
	Email => $email,
	Organization => $org,
	Handle => $handle,
	Street1 => $street,
	City => $city,
	State => $state,
	PostalCode => $zip,
	Country => $country,
	Phone => $phone,
    };
    return $contact;
}
    

################################################################
#### testing ####
# these are the top .COM registrars, at least according to some sources
my $TEST_COMS = [
    'ibm.com',			     # networksolutions.com
    'microsoft.com',		     # opensrs.net (tucows)
    'pepsi.com',		     # register.com
    'melbourneit.com',		     # melbourneit.com
    'bulkregister.com',		     # bulkregister.com. could try 'icomdesign.com' too, as a customer.
    'namecheap.com',		     # enom.com (reseller)
    'godaddy.com',		     # godaddy.com
    'directnic.com',		     # directnic.com
    'dotster.com',		     # dotster.com
    'hotmail.com',		     # alldomains.com
    'discerning.com',		     # easydns
];

my $TEST_OTHER = [
    'slashdot.org',
    'freshmeat.net',
    'mit.edu',
    'great.info',
    'great.biz',
    'great.name',
    'great.us',
    'bbc.co.uk',
    'hello.jp',
    # 'sxc.hu',  # hungary
];

my $TEST_BUGS = ['apspuhuru.org'];

# known to not work, and might not ever
my $TEST_HOPELESS = [
    'whitehouse.gov', # gov.whois-servers.net times out, and whois.nic.gov has little to say
    'army.mil', # whois.nic.mil has nothing to say
];

sub testme {

    $DEBUG_LEVEL = 2;
    for my $dn ((@$TEST_COMS, @$TEST_OTHER)) {
    # for my $dn (@$TEST_BUGS) {

	# first do it with mine
	print "**** MyWhois of $dn\n";
	my $mw = get_whois($dn);
	print Dumper($mw);

	if (0) {
	    use Net::Whois::Raw qw( whois $OMIT_MSG $CHECK_FAIL $CACHE_DIR $CACHE_TIME $USE_CNAMES $TIMEOUT );
	    $OMIT_MSG = 2;
	    $CHECK_FAIL = 2;
	    print "**** Net::Whois::Raw of $dn\n";
	    my $s = whois($dn);
	    print $s;
	    my $mw2 = parse_whois($dn, $s);
	}
	if (0) {
	    require Net::WhoisNG;
	    print "**** Net::WhoisNG of $dn\n";
	    my $wn = new Net::WhoisNG($dn);
	    if (! $wn->lookUp()) {
		print "WARNING: Net::WhoisNG failed for $dn\n";
	    }
	    else {
		print "ExpirationDate=",$wn->getExpirationDate(), "\n";
		my @ns = $wn->getNameServers();
		print "Name Servers=", Dumper(\@ns);
		my $registrant = $wn->getPerson('registrant');
		print "registrant=", Dumper($registrant);
		# print "registrant Name=", $registrant->getName(), "\n";
		my $admin = $wn->getPerson('admin');
		print "admin=", Dumper($admin);
		# print "admin Name=", $admin->getName(), "\n";
	    }
	}

	# Net::ParseWhois does not seem to work at all
        if (0) {
	    require Net::ParseWhois;
	    print "**** Net::ParseWhois of $dn\n";
	    # does a direct socket connection. requires 'whois' in /etc/services, which is 'nicname' in mac osx
	    my $w = Net::ParseWhois::Domain->new($dn);
	    if(! $w->ok) {
		print "WARNING: Net::ParseWhois not ok for $dn" . ($w->{error} ? " error: " . $w->{'error'} : '') . "\n";
	    }
	    else {
		print "Registrar: ", $w->registrar, "\n";
		print "Domain: ", $w->domain, "\n";
		print "Name: ", $w->name, "\n";
		print "Tag: ", $w->tag, "\n";
		print "Address:\n", map { "    $_\n" } $w->address;
		print "Country: ", $w->country, "\n";
		print "Name Servers:\n", map { "    $$_[0] ($$_[1])\n" }  @{$w->servers};
		my ($c, $t);
		if ($c = $w->contacts) {
		    print "Contacts:\n";
		    for $t (sort keys %$c) {
			print "    $t:\n";
			print map { "\t$_\n" } @{$$c{$t}};
		    }
		}
		print "Record created:", $w->record_created ;
		print "\nRecord updated:", $w->record_updated,"\n";
	    }
	}

	# Net::XWhois does not seem to work at all
	if (0) {
	    require Net::XWhois;
	    print "**** Net::XWhois of $dn\n";
	    my $xw = new Net::XWhois (Domain => $dn, Server => 'whois.internic.net');
	    # $xw->lookup(Domain => $dn); # or could have done it at once
	    print "name=", $xw->name(), "\n";
	    print "nameservers=", $xw->nameservers(), "\n";
	    print "registrant=", $xw->registrant(), "\n";
	    print "contact_admin=", $xw->contact_admin(), "\n";
	    print "contact_tech=", $xw->contact_tech(), "\n";
	    print "response=", $xw->response();
	}

    }
}
1;
