1 package FS::part_export::domreg_opensrs;
3 use vars qw(@ISA %info %options $conf $me $DEBUG);
6 use FS::Record qw(qsearchs qsearch);
8 use FS::part_export::null;
14 FS::part_export::domreg_opensrs - Register or transfer domains with Tucows OpenSRS
18 This module handles registering and transferring domains using a registration service provider (RSP) account
19 at Tucows OpenSRS, an ICANN-approved domain registrar.
21 As a part_export, this module can be designated for use with svc_domain services. When the svc_domain object
22 is inserted into the Freeside database, registration or transferring of the domain may be initiated, depending
23 on the setting of the svc_domain's action field.
27 =item N - Register the domain
29 =item M - Transfer the domain
31 =item I - Ignore the domain for registration purposes
35 This export uses Net::OpenSRS. Registration and transfer attempts will fail unless Net::OpenSRS is installed
36 and LWP::UserAgent is able to make HTTPS posts. You can turn on debugging messages and use the OpenSRS test
37 gateway when setting up this export.
41 @ISA = qw(FS::part_export::null);
42 $me = '[' . __PACKAGE__ . ']';
45 my @tldlist = qw/com net org biz info name mobi at be ca cc ch cn de dk es eu fr it mx nl tv uk us asn.au com.au id.au net.au org.au/;
47 tie %options, 'Tie::IxHash',
48 'username' => { label => 'Reseller user name at OpenSRS',
50 'privatekey' => { label => 'Private key',
52 'password' => { label => 'Password for management account',
54 'masterdomain' => { label => 'Master domain at OpenSRS',
56 'wait_for_pay' => { label => 'Do not provision until payment is received',
60 'debug_level' => { label => 'Net::OpenSRS debug level',
62 options => [ 0, 1, 2, 3 ],
64 # 'register' => { label => 'Use for registration',
67 # 'transfer' => { label => 'Use for transfer',
70 'tlds' => { label => 'Use this export for these top-level domains (TLDs)',
73 size => scalar(@tldlist),
74 options => [ @tldlist ],
75 default => 'com net org' },
76 'auoptions' => { label => 'Enable AU-specific registration fields',
82 'svc' => 'svc_domain',
83 'desc' => 'Domain registration via Tucows OpenSRS',
84 'options' => \%options,
86 Registers and transfers domains via the <a href="http://opensrs.com/">Tucows OpenSRS</a> registrar (using <a href="http://search.cpan.org/dist/Net-OpenSRS">Net::OpenSRS</a>).
87 All of the Net::OpenSRS restrictions apply:
89 <LI>You must have a reseller account with Tucows.
90 <LI>You must add the public IP address of the Freeside server to the 'Script API allow' list in the OpenSRS web interface.
91 <LI>You must generate an API access key in the OpenSRS web interface and enter it below.
92 <LI>All domains are managed using the same user name and password, but you can create sub-accounts for clients.
93 <LI>The user name must be the same as your OpenSRS reseller ID.
94 <LI>You must enter a master domain that all other domains are associated with. That domain must be registered through your OpenSRS account.
96 Some top-level domains offered by OpenSRS have additional business rules not supported by this export. These TLDs cannot be registered or transfered with this export.
97 <BR><BR>Use these buttons for some useful presets:
100 <INPUT TYPE="button" VALUE="OpenSRS Live System (rr-n1-tor.opensrs.net)" onClick='
101 document.dummy.machine.value = "rr-n1-tor.opensrs.net";
102 this.form.machine.value = "rr-n1-tor.opensrs.net";
105 <INPUT TYPE="button" VALUE="OpenSRS Test System (horizon.opensrs.net)" onClick='
106 document.dummy.machine.value = "horizon.opensrs.net";
107 this.form.machine.value = "horizon.opensrs.net";
113 install_callback FS::UID sub {
114 $conf = new FS::Conf;
123 Reformats a phone number according to registry rules. Currently Freeside stores phone numbers
124 in NANPA format and the registry prefers "+CCC.NPANPXNNNN"
131 #if ($tel =~ /^(\d{3})-(\d{3})-(\d{4})( x(\d+))?$/) {
132 if ($tel =~ /^(\d{3})-(\d{3})-(\d{4})$/) {
134 # if $tel .= "$4" if $4;
139 =item gen_contact_info
141 Generates contact data for the domain based on the customer data.
143 Currently relies on Net::OpenSRS to format the telephone number for OpenSRS.
151 my @invoicing_list = $co->invoicing_list_emailonly;
152 if ( $conf->exists('emailinvoiceautoalways')
153 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
154 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
155 push @invoicing_list, $co->all_emails;
158 my $email = ($conf->exists('business-onlinepayment-email-override'))
159 ? $conf->config('business-onlinepayment-email-override')
160 : $invoicing_list[0];
163 firstname => $co->first,
164 lastname => $co->last,
165 company => $co->company,
166 address => $co->address1,
168 state => $co->state(),
170 country => uc($co->country()),
172 #phone => format_tel($co->daytime()),
173 phone => $co->daytime() || $co->night,
178 =item validate_contact_info
180 Attempts to validate contact data for the domain based on OpenSRS rules.
182 Returns undef if the contact data is acceptable, an error message if the contact
183 data lacks one or more required fields.
187 sub validate_contact_info {
191 firstname => "first name",
192 lastname => "last name",
193 address => "street address",
196 zip => "ZIP/postal code",
197 country => "country",
198 email => "email address",
199 phone => "phone number",
202 foreach (keys %fields) {
203 if (!defined($c->{$_}) || !$c->{$_}) {
204 push @err, $fields{$_};
207 if (scalar(@err) > 0) {
208 return "Contact information needs: " . join(', ', @err);
215 Returns the Net::OpenSRS-required test mode string based on whether the export
216 is configured to use the live or the test gateway.
223 return 'live' if $self->machine eq "rr-n1-tor.opensrs.net";
224 return 'test' if $self->machine eq "horizon.opensrs.net";
231 Attempts to "export" the domain, i.e. register or transfer it if the user selected
232 that option when editing the domain.
234 Returns an error message on failure or undef on success.
236 May also return an error message if it cannot load the required Perl module Net::OpenSRS,
237 or if the domain is not registerable, or if insufficient data is provided in the customer
238 record to generate the required contact information to register or transfer the domain.
243 my( $self, $svc_domain ) = ( shift, shift );
245 return if $svc_domain->action eq 'I'; # Ignoring registration, just doing DNS
247 if ($svc_domain->action eq 'N') {
248 return $self->register( $svc_domain );
249 } elsif ($svc_domain->action eq 'M') {
250 return $self->transfer( $svc_domain );
252 return "Unknown domain action " . $svc_domain->action;
255 sub _export_insert_on_payment {
256 my( $self, $svc_domain ) = ( shift, shift );
257 warn "$me:_export_insert_on_payment called\n" if $DEBUG;
258 return '' unless $self->option('wait_for_pay');
260 my $queue = new FS::queue {
261 'svcnum' => $svc_domain->svcnum,
262 'job' => 'FS::part_export::domreg_opensrs::renew_through',
264 $queue->insert( $self, $svc_domain ); #_export_insert with 'R' action?
267 ## Domain registration exports do nothing on replace. Mainly because we haven't decided what they should do.
268 #sub _export_replace {
269 # my( $self, $new, $old ) = (shift, shift, shift);
275 ## Domain registration exports do nothing on delete. You're just removing the domain from Freeside, not the registry
276 #sub _export_delete {
277 # my( $self, $svc_domain ) = ( shift, shift );
282 =item is_supported_domain
284 Return undef if the domain name uses a TLD or SLD that is supported by this registrar.
285 Otherwise return an error message explaining what's wrong.
289 sub is_supported_domain {
291 my $svc_domain = shift;
293 # Get the TLD of the new domain
294 my @bits = split /\./, $svc_domain->domain;
296 return "Can't register subdomains: " . $svc_domain->domain
297 if (scalar(@bits) != 2 && scalar(@bits) != 3);
302 # See if it's one this export supports
303 my @tlds = split /\s+/, $self->option('tlds');
304 @tlds = map { s/\.//; $_ } @tlds;
305 return "Can't register top-level domain $tld, restricted to: "
306 . $self->option('tlds') if ! grep { $_ eq $tld || $_ eq "$sld$tld" } @tlds;
317 my $srs = Net::OpenSRS->new();
319 $srs->debug_level( $self->option('debug_level') ); # Output should be in the Apache error log
321 $srs->environment( $self->testmode() );
322 $srs->set_key( $self->option('privatekey') );
324 $srs->set_manage_auth( $self->option('username'), $self->option('password') );
330 Returns a reference to a hashref containing information on the domain's status. The keys
331 defined depend on the status.
333 'unregistered' means the domain is not registered.
335 Otherwise, if the domain is in an asynchronous operation such as a transfer, returns the state
338 Otherwise returns a value indicating if the domain can be managed through our reseller account.
343 my ( $self, $svc_domain ) = @_;
346 eval "use Net::OpenSRS;";
349 my $srs = $self->get_srs;
351 if ($srs->is_available( $svc_domain->domain )) {
352 $rslt->{'unregistered'} = 1;
354 $rslt = $srs->check_transfer( $svc_domain->domain );
355 if (defined($rslt->{'reason'})) {
356 my $rv = $srs->make_request(
358 action => 'belongs_to_rsp',
361 domain => $svc_domain->domain
366 $self->_set_response;
367 if ( $rv->{attributes}->{'domain_expdate'} ) {
368 $rslt->{'expdate'} = $rv->{attributes}->{'domain_expdate'};
374 return $rslt; # Success
379 Attempts to register the domain through the reseller account associated with this export.
381 Like most export functions, returns an error message on failure or undef on success.
386 my ( $self, $svc_domain, $years ) = @_;
388 $years = 1 unless $years; #default to 1 year since we don't seem to pass it
390 return "Net::OpenSRS does not support period other than 1 year" if $years != 1;
392 eval "use Net::OpenSRS;";
395 my $err = $self->is_supported_domain( $svc_domain );
398 my $cust_main = $svc_domain->cust_svc->cust_pkg->cust_main;
400 my $c = gen_contact_info($cust_main);
402 if ( $svc_domain->domain =~ /\.au$/ ) {
403 $c->{'registrant_name'} = $svc_domain->au_registrant_name;
404 $c->{'eligibility_type'} = $svc_domain->au_eligibility_type;
407 $err = validate_contact_info($c);
410 my $srs = $self->get_srs;
412 # cookie not required for registration
413 # my $cookie = $srs->get_cookie( $self->option('masterdomain') );
415 # return "Unable to get cookie at OpenSRS: " . $srs->last_response();
418 # return "Domain registration not enabled" if !$self->option('register');
419 return $srs->last_response() if !$srs->register_domain( $svc_domain->domain, $c);
421 return ''; # Should only get here if register succeeded
426 Attempts to transfer the domain into the reseller account associated with this export.
428 Like most export functions, returns an error message on failure or undef on success.
433 my ( $self, $svc_domain ) = @_;
435 eval "use Net::OpenSRS;";
438 my $err = $self->is_supported_domain( $svc_domain );
441 my $cust_main = $svc_domain->cust_svc->cust_pkg->cust_main;
443 my $c = gen_contact_info($cust_main);
445 $err = validate_contact_info($c);
448 my $srs = $self->get_srs;
450 my $cookie = $srs->get_cookie( $self->option('masterdomain') );
452 return "Unable to get cookie at OpenSRS: " . $srs->last_response();
455 # return "Domain transfer not enabled" if !$self->option('transfer');
456 return $srs->last_response() if !$srs->transfer_domain( $svc_domain->domain, $c);
458 return ''; # Should only get here if transfer succeeded
463 Attempts to renew the domain for the specified number of years.
465 Like most export functions, returns an error message on failure or undef on success.
470 my ( $self, $svc_domain, $years ) = @_;
472 eval "use Net::OpenSRS;";
475 my $err = $self->is_supported_domain( $svc_domain );
478 my $srs = $self->get_srs;
480 my $cookie = $srs->get_cookie( $self->option('masterdomain') );
482 return "Unable to get cookie at OpenSRS: " . $srs->last_response();
485 # return "Domain renewal not enabled" if !$self->option('renew');
486 return $srs->last_response() if !$srs->renew_domain( $svc_domain->domain, $years );
488 return ''; # Should only get here if renewal succeeded
491 =item renew_through [ EPOCH_DATE ]
493 Attempts to renew the domain through the specified date. If no date is
494 provided it is gleaned from the associated cust_pkg bill date
496 Like some export functions, dies on failure or returns undef on success.
497 It is always called from the queue.
502 my ( $self, $svc_domain, $date ) = @_;
504 warn "$me: renew_through called\n" if $DEBUG;
505 eval "use Net::OpenSRS;";
509 my $cust_pkg = $svc_domain->cust_svc->cust_pkg;
510 die "Can't renew: no date specified and domain is not in a package."
512 $date = $cust_pkg->bill;
515 my $err = $self->is_supported_domain( $svc_domain );
518 warn "$me: checking status\n" if $DEBUG;
519 my $rv = $self->get_status($svc_domain);
520 die "Domain ". $svc_domain->domain. " is not renewable"
521 unless $rv->{expdate};
523 die "Can't parse expiration date for ". $svc_domain->domain
524 unless $rv->{expdate} =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/;
526 my ($year,$month,$day,$hour,$minute,$second) = ($1,$2,$3,$4,$5,$6);
527 my $exp = DateTime->new( year => $year,
533 time_zone => 'America/New_York',#timezone of opensrs
536 my $bill = DateTime->
537 from_epoch( 'epoch' => $date,
538 'time_zone' => DateTime::TimeZone->new( name => 'local' ),
542 while ( DateTime->compare( $bill, $exp ) > 0 ) {
544 $exp->add( 'years' => 1 );
546 die "Can't renew ". $svc_domain->domain. " for more than 10 years."
547 if $years > 10; #no infinite loop
550 return '' unless $years;
552 warn "$me: renewing ". $svc_domain->domain. " for $years years\n" if $DEBUG;
553 my $srs = $self->get_srs;
554 $rv = $srs->make_request(
559 domain => $svc_domain->domain,
563 currentexpirationyear => $year,
567 die $rv->{response_text} unless $rv->{is_success};
569 return ''; # Should only get here if renewal succeeded
574 Attempts to revoke the domain registration. Only succeeds if invoked during the OpenSRS
575 grace period immediately after registration.
577 Like most export functions, returns an error message on failure or undef on success.
582 my ( $self, $svc_domain ) = @_;
584 eval "use Net::OpenSRS;";
587 my $err = $self->is_supported_domain( $svc_domain );
590 my $srs = $self->get_srs;
592 my $cookie = $srs->get_cookie( $self->option('masterdomain') );
594 return "Unable to get cookie at OpenSRS: " . $srs->last_response();
597 # return "Domain registration revocation not enabled" if !$self->option('revoke');
598 return $srs->last_response() if !$srs->revoke_domain( $svc_domain->domain);
600 return ''; # Should only get here if transfer succeeded
605 Should return a full-blown object representing OpenSRS, but current just returns a hashref
606 containing the registrar name.
620 L<Net::OpenSRS>, L<FS::part_export_option>, L<FS::export_svc>, L<FS::svc_domain>,
621 L<FS::Record>, schema.html from the base documentation.