File Coverage

blib/lib/SRS/EPP/Proxy.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             #
2             # Copyright (C) 2009, 2010 NZ Registry Services
3             #
4             # This program is free software: you can redistribute it and/or modify
5             # it under the terms of the Artistic License 2.0 or later. You should
6             # have received a copy of the Artistic License the file COPYING.txt.
7             # If not, see <http://www.perlfoundation.org/artistic_license_2_0>
8              
9             package SRS::EPP::Proxy;
10              
11 1     1   3595 use MooseX::Singleton;
  1         35226  
  1         7  
12 1     1   35517 use MooseX::Params::Validate;
  1         3  
  1         8  
13 1     1   610 use SRS::EPP::Session;
  0            
  0            
14             use Event;
15             use Log::Log4perl qw(:easy);
16             use POSIX ":sys_wait_h";
17              
18             with 'SRS::EPP::Proxy::SimpleConfig';
19             with 'MooseX::Getopt';
20             with 'MooseX::Log::Log4perl::Easy';
21             with 'MooseX::Daemonize';
22              
23             has '+configfile' => (
24             default => sub {
25             [
26             "$ENV{HOME}/.srs_epp_proxy.yaml",
27             '/etc/srs-epp-proxy.yaml'
28             ];
29             }
30             );
31              
32             sub BUILD {
33             my $self = shift;
34              
35             # should have already done SimpleConfig; with a bit of luck,
36             # all properties in this master object may be specified there.
37              
38             # pass configuration via this method to log4perl
39             my $logging = $self->logging;
40              
41             if ( !defined $logging ) {
42             $logging = "INFO";
43             }
44              
45             if ( !ref $logging and !-f $logging ) {
46              
47             # 'default'
48             if ( $self->is_daemon ) {
49             $logging = {
50             rootLogger => "$logging, Syslog",
51             "appender.Syslog" => "nz.net.nzrs.SyslogAppender",
52             "appender.Syslog.logopt" => "pid",
53             "appender.Syslog.Facility" => "daemon",
54             "appender.Syslog.layout" =>
55             "Log::Log4perl::Layout::SimpleLayout",
56             };
57             }
58             else {
59             $logging = {
60             rootLogger => "$logging, Screen",
61             "appender.Screen" => "Log::Log4perl::Appender::Screen",
62             "appender.Screen.stderr" => 1,
63             "appender.Screen.layout" =>
64             "Log::Log4perl::Layout::SimpleLayout",
65             };
66             }
67             }
68              
69             # prepend "log4perl." to config hashes
70             if ( ref $logging and ref $logging eq "HASH" ) {
71             for my $key ( keys %$logging ) {
72             if (
73             $key !~ /^log4perl\./
74             and
75             !exists $logging->{"log4perl.$key"}
76             )
77             {
78             $logging->{"log4perl.$key"} =
79             delete $logging->{$key};
80             }
81             }
82             }
83            
84             $Log::Log4perl::JavaMap::user_defined{'nz.net.nzrs.SyslogAppender'} = 'SRS::EPP::Logging::SyslogAppender';
85              
86             Log::Log4perl->init($logging);
87              
88             # pass configuration options to the session class?
89             }
90              
91             our $VERSION = "0.21";
92              
93             has 'logging' =>
94             is => "ro",
95             isa => "HashRef[Str]",
96             ;
97              
98             has 'listen' =>
99             is => "ro",
100             isa => "ArrayRef[Str]",
101             metaclass => "Getopt",
102             ;
103              
104             has 'listener' =>
105             is => "rw",
106             isa => "SRS::EPP::Proxy::Listener",
107             default => sub {
108             require SRS::EPP::Proxy::Listener;
109             my $self = shift;
110             SRS::EPP::Proxy::Listener->new(
111             ($self->listen ? (listen => $self->listen) : () ),
112             );
113             },
114             lazy => 1,
115             handles => {
116             'init_listener' => 'init',
117             },
118             ;
119              
120             has 'ssl_key_file' =>
121             metaclass => "Getopt",
122             is => "ro",
123             isa => "Str",
124             required => 1,
125             ;
126              
127             has 'ssl_cert_file' =>
128             metaclass => "Getopt",
129             is => "ro",
130             isa => "Str",
131             required => 1,
132             ;
133              
134             has 'ssl_cert_dir' =>
135             is => "ro",
136             isa => "Str",
137             default => "",
138             ;
139              
140             use Sys::Hostname qw(hostname);
141             has 'server_name' =>
142             is => "ro",
143             isa => "Str",
144             lazy => 1,
145             default => sub {
146             my $self = shift;
147             my @listen = @{ $self->listen };
148             if ( @listen == 1 and $listen[0] !~ /^(?:\d+\.|\[)/ ) {
149              
150             # listen address seems a reasonable default...
151             $listen[0];
152             }
153             else {
154             hostname;
155             }
156             };
157              
158             has 'ssl_engine' =>
159             is => "rw",
160             isa => "Net::SSLeay::OO::Context",
161             ;
162              
163             has 'rfc_compliant_ssl' =>
164             is => "rw",
165             traits => [qw[Getopt]],
166             isa => "Bool",
167             ;
168              
169             use Net::SSLeay::OO;
170             use Net::SSLeay::OO::Error qw(die_if_ssl_error);
171             use Net::SSLeay::OO::Constants
172             qw(MODE_ENABLE_PARTIAL_WRITE MODE_ACCEPT_MOVING_WRITE_BUFFER
173             OP_ALL OP_NO_SSLv2 VERIFY_PEER VERIFY_FAIL_IF_NO_PEER_CERT
174             FILETYPE_PEM);
175              
176             sub init_ssl {
177             my ($self) = @_;
178              
179             my $ctx = Net::SSLeay::OO::Context->new(
180             use_default_verify_paths => 0,
181             );
182             $ctx->set_options(&OP_ALL | OP_NO_SSLv2);
183             my $options = VERIFY_PEER;
184             if ( $self->rfc_compliant_ssl) {
185             $self->log_info(
186             "Strict RFC5734-compliant SSL enabled (client certificates required)"
187             );
188             $options |= VERIFY_FAIL_IF_NO_PEER_CERT;
189             }
190             $ctx->set_verify($options);
191             $self->log_info("SSL Certificates from ".$self->ssl_cert_dir);
192             $ctx->load_verify_locations("", $self->ssl_cert_dir);
193             $self->log_info(
194             "SSL private key: ".$self->ssl_key_file
195             .", public certificate chain: ".$self->ssl_cert_file
196             );
197             $ctx->use_PrivateKey_file($self->ssl_key_file, FILETYPE_PEM);
198             $ctx->use_certificate_chain_file($self->ssl_cert_file);
199             die_if_ssl_error; # one last check...
200             $self->ssl_engine($ctx);
201             }
202              
203             sub init {
204             my ($self) = @_;
205              
206             $self->log_info("Initializing PGP");
207             $self->init_pgp;
208             $self->log_info("Initializing SSL");
209             $self->init_ssl;
210             $self->log_info("Initializing URIs");
211             $self->init_uris;
212             $self->log_info("Initializing Listener");
213             $self->init_listener;
214             }
215              
216             has 'openpgp' =>
217             is => "ro",
218             isa => "SRS::EPP::OpenPGP",
219             lazy => 1,
220             default => sub {
221             my $self = shift;
222             require SRS::EPP::OpenPGP;
223             my $pgp_dir = $self->pgp_dir;
224             my $secring_file = "$pgp_dir/secring.gpg";
225             my $pubring_file = "$pgp_dir/pubring.gpg";
226             my $pgp = SRS::EPP::OpenPGP->new(
227             public_keyring => $pubring_file,
228             secret_keyring => $secring_file,
229             );
230             $pgp->uid($self->pgp_keyid) if $self->pgp_keyid;
231             my $key = $pgp->default_signing_key;
232             $pgp;
233             },
234             handles => ["pgp"],
235             ;
236              
237             has 'pgp_keyid' =>
238             metaclass => "Getopt",
239             is => "ro",
240             isa => "Str",
241             ;
242              
243             has 'pgp_dir' =>
244             is => "ro",
245             isa => "Str",
246             default => sub {
247             $ENV{GNUPGHOME} || "$ENV{HOME}/.gnupg";
248             },
249             ;
250              
251             sub init_pgp {
252             my ($self) = @_;
253              
254             $self->pgp;
255             }
256              
257             has 'extensions' =>
258             metaclass => "Getopt",
259             is => "ro",
260             isa => "HashRef",
261             required => 0,
262             ;
263            
264             has 'services' =>
265             metaclass => "Getopt",
266             is => "ro",
267             isa => "ArrayRef",
268             required => 1,
269             ;
270              
271             sub init_uris {
272             my ($self) = @_;
273              
274             # Register namespaces to be returned by greeting
275             use XML::EPP;
276             XML::EPP::register_obj_uri(
277             @{ $self->services },
278             );
279            
280             if ($self->extensions) {
281             XML::EPP::register_ext_uri(
282             %{ $self->extensions },
283             );
284             }
285             }
286              
287             has 'running' =>
288             is => "rw",
289             isa => "Bool",
290             default => 1,
291             ;
292              
293             has 'child_pids' =>
294             is => "ro",
295             isa => "ArrayRef[Int]",
296             default => sub { [] },
297             ;
298              
299             has 'backend' =>
300             is => "ro",
301             isa => "Str",
302             default => "https://srstest.srs.net.nz/srs/registrar",
303             ;
304              
305             has 'timeout' =>
306             is => "ro",
307             isa => "Int",
308             default => 300,
309             ;
310            
311             has 'connection_count' =>
312             is => 'rw',
313             isa => 'Num',
314             default => 0,
315             traits => ['Counter'],
316             handles => {
317             inc_connection_count => 'inc',
318             dec_connection_count => 'dec',
319             reset_connection_count => 'reset',
320             },
321             ;
322            
323             has 'max_connections' =>
324             metaclass => "Getopt",
325             is => "ro",
326             isa => "Int",
327             default => 50,
328             ;
329              
330             sub accept_one {
331             my ($self) = @_;
332              
333             $self->log_trace("accepting connections");
334             my $socket = $self->listener->accept
335             or return;
336            
337             if ($self->connection_count >= $self->max_connections) {
338             $self->log_info("At threshold of " . $self->connection_count . " simultaneous connections. " .
339             "Not responding to connection attempt from " . $socket->peerhost);
340             $socket->print("Server Busy");
341             $socket->close();
342             return;
343             }
344            
345             $self->inc_connection_count;
346             $self->log_debug("Connection begun. Connection count now at: " . $self->connection_count);
347              
348             if ( !$self->foreground and (my $pid = fork) ) {
349             push @{ $self->child_pids }, $pid;
350             $self->log_debug("forked $pid for connection");
351             return ();
352             }
353             else {
354              
355             # We'll also want to know the address of the other end
356             # of the socket, for checking it against the back-end
357             # ACL
358             my $peerhost = $socket->peerhost;
359             $self->log_info("connection from $peerhost, starting SSL");
360             $0 = "srs-epp-proxy [$peerhost] - SSL init";
361            
362             my $ssl;
363             eval {
364             # Don't use catch_signal(), as we need to handle it right away
365             local $SIG{ALRM} = sub {
366             die "Timed out waiting for a SSL handshake to complete\n";
367             };
368              
369             alarm $self->timeout;
370             $ssl = $self->ssl_engine->accept($socket);
371             alarm 0;
372             };
373              
374             my $error = $@;
375             if ($error) {
376              
377             # We got an SSL error - send it back to the client, and close the connection
378             $socket->print($error);
379             $socket->close();
380             die $error;
381             }
382              
383             $0 = "srs-epp-proxy [$peerhost] - setup";
384              
385             # RFC3734 and updates specify the use of client
386             # certificates. So, fetch it and get its subject.
387             my $client_cert = $ssl->get_peer_certificate;
388             my $peer_cn;
389             if ($client_cert) {
390              
391             # should use subjectAltName if present..
392             $peer_cn = $client_cert->get_subject_name->cn;
393             $self->log_info("have a valid peer certificate, cn=$peer_cn");
394             }
395             else {
396             $self->log_info("no peer certificate presented");
397             }
398              
399             # set the socket to non-blocking for event-driven fun.
400             my $mode = (
401             MODE_ENABLE_PARTIAL_WRITE |
402             MODE_ACCEPT_MOVING_WRITE_BUFFER
403             );
404             $ssl->set_mode($mode);
405             $socket->blocking(0);
406              
407             # create a new session...
408             my $session = SRS::EPP::Session->new(
409             io => $ssl,
410             proxy => $self,
411             socket => $socket,
412             ($self->timeout ? (timeout => $self->timeout) : ()),
413             backend_url => $self->backend,
414             event => "Event",
415             peerhost => $peerhost,
416             ($self->rfc_compliant_ssl ? (peer_cn => lc $peer_cn) : ()),
417             );
418              
419             # let it know it's connected.
420             $session->connected;
421              
422             return $session;
423             }
424             }
425              
426             sub show_state {
427             my $self = shift;
428             my ($state,$session) = pos_validated_list(
429             \@_,
430             { isa => 'Str' },
431             { isa => 'SRS::EPP::Session', optional => 1 },
432             );
433              
434             my ($regid, $peer_host_or_cn);
435             if ($session) {
436             $regid = $session->user;
437             $peer_host_or_cn = $session->peer_cn
438             || $session->peerhost;
439             }
440             $0 = "srs-epp-proxy [$peer_host_or_cn] - ".
441             ($regid?"registrar $regid - ":"").$state;
442             }
443              
444             has signals =>
445             is => "rw",
446             isa => "HashRef[Int]",
447             default => sub { {} },
448             ;
449              
450             has handlers =>
451             is => "rw",
452             isa => "HashRef[CodeRef]",
453             default => sub { {} },
454             ;
455              
456             sub signal_handler {
457             my ($self,$signal) = @_;
458              
459             $self->log_debug("caught SIG$signal");
460             $self->signals->{$signal}++;
461             }
462              
463             sub process_signals {
464             my ($self) = @_;
465              
466             my $sig_h = $self->signals;
467             while (my ($signal,$handler) = each %{ $self->handlers }) {
468             if ($sig_h->{$signal}) {
469             $sig_h->{$signal} = 0;
470             $self->log_debug("processing SIG$signal");
471             $handler->();
472             }
473             }
474             }
475              
476             sub catch_signal {
477             my $self = shift;
478             my ($sig,$sub) = pos_validated_list(
479             \@_,
480             { isa => 'Str' },
481             { isa => 'CodeRef' }
482             );
483              
484             $self->handlers->{$sig} = $sub;
485             $SIG{$sig} = sub { $self->signal_handler($sig) };
486             }
487              
488             sub accept_loop {
489             my ($self) = @_;
490              
491             # Setup sig handle for INT/TERM
492             # (note, shutting down via MooseX::Daemonize normally uses INT)
493             my $sig_handler = sub {
494             $self->log_info("Shutting down.");
495             for my $kid ( @{ $self->child_pids } ) {
496             kill "TERM", $kid;
497             }
498             $self->running(0);
499             };
500            
501             for my $sig (qw(INT TERM)) {
502             $self->catch_signal(
503             $sig => $sig_handler,
504             );
505             }
506            
507             if ( !$self->foreground ) {
508             $self->catch_signal(CHLD => sub {
509             $self->reap_children;
510             });
511             }
512             $0 = "srs-epp-proxy - listener";
513             while ( $self->running ) {
514             my $session = eval {
515             $self->accept_one;
516             };
517             my $error = $@;
518             if ($error) {
519             $self->log_error("Got error while accepting connection: $error");
520             exit unless $self->foreground;
521             }
522            
523             if ($session) {
524             unless ( $self->foreground ) {
525             $self->catch_signal(
526             TERM => sub {
527             $session->shutdown;
528             }
529             );
530             }
531             $self->log_trace("accepted a new session, entering event loop");
532              
533             # if an untrapped error occurs, we have to just bail out.
534             # events need to trap errors themselves to avoid this.
535             local($Event::DIED) = sub {
536             my $event = shift;
537             my $exception = shift;
538             my $estr = "Unhandled exception during ".$event->w->desc."; $exception";
539             if ( open(my $out,">","/tmp/proxy-crashlog") ) {
540             # Just in case the logging isn't working....
541             print $out "$estr\n";
542             }
543             $self->log_error($estr);
544             Event::unloop_all;
545             };
546             Event::loop();
547             $self->log_info("Session ends");
548             exit unless $self->foreground;
549             }
550             else {
551             $self->log_trace("no new session, processing signals");
552             $self->process_signals;
553             }
554             }
555             }
556              
557             sub reap_children {
558             my ($self) = @_;
559             my $kid;
560             my %reaped;
561             do {
562             $kid = waitpid(-1, WNOHANG);
563             if ($kid > 0) {
564             $reaped{$kid} = $?;
565             $self->log_info(
566             "child $kid, ".(
567             $?&255
568             ?" killed by signal "
569             .($?&127)
570             .($?&128?" (core dumped)":"")
571             :"exited with error code ".($?>>8)
572             )
573             );
574              
575             $self->dec_connection_count;
576             $self->log_debug("Connection ended. Connection count now at: " . $self->connection_count);
577            
578             }
579             } while ($kid > 0);
580             my $child_pids = $self->child_pids;
581             @$child_pids = grep { exists $reaped{$_} } @$child_pids;
582             }
583              
584             {
585             no warnings 'redefine';
586             my $daemonize = \&daemonize;
587             *daemonize = sub {
588             my $self = shift;
589             my %args = @_;
590             $args{dont_close_all_files} = 1;
591             $SIG{__DIE__} = sub {
592              
593             # be sure to re-throw exceptions whilst inside
594             # eval { }
595             if ($^S) {
596             die @_;
597             }
598             else {
599             $self->log_error("Uncaught exception, exiting: @_");
600             $self->log_error("stack trace: ".Carp::longmess);
601             exit(1);
602             }
603             };
604             my $no_recurse;
605             $SIG{__WARN__} = sub {
606             return if $no_recurse;
607             $no_recurse = 1;
608             eval { $self->log_warn("caught warning: @_") };
609             $no_recurse = 0;
610             };
611             $daemonize->($self, %args);
612             };
613             }
614              
615             before 'start' => sub {
616             my $self = shift;
617             $self->init;
618             };
619              
620             after 'start' => sub {
621             my $self = shift;
622             if ($self->is_daemon) {
623             $self->log_info("Dropping Privileges");
624             $self->drop_privs;
625             $self->accept_loop;
626             }
627             };
628              
629             has 'user' =>
630             is => "ro",
631             isa => "Str",
632             default => "nobody",
633             ;
634              
635             sub drop_privs {
636             my ($self) = @_;
637              
638             if ( $< and $> ) {
639             $self->log_info("Not dropping privilegs, already UID $<");
640             }
641             my $user = $self->user;
642             my ($uid, $gid) = (getpwnam $user)[2,3] or do {
643             $self->log_error("cannot drop privileges; no such user '$user'");
644             };
645             my $group = getgrgid($gid) || $gid;
646             $self->log_debug("Setting UID:GID to $user:$group");
647              
648             $( = $) = $gid;
649             $< = $> = $uid;
650             }
651              
652             1;
653              
654             __END__
655              
656             =head1 NAME
657              
658             SRS::EPP::Proxy - IETF EPP <=> SRS XML proxy software
659              
660             =head1 SYNOPSIS
661              
662             my $proxy = SRS::EPP::Proxy->new(
663              
664             # where to listen for inbound connections
665             listen => [ "$addr:$port", "[$addr6]:$port" ],
666              
667             # SSL engine: certificate for presentation
668             ssl_key_file => $ssl_key_filename,
669             ssl_cert_file => $ssl_key_filename,
670              
671             # path for verifying client certificates
672             ssl_cert_dir => $ssl_cert_path,
673             # and of course, revocations
674             ssl_crl_file => $ssl_crl_file,
675              
676             # PGP home for dealing with the SRS
677             pgp_dir => $path,
678              
679             );
680              
681             # initialises everything - listens on sockets, checks SSL
682             # keys and PGP home dir valid
683             $proxy->init();
684              
685             # main entry mechanism
686             $proxy->accept_loop();
687              
688             # alternate piecemeal interfaces, mostly for testing
689             $proxy->init_listener;
690             $proxy->init_ssl;
691             $proxy->init_pgp;
692             my $session = $proxy->accept_one; # doesn't fork
693              
694             =head1 DESCRIPTION
695              
696             SRS::EPP::Proxy implements an XML to XML gateway between two
697             contemporary protocols for domain name management; EPP as defined by
698             RFC 3730 and later, and the SRS protocol used by the .nz registry.
699              
700             This module implements the SSL listener; it accepts connections, forks
701             a new child for each one, collects client certificate information
702             about the SSL connection as recommended by RFC 3734, and then starts
703             an Event loop (using L<Event>) and passes control to the
704             L<SRS::EPP::Session> module.
705              
706             Other modules of interest; ie the key modules in this stack are:
707              
708             =over
709              
710             =item L<Moose>
711              
712             Almost every module on this list is written using L<Moose>.
713              
714             =item L<SRS::EPP::Session>
715              
716             Implements the session logic which manages connections, and "oversees"
717             the general flow of converting incoming messages to messages which are
718             sent to the back-end. Has slave classes for managing the various
719             queues which can build up.
720              
721             =item L<Net::SSLeay::OO>
722              
723             This module provides the interface to the OpenSSL library that this
724             stack uses, and in particular is used by SRS::EPP::Session to gather
725             information about the client certificate.
726              
727             =item L<XML::Relax::Generate>
728              
729             Relax NG to Moose class component. The classes this module generates
730             are used as basis for below classes.
731              
732             =item L<XML::Relax::Marshall>
733              
734             XML to and from Moose data structure component. This module can
735             create data structures which match the class structure made by
736             L<XML::Relax::Generate>
737              
738             =item L<SRS::EPP::Message::*>
739              
740             (based on) XML::Relax::Generate conversions of the various XML Schema
741             files in RFCs 3730 - 3733 (actually their later updates, RFC 4930 and
742             above) to Moose classes. These are marshalled to and from XML using
743             XML::Relax::Marshall, above.
744              
745             =item L<SRS::Message::*>
746              
747             These classes are similar conversions, but for the SRS protocol
748             messages. These are based on a conversion of the Relax schema which
749             is used to generate the RFC (not yet assigned an IETF number).
750              
751             =item L<Crypt::OpenPGP>
752              
753             An oldie but a goodie, this module is a nice pure perl implementation
754             of PGP, which is used to sign requests and verify responses to and
755             from the SRS back-end system.
756              
757             =back
758              
759             =cut
760