package Net::EPP::Server; # ABSTRACT: A simple EPP server implementation. use Carp; use Crypt::OpenSSL::Random; use DateTime; use Digest::SHA qw(sha512_hex); use IO::Socket::SSL; use List::Util qw(any none); use Mozilla::CA; use Net::EPP 0.27; use Net::EPP::Frame; use Net::EPP::Protocol; use Net::EPP::ResponseCodes; use No::Worries::DN qw(dn_parse); use Socket; use Socket6; use Sys::Hostname; use Time::HiRes qw(ualarm); use XML::LibXML; use base qw(Net::Server::PreFork); use bytes; use utf8; use open qw(:encoding(utf8)); use feature qw(state); use vars qw(%MESSAGES); use strict; use warnings; our %MESSAGES = ( 1000 => 'Command completed successfully.', 1001 => 'Command completed successfully; action pending.', 1300 => 'Command completed successfully; no messages.', 1301 => 'Command completed successfully; ack to dequeue.', 1500 => 'Command completed successfully; ending session.', 2000 => 'Unknown command.', 2001 => 'Command syntax error.', 2002 => 'Command use error.', 2003 => 'Required parameter missing.', 2004 => 'Parameter value range error.', 2005 => 'Parameter value syntax error.', 2100 => 'Unimplemented protocol version.', 2101 => 'Unimplemented command.', 2102 => 'Unimplemented option.', 2103 => 'Unimplemented extension.', 2104 => 'Billing failure.', 2105 => 'Object is not eligible for renewal.', 2106 => 'Object is not eligible for transfer.', 2200 => 'Authentication error.', 2201 => 'Authorization error.', 2202 => 'Invalid authorization information.', 2300 => 'Object pending transfer.', 2301 => 'Object not pending transfer.', 2302 => 'Object exists.', 2303 => 'Object does not exist.', 2304 => 'Object status prohibits operation.', 2305 => 'Object association prohibits operation.', 2306 => 'Parameter value policy error.', 2307 => 'Unimplemented object service.', 2308 => 'Data management policy violation.', 2400 => 'Command failed.', 2500 => 'Command failed; server closing connection.', 2501 => 'Authentication error; server closing connection.', 2502 => 'Session limit exceeded; server closing connection.', ); sub new { my $package = shift; return bless($package->SUPER::new, $package); } sub run { my ($self, %args) = @_; $args{'host'} ||= 'localhost'; $args{'port'} ||= 7000; $args{'proto'} ||= 'ssl'; $self->{'epp'} = { 'handlers' => delete($args{'handlers'}) || {}, 'timeout' => delete($args{'timeout'}) || 30, 'client_ca_file' => delete($args{'client_ca_file'}), 'xsd_file' => delete($args{'xsd_file'}), }; if ($self->{'epp'}->{'client_ca_file'}) { $args{'SSL_verify_mode'} = SSL_VERIFY_FAIL_IF_NO_PEER_CERT; $args{'SSL_ca_file'} = $self->{'epp'}->{'client_ca_file'}; } return $self->SUPER::run(%args); } # # This method is called when a new connection is received. It sends the # <greeting> to the client, then enters the main loop. # sub process_request { my ($self, $socket) = @_; $self->send_frame($socket, $self->generate_greeting); $self->main_loop($socket); $socket->flush; $socket->close; } # # This method initialises the session, and calls main_loop_iteration() in a # loop. That method returns the result code, and the loop will terminate if the # code indicates that it should. # sub main_loop { my ($self, $socket) = @_; my $session = $self->init_session($socket); while (1) { my $code = $self->main_loop_iteration($socket, $session); last if (OK_BYE == $code || $code >= COMMAND_FAILED_BYE); } } # # This method initialises a new session # sub init_session { my ($self, $socket) = @_; my $session = { 'session_id' => $self->generate_svTRID, 'remote_addr' => inet_ntop(4 == length($socket->peeraddr) ? AF_INET : AF_INET6, $socket->peeraddr), 'remote_port' => $socket->peerport, }; if ($socket->peer_certificate) { $session->{'client_cert'} = { 'issuer' => dn_to_hashref($socket->peer_certificate('issuer')), 'subject' => dn_to_hashref($socket->peer_certificate('subject')), 'common_name' => $socket->peer_certificate('commonName'), }; }; return $session; } # # this function wraps No::Worries::DN::dn_parse() and returns a hashref instead # of an array # sub dn_to_hashref { my $ref = {}; foreach (@{dn_parse(shift)}) { my ($k, $v) = split(/=/, $_, 2); $ref->{$k} = $v; } return $ref; } # # This method reads a frame from the client, passes it to process_frame(), # sends the response back to the client, and returns the result code back to # main_loop(). # sub main_loop_iteration { my ($self, $socket, $session) = @_; my $xml = $self->get_frame($socket); return COMMAND_FAILED_BYE if (!$xml); my $response = $self->process_frame($xml, $session); $self->send_frame($socket, $response); if ('greeting' eq $response->documentElement->firstChild->localName) { return OK; } else { return $response->getElementsByTagName('result')->item(0)->getAttribute('code'); } } # # This method is a wrapper around Net::EPP::Protocol->get_frame() which # implements a timeout and exception handler. # sub get_frame { my ($self, $socket) = @_; my $xml; eval { local $SIG{ALRM} = sub { die("ALARM\n") }; ualarm(1000 * 1000 * ($self->{'epp'}->{'timeout'})); $xml = Net::EPP::Protocol->get_frame($socket); ualarm(0); }; return ($@ ? undef : $xml); } # # This method processes an XML frame received from a client and returns a # response frame. It manages session state, to ensure that clients that haven't # authenticated yet can't do anything except login. # sub process_frame { my ($self, $xml, $session) = @_; my $svTRID = $self->generate_svTRID; my $frame = $self->parse_frame($xml); if (!$frame->isa('XML::LibXML::Document')) { return $self->generate_error( code => SYNTAX_ERROR, msg => 'XML parse error.', svTRID => $svTRID, ); } my ($code, $msg) = $self->validate_frame($frame); if (OK != $code) { return $self->generate_error( code => $code, msg => $msg, svTRID => $svTRID, ); } eval { $self->run_callback( event => 'frame_received', frame => $frame ) }; my $fcname = $frame->getElementsByTagName('epp')->item(0)->firstChild->localName; if ('hello' eq $fcname) { return $self->generate_greeting; } my $clTRID = $frame->getElementsByTagName('clTRID')->item(0)->textContent; my $command; if ('command' eq $fcname) { $command = $frame->documentElement->firstChild->firstChild->localName; } elsif ('extension' eq $fcname) { $command = 'other'; } if (!$command) { return $self->generate_error( code => SYNTAX_ERROR, msg => 'First child element of <epp> is not <command> or <extension>.', clTRID => $clTRID, svTRID => $svTRID, ); } if (!defined($session->{'clid'}) && 'login' ne $command) { return $self->generate_error( code => AUTHENTICATION_ERROR, msg => 'You are not logged in.', clTRID => $clTRID, svTRID => $svTRID, ); } if (defined($session->{'clid'}) && 'login' eq $command) { return $self->generate_error( code => AUTHENTICATION_ERROR, msg => 'You are already logged in.', clTRID => $clTRID, svTRID => $svTRID, ); } if ('logout' eq $command) { eval { $self->run_callback(event => 'session_closed', session => $session) }; return $self->generate_response( code => OK_BYE, msg => 'Command completed successfully; ending session.', clTRID => $clTRID, svTRID => $svTRID, ); } my $response = $self->handle_command( command => $command, frame => $frame, session => $session, clTRID => $clTRID, svTRID => $svTRID, ); if ('login' eq $command && $response->getElementsByTagName('result')->item(0)->getAttribute('code') < UNKNOWN_COMMAND) { $session->{'clid'} = $frame->getElementsByTagName('clID')->item(0)->textContent; $session->{'lang'} = $frame->getElementsByTagName('lang')->item(0)->textContent; $session->{'objects'} = [ map { $_->textContent } $frame->getElementsByTagName('objURI') ]; $session->{'extensions'} = [ map { $_->textContent } $frame->getElementsByTagName('extURI') ]; } eval { $self->run_callback( event => 'response_prepared', frame => $frame, response => $response ) }; return $response; } # # This method invokes the event handler for a given event/command, and passes # back the response, returning an error if the command references an # unimplemented command, object service or extension. # sub handle_command { my $self = shift; my %args = @_; my $command = $args{'command'}; my $frame = $args{'frame'}; my $session = $args{'session'}; my $clTRID = $args{'clTRID'}; my $svTRID = $args{'svTRID'}; my $response; # # check for an unimplemented command # if (!defined($self->{'epp'}->{'handlers'}->{$command})) { return $self->generate_error( code => UNIMPLEMENTED_COMMAND, msg => sprintf('This server does not implement the <%s> command.', $command), clTRID => $clTRID, svTRID => $svTRID, ); } if ('login' ne $command) { # # check for an unimplemented object # if (any { $command eq $_ } qw(check info create delete renew transfer update)) { my $type = $frame->getElementsByTagName('epp')->item(0)->firstChild->firstChild->firstChild->namespaceURI; if (none { $type eq $_ } @{$session->{'objects'}}) { return $self->generate_error( code => UNIMPLEMENTED_OBJECT_SERVICE, msg => sprintf('This server does not support %s objects.', $type), clTRID => $clTRID, svTRID => $svTRID, ); } } # # check for an unimplemented extension # my $extn = $frame->getElementsByTagName('extension')->item(0); if ($extn) { foreach my $el ($extn->childNodes) { if (none { $el->namespaceURI eq $_ } @{$session->{'extensions'}}) { return $self->generate_error( code => UNIMPLEMENTED_EXTENSION, msg => sprintf('This server does not support the %s extension.', $el->namespaceURI), clTRID => $clTRID, svTRID => $svTRID, ); } } } } return $self->run_command(%args); } sub generate_greeting { my $self = shift; state ($hello, $frame); # # this ensures that the hello handler always receives a valid EPP frame # if (!$hello) { $hello = XML::LibXML::Document->new; $hello->setDocumentElement($hello->createElementNS($Net::EPP::Frame::EPP_URN, 'epp')); $hello->documentElement->appendChild($hello->createElement('hello')); } if (!$frame) { my $data = $self->run_callback(event => 'hello', frame => $hello); $frame = XML::LibXML::Document->new; $frame->setDocumentElement($frame->createElementNS($Net::EPP::Frame::EPP_URN, 'epp')); my $greeting = $frame->documentElement->appendChild($frame->createElement('greeting')); $greeting->appendChild($frame->createElement('svID'))->appendText($data->{'svID'} || lc(hostname)); # the <svDate> element is populated dynamically $greeting->appendChild($frame->createElement('svDate'))->appendChild($frame->createTextNode('')); my $svcMenu = $greeting->appendChild($frame->createElement('svcMenu')); $svcMenu->appendChild($frame->createElement('version'))->appendText('1.0'); foreach my $lang (@{$data->{'lang'} || [qw(en)]}) { $svcMenu->appendChild($frame->createElement('lang'))->appendText($lang); } foreach my $objURI (@{$data->{'objects'}}) { $svcMenu->appendChild($frame->createElement('objURI'))->appendText($objURI); } if (scalar(@{$data->{'extensions'}}) > 0) { my $svcExtension = $svcMenu->appendChild($frame->createElement('svcExtension')); foreach my $extURI (@{$data->{'extensions'}}) { $svcExtension->appendChild($frame->createElement('extURI'))->appendText($extURI); } } my $dcp = $greeting->appendChild($frame->createElement('dcp')); $dcp->appendChild($frame->createElement('access'))->appendChild($frame->createElement('all')); my $statement = $dcp->appendChild($frame->createElement('statement')); $statement->appendChild($frame->createElement('purpose'))->appendChild($frame->createElement('prov')); $statement->appendChild($frame->createElement('recipient'))->appendChild($frame->createElement('public')); $statement->appendChild($frame->createElement('retention'))->appendChild($frame->createElement('legal')); } $frame->getElementsByTagName('svDate')->item(0)->firstChild->setData(DateTime->now->strftime('%FT%T.0Z')); return $frame; } sub run_command { my $self = shift; my %args = @_; my $command = $args{'command'}; my $frame = $args{'frame'}; my $session = $args{'session'}; my $clTRID = $args{'clTRID'}; my $svTRID = $args{'svTRID'}; my @result = eval { $self->run_callback( event => $command, frame => $frame, session => $session, clTRID => $clTRID, svTRID => $svTRID, ) }; if ($@) { carp($@); return $self->generate_error( code => COMMAND_FAILED, clTRID => $clTRID, svTRID => $svTRID, ); } # # the command handler returned nothing # if (0 == scalar(@result)) { carp(sprintf('<%s> command handler returned nothing', $command)); return $self->generate_error( code => COMMAND_FAILED, clTRID => $clTRID, svTRID => $svTRID, ); } # # single return value # if (1 == scalar(@result)) { my $result = shift(@result); if ($result->isa('XML::LibXML::Document')) { return $result; } if (is_result_code($result)) { return $self->generate_response( code => $result, clTRID => $clTRID, svTRID => $svTRID, ); } carp(sprintf('<%s> command handler did not return a result code or an XML document', $command)); return $self->generate_error( code => COMMAND_FAILED, clTRID => $clTRID, svTRID => $svTRID, ); } if (!is_result_code($result[0])) { carp(sprintf('<%s> command handler returned something that is not a result code', $command)); return $self->generate_error( code => COMMAND_FAILED, clTRID => $clTRID, svTRID => $svTRID, ); } my $code = shift(@result); if (!ref($result[0])) { # # assume that the next member is a string containing a message # return $self->generate_response( code => $code, msg => $result[0], clTRID => $clTRID, svTRID => $svTRID, ); } # # generate a basic response that we will then insert elements into # my $response = $self->generate_response( code => $code, clTRID => $clTRID, svTRID => $svTRID, ); my %els; foreach my $el (@result) { # # anything that isn't an element is ignored # if ($el->isa('XML::LibXML::Element')) { # # if multiple elements with the same local name are present, # the last will clobber any previous elements. # $els{$el->localName} = $el; } } my $response_el = $response->getElementsByTagName('response')->item(0); # # now append elements in the correct order, if provided # foreach my $name (grep { exists($els{$_}) } qw(resData msgQ extension)) { $response_el->appendChild($response->importNode($els{$name})); } return $response; } sub generate_response { my $self = shift; my %args = @_; my $clTRID = $args{'clTRID'}; my $svTRID = $args{'svTRID'}; my $code = $args{'code'} || OK; my $msg = $args{'msg'} || $MESSAGES{$code} || ($code < UNKNOWN_COMMAND ? $MESSAGES{OK} : $MESSAGES{COMMAND_FAILED}); my $frame = XML::LibXML::Document->new; $frame->setDocumentElement($frame->createElementNS($Net::EPP::Frame::EPP_URN, 'epp')); my $response = $frame->documentElement->appendChild($frame->createElement('response')); my $result = $response->appendChild($frame->createElement('result')); $result->setAttribute('code', $code); $result->appendChild($frame->createElement('msg'))->appendText($msg); if ($args{'resData'}) { $response->appendChild($frame->createElement('resData')); } if ($clTRID || $svTRID) { my $trID = $response->appendChild($frame->createElement('trID')); $trID->appendChild($frame->createElement('clTRID'))->appendText($clTRID) if ($clTRID); $trID->appendChild($frame->createElement('svTRID'))->appendText($svTRID) if ($svTRID); } return $frame; } sub generate_error { my ($self, %args) = @_; $args{'code'} ||= COMMAND_FAILED; $args{'msg'} = $args{'msg'} || $MESSAGES{$args{'code'}} || 'An internal error occurred. Please try again later.'; return $self->generate_response(%args); } sub generate_svTRID { state $counter = time(); return substr(sha512_hex( pack('Q', ++$counter) .chr(0) .Crypt::OpenSSL::Random::random_pseudo_bytes(32) ), 0, 64); } sub parse_frame { my ($self, $xml) = @_; return XML::LibXML->load_xml( string => $xml, no_blanks => 1, no_cdata => 1, ); } sub validate_frame { my ($self, $frame) = @_; if ($self->{'epp'}->{'xsd_file'}) { state $xsd = XML::LibXML::Schema->new(location => $self->{'epp'}->{'xsd_file'}); eval { $xsd->validate($frame) }; return (SYNTAX_ERROR, $@) if ($@); } return OK; } # # This method finds the callback for the given event, and if found, runs it and # passes back its return value(s). # sub run_callback { my $self = shift; my %args = @_; $args{'server'} ||= $self; my $ref = $self->{'epp'}->{'handlers'}->{$args{'event'}}; return &{$ref}(%args) if ($ref); } sub is_result_code { my $value = shift; return (int($value) >= OK && int($value) <= 2502); } # # This method is a wrapper around Net::EPP::Protocol->send_frame() which # validates the response and reports any errors # sub send_frame { my ($self, $socket, $frame) = @_; # # note: we need to do a round-trip here otherwise we get namespace issues # $frame = XML::LibXML->load_xml(string => $frame->toString); my ($code, $msg) = $self->validate_frame($frame); if (OK != $code) { carp($msg); } Net::EPP::Protocol->send_frame($socket, $frame->toString); } 1; __END__ =pod =encoding UTF-8 =head1 NAME Net::EPP::Server - A simple EPP server implementation. =head1 VERSION version 0.003 =head1 SYNOPSIS use Net::EPP::Server; use Net::EPP::ResponseCodes; # # these are the objects we want to support # my @OBJECTS = qw(domain host contact); # # these are the extensions we want to support # my @EXTENSIONS = qw(secDNS rgp loginSec allocationToken launch); # # You can pass any arguments supported by Net::Server::Proto::SSL, but # by default the server will listen on localhost port 7000 using a # self-signed certificate. # Net::EPP::Server->new->run( # # this defines callbacks that will be invoked when an EPP frame is # received # handlers => { hello => \&hello_handler, login => \&login_handler, check => \&check_handler, info => \&info_handler, create => \&create_handler, # add more here } ); # # The <hello> handler is special and just needs # to return a hashref containing server metadata. # sub hello_handler { return { # this is the server ID and is optional, if not provided the system # hostname will be used svID => 'epp.example.com', # this is optional lang => [ qw(en fr de) ], # these are arrayrefs of namespace URIs objects => [ map { Net::EPP::Frame::ObjectSpec->xmlns($_) } @OBJECTS ], extensions => [ map { Net::EPP::Frame::ObjectSpec->xmlns($_) } @EXTENSIONS ], }; } # # All other handlers work the same. They are passed a hash of arguments and # can return a simple result code, a result code and message, a # XML::LibXML::Document object, or a result code and an array of # XML::LibXML::Element objects. # sub login_handler { my %args = @_; my $frame = $args{'frame'}; my $clid = $frame->getElementsByTagName('clid')->item(0)->textContent; my $pw = $frame->getElementsByTagName('pw')->item(0)->textContent; if (!validate_credentials($clid, $pw)) { return AUTHENTICATION_FAILED; } else { return OK; } } =head1 INTRODUCTION C<Net::EPP::Server> provides a high-level framework for developing L<Extensible Provisioning Protocol (EPP)|https://www.rfc-editor.org/info/std69> servers. It implements the TLS/TCP transport described in L<RFC 5734|https://www.rfc-editor.org/info/rfc5734>, and the L<EPP Server State Machine|https://www.rfc-editor.org/rfc/rfc5730.html#:~:text=Figure%201:%20EPP%20Server%20State%20Machine> described in L<Section 2 of RFC 5730|https://www.rfc-editor.org/rfc/rfc5730.html#section-2>. =head1 SERVER CONFIGURATION C<Net::EPP::Server> inherits from L<Net::Server> I<(specifically L<Net::Server::PreFork>)>, and so the C<run()> method accepts all the parameters supported by that module, plus the following: =over =item * C<handlers>, which is a hashref which maps events (including EPP commands) to callback functions. See below for details. =item * C<timeout> (optional), which is how long (in seconds) to wait for a client to send a command before dropping the connection. This parameter may be a decimal (e.g. C<3.14>) or an integer (e.g. C<42>). The default timeout is 30 seconds. =item * C<client_ca_file> (optional), which is the location on disk of a file which can be use to validate client certificates. If this parameter is not provided, clients will not be required to use a certificate. =item * C<xsd_file> (optional), which is the location on disk of an XSD file which should be used to validate all frames received from clients. This XSD file can include other XSD files using C<E<lt>importE<gt>>. =back =head1 EVENT HANDLERS You implement the business logic of your EPP server by specifying callbacks that are invoked for certain events. These come in two flavours: I<events> and I<commands>. All event handlers receive a hash containing one or more arguments that are described below. =head2 C<frame_received> Called when a frame has been successfully parsed and validated, but before it has been processed. The input frame will be passed as the C<frame> argument. =head2 C<response_prepared> Called when a response has been generated, but before it has been sent back to the client. The response will be passed as the C<response> argument, while the input frame will be passed as the C<frame> argument. It is B<not> called for C<E<lt>helloE<gt>> and C<E<lt>logoutE<gt>>commands. =head2 C<session_closed> C<Net::EPP::Server> takes care of handling session management, but this event handler will be called once a C<E<lt>logoutE<gt>> command has been successfully processed, and before the client connection has been closed. The C<session> argument will contain a hashref of the session (see below). =head2 C<hello> The C<hello> event handler is called when a new client connects, or a C<E<lt>helloE<gt>> frame is received. Unlike the other event handlers, this handler B<MUST> respond with a hashref which contains the following entries: =over =item * C<svID> (OPTIONAL) - the server ID. If not provided, the system hostname will be used. =item * C<lang> (OPTIONAL) - an arrayref containing language codes. It not provided, C<en> will be used as the only supported language. =item * C<objects> (REQUIRED) - an arrayref of namespace URIs for =back =head2 COMMAND HANDLERS The standard EPP command repertoire is: =over =item * C<login> =item * C<logout> =item * C<poll> =item * C<check> =item * C<info> =item * C<create> =item * C<delete> =item * C<renew> =item * C<transfer> =item * C<delete> =back A command handler may be specified for all of these commands except C<logout>, since C<Net::EPP::Server> handles this itself. Since EPP allows the command repertoire to be extended (by omitting the C<E<lt>commandE<gt>> element and using the C<E<lt>extensionE<gt>> element only), C<Net::EPP::Server> also supports the C<other> event which will be called when processing such frames. All command handlers receive a hash containing the following arguments: =over =item * C<server> - the server. =item * C<event> - the name of the command. =item * C<frame> - an L<XML::LibXML::Document> object representing the frame received from the client. =item * C<session> - a hashref containing the session information. =item * C<clTRID> - the value of the C<E<lt>clTRIDE<gt>> element taken from the frame received from the client. =item * C<svTRID> - a value suitable for inclusion in the C<E<lt>clTRIDE<gt>> element of the response. =back =head3 SESSION PARAMETERS As mentioned above, the C<session> parameter is a hashref which contains information about the session. It contains the following values: =over =item * C<session_id> - a unique session ID. =item * C<remote_addr> - the client's remote IP address (IPv4 or IPv6). =item * C<remote_port> - the client's remote port. =item * C<clid> - the client ID used to log in. =item * C<lang> - the language specified at login. =item * C<objects> - an arrayref of the object URI(s) specified at login. =item * C<extensions> - an arrayref of the extension URI(s) specified at login. =item * C<client_cert> - a hashref containing information about the client certificate (if any), which looks something like this: { 'issuer' => $dnref, 'common_name' => 'example.com', 'subject' => $dnref, } C<$dnref> is a hashref representing the Distinguished Name of the issuer or subject and looks like this: { 'O' => 'Example Inc.', 'OU' => 'Registry Services', 'emailAddress' => 'registry@example.com', 'CN' => 'EPP Server Private CA', } Other members, such as C<C> (country), C<ST> (state/province), and C<L> (city) may also be present. =back =head3 RETURN VALUES Command handlers can return result information in four different ways that are explained below. =head4 1. SIMPLE RESULT CODE Command handlers can signal the result of a command by simply passing a single integer value. L<Net::EPP::ResponseCodes> may be used to avoid literal integers. Example: sub delete_handler { my %args = @_; # business logic here if ($success) { return OK; } else { return COMMAND_FAILED; } } C<Net::EPP::Server> will construct a standard EPP response frame using the result code and send it to the client. =head4 2. RESULT CODE + MESSAGE If the command handler returns two values, and the first is a valid result code, then the second can be a message. Example: sub delete_handler { my %args = @_; # business logic here if ($success) { return (OK, 'object deleted'); } else { return (COMMAND_FAILED, 'object not deleted'); } } C<Net::EPP::Server> will construct a standard EPP response frame using the result code and message, and send it to the client. =head4 3. RESULT CODE + XML ELEMENTS The command handler may return a result code followed by an array of between one and three L<XML::LibXML::Element> objects, in any order, representing the C<E<lt>resDataE<gt>>, C<E<lt>msgQE<gt>> and C<E<lt>extensionE<gt>> elements. Example: sub delete_handler { my %args = @_; # business logic here return ( OK, $resData_element, $msgQ_element, $extension_element, ); } C<Net::EPP::Server> will construct a standard EPP response frame using the result code and supplied elements which will be imported and inserted into the appropriate positions, and send it to the client. =head4 4. L<XML::LibXML::Document> OBJECT A return value that is a single L<XML::LibXML::Document> object will be sent back to the client verbatim. =head3 EXCEPTIONS C<Net::EPP::Server> will catch any exceptions thrown by the command handler, will C<carp($@)>, and then send a C<2400> result code back to the client. =head1 UTILITY METHODS =head2 C<generate_response(%args)> This method returns a L<XML::LibXML::Document> object representing the response described by C<%args>, which should contain the following: =over =item * C<code> (OPTIONAL) - the result code. See L<Net::EPP::ResponseCodes>. If not provided, C<1000> will be used. =item * C<msg> - a human-readable error message. If not provided, the string C<"Command completed successfully."> will be used if C<code> is less than C<2000>, and C<"Command failed."> if C<code> is C<2000> or higher. =item * C<resData> (OPTIONAL) - if defined, an empty C<E<lt>resDataE<gt>> element will be added to the frame. =item * C<clTRID> (OPTIONAL) - the client transaction ID. =item * C<svTRID> (OPTIONAL) - the server's transaction ID. =back Once created, it is straightforward to modify the object to add, remove or change its contents as needed. =head2 C<generate_error(%args)> This method is identical to C<generate_response()> except the default value for the C<code> parameter is C<2400>, indicating that the command failed for unspecified reasons. =head2 C<generate_svTRID()> This method returns a unique string suitable for use in the C<E<lt>svTRIDE<gt>> and similar elements. =head2 C<parse_frame($xml)> Attempts to parse C<$xml> and returns a L<XML::LibXML::Document> if successful. =head2 C<is_valid($frame)> Returns a result code and optionally a message if C<$frame> cannot be validated against the XSD file provided in the C<xsd_file> parameter. =head2 C<is_result_code($value)> Returns true if C<$value> is a recognised EPP result code. =head1 AUTHOR Gavin Brown <gavin.brown@icann.org> =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2025 by Internet Corporation for Assigned Names and Number (ICANN). This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut