Bug 30477: Add new UNIMARC installer translation files
[koha.git] / C4 / SIP / SIPServer.pm
1 #!/usr/bin/perl
2
3 package C4::SIP::SIPServer;
4
5 use strict;
6 use warnings;
7 use FindBin qw($Bin);
8 use lib "$Bin";
9 use Net::Server::PreFork;
10 use IO::Socket::INET;
11 use Socket qw(:DEFAULT :crlf);
12 use Scalar::Util qw(blessed);
13 require UNIVERSAL::require;
14
15 use C4::Context;
16 use C4::SIP::Sip qw(siplog);
17 use C4::SIP::Sip::Constants qw(:all);
18 use C4::SIP::Sip::Configuration;
19 use C4::SIP::Sip::Checksum qw(checksum verify_cksum);
20 use C4::SIP::Sip::MsgType qw( handle login_core );
21 use C4::SIP::Logger qw(set_logger);
22
23 use Koha::Caches;
24 use Koha::Logger;
25
26 use C4::SIP::Trapper;
27 tie *STDERR, "C4::SIP::Trapper";
28
29 use base qw(Net::Server::PreFork);
30
31 use constant LOG_SIP => "local6"; # Local alias for the logging facility
32
33
34 set_logger( Koha::Logger->get( { interface => 'sip' } ) );
35
36 #
37 # Main  # not really, since package SIPServer
38 #
39 # FIXME: Is this a module or a script?  
40 # A script with no MAIN namespace?
41 # A module that takes command line args?
42
43 # Set interface to 'sip'
44 C4::Context->interface('sip');
45
46 my %transports = (
47     RAW    => \&raw_transport,
48     telnet => \&telnet_transport,
49 );
50
51 #
52 # Read configuration
53 #
54 my $config = C4::SIP::Sip::Configuration->new( $ARGV[0] );
55 my @parms;
56
57 #
58 # Ports to bind
59 #
60 foreach my $svc (keys %{$config->{listeners}}) {
61     push @parms, "port=" . $svc;
62 }
63
64 #
65 # Logging
66 #
67 # Log lines look like this:
68 # Jun 16 21:21:31 server08 steve_sip[19305]: ILS::Transaction::Checkout performing checkout...
69 # [  TIMESTAMP  ] [ HOST ] [ IDENT ]  PID  : Message...
70 #
71 # The IDENT is determined by config file 'server-params' arguments
72
73
74 #
75 # Server Management: set parameters for the Net::Server::PreFork
76 # module.  The module silently ignores parameters that it doesn't
77 # recognize, and complains about invalid values for parameters
78 # that it does.
79 #
80 if (defined($config->{'server-params'})) {
81     while (my ($key, $val) = each %{$config->{'server-params'}}) {
82         push @parms, $key . '=' . $val;
83     }
84 }
85
86
87 #
88 # This is the main event.
89 __PACKAGE__ ->run(@parms);
90
91 #
92 # Child
93 #
94
95 # process_request is the callback used by Net::Server to handle
96 # an incoming connection request.
97
98 sub process_request {
99     my $self = shift;
100     my $service;
101     my ($sockaddr, $port, $proto);
102     my $transport;
103
104     $self->{config} = $config;
105
106     # Flushing L1 to make sure the request will be processed using the correct data
107     Koha::Caches->flush_L1_caches();
108
109     $self->{account} = undef;  # Clear out the account from the last request, it may be different
110     $self->{logger} = set_logger( Koha::Logger->get( { interface => 'sip' } ) );
111
112     # Flush previous MDCs to prevent accidentally leaking incorrect MDC-entries
113     Koha::Logger->clear_mdc();
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         siplog("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         siplog("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     siplog('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             siplog("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     Koha::Logger->put_mdc( "accountid", $self->{account}->{id} );
191     Koha::Logger->put_mdc( "peeraddr",  $self->{server}->{peeraddr} );
192
193     siplog("LOG_DEBUG", "raw_transport: uname/inst: '%s/%s'",
194         $self->{account}->{id},
195         $self->{account}->{institution});
196     if (! $self->{account}->{id}) {
197         siplog("LOG_ERR","Login failed shutting down");
198         return;
199     }
200
201     $self->sip_protocol_loop();
202     siplog("LOG_INFO", "raw_transport: shutting down");
203     return;
204 }
205
206 sub get_clean_string {
207     my $string = shift;
208     if (defined $string) {
209         siplog("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         siplog("LOG_DEBUG", "get_clean_string post-clean(length %s): %s", length($string), $string);
214     } else {
215         siplog("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         siplog("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     siplog("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         siplog("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         siplog("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         siplog("LOG_WARNING", "Invalid login attempt: '%s'", ($uid||''));
270         print("Invalid login$CRLF");
271     }
272     }; # End of eval
273
274     if ($@) {
275         siplog("LOG_ERR", "telnet_transport: Login timed out");
276         die "Telnet Login Timed out";
277     } elsif (!defined($account)) {
278         siplog("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     siplog("LOG_DEBUG", "telnet_transport: uname/inst: '%s/%s'", $account->{id}, $account->{institution});
286     $self->sip_protocol_loop();
287     siplog("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             siplog( '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                 siplog( "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                 siplog(
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       # SIP connections might be active for weeks, clear L1 cache on every request
358       Koha::Caches->flush_L1_caches();
359
360     # proper SPEC: (octal) \015 = (hex) x0D = (dec) 13 = (ascii) carriage return
361       my $buffer = <STDIN>;
362       if ( defined $buffer ) {
363           STDIN->flush();    # clear an extra linefeed
364           chomp $buffer;
365           $raw_length = length $buffer;
366           $buffer =~ s/^\s*[^A-z0-9]+//s;
367 # Every line must start with a "real" character.  Not whitespace, control chars, etc.
368           $buffer =~ s/[^A-z0-9]+$//s;
369
370 # Same for the end.  Note this catches the problem some clients have sending empty fields at the end, like |||
371           $buffer =~ s/\015?\012//g;    # Extra line breaks must die
372           $buffer =~ s/\015?\012//s;    # Extra line breaks must die
373           $buffer =~ s/\015*\012*$//s;
374
375     # treat as one line to include the extra linebreaks we are trying to remove!
376       }
377       else {
378           siplog( 'LOG_DEBUG', 'EOF returned on read' );
379           return;
380       }
381       my $len = length $buffer;
382       if ( $len != $raw_length ) {
383           my $trim = $raw_length - $len;
384           siplog( 'LOG_DEBUG', "read_request trimmed $trim character(s) " );
385       }
386
387       siplog( 'LOG_INFO', "INPUT MSG: '$buffer'" );
388       return $buffer;
389 }
390
391 # $server->get_timeout({ $type => 1, fallback => $fallback });
392 #     where $type is transport | client | policy
393 #
394 # Centralizes all timeout logic.
395 # Transport refers to login process, client to active connections.
396 # Policy timeout is transaction timeout (used in ACS status message).
397 #
398 # Fallback is optional. If you do not pass transport, client or policy,
399 # you will get fallback or hardcoded default.
400
401 sub get_timeout {
402     my ( $server, $params ) = @_;
403     my $fallback = $params->{fallback} || 30;
404     my $service = $server->{service} // {};
405     my $config = $server->{config} // {};
406
407     if( $params->{transport} ||
408         ( $params->{client} && !exists $service->{client_timeout} )) {
409         # We do not allow zero values here.
410         # Note: config/timeout seems to be deprecated.
411         return $service->{timeout} || $config->{timeout} || $fallback;
412
413     } elsif( $params->{client} ) {
414         # We know that client_timeout exists now.
415         # We do allow zero values here to indicate no timeout.
416         return 0 if $service->{client_timeout} =~ /^0+$|\D/;
417         return $service->{client_timeout};
418
419     } elsif( $params->{policy} ) {
420         my $policy = $server->{policy} // {};
421         my $rv = sprintf( "%03d", $policy->{timeout} // 0 );
422         if( length($rv) != 3 ) {
423             siplog( "LOG_ERR", "Policy timeout has wrong size: '%s'", $rv );
424             return '000';
425         }
426         return $rv;
427
428     } else {
429         return $fallback;
430     }
431 }
432
433 1;
434
435 __END__