From 1d3af1f921c52bc1cd1dde7a73b9a22f097bf606 Mon Sep 17 00:00:00 2001 From: rsiddall Date: Tue, 14 Jul 2009 00:28:05 +0000 Subject: [PATCH] New export to register/transfer/renew/revoke domains using Net::DRI. Currently optimized for OpenSRS. Should become more generalized in later releases. Modified Makefile to insert the Freeside log folder into the new export. Modified svc_domain.pm to prevent generation of transfer requests when a domain is moved to a different package with a domain registration attached to one of the included services. Modified domreg.cgi to display errors on a separate page. --- FS/FS/part_export/domreg_net_dri.pm | 614 ++++++++++++++++++++++++++++++++++++ FS/FS/svc_domain.pm | 2 +- Makefile | 1 + httemplate/edit/process/domreg.cgi | 2 +- 4 files changed, 617 insertions(+), 2 deletions(-) create mode 100644 FS/FS/part_export/domreg_net_dri.pm diff --git a/FS/FS/part_export/domreg_net_dri.pm b/FS/FS/part_export/domreg_net_dri.pm new file mode 100644 index 000000000..885d6b6b0 --- /dev/null +++ b/FS/FS/part_export/domreg_net_dri.pm @@ -0,0 +1,614 @@ +package FS::part_export::domreg_net_dri; + +use vars qw(@ISA %info %options $conf); +use Tie::IxHash; +use FS::part_export::null; + +=head1 NAME + +FS::part_export::domreg_net_dri - Register or transfer domains with Net::DRI + +=head1 DESCRIPTION + +This module handles registering and transferring domains with select registrars or registries supported +by L. + +As a part_export, this module can be designated for use with svc_domain services. When the svc_domain object +is inserted into the Freeside database, registration or transferring of the domain may be initiated, depending +on the setting of the svc_domain's action field. Further operations can be performed from the View Domain screen. + +Logging information is written to the Freeside log folder. + +For correct operation you must add name/value pairs to the protcol and transport options fields. The setttings +depend on the domain registry driver (DRD) selected. + +=over 4 + +=item N - Register the domain + +=item M - Transfer the domain + +=item I - Ignore the domain for registration purposes + +=back + +=cut + +@ISA = qw(FS::part_export::null); + +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/; + +my $opensrs_protocol_opts=<<'END'; +username= +password= +auto_renew=0 +affiliate_id= +reseller_id= +END + +my $opensrs_transport_opts=<<'END'; +client_login= +client_password= +END + +tie %options, 'Tie::IxHash', + 'drd' => { label => 'Domain Registry Driver (DRD)', + type => 'select', + options => [ qw/BookMyName CentralNic Gandi OpenSRS OVH VNDS/ ], + default => 'OpenSRS' }, + 'log_level' => { label => 'Logging', + type => 'select', + options => [ qw/debug info notice warning error critical alert emergency/ ], + default => 'warning' }, + 'protocol_opts' => { + label => 'Protocol Options', + type => 'textarea', + default => $opensrs_protocol_opts, + }, + 'transport_opts' => { + label => 'Transport Options', + type => 'textarea', + default => $opensrs_transport_opts, + }, +# 'register' => { label => 'Use for registration', +# type => 'checkbox', +# default => '1' }, +# 'transfer' => { label => 'Use for transfer', +# type => 'checkbox', +# default => '1' }, +# 'delete' => { label => 'Use for deletion', +# type => 'checkbox', +# default => '1' }, +# 'renew' => { label => 'Use for renewals', +# type => 'checkbox', +# default => '1' }, + 'tlds' => { label => 'Use this export for these top-level domains (TLDs)', + type => 'select', + multi => 1, + size => scalar(@tldlist), + options => [ @tldlist ], + default => 'com net org' }, +; + +my $opensrs_protocol_defaults = $opensrs_protocol_opts; +$opensrs_protocol_defaults =~ s|\n|\\n|g; + +my $opensrs_transport_defaults = $opensrs_transport_opts; +$opensrs_transport_defaults =~ s|\n|\\n|g; + +%info = ( + 'svc' => 'svc_domain', + 'desc' => 'Domain registration via Net::DRI', + 'options' => \%options, + 'notes' => <<"END" +Registers and transfers domains via a Net::DRI registrar or registry. +Net::DRI +must be installed. You must have an account at the selected registrar/registry. +
+Some top-level domains have additional business rules not supported by this export. These TLDs cannot be registered or transfered with this export. +

