]> git.evergreen-ils.org Git - working/SIPServer.git/blob - SIPServer.pm
LP#1338731: support clients that send 99 then 93 when starting a raw connection
[working/SIPServer.git] / SIPServer.pm
1 #
2 # Copyright (C) 2006-2008  Georgia Public Library Service
3
4 # Author: David J. Fiander
5
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of version 2 of the GNU General Public
8 # License as published by the Free Software Foundation.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14
15 # You should have received a copy of the GNU General Public
16 # License along with this program; if not, write to the Free
17 # Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
18 # MA 02111-1307 USA
19
20 package SIPServer;
21
22 use strict;
23 use warnings;
24 use Exporter;
25 use Sys::Syslog qw(syslog);
26 use Net::Server::PreFork;
27 use Net::Server::Proto;
28 use IO::Socket::INET;
29 use Socket qw(:crlf SOL_SOCKET SO_KEEPALIVE IPPROTO_TCP TCP_KEEPALIVE);
30 use Data::Dumper;               # For debugging
31 require UNIVERSAL::require;
32
33 #use Sip qw(readline);
34 use Sip::Constants qw(:all);
35 use Sip::Configuration;
36 use Sip::Checksum qw(checksum verify_cksum);
37 use Sip::MsgType;
38
39 use constant LOG_SIP => "local6"; # Local alias for the logging facility
40
41 our $VERSION = 0.02;
42 our @ISA = qw(Net::Server::PreFork);
43 #
44 # Main
45 #
46
47 my %transports = (
48     RAW    => \&raw_transport,
49     telnet => \&telnet_transport,
50     http   => \&http_transport,
51 );
52
53 # Read configuration
54
55 my $config = Sip::Configuration->new($ARGV[0]);
56
57 my @parms;
58
59 #
60 # Ports to bind
61 #
62 foreach my $svc (keys %{$config->{listeners}}) {
63     push @parms, "port=" . $svc;
64 }
65
66 #
67 # Logging
68 #
69 # Log lines look like this:
70 # Jun 16 21:21:31 server08 steve_sip: Sip::MsgType::_initialize('Login', ...)
71 # [  TIMESTAMP  ] [ HOST ] [ IDENT ]: Message...
72 #
73 # The IDENT is determined by $ENV{SIP_LOG_IDENT}, if present.
74 # Otherwise it is "_sip" appended to $USER, if present, or "acs-server" as a fallback.
75 #
76
77 my $syslog_ident = $ENV{SIP_LOG_IDENT} || ($ENV{USER} ? $ENV{USER} . "_sip" : 'acs-server');
78
79 push @parms,
80     "log_file=Sys::Syslog",
81     "syslog_ident=$syslog_ident",
82     "syslog_facility=" . LOG_SIP;
83
84 #
85 # Server Management: set parameters for the Net::Server::PreFork
86 # module.  The module silently ignores parameters that it doesn't
87 # recognize, and complains about invalid values for parameters
88 # that it does.
89 #
90 if (defined($config->{'server-params'})) {
91     while (my ($key, $val) = each %{$config->{'server-params'}}) {
92         push @parms, $key . '=' . $val;
93     }
94 }
95
96 print Dumper(@parms);
97
98 #
99 # This is the main event.
100 SIPServer->run(@parms);
101
102 #
103 # Child
104 #
105
106 # process_request is the callback used by Net::Server to handle
107 # an incoming connection request.
108
109 sub process_request {
110     my $self = shift;
111     my $service;
112     my $sockname;
113     my ($sockaddr, $port, $proto);
114     my $transport;
115
116     # This is kind of kinky, but allows us to avoid requiring Socket::Linux.
117     # A simple "Socket::Linux"->use won't suffice since we need access to
118     # all of it's bareword constants as well.
119     eval <<'    EVAL';
120     use Socket::Linux qw(TCP_KEEPINTVL TCP_KEEPIDLE TCP_KEEPCNT);
121     setsockopt($self->{server}->{client}, SOL_SOCKET,  SO_KEEPALIVE, 1);
122     setsockopt($self->{server}->{client}, IPPROTO_TCP, TCP_KEEPIDLE, 120);
123     setsockopt($self->{server}->{client}, IPPROTO_TCP, TCP_KEEPINTVL, 10);
124     EVAL
125
126     syslog('LOG_DEBUG', 
127         "Consider installing Socket::Linux for TCP keepalive: $@") if $@;
128
129     $self->{account} = undef; # New connection, no need to keep login info
130     $self->{config} = $config;
131
132     $sockaddr = $self->{server}->{sockaddr};
133     $port     = $self->{server}->{sockport};
134     $proto    = $self->{server}->{client}->NS_proto();
135     syslog('LOG_INFO', "Inbound connection from $sockaddr on port $port and proto $proto");
136
137     $self->{service} = $config->find_service( $sockaddr, $port, $proto );
138
139     if (! defined($self->{service})) {
140         syslog( "LOG_ERR", "process_request: Unrecognized server connection: %s:%s/%s",
141             $sockaddr, $port, $proto );
142         die "process_request: Bad server connection";
143     }
144
145     $transport = $transports{ $self->{service}->{transport} };
146
147     if ( !defined($transport) ) {
148         syslog("LOG_WARNING", "Unknown transport '%s', dropping", $service->{transport});
149         return;
150     } else {
151         &$transport($self);
152         # Transport has shut down, remove any lingering login info
153         $self->{account} = undef;
154     }
155 }
156
157 #
158 # Transports
159 #
160
161 sub raw_transport {
162     my $self = shift;
163     my ($uid, $pwd);
164     my $input;
165     my $service = $self->{service};
166     my $strikes = 3;
167     my $inst;
168     my $timeout = $self->{service}->{timeout} || $self->{config}->{timeout} || 0;
169
170     eval {
171         local $SIG{ALRM} = sub { die "raw_transport Timed Out!\n"; };
172         syslog("LOG_DEBUG", "raw_transport: timeout is $timeout");
173
174     while ($strikes--) {
175         alarm $timeout;
176         $input = Sip::read_SIP_packet(*STDIN);
177         alarm 0;
178
179         if (!$input) {
180             # EOF on the socket
181             syslog("LOG_INFO", "raw_transport: shutting down: EOF during login");
182             return;
183         } elsif ($input !~ /\S/) {
184             syslog("LOG_INFO", "raw_transport: received whitespace line (length %s) during login, skipping", length($input));
185             next;
186         }
187         $input =~ s/[\r\n]+$//sm;       # Strip off trailing line terminator
188         if ($input =~ /^99/) { # SC Status
189             unless ($service->allow_sc_status_then_login()) {
190                 die 'raw_transport: sending SC status before login not enabled, exiting';
191             }
192             Sip::MsgType::handle($input, $self, SC_STATUS);
193             next;
194         }
195         last if Sip::MsgType::handle($input, $self, LOGIN);
196     }
197     };
198
199     if ($@) {
200         syslog("LOG_ERR", "raw_transport: LOGIN ERROR: '$@'");
201         die "raw_transport: login error (timeout? $@), exiting";
202     } elsif (!$self->{account}) {
203         syslog("LOG_ERR", "raw_transport: LOGIN FAILED");
204         die "raw_transport: Login failed (no account), exiting";
205     }
206
207     syslog("LOG_DEBUG", "raw_transport: uname/inst: '%s/%s'",
208         $self->{account}->{id},
209         $self->{account}->{institution});
210
211     $self->sip_protocol_loop();
212
213     syslog("LOG_INFO", "raw_transport: shutting down");
214 }
215
216 sub telnet_transport {
217     my $self = shift;
218     my ($uid, $pwd);
219     my $strikes = 3;
220     my $account = undef;
221     my $input;
222     my $config = $self->{config};
223     my $timeout = $self->{service}->{timeout} || $config->{timeout} || 0;
224     syslog("LOG_DEBUG", "telnet_transport: timeout is %s", $timeout);
225
226     # Until the terminal has logged in, we don't trust it
227     # so use a timeout to protect ourselves from hanging.
228     eval {
229     local $SIG{ALRM} = sub { die "telnet_transport: Timed Out ($timeout seconds)!\n";; };
230     local $| = 1;                       # Unbuffered output
231
232     while ($strikes--) {
233         print "login: ";
234         alarm $timeout;
235         $uid = <STDIN>;
236         alarm 0;
237
238         print "password: ";
239         alarm $timeout;
240         $pwd = <STDIN>;
241         alarm 0;
242
243         $uid =~ s/[\r\n]+$//;
244         $pwd =~ s/[\r\n]+$//;
245
246         if (exists($config->{accounts}->{$uid})
247         && ($pwd eq $config->{accounts}->{$uid}->password())) {
248             $account = $config->{accounts}->{$uid};
249             last;
250         } else {
251             syslog("LOG_WARNING", "Invalid login attempt: '%s'", $uid);
252             print("Invalid login$CRLF");
253         }
254     }
255     }; # End of eval
256
257     if ($@) {
258         syslog("LOG_ERR", "telnet_transport: Login timed out");
259         die "Telnet Login Timed out";
260     } elsif (!defined($account)) {
261         syslog("LOG_ERR", "telnet_transport: Login Failed");
262         die "Login Failure";
263     } else {
264         print "Login OK.  Initiating SIP$CRLF";
265     }
266
267     $self->{account} = $account;
268     syslog("LOG_DEBUG", "telnet_transport: uname/inst: '%s/%s'", $account->{id}, $account->{institution});
269     $self->sip_protocol_loop();
270     syslog("LOG_INFO", "telnet_transport: shutting down");
271 }
272
273
274 sub http_transport {
275 }
276
277 #
278 # The terminal has logged in, using either the SIP login process
279 # over a raw socket, or via the pseudo-unix login provided by the
280 # telnet transport.  From that point on, both the raw and the telnet
281 # processes are the same:
282 sub sip_protocol_loop {
283     my $self = shift;
284     my $expect;
285     my $service = $self->{service};
286     my $config  = $self->{config};
287     my $input;
288     my $timeout = $self->{service}->{timeout} || $config->{timeout} || 0;
289
290     # Now that the terminal has logged in, the first message
291     # we recieve must be an SC_STATUS message.  But it might be
292     # an SC_REQUEST_RESEND.  So, as long as we keep receiving
293     # SC_REQUEST_RESEND, we keep waiting for an SC_STATUS
294
295     # Comprise reports that no other ILS actually enforces this
296     # constraint, so we'll relax about it too.  As long as everybody
297     # uses the SIP "raw" login process, rather than telnet, this
298     # will be fine, becaues the LOGIN protocol exchange will force
299     # us into SIP 2.00 anyway.  Machines that want to log in using
300     # telnet MUST send an SC Status message first, even though we're
301     # not enforcing it.
302     # 
303     #$expect = SC_STATUS;
304     $expect = '';
305
306     alarm $timeout; # First loop timeout
307     while ( $input = Sip::read_SIP_packet(*STDIN) ) {
308         alarm 0; # Don't timeout while we are processing
309         $input =~ s/[\r\n]+$//sm;    # Strip off any trailing line ends
310
311         my $status = Sip::MsgType::handle($input, $self, $expect);
312         next if $status eq REQUEST_ACS_RESEND;
313
314         if (!$status) {
315             syslog("LOG_ERR", "raw_transport: failed to handle %s", substr($input,0,2));
316             die "sip_protocol_loop: failed Sip::MsgType::handle('$input', $self, '$expect')";
317         }
318         elsif ($expect && ($status ne $expect)) {
319             # We received a non-"RESEND" that wasn't what we were expecting.
320             syslog("LOG_ERR", "raw_transport: expected %s, received %s, exiting", $expect, $input);
321             die "sip_protocol_loop: exiting: expected '$expect', received '$status'";
322         }
323
324         # We successfully received and processed what we were expecting
325         $expect = '';
326         alarm $timeout; # Next loop timeout
327     }
328 }