File Coverage

blib/lib/Net/EPP/Server.pm
Criterion Covered Total %
statement 87 342 25.4
branch 0 94 0.0
condition 0 40 0.0
subroutine 29 56 51.7
pod 8 21 38.1
total 124 553 22.4


line stmt bran cond sub pod time code
1             package Net::EPP::Server;
2             # ABSTRACT: A simple EPP server implementation.
3 1     1   360804 use Carp;
  1         3  
  1         157  
4 1     1   705 use Crypt::OpenSSL::Random;
  1         2424  
  1         72  
5 1     1   13 use Cwd qw(abs_path);
  1         3  
  1         74  
6 1     1   4299 use DateTime;
  1         757827  
  1         87  
7 1     1   856 use Digest::SHA qw(sha512_hex);
  1         5938  
  1         138  
8 1     1   10 use File::Path qw(make_path);
  1         2  
  1         78  
9 1     1   8 use File::Spec;
  1         3  
  1         30  
10 1     1   915 use File::Slurp qw(write_file);
  1         32957  
  1         89  
11 1     1   1065 use IO::Socket::SSL;
  1         132748  
  1         39  
12 1     1   238 use List::Util qw(any none);
  1         2  
  1         88  
13 1     1   652 use Mozilla::CA;
  1         362  
  1         71  
14 1     1   826 use Net::EPP 0.27;
  1         132964  
  1         41  
15 1     1   12 use Net::EPP::Frame;
  1         3  
  1         24  
16 1     1   6 use Net::EPP::Protocol;
  1         2  
  1         24  
17 1     1   15 use Net::EPP::ResponseCodes;
  1         2  
  1         263  
18 1     1   768 use No::Worries::DN qw(dn_parse);
  1         21538  
  1         10  
19 1     1   131 use Socket;
  1         2  
  1         786  
20 1     1   840 use Socket6;
  1         2013  
  1         389  
21 1     1   10 use Sys::Hostname;
  1         3  
  1         73  
22 1     1   8 use Time::HiRes qw(ualarm);
  1         3  
  1         11  
23 1     1   128 use XML::LibXML;
  1         14  
  1         14  
24 1     1   260 use base qw(Net::Server::PreFork);
  1         2  
  1         856  
25 1     1   50098 use bytes;
  1         33  
  1         11  
26 1     1   42 use utf8;
  1         2  
  1         10  
27 1     1   970 use open qw(:encoding(utf8));
  1         1849  
  1         7  
28 1     1   2400 use feature qw(say state);
  1         2  
  1         188  
29 1     1   9 use vars qw($VERSION %MESSAGES $HELLO);
  1         2  
  1         76  
30 1     1   7 use strict;
  1         2  
  1         25  
31 1     1   5 use warnings;
  1         18  
  1         8091  