Use these buttons for some useful presets: + +END +); + +install_callback FS::UID sub { + $conf = new FS::Conf; +}; + +#sub rebless { shift; } + +# experiment: want the status of these right away, so no queueing + +sub _export_insert { + my( $self, $svc_domain ) = ( shift, shift ); + + return if $svc_domain->action eq 'I'; # Ignoring registration, just doing DNS + + if ($svc_domain->action eq 'N') { + return $self->register( $svc_domain ); + } elsif ($svc_domain->action eq 'M') { + return $self->transfer( $svc_domain ); + } + return "Unknown domain action " . $svc_domain->action; +} + +=item get_portfolio_credentials + +Returns, in list context, the user name and password for the domain portfolio. + +This is currently specified via the username and password keys in the protocol options. + +=cut + +sub get_portfolio_credentials { + my $self = shift; + + my %opts = $self->get_protocol_options(); + return ($opts{username}, $opts{password}); +} + +=item format_tel + +Reformats a phone number according to registry rules. Currently Freeside stores phone numbers +in NANPA format and most registries prefer "+CCC.NPANPXNNNN" + +=cut + +sub format_tel { + my $tel = shift; + + #if ($tel =~ /^(\d{3})-(\d{3})-(\d{4})\s*(x\s*(\d+))?$/) { + if ($tel =~ /^(\d{3})-(\d{3})-(\d{4})$/) { + $tel = "+1.$1$2$3"; # TBD: other country codes +# if $tel .= "$4" if $4; + } + return $tel; +} + +sub gen_contact_set { + my ($self, $dri, $cust_main) = @_; + + my @invoicing_list = $cust_main->invoicing_list_emailonly; + if ( $conf->exists('emailinvoiceautoalways') + || $conf->exists('emailinvoiceauto') && ! @invoicing_list + || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) { + push @invoicing_list, $cust_main->all_emails; + } + + my $email = ($conf->exists('business-onlinepayment-email-override')) + ? $conf->config('business-onlinepayment-email-override') + : $invoicing_list[0]; + + my $cs=$dri->local_object('contactset'); + my $co=$dri->local_object('contact'); + + my ($user, $pass) = $self->get_portfolio_credentials(); + + $co->srid($user); # Portfolio user name for OpenSRS? + $co->auth($pass); # Portfolio password for OpenSRS? + + $co->firstname($cust_main->first); + $co->name($cust_main->last); + $co->org($cust_main->company || '-'); + $co->street([$cust_main->address1, $cust_main->address2]); + $co->city($cust_main->city); + $co->sp($cust_main->state); + $co->pc($cust_main->zip); + $co->cc($cust_main->country); + $co->voice(format_tel($cust_main->daytime())); + $co->email($email); + + $cs->set($co, 'registrant'); + $cs->set($co, 'admin'); + $cs->set($co, 'billing'); + + return $cs; +} + +=item validate_contact_set + +Attempts to validate contact data for the domain based on OpenSRS rules. + +Returns undef if the contact data is acceptable, an error message if the contact +data lacks one or more required fields. + +=cut + +sub validate_contact_set { + my $c = shift; + + my %fields = ( + firstname => "first name", + name => "last name", + street => "street address", + city => "city", + sp => "state", + pc => "ZIP/postal code", + cc => "country", + email => "email address", + voice => "phone number", + ); + my @err = (); + foreach my $which (qw/registrant admin billing/) { + my $co = $c->get($which); + foreach (keys %fields) { + if (!$co->$_()) { + push @err, $fields{$_}; + } + } + } + if (scalar(@err) > 0) { + return "Contact information needs: " . join(', ', @err); + } + undef; +} + +#sub _export_replace { +# my( $self, $new, $old ) = (shift, shift, shift); +# +# return ''; +# +#} + +## Domain registration exports do nothing on delete. You're just removing the domain from Freeside, not the registry +#sub _export_delete { +# my( $self, $www ) = ( shift, shift ); +# +# return ''; +#} + +=item split_textarea_options + +Split textarea contents into lines, split lines on =, and then trim the results; + +=cut + +sub split_textarea_options { + my ($self, $optname) = @_; + my %opts = map { + my ($key, $value) = split /=/, $_; + $key =~ s/^\s*//; + $key =~ s/\s*$//; + $value =~ s/^\s*//; + $value =~ s/\s*$//; + $key => $value } split /\n/, $self->option($optname); + %opts; +} + +=item get_protocol_options + +Return a hash of protocol options + +=cut + +sub get_protocol_options { + my $self = shift; + my %opts = $self->split_textarea_options('protocol_opts'); + if ($self->machine =~ /opensrs\.net/) { + my %topts = $self->get_transport_options; + $opts{reseller_id} = $topts{client_login}; + } + %opts; +} + +=item get_transport_options + +Return a hash of transport options + +=cut + +sub get_transport_options { + my $self = shift; + my %opts = $self->split_textarea_options('transport_opts'); + $opts{remote_url} = "https://" . $self->machine . ":55443/resellers" if $self->machine =~ /opensrs\.net/; + %opts; +} + +=item is_supported_domain + +Return undef if the domain name uses a TLD or SLD that is supported by this registrar. +Otherwise return an error message explaining what's wrong. + +=cut + +sub is_supported_domain { + my $self = shift; + my $svc_domain = shift; + + # Get the TLD of the new domain + my @bits = split /\./, $svc_domain->domain; + + return "Can't register subdomains: " . $svc_domain->domain if scalar(@bits) != 2; + + my $tld = pop @bits; + + # See if it's one this export supports + my @tlds = split /\s+/, $self->option('tlds'); + @tlds = map { s/\.//; $_ } @tlds; + return "Can't register top-level domain $tld, restricted to: " . $self->option('tlds') if ! grep { $_ eq $tld } @tlds; + return undef; +} + +=item get_dri + +=cut + +sub get_dri { + my $self = shift; + my $dri; + +# return $self->{dri} if $self->{dri}; #!!!TBD!!! connection caching. + + eval "use Net::DRI;"; + return $@ if $@; + +# $dri=Net::DRI->new(...) to create the global object. Save the result, + + eval { + #$dri = Net::DRI::TrapExceptions->new(10); + $dri = Net::DRI->new({logging => [ 'files', { output_directory => '%%%FREESIDE_LOG%%%' } ]}); #!!!TBD!!! + $dri->logging->level( $self->option('log_level') ); + $dri->add_registry( $self->option('drd') ); + my $protocol; + $protocol = 'xcp' if $self->option('drd') eq 'OpenSRS'; + + $dri->target( $self->option('drd') )->add_current_profile($self->option('drd') . '1', +# 'Net::DRI::Protocol::' . $self->option('protocol_type'), +# $self->option('protocol_type'), +# 'xcp', #TBD!!!! + $protocol, # Implies transport +# 'Net::DRI::Transport::' . $self->option('transport_type'), + { $self->get_transport_options() }, +# [ $self->get_protocol_options() ] + ); + }; + return $@ if $@; + + $self->{dri} = $dri; + return $dri; +} + +=item get_status + +Returns a reference to a hashref containing information on the domain's status. The keys +defined depend on the status. + +'unregistered' means the domain is not registered. + +Otherwise, if the domain is in an asynchronous operation such as a transfer, returns the state +of that operation. + +Otherwise returns a value indicating if the domain can be managed through our reseller account. + +=cut + +sub get_status { + my ( $self, $svc_domain ) = @_; + my $rc; + my $rslt = {}; + + my $dri = $self->get_dri; + + if (UNIVERSAL::isa($dri, 'Net::DRI::Exception')) { + $rslt->{'message'} = $dri->as_string; + return $rslt; + } + eval { + $rc = $dri->domain_check( $svc_domain->domain ); + if (!$rc->is_success()) { + # Problem accessing the registry/registrar + $rslt->{'message'} = $rc->message; + } elsif (!$dri->get_info('exist')) { + # Domain is not registered + $rslt->{'unregistered'} = 1; + } else { + $rc = $dri->domain_transfer_query( $svc_domain->domain ); + if ($rc->is_success() && $dri->get_info('status')) { + # Transfer in progress + $rslt->{status} = $dri->get_info('status'); + $rslt->{contact_email} = $dri->get_info('request_address'); + $rslt->{last_update_time} = $dri->get_info('unixtime'); + } elsif ($dri->get_info('reason')) { + $rslt->{'reason'} = $dri->get_info('reason'); + # Domain is not being transferred... + $rc = $dri->domain_info( $svc_domain->domain, { $self->get_protocol_options() } ); + if ($rc->is_success() && $dri->get_info('exDate')) { + $rslt->{'expdate'} = $dri->get_info('exDate'); + } + } else { + $rslt->{status} = 'Unknown'; + } + } + }; +# rslt->{'message'} = $@->as_string if $@; + if ($@) { + rslt->{'message'} = (UNIVERSAL::isa($@, 'Net::DRI::Exception')) ? $@->as_string : $@->message; + } + + return $rslt; # Success +} + +=item register + +Attempts to register the domain through the reseller account associated with this export. + +Like most export functions, returns an error message on failure or undef on success. + +=cut + +sub register { + my ( $self, $svc_domain, $years ) = @_; + + my $err = $self->is_supported_domain( $svc_domain ); + return $err if $err; + + my $dri = $self->get_dri; + return $dri->as_string if (UNIVERSAL::isa($dri, 'Net::DRI::Exception')); + + eval { # All $dri methods can throw an exception. + +# Call methods + my $cust_main = $svc_domain->cust_svc->cust_pkg->cust_main; + + my $cs = $self->gen_contact_set($dri, $cust_main); + + $err = validate_contact_set($cs); + return $err if $err; + +# !!!TBD!!! add custom name servers when supported; add ns => $ns to hash passed to domain_create + + $res = $dri->domain_create($svc_domain->domain, { $self->get_protocol_options(), pure_create => 1, contact => $cs, duration => DateTime::Duration->new(years => $years) }); + $err = $res->is_success ? '' : $res->message; + }; + if ($@) { + $err = (UNIVERSAL::isa($@, 'Net::DRI::Exception')) ? $@->msg : $@->message; + } + + return $err; +} + +=item transfer + +Attempts to transfer the domain into the reseller account associated with this export. + +Like most export functions, returns an error message on failure or undef on success. + +=cut + +sub transfer { + my ( $self, $svc_domain ) = @_; + + my $err = $self->is_supported_domain( $svc_domain ); + return $err if $err; + +# $dri=Net::DRI->new(...) to create the global object. Save the result, + my $dri = $self->get_dri; + return $dri->as_string if (UNIVERSAL::isa($dri, 'Net::DRI::Exception')); + + eval { # All $dri methods can throw an exception + +# Call methods + my $cust_main = $svc_domain->cust_svc->cust_pkg->cust_main; + + my $cs = $self->gen_contact_set($dri, $cust_main); + + $err = validate_contact_set($cs); + return $err if $err; + +# !!!TBD!!! add custom name servers when supported; add ns => $ns to hash passed to domain_transfer_start + + $res = $dri->domain_transfer_start($svc_domain->domain, { $self->get_protocol_options(), contact => $cs }); + $err = $res->is_success ? '' : $res->message; + }; + if ($@) { + $err = (UNIVERSAL::isa($@, 'Net::DRI::Exception')) ? $@->msg : $@->message; + } + + return $err; +} + +=item renew + +Attempts to renew the domain for the specified number of years. + +Like most export functions, returns an error message on failure or undef on success. + +=cut + +sub renew { + my ( $self, $svc_domain, $years ) = @_; + + my $err = $self->is_supported_domain( $svc_domain ); + return $err if $err; + + my $dri = $self->get_dri; + return $dri->as_string if (UNIVERSAL::isa($dri, 'Net::DRI::Exception')); + + eval { # All $dri methods can throw an exception + my $expdate; + my $res = $dri->domain_info( $svc_domain->domain, { $self->get_protocol_options() } ); + if ($res->is_success() && $dri->get_info('exDate')) { + $expdate = $dri->get_info('exDate'); + +# return "Domain renewal not enabled" if !$self->option('renew'); + $res = $dri->domain_renew( $svc_domain->domain, { $self->get_protocol_options(), duration => DateTime::Duration->new(years => $years), current_expiration => $expdate }); + } + $err = $res->is_success ? '' : $res->message; + }; + if ($@) { + $err = (UNIVERSAL::isa($@, 'Net::DRI::Exception')) ? $@->msg : $@->message; + } + + return $err; +} + +=item revoke + +Attempts to revoke the domain registration. Only succeeds if invoked during the DRI +grace period immediately after registration. + +Like most export functions, returns an error message on failure or undef on success. + +=cut + +sub revoke { + my ( $self, $svc_domain ) = @_; + + my $err = $self->is_supported_domain( $svc_domain ); + return $err if $err; + + my $dri = $self->get_dri; + return $dri->as_string if (UNIVERSAL::isa($dri, 'Net::DRI::Exception')); + + eval { # All $dri methods can throw an exception + +# return "Domain registration revocation not enabled" if !$self->option('revoke'); + my $res = $dri->domain_delete( $svc_domain->domain, { $self->get_protocol_options(), domain => $svc_domain->domain, pure_delete => 1 }); + $err = $res->is_success ? '' : $res->message; + }; + if ($@) { + $err = (UNIVERSAL::isa($@, 'Net::DRI::Exception')) ? $@->msg : $@->message; + } + + return $err; +} + +=item registrar + +Should return a full-blown object representing the Net::DRI DRD, but current just returns a hashref +containing the registrar name. + +=cut + +sub registrar { + my $self = shift; + return { + name => $self->option('drd'), + }; +} + +=head1 SEE ALSO + +L, L, L, +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/svc_domain.pm b/FS/FS/svc_domain.pm index dd4f2c52f..07fe7ce51 100644 --- a/FS/FS/svc_domain.pm +++ b/FS/FS/svc_domain.pm @@ -300,7 +300,7 @@ sub replace { if $old->getfield('domain') ne $new->getfield('domain'); # Better to do it here than to force the caller to remember that svc_domain is weird. - $new->setfield(action => 'M'); + $new->setfield(action => 'I'); my $error = $new->SUPER::replace($old, @_); return $error if $error; } diff --git a/Makefile b/Makefile index cc1e67997..1795f0459 100644 --- a/Makefile +++ b/Makefile @@ -203,6 +203,7 @@ perl-modules: " blib/lib/FS/Cron/*.pm;\ perl -p -i -e "\ s|%%%FREESIDE_EXPORT%%%|${FREESIDE_EXPORT}|g;\ + s|%%%FREESIDE_LOG%%%|${FREESIDE_LOG}|g;\ " blib/lib/FS/part_export/*.pm;\ perl -p -i -e "\ s|%%%FREESIDE_CACHE%%%|${FREESIDE_CACHE}|g;\ diff --git a/httemplate/edit/process/domreg.cgi b/httemplate/edit/process/domreg.cgi index b643638f4..a95474e44 100755 --- a/httemplate/edit/process/domreg.cgi +++ b/httemplate/edit/process/domreg.cgi @@ -1,6 +1,6 @@ %if ($error) { % $cgi->param('error', $error); -<% $cgi->redirect(popurl(3). "view/svc_domain.cgi?$svcnum") %> +% errorpage($error); %} else { <% $cgi->redirect(popurl(3). "view/svc_domain.cgi?$svcnum") %> %} -- 2.11.0