File Coverage

blib/lib/SRS/EPP/Proxy.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


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   1511 use MooseX::Singleton;
  0            
  0            
12             use MooseX::Method::Signatures;
13              
14             use SRS::EPP::Session;
15             use Event;
16              
17             use Log::Log4perl qw(:easy);
18              
19             use POSIX ":sys_wait_h";
20              
21             with 'SRS::EPP::Proxy::SimpleConfig';
22             with 'MooseX::Getopt';
23             with 'MooseX::Log::Log4perl::Easy';
24             with 'MooseX::Daemonize';
25              
26             has '+configfile' => (
27             default => sub { [
28             "$ENV{HOME}/.srs_epp_proxy.yaml",
29             '/etc/srs-epp-proxy.yaml'
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             # 'default'
47             if ( $self->is_daemon ) {
48             $logging = {
49             rootLogger => "$logging, Syslog",
50             "appender.Syslog" => "Log::Log4perl::JavaMap::SyslogAppender",
51             "appender.Syslog.logopt" => "pid",
52             "appender.Syslog.Facility" => "daemon",
53             "appender.Syslog.layout" =>
54             "Log::Log4perl::Layout::SimpleLayout",
55             };
56             }
57             else {
58             $logging = {
59             rootLogger => "$logging, Screen",
60             "appender.Screen" => "Log::Log4perl::Appender::Screen",
61             "appender.Screen.stderr" => 1,
62             "appender.Screen.layout" =>
63             "Log::Log4perl::Layout::SimpleLayout",
64             };
65             }
66             }
67              
68             # prepend "log4perl." to config hashes
69             if ( ref $logging and ref $logging eq "HASH" ) {
70             for my $key ( keys %$logging ) {
71             if ( $key !~ /^log4perl\./ and
72             !exists $logging->{"log4perl.$key"}
73             ) {
74             $logging->{"log4perl.$key"} =
75             delete $logging->{$key};
76             }
77             }
78             }
79              
80             Log::Log4perl->init( $logging );
81             # pass configuration options to the session class?
82             }
83              
84             our $VERSION = "0.20_01";
85              
86             has 'logging' =>
87             is => "ro",
88             isa => "HashRef[Str]",
89             ;
90              
91             has 'listen' =>
92             is => "ro",
93             isa => "ArrayRef[Str]",
94             metaclass => "Getopt",
95             ;
96              
97             has 'listener' =>
98             is => "rw",
99             isa => "SRS::EPP::Proxy::Listener",
100             default => sub {
101             require SRS::EPP::Proxy::Listener;
102             my $self = shift;
103             SRS::EPP::Proxy::Listener->new(
104             ($self->listen ? (listen => $self->listen) : () ),
105             );
106             },
107             lazy => 1,
108             handles => {
109             'init_listener' => 'init',
110             },
111             ;
112              
113             has 'ssl_key_file' =>
114             metaclass => "Getopt",
115             is => "ro",
116             isa => "Str",
117             required => 1,
118             ;
119              
120             has 'ssl_cert_file' =>
121             metaclass => "Getopt",
122             is => "ro",
123             isa => "Str",
124             required => 1,
125             ;
126              
127             has 'ssl_cert_dir' =>
128             is => "ro",
129             isa => "Str",
130             default => "",
131             ;
132              
133             use Sys::Hostname qw(hostname);
134             has 'server_name' =>
135             is => "ro",
136             isa => "Str",
137             lazy => 1,
138             default => sub {
139             my $self = shift;
140             my @listen = @{ $self->listen };
141             if ( @listen == 1 and $listen[0] !~ /^(?:\d+\.|\[)/ ) {
142             # listen address seems a reasonable default...
143             $listen[0];
144             }
145             else {
146             hostname;
147             }
148             };
149              
150             has 'ssl_engine' =>
151             is => "rw",
152             isa => "Net::SSLeay::OO::Context",
153             ;
154              
155             has 'rfc_compliant_ssl' =>
156             is => "rw",
157             traits => [qw[Getopt]],
158             isa => "Bool",
159             ;
160              
161             use Net::SSLeay::OO;
162             use Net::SSLeay::OO::Error qw(die_if_ssl_error);
163             use Net::SSLeay::OO::Constants
164             qw(MODE_ENABLE_PARTIAL_WRITE MODE_ACCEPT_MOVING_WRITE_BUFFER
165             OP_ALL OP_NO_SSLv2 VERIFY_PEER VERIFY_FAIL_IF_NO_PEER_CERT
166             FILETYPE_PEM);
167              
168             method init_ssl() {
169             my $ctx = Net::SSLeay::OO::Context->new;
170             $ctx->set_options(&OP_ALL | OP_NO_SSLv2);
171             my $options = VERIFY_PEER;
172             if ( $self->rfc_compliant_ssl) {
173             $self->log_info(
174             "Strict RFC5734-compliant SSL enabled (client certificates required)"
175             );
176             $options |= VERIFY_FAIL_IF_NO_PEER_CERT;
177             }
178             $ctx->set_verify($options);
179             $self->log_info("SSL Certificates from ".$self->ssl_cert_dir);
180             $ctx->load_verify_locations("", $self->ssl_cert_dir);
181             $self->log_info(
182             "SSL private key: ".$self->ssl_key_file
183             .", public certificate chain: ".$self->ssl_cert_file
184             );
185             $ctx->use_PrivateKey_file($self->ssl_key_file, FILETYPE_PEM);
186             $ctx->use_certificate_chain_file($self->ssl_cert_file);
187             die_if_ssl_error; # one last check...
188             $self->ssl_engine($ctx);
189             }
190              
191             method init() {
192             $self->log_info("Initializing PGP");
193             $self->init_pgp;
194             $self->log_info("Initializing SSL");
195             $self->init_ssl;
196             $self->log_info("Initializing Listener");
197             $self->init_listener;
198             }
199              
200             has 'openpgp' =>
201             is => "ro",
202             isa => "SRS::EPP::OpenPGP",
203             lazy => 1,
204             default => sub {
205             my $self = shift;
206             require SRS::EPP::OpenPGP;
207             my $pgp_dir = $self->pgp_dir;
208             my $secring_file = "$pgp_dir/secring.gpg";
209             my $pubring_file = "$pgp_dir/pubring.gpg";
210             my $pgp = SRS::EPP::OpenPGP->new(
211             public_keyring => $pubring_file,
212             secret_keyring => $secring_file,
213             );
214             $pgp->uid($self->pgp_keyid);
215             $pgp;
216             },
217             handles => ["pgp"],
218             ;
219              
220             has 'pgp_keyid' =>
221             metaclass => "Getopt",
222             is => "ro",
223             isa => "Str",
224             required => 1,
225             ;
226              
227             has 'pgp_dir' =>
228             is => "ro",
229             isa => "Str",
230             default => sub {
231             $ENV{GNUPGHOME} || "$ENV{HOME}/.gnupg";
232             },
233             ;
234              
235             method init_pgp() {
236             $self->pgp;
237             }
238              
239             has 'running' =>
240             is => "rw",
241             isa => "Bool",
242             default => 1,
243             ;
244              
245             has 'child_pids' =>
246             is => "ro",
247             isa => "ArrayRef[Int]",
248             default => sub { [] },
249             ;
250              
251             has 'backend' =>
252             is => "ro",
253             isa => "Str",
254             default => "https://srstest.srs.net.nz/srs/registrar",
255             ;
256              
257             method accept_one() {
258             $self->log_trace("accepting connections");
259             my $socket = $self->listener->accept
260             or return;
261              
262             if ( !$self->foreground and (my $pid = fork) ) {
263             push @{ $self->child_pids }, $pid;
264             $self->log_debug("forked $pid for connection");
265             return ();
266             }
267             else {
268             # We'll also want to know the address of the other end
269             # of the socket, for checking it against the back-end
270             # ACL
271             my $peerhost = $socket->peerhost;
272             $self->log_info("connection from $peerhost, starting SSL");
273             $0 = "srs-epp-proxy [$peerhost] - SSL init";
274              
275             my $ssl = $self->ssl_engine->accept($socket);
276             $0 = "srs-epp-proxy [$peerhost] - setup";
277              
278             # RFC3734 and updates specify the use of client
279             # certificates. So, fetch it and get its subject.
280             my $client_cert = $ssl->get_peer_certificate;
281             my $peer_cn;
282             if ( $client_cert ) {
283             # should use subjectAltName if present..
284             $peer_cn = $client_cert->get_subject_name->cn;
285             $self->log_info("have a valid peer certificate, cn=$peer_cn");
286             }
287             else {
288             $self->log_info("no peer certificate presented");
289             }
290              
291             # set the socket to non-blocking for event-driven fun.
292             my $mode = ( MODE_ENABLE_PARTIAL_WRITE |
293             MODE_ACCEPT_MOVING_WRITE_BUFFER );
294             $ssl->set_mode($mode);
295             $socket->blocking(0);
296              
297             # create a new session...
298             my $session = SRS::EPP::Session->new(
299             io => $ssl,
300             proxy => $self,
301             socket => $socket,
302             backend_url => $self->backend,
303             event => "Event",
304             peerhost => $peerhost,
305             ($self->rfc_compliant_ssl ? (peer_cn => $peer_cn) : ()),
306             );
307             # let it know it's connected.
308             $session->connected;
309              
310             return $session;
311             }
312             }
313              
314             method show_state( Str $state, SRS::EPP::Session $session? ) {
315             my ($regid, $peer_host_or_cn);
316             if ( $session ) {
317             $regid = $session->user;
318             $peer_host_or_cn = $session->peer_cn
319             || $session->peerhost;
320             }
321             $0 = "srs-epp-proxy [$peer_host_or_cn] - ".
322             ($regid?"registrar $regid - ":"").$state;
323             }
324              
325             has signals =>
326             is => "rw",
327             isa => "HashRef[Int]",
328             default => sub { {} },
329             ;
330              
331             has handlers =>
332             is => "rw",
333             isa => "HashRef[CodeRef]",
334             default => sub { {} },
335             ;
336              
337             method signal_handler( Str $signal ) {
338             $self->log_debug("caught SIG$signal");
339             $self->signals->{$signal}++;
340             }
341              
342             method process_signals() {
343             my $sig_h = $self->signals;
344             while (my ($signal,$handler) = each %{ $self->handlers }) {
345             if ($sig_h->{$signal}) {
346             $sig_h->{$signal} = 0;
347             $self->log_debug("processing SIG$signal");
348             $handler->();
349             }
350             }
351             }
352              
353             method catch_signal(Str $sig, CodeRef $sub) {
354             $self->handlers->{$sig} = $sub;
355             $SIG{$sig} = sub { $self->signal_handler($sig) };
356             }
357              
358             method accept_loop() {
359             $self->catch_signal(TERM => sub {
360             $self->log_info("Shutting down.");
361             for my $kid ( @{ $self->child_pids } ) {
362             kill "TERM", $kid;
363             }
364             $self->running(0);
365             });
366             if ( !$self->foreground ) {
367             $self->catch_signal(CHLD => sub { $self->reap_children });
368             }
369             $0 = "srs-epp-proxy - listener";
370             while ( $self->running ) {
371             my $session = $self->accept_one;
372             if ( $session ) {
373             unless ( $self->foreground ) {
374             $self->catch_signal(TERM => sub {
375             $session->shutdown;
376             });
377             }
378             $self->log_trace("accepted a new session, entering event loop");
379             local($Event::DIED) = sub {
380             my $event = shift;
381             my $exception = shift;
382             $self->log_error("Exception during ".$event->w->desc."; $exception");
383             Event::unloop_all;
384             };
385             Event::loop(120);
386             $self->log_info("Session ends");
387             exit unless $self->foreground;
388             }
389             else {
390             $self->log_trace("no new session, processing signals");
391             $self->process_signals;
392             }
393             }
394             }
395              
396             method reap_children() {
397             my $kid;
398             my %reaped;
399             do {
400             $kid = waitpid(-1, WNOHANG);
401             if ($kid > 0) {
402             $reaped{$kid} = $?;
403             $self->log_debug(
404             "child $kid, ".($?&255 ?" killed by signal ".($?&127)
405             .($?&128?" (core dumped)":"")
406             :"exited with error code ".($?>>8))
407             );
408             }
409             } while ($kid > 0);
410             my $child_pids = $self->child_pids;
411             @$child_pids = grep { exists $reaped{$_} } @$child_pids;
412             }
413              
414             {
415             no warnings 'redefine';
416             my $daemonize = \&daemonize;
417             *daemonize = sub {
418             my $self = shift;
419             my %args = @_;
420             $args{dont_close_all_files} = 1;
421             $SIG{__DIE__} = sub {
422             # be sure to re-throw exceptions whilst inside
423             # eval { }
424             if ( $^S ) {
425             die @_;
426             } else {
427             $self->log_error("Uncaught exception, exiting: @_");
428             $self->log_error("stack trace: ".Carp::longmess);
429             exit(1);
430             }
431             };
432             my $no_recurse;
433             $SIG{__WARN__} = sub {
434             return if $no_recurse;
435             $no_recurse = 1;
436             eval { $self->log_warn("caught warning: @_") };
437             $no_recurse = 0;
438             };
439             $daemonize->($self, %args);
440             };
441             }
442              
443             before 'start' => sub {
444             my $self = shift;
445             $self->init;
446             };
447              
448             after 'start' => sub {
449             my $self = shift;
450             $self->accept_loop
451             if $self->is_daemon;
452             };
453              
454             1;
455              
456             __END__
457              
458             =head1 NAME
459              
460             SRS::EPP::Proxy - IETF EPP <=> SRS XML proxy software
461              
462             =head1 SYNOPSIS
463              
464             my $proxy = SRS::EPP::Proxy->new(
465              
466             # where to listen for inbound connections
467             listen => [ "$addr:$port", "[$addr6]:$port" ],
468              
469             # SSL engine: certificate for presentation
470             ssl_key_file => $ssl_key_filename,
471             ssl_cert_file => $ssl_key_filename,
472              
473             # path for verifying client certificates
474             ssl_cert_dir => $ssl_cert_path,
475             # and of course, revocations
476             ssl_crl_file => $ssl_crl_file,
477              
478             # PGP home for dealing with the SRS
479             pgp_dir => $path,
480              
481             );
482              
483             # initialises everything - listens on sockets, checks SSL
484             # keys and PGP home dir valid
485             $proxy->init();
486              
487             # main entry mechanism
488             $proxy->accept_loop();
489              
490             # alternate piecemeal interfaces, mostly for testing
491             $proxy->init_listener;
492             $proxy->init_ssl;
493             $proxy->init_pgp;
494             my $session = $proxy->accept_one; # doesn't fork
495              
496             =head1 DESCRIPTION
497              
498             SRS::EPP::Proxy implements an XML to XML gateway between two
499             contemporary protocols for domain name management; EPP as defined by
500             RFC 3730 and later, and the SRS protocol used by the .nz registry.
501              
502             This module implements the SSL listener; it accepts connections, forks
503             a new child for each one, collects client certificate information
504             about the SSL connection as recommended by RFC 3734, and then starts
505             an Event loop (using L<Event>) and passes control to the
506             L<SRS::EPP::Session> module.
507              
508             Other modules of interest; ie the key modules in this stack are:
509              
510             =over
511              
512             =item L<Moose>
513              
514             Almost every module on this list is written using L<Moose>.
515              
516             =item L<SRS::EPP::Session>
517              
518             Implements the session logic which manages connections, and "oversees"
519             the general flow of converting incoming messages to messages which are
520             sent to the back-end. Has slave classes for managing the various
521             queues which can build up.
522              
523             =item L<Net::SSLeay::OO>
524              
525             This module provides the interface to the OpenSSL library that this
526             stack uses, and in particular is used by SRS::EPP::Session to gather
527             information about the client certificate.
528              
529             =item L<XML::Relax::Generate>
530              
531             Relax NG to Moose class component. The classes this module generates
532             are used as basis for below classes.
533              
534             =item L<XML::Relax::Marshall>
535              
536             XML to and from Moose data structure component. This module can
537             create data structures which match the class structure made by
538             L<XML::Relax::Generate>
539              
540             =item L<SRS::EPP::Message::*>
541              
542             (based on) XML::Relax::Generate conversions of the various XML Schema
543             files in RFCs 3730 - 3733 (actually their later updates, RFC 4930 and
544             above) to Moose classes. These are marshalled to and from XML using
545             XML::Relax::Marshall, above.
546              
547             =item L<SRS::Message::*>
548              
549             These classes are similar conversions, but for the SRS protocol
550             messages. These are based on a conversion of the Relax schema which
551             is used to generate the RFC (not yet assigned an IETF number).
552              
553             =item L<Crypt::OpenPGP>
554              
555             An oldie but a goodie, this module is a nice pure perl implementation
556             of PGP, which is used to sign requests and verify responses to and
557             from the SRS back-end system.
558              
559             =back
560              
561             =cut
562