32              
33             our $VERSION = '0.10';
34              
35             our %MESSAGES = (
36             1000 => 'Command completed successfully.',
37             1001 => 'Command completed successfully; action pending.',
38             1300 => 'Command completed successfully; no messages.',
39             1301 => 'Command completed successfully; ack to dequeue.',
40             1500 => 'Command completed successfully; ending session.',
41             2000 => 'Unknown command.',
42             2001 => 'Command syntax error.',
43             2002 => 'Command use error.',
44             2003 => 'Required parameter missing.',
45             2004 => 'Parameter value range error.',
46             2005 => 'Parameter value syntax error.',
47             2100 => 'Unimplemented protocol version.',
48             2101 => 'Unimplemented command.',
49             2102 => 'Unimplemented option.',
50             2103 => 'Unimplemented extension.',
51             2104 => 'Billing failure.',
52             2105 => 'Object is not eligible for renewal.',
53             2106 => 'Object is not eligible for transfer.',
54             2200 => 'Authentication error.',
55             2201 => 'Authorization error.',
56             2202 => 'Invalid authorization information.',
57             2300 => 'Object pending transfer.',
58             2301 => 'Object not pending transfer.',
59             2302 => 'Object exists.',
60             2303 => 'Object does not exist.',
61             2304 => 'Object status prohibits operation.',
62             2305 => 'Object association prohibits operation.',
63             2306 => 'Parameter value policy error.',
64             2307 => 'Unimplemented object service.',
65             2308 => 'Data management policy violation.',
66             2400 => 'Command failed.',
67             2500 => 'Command failed; server closing connection.',
68             2501 => 'Authentication error; server closing connection.',
69             2502 => 'Session limit exceeded; server closing connection.',
70             );
71              
72             $HELLO = XML::LibXML::Document->new;
73             $HELLO->setDocumentElement($HELLO->createElementNS($Net::EPP::Frame::EPP_URN, 'epp'));
74             $HELLO->documentElement->appendChild($HELLO->createElement('hello'));
75              
76              
77             sub new {
78 0     0 1   my $package = shift;
79              
80 0           return bless($package->SUPER::new, $package);
81             }
82              
83              
84             sub run {
85 0     0 1   my ($self, %args) = @_;
86              
87 0   0       $args{'host'} ||= 'localhost';
88 0   0       $args{'port'} ||= 7000;
89 0   0       $args{'proto'} ||= 'ssl';
90              
91             $self->{'epp'} = {
92             'handlers' => delete($args{'handlers'}) || {},
93             'timeout' => delete($args{'timeout'}) || 30,
94             'client_ca_file' => delete($args{'client_ca_file'}),
95             'xsd_file' => delete($args{'xsd_file'}),
96 0   0       'log_dir' => delete($args{'log_dir'}),
      0        
97             };
98              
99 0 0         if ($self->{'epp'}->{'client_ca_file'}) {
100 0           $args{'SSL_verify_mode'} = SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
101 0           $args{'SSL_client_ca_file'} = $self->{'epp'}->{'client_ca_file'};
102             }
103              
104 0           return $self->SUPER::run(%args);
105             }
106              
107             #
108             # This method is called when a new connection is received. It sends the
109             # <greeting> to the client, then enters the main loop.
110             #
111             sub process_request {
112 0     0 1   my ($self, $socket) = @_;
113              
114 0           $self->send_frame($socket, $self->generate_greeting);
115              
116 0           $self->main_loop($socket);
117              
118 0           $socket->flush;
119 0           $socket->close;
120             }
121              
122             #
123             # This method initialises the session, and calls main_loop_iteration() in a
124             # loop. That method returns the result code, and the loop will terminate if the
125             # code indicates that it should.
126             #
127             sub main_loop {
128 0     0 0   my ($self, $socket) = @_;
129              
130 0           my $session = $self->init_session($socket);
131              
132 0           while (1) {
133 0           my $code = $self->main_loop_iteration($socket, $session);
134              
135 0 0 0       last if (OK_BYE == $code || $code >= COMMAND_FAILED_BYE);
136             }
137             }
138              
139             #
140             # This method initialises a new session
141             #
142             sub init_session {
143 0     0 0   my ($self, $socket) = @_;
144              
145 0 0         my $session = {
146             'session_id' => $self->generate_svTRID,
147             'remote_addr' => inet_ntop(4 == length($socket->peeraddr) ? AF_INET : AF_INET6, $socket->peeraddr),
148             'remote_port' => $socket->peerport,
149             'counter' => 0,
150             };
151              
152 0 0         if ($socket->peer_certificate) {
153 0           $session->{'client_cert'} = {
154             'issuer' => dn_to_hashref($socket->peer_certificate('issuer')),
155             'subject' => dn_to_hashref($socket->peer_certificate('subject')),
156             'common_name' => $socket->peer_certificate('commonName'),
157             };
158             };
159              
160 0           return $session;
161             }
162              
163             #
164             # this function wraps No::Worries::DN::dn_parse() and returns a hashref instead
165             # of an array
166             #
167             sub dn_to_hashref {
168 0     0 0   my $ref = {};
169              
170 0           foreach (@{dn_parse(shift)}) {
  0            
171 0           my ($k, $v) = split(/=/, $_, 2);
172 0           $ref->{$k} = $v;
173             }
174              
175 0           return $ref;
176             }
177              
178             #
179             # This method reads a frame from the client, passes it to process_frame(),
180             # sends the response back to the client, and returns the result code back to
181             # main_loop().
182             #
183             sub main_loop_iteration {
184 0     0 0   my ($self, $socket, $session) = @_;
185              
186 0           my $xml = $self->get_frame($socket);
187              
188 0 0         return COMMAND_FAILED_BYE if (!$xml);
189              
190 0           my $response = $self->process_frame($xml, $session);
191              
192 0           $self->send_frame($socket, $response);
193              
194 0           $session->{'counter'}++;
195              
196 0           $self->write_log($session, $xml, $response);
197              
198 0 0         if ('greeting' eq $response->documentElement->firstChild->localName) {
199 0           return OK;
200              
201             } else {
202 0           return $response->getElementsByTagName('result')->item(0)->getAttribute('code');
203              
204             }
205             }
206              
207             #
208             # write the command and response to the log
209             #
210             sub write_log {
211 0     0 0   my ($self, $session, $command, $response) = @_;
212              
213 0 0         return unless exists($self->{'epp'}->{'log_dir'});
214              
215             my $dir = File::Spec->catdir(
216             abs_path($self->{'epp'}->{'log_dir'}),
217 0           $session->{'session_id'}
218             );
219              
220 0           make_path($dir, { mode => 0700});
221              
222 0           write_file(File::Spec->catfile($dir, sprintf('%016u-command.xml', $session->{'counter'})), $command);
223 0           write_file(File::Spec->catfile($dir, sprintf('%016u-response.xml', $session->{'counter'})), $response);
224             }
225              
226             #
227             # This method is a wrapper around Net::EPP::Protocol->get_frame() which
228             # implements a timeout and exception handler.
229             #
230             sub get_frame {
231 0     0 0   my ($self, $socket) = @_;
232              
233 0           my $xml;
234              
235 0           eval {
236 0     0     local $SIG{ALRM} = sub { die("ALARM\n") };
  0            
237              
238 0           ualarm(1000 * 1000 * ($self->{'epp'}->{'timeout'}));
239              
240 0           $xml = Net::EPP::Protocol->get_frame($socket);
241              
242 0           ualarm(0);
243             };
244              
245 0 0         return ($@ ? undef : $xml);
246             }
247              
248              
249             #
250             # This method processes an XML frame received from a client and returns a
251             # response frame. It manages session state, to ensure that clients that haven't
252             # authenticated yet can't do anything except login.
253             #
254             sub process_frame {
255 0     0 0   my ($self, $xml, $session) = @_;
256              
257 0           my $svTRID = $self->generate_svTRID;
258              
259 0           my $frame = $self->parse_frame($xml);
260              
261 0 0         if (!$frame->isa('XML::LibXML::Document')) {
262 0           return $self->generate_error(
263             code => SYNTAX_ERROR,
264             msg => 'XML parse error.',
265             svTRID => $svTRID,
266             );
267             }
268              
269 0           my ($code, $msg) = $self->validate_frame($frame);
270              
271 0 0         if (OK != $code) {
272 0           return $self->generate_error(
273             code => $code,
274             msg => $msg,
275             svTRID => $svTRID,
276             );
277             }
278              
279 0           eval { $self->run_callback(
  0            
280             event => 'frame_received',
281             frame => $frame
282             ) };
283              
284 0           my $fcname = $frame->getElementsByTagName('epp')->item(0)->firstChild->localName;
285              
286 0 0         if ('hello' eq $fcname) {
287 0           return $self->generate_greeting;
288             }
289              
290 0           my $clTRID = $frame->getElementsByTagName('clTRID')->item(0);
291 0 0         $clTRID = $clTRID->textContent if ($clTRID);
292              
293 0           my $command;
294              
295 0 0         if ('command' eq $fcname) {
    0          
296 0           $command = $frame->documentElement->firstChild->firstChild->localName;
297              
298             } elsif ('extension' eq $fcname) {
299 0           $command = 'other';
300              
301             }
302              
303 0 0         if (!$command) {
304 0           return $self->generate_error(
305             code => SYNTAX_ERROR,
306             msg => 'First child element of <epp> is not <command> or <extension>.',
307             clTRID => $clTRID,
308             svTRID => $svTRID,
309             );
310             }
311              
312 0 0 0       if (!defined($session->{'clid'}) && 'login' ne $command) {
313 0           return $self->generate_error(
314             code => AUTHENTICATION_ERROR,
315             msg => 'You are not logged in.',
316             clTRID => $clTRID,
317             svTRID => $svTRID,
318             );
319             }
320              
321 0 0         if ('login' eq $command) {
322 0 0         if (defined($session->{'clid'})) {
323 0           return $self->generate_error(
324             code => AUTHENTICATION_ERROR,
325             msg => 'You are already logged in.',
326             clTRID => $clTRID,
327             svTRID => $svTRID,
328             );
329             }
330              
331 0           my $meta = $self->run_callback(event => 'hello', frame => $HELLO);
332              
333 0           foreach my $uri (map { $_->textContent } $frame->getElementsByTagName('objURI')) {
  0            
334 0 0   0     if (none { $_ eq $uri } @{$meta->{objects}}) {
  0            
  0            
335 0           return $self->generate_error(
336             code => UNIMPLEMENTED_OBJECT_SERVICE,
337             msg => sprintf("This server does not support '%s' objects.", $uri),
338             clTRID => $clTRID,
339             svTRID => $svTRID,
340             );
341             }
342             }
343              
344 0           foreach my $uri (map { $_->textContent } $frame->getElementsByTagName('extURI')) {
  0            
345 0 0   0     if (none { $_ eq $uri } @{$meta->{extensions}}) {
  0            
  0            
346 0           return $self->generate_error(
347             code => UNIMPLEMENTED_EXTENSION,
348             msg => sprintf("This server does not support the '%s' extension.", $uri),
349             clTRID => $clTRID,
350             svTRID => $svTRID,
351             );
352             }
353             }
354             }
355              
356 0 0         if ('logout' eq $command) {
357 0           eval { $self->run_callback(event => 'session_closed', session => $session) };
  0            
358              
359 0           return $self->generate_response(
360             code => OK_BYE,
361             msg => 'Command completed successfully; ending session.',
362             clTRID => $clTRID,
363             svTRID => $svTRID,
364             );
365             }
366              
367 0           my $response = $self->handle_command(
368             command => $command,
369             frame => $frame,
370             session => $session,
371             clTRID => $clTRID,
372             svTRID => $svTRID,
373             );
374              
375 0 0 0       if ('login' eq $command && $response->getElementsByTagName('result')->item(0)->getAttribute('code') < UNKNOWN_COMMAND) {
376 0           $session->{'clid'} = $frame->getElementsByTagName('clID')->item(0)->textContent;
377 0           $session->{'lang'} = $frame->getElementsByTagName('lang')->item(0)->textContent;
378 0           $session->{'objects'} = [ map { $_->textContent } $frame->getElementsByTagName('objURI') ];
  0            
379 0           $session->{'extensions'} = [ map { $_->textContent } $frame->getElementsByTagName('extURI') ];
  0            
380             }
381              
382 0           eval { $self->run_callback(
  0            
383             event => 'response_prepared',
384             frame => $frame,
385             response => $response
386             ) };
387              
388 0           return $response;
389             }
390              
391             #
392             # This method invokes the event handler for a given event/command, and passes
393             # back the response, returning an error if the command references an
394             # unimplemented command, object service or extension.
395             #
396             sub handle_command {
397 0     0 0   my $self = shift;
398 0           my %args = @_;
399 0           my $command = $args{'command'};
400 0           my $frame = $args{'frame'};
401 0           my $session = $args{'session'};
402 0           my $clTRID = $args{'clTRID'};
403 0           my $svTRID = $args{'svTRID'};
404              
405 0           my $response;
406              
407             #
408             # check for an unimplemented command
409             #
410 0 0         if (!defined($self->{'epp'}->{'handlers'}->{$command})) {
411 0           return $self->generate_error(
412             code => UNIMPLEMENTED_COMMAND,
413             msg => sprintf('This server does not implement the <%s> command.', $command),
414             clTRID => $clTRID,
415             svTRID => $svTRID,
416             );
417             }
418              
419 0 0         if ('login' ne $command) {
420             #
421             # check for an unimplemented object
422             #
423 0 0   0     if (any { $command eq $_ } qw(check info create delete renew transfer update)) {
  0            
424 0           my $type = $frame->getElementsByTagName('epp')->item(0)->firstChild->firstChild->firstChild->namespaceURI;
425              
426 0 0   0     if (none { $type eq $_ } @{$session->{'objects'}}) {
  0            
  0            
427 0           return $self->generate_error(
428             code => UNIMPLEMENTED_OBJECT_SERVICE,
429             msg => sprintf('This server does not support %s objects.', $type),
430             clTRID => $clTRID,
431             svTRID => $svTRID,
432             );
433             }
434             }
435              
436             #
437             # check for an unimplemented extension
438             #
439 0           my $extn = $frame->getElementsByTagName('extension')->item(0);
440 0 0         if ($extn) {
441 0           foreach my $el ($extn->childNodes) {
442 0 0   0     if (none { $el->namespaceURI eq $_ } @{$session->{'extensions'}}) {
  0            
  0            
443 0           return $self->generate_error(
444             code => UNIMPLEMENTED_EXTENSION,
445             msg => sprintf('This server does not support the %s extension.', $el->namespaceURI),
446             clTRID => $clTRID,
447             svTRID => $svTRID,
448             );
449             }
450             }
451             }
452             }
453              
454 0           return $self->run_command(%args);
455             }
456              
457              
458             sub generate_greeting {
459 0     0 0   my $self = shift;
460              
461 0           state $frame;
462              
463 0 0         if (!$frame) {
464 0           my $data = $self->run_callback(event => 'hello', frame => $HELLO);
465              
466 0           $frame = XML::LibXML::Document->new;
467              
468 0           $frame->setDocumentElement($frame->createElementNS($Net::EPP::Frame::EPP_URN, 'epp'));
469 0           my $greeting = $frame->documentElement->appendChild($frame->createElement('greeting'));
470              
471 0   0       $greeting->appendChild($frame->createElement('svID'))->appendText($data->{'svID'} || lc(hostname));
472              
473             # the <svDate> element is populated dynamically
474 0           $greeting->appendChild($frame->createElement('svDate'))->appendChild($frame->createTextNode(''));
475              
476 0           my $svcMenu = $greeting->appendChild($frame->createElement('svcMenu'));
477 0           $svcMenu->appendChild($frame->createElement('version'))->appendText('1.0');
478              
479 0 0         foreach my $lang (@{$data->{'lang'} || [qw(en)]}) {
  0            
480 0           $svcMenu->appendChild($frame->createElement('lang'))->appendText($lang);
481             }
482              
483 0           foreach my $objURI (@{$data->{'objects'}}) {
  0            
484 0           $svcMenu->appendChild($frame->createElement('objURI'))->appendText($objURI);
485             }
486              
487 0 0         if (scalar(@{$data->{'extensions'}}) > 0) {
  0            
488 0           my $svcExtension = $svcMenu->appendChild($frame->createElement('svcExtension'));
489              
490 0           foreach my $extURI (@{$data->{'extensions'}}) {
  0            
491 0           $svcExtension->appendChild($frame->createElement('extURI'))->appendText($extURI);
492             }
493             }
494              
495 0           my $dcp = $greeting->appendChild($frame->createElement('dcp'));
496 0           $dcp->appendChild($frame->createElement('access'))->appendChild($frame->createElement('all'));
497              
498 0           my $statement = $dcp->appendChild($frame->createElement('statement'));
499 0           $statement->appendChild($frame->createElement('purpose'))->appendChild($frame->createElement('prov'));
500 0           $statement->appendChild($frame->createElement('recipient'))->appendChild($frame->createElement('public'));
501 0           $statement->appendChild($frame->createElement('retention'))->appendChild($frame->createElement('legal'));
502             }
503              
504 0           $frame->getElementsByTagName('svDate')->item(0)->firstChild->setData(DateTime->now->strftime('%FT%T.0Z'));
505              
506 0           return $frame;
507             }
508              
509              
510             sub run_command {
511 0     0 0   my $self = shift;
512 0           my %args = @_;
513 0           my $command = $args{'command'};
514 0           my $frame = $args{'frame'};
515 0           my $session = $args{'session'};
516 0           my $clTRID = $args{'clTRID'};
517 0           my $svTRID = $args{'svTRID'};
518              
519 0           my @result = eval { $self->run_callback(
  0            
520             event => $command,
521             frame => $frame,
522             session => $session,
523             clTRID => $clTRID,
524             svTRID => $svTRID,
525             ) };
526              
527 0 0         if ($@) {
528 0           carp($@);
529              
530 0           return $self->generate_error(
531             code => COMMAND_FAILED,
532             clTRID => $clTRID,
533             svTRID => $svTRID,
534             );
535             }
536              
537             #
538             # the command handler returned nothing
539             #
540 0 0         if (0 == scalar(@result)) {
541 0           carp(sprintf('<%s> command handler returned nothing', $command));
542              
543 0           return $self->generate_error(
544             code => COMMAND_FAILED,
545             clTRID => $clTRID,
546             svTRID => $svTRID,
547             );
548             }
549              
550             #
551             # single return value
552             #
553 0 0         if (1 == scalar(@result)) {
554 0           my $result = shift(@result);
555              
556 0 0         if ($result->isa('XML::LibXML::Document')) {
557 0           return $result;
558             }
559              
560 0 0         if (is_result_code($result)) {
561 0           return $self->generate_response(
562             code => $result,
563             clTRID => $clTRID,
564             svTRID => $svTRID,
565             );
566             }
567              
568 0           carp(sprintf('<%s> command handler did not return a result code or an XML document', $command));
569              
570 0           return $self->generate_error(
571             code => COMMAND_FAILED,
572             clTRID => $clTRID,
573             svTRID => $svTRID,
574             );
575             }
576              
577 0 0         if (!is_result_code($result[0])) {
578 0           carp(sprintf('<%s> command handler returned something that is not a result code', $command));
579              
580 0           return $self->generate_error(
581             code => COMMAND_FAILED,
582             clTRID => $clTRID,
583             svTRID => $svTRID,
584             );
585             }
586              
587 0           my $code = shift(@result);
588              
589 0 0         if (!ref($result[0])) {
590             #
591             # assume that the next member is a string containing a message
592             #
593 0           return $self->generate_response(
594             code => $code,
595             msg => $result[0],
596             clTRID => $clTRID,
597             svTRID => $svTRID,
598             );
599             }
600              
601             #
602             # generate a basic response that we will then insert elements into
603             #
604 0           my $response = $self->generate_response(
605             code => $code,
606             clTRID => $clTRID,
607             svTRID => $svTRID,
608             );
609              
610 0           my %els;
611 0           foreach my $el (@result) {
612             #
613             # anything that isn't an element is ignored
614             #
615 0 0         if ($el->isa('XML::LibXML::Element')) {
616             #
617             # if multiple elements with the same local name are present,
618             # the last will clobber any previous elements.
619             #
620 0           $els{$el->localName} = $el;
621             }
622             }
623              
624 0           my $response_el = $response->getElementsByTagName('response')->item(0);
625              
626             #
627             # now append elements in the correct order, if provided
628             #
629 0           foreach my $name (grep { exists($els{$_}) } qw(resData msgQ extension)) {
  0            
630 0           $response_el->appendChild($response->importNode($els{$name}));
631             }
632              
633 0           return $response;
634             }
635              
636              
637             sub generate_response {
638 0     0 1   my $self = shift;
639 0           my %args = @_;
640              
641 0           my $clTRID = $args{'clTRID'};
642 0           my $svTRID = $args{'svTRID'};
643              
644 0   0       my $code = $args{'code'} || OK;
645 0   0       my $msg = $args{'msg'} || $MESSAGES{$code} || ($code < UNKNOWN_COMMAND ? $MESSAGES{OK} : $MESSAGES{COMMAND_FAILED});
646              
647 0           my $frame = XML::LibXML::Document->new;
648              
649 0           $frame->setDocumentElement($frame->createElementNS($Net::EPP::Frame::EPP_URN, 'epp'));
650 0           my $response = $frame->documentElement->appendChild($frame->createElement('response'));
651              
652 0           my $result = $response->appendChild($frame->createElement('result'));
653              
654 0           $result->setAttribute('code', $code);
655 0           $result->appendChild($frame->createElement('msg'))->appendText($msg);
656              
657 0 0         if ($args{'resData'}) {
658 0           $response->appendChild($frame->createElement('resData'));
659             }
660              
661 0 0 0       if ($clTRID || $svTRID) {
662 0           my $trID = $response->appendChild($frame->createElement('trID'));
663 0 0         $trID->appendChild($frame->createElement('clTRID'))->appendText($clTRID) if ($clTRID);
664 0 0         $trID->appendChild($frame->createElement('svTRID'))->appendText($svTRID) if ($svTRID);
665             }
666              
667 0           return $frame;
668             }
669              
670              
671             sub generate_error {
672 0     0 1   my ($self, %args) = @_;
673 0   0       $args{'code'} ||= COMMAND_FAILED;
674 0   0       $args{'msg'} = $args{'msg'} || $MESSAGES{$args{'code'}} || 'An internal error occurred. Please try again later.';
675 0           return $self->generate_response(%args);
676             }
677              
678              
679             sub generate_svTRID {
680 0     0 1   state $counter = time();
681              
682 0           return substr(sha512_hex(
683             pack('Q', ++$counter)
684             .chr(0)
685             .Crypt::OpenSSL::Random::random_pseudo_bytes(32)
686             ), 0, 64);
687             }
688              
689              
690             sub parse_frame {
691 0     0 1   my ($self, $xml) = @_;
692              
693 0           return XML::LibXML->load_xml(
694             string => $xml,
695             no_blanks => 1,
696             no_cdata => 1,
697             );
698             }
699              
700              
701             sub validate_frame {
702 0     0 0   my ($self, $frame) = @_;
703              
704 0 0         if ($self->{'epp'}->{'xsd_file'}) {
705 0           state $xsd = XML::LibXML::Schema->new(location => $self->{'epp'}->{'xsd_file'});
706              
707 0           eval { $xsd->validate($frame) };
  0            
708              
709 0 0         return (SYNTAX_ERROR, $@) if ($@);
710             }
711              
712 0           return OK;
713             }
714              
715             #
716             # This method finds the callback for the given event, and if found, runs it and
717             # passes back its return value(s).
718             #
719             sub run_callback {
720 0     0 0   my $self = shift;
721 0           my %args = @_;
722              
723 0   0       $args{'server'} ||= $self;
724              
725 0           my $ref = $self->{'epp'}->{'handlers'}->{$args{'event'}};
726              
727 0 0         return &{$ref}(%args) if ($ref);
  0            
728             }
729              
730              
731             sub is_result_code {
732 0     0 1   my $value = shift;
733 0   0       return (int($value) >= OK && int($value) <= 2502);
734             }
735              
736             #
737             # This method is a wrapper around Net::EPP::Protocol->send_frame() which
738             # validates the response and reports any errors
739             #
740             sub send_frame {
741 0     0 0   my ($self, $socket, $frame) = @_;
742              
743             #
744             # note: we need to do a round-trip here otherwise we get namespace issues
745             #
746 0           $frame = XML::LibXML->load_xml(string => $frame->toString);
747              
748 0           my ($code, $msg) = $self->validate_frame($frame);
749 0 0         if (OK != $code) {
750 0           carp($msg);
751             }
752              
753 0           Net::EPP::Protocol->send_frame($socket, $frame->toString);
754             }
755              
756             1;
757              
758             __END__
759              
760             =pod
761              
762             =encoding UTF-8
763              
764             =head1 NAME
765              
766             Net::EPP::Server - A simple EPP server implementation.
767              
768             =head1 VERSION
769              
770             version 0.10
771              
772             =head1 SYNOPSIS
773              
774             use Net::EPP::Server;
775             use Net::EPP::ResponseCodes;
776              
777             #
778             # these are the objects we want to support
779             #
780             my @OBJECTS = qw(domain host contact);
781              
782             #
783             # these are the extensions we want to support
784             #
785             my @EXTENSIONS = qw(secDNS rgp loginSec allocationToken launch);
786              
787             #
788             # You can pass any arguments supported by Net::Server::Proto::SSL, but
789             # by default the server will listen on localhost port 7000 using a
790             # self-signed certificate.
791             #
792             Net::EPP::Server->new->run(
793              
794             #
795             # this defines callbacks that will be invoked when an EPP frame is
796             # received
797             #
798             handlers => {
799             hello => \&hello_handler,
800             login => \&login_handler,
801             check => \&check_handler,
802             info => \&info_handler,
803             create => \&create_handler,
804              
805             # add more here
806             }
807             );
808              
809             #
810             # The <hello> handler is special and just needs
811             # to return a hashref containing server metadata.
812             #
813             sub hello_handler {
814             return {
815             # this is the server ID and is optional, if not provided the system
816             # hostname will be used
817             svID => 'epp.example.com',
818              
819             # this is optional
820             lang => [ qw(en fr de) ],
821              
822             # these are arrayrefs of namespace URIs
823             objects => [
824             map { Net::EPP::Frame::ObjectSpec->xmlns($_) } @OBJECTS
825             ],
826              
827             extensions => [
828             map { Net::EPP::Frame::ObjectSpec->xmlns($_) } @EXTENSIONS
829             ],
830             };
831             }
832              
833             #
834             # All other handlers work the same. They are passed a hash of arguments and
835             # can return a simple result code, a result code and message, a
836             # XML::LibXML::Document object, or a result code and an array of
837             # XML::LibXML::Element objects.
838             #
839             sub login_handler {
840             my %args = @_;
841              
842             my $frame = $args{'frame'};
843              
844             my $clid = $frame->getElementsByTagName('clid')->item(0)->textContent;
845             my $pw = $frame->getElementsByTagName('pw')->item(0)->textContent;
846              
847             if (!validate_credentials($clid, $pw)) {
848             return AUTHENTICATION_FAILED;
849              
850             } else {
851             return OK;
852              
853             }
854             }
855              
856             =head1 INTRODUCTION
857              
858             C<Net::EPP::Server> provides a high-level framework for developing L<Extensible
859             Provisioning Protocol (EPP)|https://www.rfc-editor.org/info/std69> servers.
860              
861             It implements the TLS/TCP transport described in L<RFC 5734|https://www.rfc-editor.org/info/rfc5734>,
862             and the L<EPP Server State Machine|https://www.rfc-editor.org/rfc/rfc5730.html#:~:text=Figure%201:%20EPP%20Server%20State%20Machine>
863             described in L<Section 2 of RFC 5730|https://www.rfc-editor.org/rfc/rfc5730.html#section-2>.
864              
865             =head1 SERVER CONFIGURATION
866              
867             C<Net::EPP::Server> inherits from L<Net::Server> I<(specifically
868             L<Net::Server::PreFork>)>, and so the C<run()> method accepts all the parameters
869             supported by that module, plus the following:
870              
871             =over
872              
873             =item * C<handlers>, which is a hashref which maps events (including EPP
874             commands) to callback functions. See below for details.
875              
876             =item * C<timeout> (optional), which is how long (in seconds) to wait for a
877             client to send a command before dropping the connection. This parameter may be a
878             decimal (e.g. C<3.14>) or an integer (e.g. C<42>). The default timeout is 30
879             seconds.
880              
881             =item * C<client_ca_file> (optional), which is the location on disk of a file
882             which can be use to validate client certificates. If this parameter is not
883             provided, clients will not be required to use a certificate.
884              
885             =item * C<xsd_file> (optional), which is the location on disk of an XSD file
886             which should be used to validate all frames received from clients. This XSD
887             file can include other XSD files using C<E<lt>importE<gt>>.
888              
889             item * C<log_dir> (optional), which is the location on disk where log files
890             will be written.
891              
892             =back
893              
894             =head1 EVENT HANDLERS
895              
896             You implement the business logic of your EPP server by specifying callbacks that
897             are invoked for certain events. These come in two flavours: I<events> and
898             I<commands>.
899              
900             All event handlers receive a hash containing one or more arguments that are
901             described below.
902              
903             =head2 C<frame_received>
904              
905             Called when a frame has been successfully parsed and validated, but before it
906             has been processed. The input frame will be passed as the C<frame> argument.
907              
908             =head2 C<response_prepared>
909              
910             Called when a response has been generated, but before it has been sent back to
911             the client. The response will be passed as the C<response> argument, while the
912             input frame will be passed as the C<frame> argument. It is B<not> called for
913             C<E<lt>helloE<gt>> and C<E<lt>logoutE<gt>>commands.
914              
915             =head2 C<session_closed>
916              
917             C<Net::EPP::Server> takes care of handling session management, but this event
918             handler will be called once a C<E<lt>logoutE<gt>> command has been successfully
919             processed, and before the client connection has been closed. The C<session>
920             argument will contain a hashref of the session (see below).
921              
922             =head2 C<hello>
923              
924             The C<hello> event handler is called when a new client connects, or a
925             C<E<lt>helloE<gt>> frame is received.
926              
927             Unlike the other event handlers, this handler B<MUST> respond with a hashref
928             which contains the following entries:
929              
930             =over
931              
932             =item * C<svID> (OPTIONAL) - the server ID. If not provided, the system hostname
933             will be used.
934              
935             =item * C<lang> (OPTIONAL) - an arrayref containing language codes. It not
936             provided, C<en> will be used as the only supported language.
937              
938             =item * C<objects> (REQUIRED) - an arrayref of namespace URIs for
939              
940             =back
941              
942             =head2 COMMAND HANDLERS
943              
944             The standard EPP command repertoire is:
945              
946             =over
947              
948             =item * C<login>
949              
950             =item * C<logout>
951              
952             =item * C<poll>
953              
954             =item * C<check>
955              
956             =item * C<info>
957              
958             =item * C<create>
959              
960             =item * C<delete>
961              
962             =item * C<renew>
963              
964             =item * C<transfer>
965              
966             =item * C<delete>
967              
968             =back
969              
970             A command handler may be specified for all of these commands except C<logout>,
971             since C<Net::EPP::Server> handles this itself.
972              
973             Since EPP allows the command repertoire to be extended (by omitting the
974             C<E<lt>commandE<gt>> element and using the C<E<lt>extensionE<gt>> element only),
975             C<Net::EPP::Server> also supports the C<other> event which will be called when
976             processing such frames.
977              
978             All command handlers receive a hash containing the following arguments:
979              
980             =over
981              
982             =item * C<server> - the server.
983              
984             =item * C<event> - the name of the command.
985              
986             =item * C<frame> - an L<XML::LibXML::Document> object representing the frame
987             received from the client.
988              
989             =item * C<session> - a hashref containing the session information.
990              
991             =item * C<clTRID> - the value of the C<E<lt>clTRIDE<gt>> element taken from the
992             frame received from the client.
993              
994             =item * C<svTRID> - a value suitable for inclusion in the C<E<lt>clTRIDE<gt>>
995             element of the response.
996              
997             =back
998              
999             =head3 SESSION PARAMETERS
1000              
1001             As mentioned above, the C<session> parameter is a hashref which contains
1002             information about the session. It contains the following values:
1003              
1004             =over
1005              
1006             =item * C<session_id> - a unique session ID.
1007              
1008             =item * C<remote_addr> - the client's remote IP address (IPv4 or IPv6).
1009              
1010             =item * C<remote_port> - the client's remote port.
1011              
1012             =item * C<clid> - the client ID used to log in.
1013              
1014             =item * C<lang> - the language specified at login.
1015              
1016             =item * C<objects> - an arrayref of the object URI(s) specified at login.
1017              
1018             =item * C<extensions> - an arrayref of the extension URI(s) specified at login.
1019              
1020             =item * C<client_cert> - a hashref containing information about the client
1021             certificate (if any), which looks something like this:
1022              
1023             {
1024             'issuer' => $dnref,
1025             'common_name' => 'example.com',
1026             'subject' => $dnref,
1027             }
1028              
1029             C<$dnref> is a hashref representing the Distinguished Name of the issuer or
1030             subject and looks like this:
1031              
1032             {
1033             'O' => 'Example Inc.',
1034             'OU' => 'Registry Services',
1035             'emailAddress' => 'registry@example.com',
1036             'CN' => 'EPP Server Private CA',
1037             }
1038              
1039             Other members, such as C<C> (country), C<ST> (state/province), and C<L> (city)
1040             may also be present.
1041              
1042             =back
1043              
1044             =head3 RETURN VALUES
1045              
1046             Command handlers can return result information in four different ways that are
1047             explained below.
1048              
1049             =head4 1. SIMPLE RESULT CODE
1050              
1051             Command handlers can signal the result of a command by simply passing a single
1052             integer value. L<Net::EPP::ResponseCodes> may be used to avoid literal integers.
1053              
1054             Example:
1055              
1056             sub delete_handler {
1057             my %args = @_;
1058              
1059             # business logic here
1060              
1061             if ($success) {
1062             return OK;
1063              
1064             } else {
1065             return COMMAND_FAILED;
1066              
1067             }
1068             }
1069              
1070             C<Net::EPP::Server> will construct a standard EPP response frame using the
1071             result code and send it to the client.
1072              
1073             =head4 2. RESULT CODE + MESSAGE
1074              
1075             If the command handler returns two values, and the first is a valid result code,
1076             then the second can be a message. Example:
1077              
1078             sub delete_handler {
1079             my %args = @_;
1080              
1081             # business logic here
1082              
1083             if ($success) {
1084             return (OK, 'object deleted');
1085              
1086             } else {
1087             return (COMMAND_FAILED, 'object not deleted');
1088              
1089             }
1090             }
1091              
1092             C<Net::EPP::Server> will construct a standard EPP response frame using the
1093             result code and message, and send it to the client.
1094              
1095             =head4 3. RESULT CODE + XML ELEMENTS
1096              
1097             The command handler may return a result code followed by an array of between
1098             one and three L<XML::LibXML::Element> objects, in any order, representing the
1099             C<E<lt>resDataE<gt>>, C<E<lt>msgQE<gt>> and C<E<lt>extensionE<gt>> elements.
1100             Example:
1101              
1102             sub delete_handler {
1103             my %args = @_;
1104              
1105             # business logic here
1106              
1107             return (
1108             OK,
1109             $resData_element,
1110             $msgQ_element,
1111             $extension_element,
1112             );
1113             }
1114              
1115             C<Net::EPP::Server> will construct a standard EPP response frame using the
1116             result code and supplied elements which will be imported and inserted into the
1117             appropriate positions, and send it to the client.
1118              
1119             =head4 4. L<XML::LibXML::Document> OBJECT
1120              
1121             A return value that is a single L<XML::LibXML::Document> object will be sent
1122             back to the client verbatim.
1123              
1124             =head3 EXCEPTIONS
1125              
1126             C<Net::EPP::Server> will catch any exceptions thrown by the command handler,
1127             will C<carp($@)>, and then send a C<2400> result code back to the client.
1128              
1129             =head1 UTILITY METHODS
1130              
1131             =head2 C<generate_response(%args)>
1132              
1133             This method returns a L<XML::LibXML::Document> object representing the response
1134             described by C<%args>, which should contain the following:
1135              
1136             =over
1137              
1138             =item * C<code> (OPTIONAL) - the result code. See L<Net::EPP::ResponseCodes>.
1139             If not provided, C<1000> will be used.
1140              
1141             =item * C<msg> - a human-readable error message. If not provided, the string
1142             C<"Command completed successfully."> will be used if C<code> is less than
1143             C<2000>, and C<"Command failed."> if C<code> is C<2000> or higher.
1144              
1145             =item * C<resData> (OPTIONAL) - if defined, an empty C<E<lt>resDataE<gt>>
1146             element will be added to the frame.
1147              
1148             =item * C<clTRID> (OPTIONAL) - the client transaction ID.
1149              
1150             =item * C<svTRID> (OPTIONAL) - the server's transaction ID.
1151              
1152             =back
1153              
1154             Once created, it is straightforward to modify the object to add, remove or
1155             change its contents as needed.
1156              
1157             =head2 C<generate_error(%args)>
1158              
1159             This method is identical to C<generate_response()> except the default value
1160             for the C<code> parameter is C<2400>, indicating that the command failed for
1161             unspecified reasons.
1162              
1163             =head2 C<generate_svTRID()>
1164              
1165             This method returns a unique string suitable for use in the C<E<lt>svTRIDE<gt>>
1166             and similar elements.
1167              
1168             =head2 C<parse_frame($xml)>
1169              
1170             Attempts to parse C<$xml> and returns a L<XML::LibXML::Document> if successful.
1171              
1172             =head2 C<is_valid($frame)>
1173              
1174             Returns a result code and optionally a message if C<$frame> cannot be validated
1175             against the XSD file provided in the C<xsd_file> parameter.
1176              
1177             =head2 C<is_result_code($value)>
1178              
1179             Returns true if C<$value> is a recognised EPP result code.
1180              
1181             =head1 AUTHOR
1182              
1183             Gavin Brown <gavin.brown@icann.org>
1184              
1185             =head1 COPYRIGHT AND LICENSE
1186              
1187             This software is copyright (c) 2025 by Internet Corporation for Assigned Names and Number (ICANN).
1188              
1189             This is free software; you can redistribute it and/or modify it under
1190             the same terms as the Perl 5 programming language system itself.
1191              
1192             =cut