8daa6274829be8bdce4aa92b4309328e36a0d2dd
[koha-equinox.git] / C4 / SIP / SIPServer.pm
1 #!/usr/bin/perl
2 package C4::SIP::SIPServer;
3
4 use strict;
5 use warnings;
6 use FindBin qw($Bin);
7 use lib "$Bin";
8 use Net::Server::PreFork;
9 use IO::Socket::INET;
10 use Socket qw(:DEFAULT :crlf);
11 use Scalar::Util qw(blessed);
12 require UNIVERSAL::require;
13
14 use C4::Context;
15 use C4::SIP::Sip qw(syslog);
16 use C4::SIP::Sip::Constants qw(:all);
17 use C4::SIP::Sip::Configuration;
18 use C4::SIP::Sip::Checksum qw(checksum verify_cksum);
19 use C4::SIP::Sip::MsgType qw( handle login_core );
20
21 use Koha::Logger;
22 use C4::SIP::Trapper;
23 tie *STDERR, "C4::SIP::Trapper";
24
25 use base qw(Net::Server::PreFork);
26
27 use constant LOG_SIP => "local6"; # Local alias for the logging facility
28
29 #
30 # Main  # not really, since package SIPServer
31 #
32 # FIXME: Is this a module or a script?  
33 # A script with no MAIN namespace?
34 # A module that takes command line args?
35
36 =head1 NAME
37
38 C4::SIP::SIPServer
39
40 =cut
41
42 # Set interface to 'sip'
43 C4::Context->interface('sip');
44
45 my %transports = (
46     RAW    => \&raw_transport,
47     telnet => \&telnet_transport,
48 );
49
50 #
51 # Read configuration
52 #
53 my $config = C4::SIP::Sip::Configuration->new( $ARGV[0] );
54 my @parms;
55
56 #
57 # Ports to bind
58 #
59 foreach my $svc (keys %{$config->{listeners}}) {
60     push @parms, "port=" . $svc;
61 }
62
63 #
64 # Logging
65 #
66 # Log lines look like this:
67 # Jun 16 21:21:31 server08 steve_sip[19305]: ILS::Transaction::Checkout performing checkout...
68 # [  TIMESTAMP  ] [ HOST ] [ IDENT ]  PID  : Message...
69 #
70 # The IDENT is determined by config file 'server-params' arguments
71
72
73 #
74 # Server Management: set parameters for the Net::Server::PreFork
75 # module.  The module silently ignores parameters that it doesn't
76 # recognize, and complains about invalid values for parameters
77 # that it does.
78 #
79 if (defined($config->{'server-params'})) {
80     while (my ($key, $val) = each %{$config->{'server-params'}}) {
81                 push @parms, $key . '=' . $val;
82     }
83 }
84
85
86 #
87 # This is the main event.
88 __PACKAGE__ ->run(@parms);
89
90 #
91 # Child
92 #
93
94 my $activeSIPServer;
95 my $activeLogger;
96
97 # process_request is the callback used by Net::Server to handle
98 # an incoming connection request.
99
100 sub process_request {
101     my $self = shift;
102     my $service;
103     my ($sockaddr, $port, $proto);
104     my $transport;
105
106     $self->{config} = $config;
107
108     $self->{account} = undef;  # Clear out the account from the last request, it may be different
109     $self->{logger} = _set_logger( Koha::Logger->get( { interface => 'sip' } ) );
110
111     # Flush previous MDCs to prevent accidentally leaking incorrect MDC-entries
112     Log::Log4perl::MDC->put( "accountid", undef );
113     Log::Log4perl::MDC->put( "peeraddr",  undef );
114
115     my $sockname = getsockname(STDIN);
116
117     # Check if socket connection is IPv6 before resolving address
118     my $family = Socket::sockaddr_family($sockname);
119     if ($family == AF_INET6) {
120       ($port, $sockaddr) = sockaddr_in6($sockname);
121       $sockaddr = Socket::inet_ntop(AF_INET6, $sockaddr);
122     } else {
123       ($port, $sockaddr) = sockaddr_in($sockname);
124       $sockaddr = inet_ntoa($sockaddr);
125     }
126     $proto = $self->{server}->{client}->NS_proto();
127
128     $self->{service} = $config->find_service($sockaddr, $port, $proto);
129
130     if (!defined($self->{service})) {
131                 syslog("LOG_ERR", "process_request: Unknown recognized server connection: %s:%s/%s", $sockaddr, $port, $proto);
132                 die "process_request: Bad server connection";
133     }
134
135     $transport = $transports{$self->{service}->{transport}};
136
137     if (!defined($transport)) {
138                 syslog("LOG_WARNING", "Unknown transport '%s', dropping", $service->{transport});
139                 return;
140     } else {
141                 &$transport($self);
142     }
143     return;
144 }
145
146 #
147 # Transports
148 #
149
150 sub raw_transport {
151     my $self = shift;
152     my $input;
153     my $service = $self->{service};
154     # If using Net::Server::PreFork you may already have account set from a previous session
155     # Ensure you dont
156     if ($self->{account}) {
157         delete $self->{account};
158     }
159
160     # Timeout the while loop if we get stuck in it
161     # In practice it should only iterate once but be prepared
162     local $SIG{ALRM} = sub { die 'raw transport Timed Out!' };
163     my $timeout = $self->get_timeout({ transport => 1 });
164     syslog('LOG_DEBUG', "raw_transport: timeout is $timeout");
165     alarm $timeout;
166     while (!$self->{account}) {
167         $input = read_request();
168         if (!$input) {
169             # EOF on the socket
170             syslog("LOG_INFO", "raw_transport: shutting down: EOF during login");
171             return;
172         }
173         $input =~ s/[\r\n]+$//sm; # Strip off trailing line terminator(s)
174         my $reg = qr/^${\(LOGIN)}/;
175         last if $input !~ $reg ||
176             C4::SIP::Sip::MsgType::handle($input, $self, LOGIN);
177     }
178     alarm 0;
179
180     $self->{logger} = _set_logger(
181         Koha::Logger->get(
182             {
183                 interface => 'sip',
184                 category  => $self->{account}->{id}, # Add id to namespace
185             }
186         )
187     );
188
189     # Set MDCs after properly authenticating
190     Log::Log4perl::MDC->put( "accountid", $self->{account}->{id} );
191     Log::Log4perl::MDC->put( "peeraddr",  $self->{server}->{peeraddr} );
192
193     syslog("LOG_DEBUG", "raw_transport: uname/inst: '%s/%s'",
194         $self->{account}->{id},
195         $self->{account}->{institution});
196     if (! $self->{account}->{id}) {
197         syslog("LOG_ERR","Login failed shutting down");
198         return;
199     }
200
201     $self->sip_protocol_loop();
202     syslog("LOG_INFO", "raw_transport: shutting down");
203     return;
204 }
205
206 sub get_clean_string {
207         my $string = shift;
208         if (defined $string) {
209                 syslog("LOG_DEBUG", "get_clean_string  pre-clean(length %s): %s", length($string), $string);
210                 chomp($string);
211                 $string =~ s/^[^A-z0-9]+//;
212                 $string =~ s/[^A-z0-9]+$//;
213                 syslog("LOG_DEBUG", "get_clean_string post-clean(length %s): %s", length($string), $string);
214         } else {
215                 syslog("LOG_INFO", "get_clean_string called on undefined");
216         }
217         return $string;
218 }
219
220 sub get_clean_input {
221         local $/ = "\012";
222         my $in = <STDIN>;
223         $in = get_clean_string($in);
224         while (my $extra = <STDIN>){
225                 syslog("LOG_ERR", "get_clean_input got extra lines: %s", $extra);
226         }
227         return $in;
228 }
229
230 sub telnet_transport {
231     my $self = shift;
232     my ($uid, $pwd);
233     my $strikes = 3;
234     my $account = undef;
235     my $input;
236     my $config  = $self->{config};
237     my $timeout = $self->get_timeout({ transport => 1 });
238     syslog("LOG_DEBUG", "telnet_transport: timeout is $timeout");
239
240     eval {
241         local $SIG{ALRM} = sub { die "telnet_transport: Timed Out ($timeout seconds)!\n"; };
242         local $| = 1;                   # Unbuffered output
243         $/ = "\015";            # Internet Record Separator (lax version)
244     # Until the terminal has logged in, we don't trust it
245     # so use a timeout to protect ourselves from hanging.
246
247         while ($strikes--) {
248             print "login: ";
249                 alarm $timeout;
250                 # $uid = &get_clean_input;
251                 $uid = <STDIN>;
252             print "password: ";
253             # $pwd = &get_clean_input || '';
254                 $pwd = <STDIN>;
255                 alarm 0;
256
257                 syslog("LOG_DEBUG", "telnet_transport 1: uid length %s, pwd length %s", length($uid), length($pwd));
258                 $uid = get_clean_string ($uid);
259                 $pwd = get_clean_string ($pwd);
260                 syslog("LOG_DEBUG", "telnet_transport 2: uid length %s, pwd length %s", length($uid), length($pwd));
261
262             if (exists ($config->{accounts}->{$uid})
263                 && ($pwd eq $config->{accounts}->{$uid}->{password})) {
264                         $account = $config->{accounts}->{$uid};
265                         if ( C4::SIP::Sip::MsgType::login_core($self,$uid,$pwd) ) {
266                 last;
267             }
268             }
269                 syslog("LOG_WARNING", "Invalid login attempt: '%s'", ($uid||''));
270                 print("Invalid login$CRLF");
271         }
272     }; # End of eval
273
274     if ($@) {
275                 syslog("LOG_ERR", "telnet_transport: Login timed out");
276                 die "Telnet Login Timed out";
277     } elsif (!defined($account)) {
278                 syslog("LOG_ERR", "telnet_transport: Login Failed");
279                 die "Login Failure";
280     } else {
281                 print "Login OK.  Initiating SIP$CRLF";
282     }
283
284     $self->{account} = $account;
285     syslog("LOG_DEBUG", "telnet_transport: uname/inst: '%s/%s'", $account->{id}, $account->{institution});
286     $self->sip_protocol_loop();
287     syslog("LOG_INFO", "telnet_transport: shutting down");
288     return;
289 }
290
291 #
292 # The terminal has logged in, using either the SIP login process
293 # over a raw socket, or via the pseudo-unix login provided by the
294 # telnet transport.  From that point on, both the raw and the telnet
295 # processes are the same:
296 sub sip_protocol_loop {
297     my $self = shift;
298     my $service = $self->{service};
299     my $config  = $self->{config};
300     my $timeout = $self->get_timeout({ client => 1 });
301
302     # The spec says the first message will be:
303     #     SIP v1: SC_STATUS
304     #     SIP v2: LOGIN (or SC_STATUS via telnet?)
305     # But it might be SC_REQUEST_RESEND.  As long as we get
306     # SC_REQUEST_RESEND, we keep waiting.
307
308     # Comprise reports that no other ILS actually enforces this
309     # constraint, so we'll relax about it too.
310     # Using the SIP "raw" login process, rather than telnet,
311     # requires the LOGIN message and forces SIP 2.00.  In that
312     # case, the LOGIN message has already been processed (above).
313
314     # In short, we'll take any valid message here.
315     eval {
316         local $SIG{ALRM} = sub {
317             syslog( 'LOG_DEBUG', 'Inactive: timed out' );
318             die "Timed Out!\n";
319         };
320         my $previous_alarm = alarm($timeout);
321
322         while ( my $inputbuf = read_request() ) {
323             if ( !defined $inputbuf ) {
324                 return;    #EOF
325             }
326             alarm($timeout);
327
328             unless ($inputbuf) {
329                 syslog( "LOG_ERR", "sip_protocol_loop: empty input skipped" );
330                 print("96$CR");
331                 next;
332             }
333
334             my $status = C4::SIP::Sip::MsgType::handle( $inputbuf, $self, q{} );
335             if ( !$status ) {
336                 syslog(
337                     "LOG_ERR",
338                     "sip_protocol_loop: failed to handle %s",
339                     substr( $inputbuf, 0, 2 )
340                 );
341             }
342             next if $status eq REQUEST_ACS_RESEND;
343         }
344         alarm($previous_alarm);
345         return;
346     };
347     if ( $@ =~ m/timed out/i ) {
348         return;
349     }
350     return;
351 }
352
353 sub read_request {
354       my $raw_length;
355       local $/ = "\015";
356
357     # proper SPEC: (octal) \015 = (hex) x0D = (dec) 13 = (ascii) carriage return
358       my $buffer = <STDIN>;
359       if ( defined $buffer ) {
360           STDIN->flush();    # clear an extra linefeed
361           chomp $buffer;
362           $raw_length = length $buffer;
363           $buffer =~ s/^\s*[^A-z0-9]+//s;
364 # Every line must start with a "real" character.  Not whitespace, control chars, etc.
365           $buffer =~ s/[^A-z0-9]+$//s;
366
367 # Same for the end.  Note this catches the problem some clients have sending empty fields at the end, like |||
368           $buffer =~ s/\015?\012//g;    # Extra line breaks must die
369           $buffer =~ s/\015?\012//s;    # Extra line breaks must die
370           $buffer =~ s/\015*\012*$//s;
371
372     # treat as one line to include the extra linebreaks we are trying to remove!
373       }
374       else {
375           syslog( 'LOG_DEBUG', 'EOF returned on read' );
376           return;
377       }
378       my $len = length $buffer;
379       if ( $len != $raw_length ) {
380           my $trim = $raw_length - $len;
381           syslog( 'LOG_DEBUG', "read_request trimmed $trim character(s) " );
382       }
383
384       syslog( 'LOG_INFO', "INPUT MSG: '$buffer'" );
385       return $buffer;
386 }
387
388 # $server->get_timeout({ $type => 1, fallback => $fallback });
389 #     where $type is transport | client | policy
390 #
391 # Centralizes all timeout logic.
392 # Transport refers to login process, client to active connections.
393 # Policy timeout is transaction timeout (used in ACS status message).
394 #
395 # Fallback is optional. If you do not pass transport, client or policy,
396 # you will get fallback or hardcoded default.
397
398 sub get_timeout {
399     my ( $server, $params ) = @_;
400     my $fallback = $params->{fallback} || 30;
401     my $service = $server->{service} // {};
402     my $config = $server->{config} // {};
403
404     if( $params->{transport} ||
405         ( $params->{client} && !exists $service->{client_timeout} )) {
406         # We do not allow zero values here.
407         # Note: config/timeout seems to be deprecated.
408         return $service->{timeout} || $config->{timeout} || $fallback;
409
410     } elsif( $params->{client} ) {
411         # We know that client_timeout exists now.
412         # We do allow zero values here to indicate no timeout.
413         return 0 if $service->{client_timeout} =~ /^0+$|\D/;
414         return $service->{client_timeout};
415
416     } elsif( $params->{policy} ) {
417         my $policy = $server->{policy} // {};
418         my $rv = sprintf( "%03d", $policy->{timeout} // 0 );
419         if( length($rv) != 3 ) {
420             syslog( "LOG_ERR", "Policy timeout has wrong size: '%s'", $rv );
421             return '000';
422         }
423         return $rv;
424
425     } else {
426         return $fallback;
427     }
428 }
429
430 =head2 get_SIPServer
431
432     my $sipServer = C4::SIP::SIPServer::get_SIPServer()
433
434 @RETURNS C4::SIP::SIPServer, the current server's child-process used to handle this SIP-transaction
435
436 =cut
437
438 sub get_SIPServer {
439     unless($activeSIPServer) {
440         my @cc = caller(1);
441         die "$cc[3]() asks for \$activeSIPServer, but it is not defined yet";
442     }
443     return $activeSIPServer;
444 }
445
446 sub _set_SIPServer {
447     my ($sipServer) = @_;
448     unless (blessed($sipServer) && $sipServer->isa('C4::SIP::SIPServer')) {
449         my @cc = caller(0);
450         die "$cc[3]():> \$sipServer '$sipServer' is not a C4::SIP::SIPServer-object";
451     }
452     $activeSIPServer = $sipServer;
453     return $activeSIPServer;
454 }
455
456 =head2 get_logger
457
458     my $logger = C4::SIP::SIPServer::get_logger()
459
460 @RETURNS Koha::Logger, the logger used to log this SIP-transaction
461
462 =cut
463
464 sub get_logger {
465     unless($activeLogger) {
466         my @cc = caller(1);
467         die "$cc[3]() asks for \$activeLogger, but it is not defined yet";
468     }
469     return $activeLogger;
470 }
471
472 sub _set_logger {
473     my ($logger) = @_;
474     unless (blessed($logger) && $logger->isa('Koha::Logger')) {
475         my @cc = caller(0);
476         die "$cc[3]():> \$logger '$logger' is not a Koha::Logger-object";
477     }
478     $activeLogger = $logger;
479     return $activeLogger;
480 }
481
482 1;
483
484 __END__