File Coverage

blib/lib/Net/Clacks/Server.pm
Criterion Covered Total %
statement 85 874 9.7
branch 0 368 0.0
condition 0 121 0.0
subroutine 28 44 63.6
pod 4 9 44.4
total 117 1416 8.2


line stmt bran cond sub pod time code
1             package Net::Clacks::Server;
2             #---AUTOPRAGMASTART---
3 1     1   15 use 5.020;
  1         3  
4 1     1   5 use strict;
  1         2  
  1         16  
5 1     1   5 use warnings;
  1         1  
  1         24  
6 1     1   5 use diagnostics;
  1         2  
  1         6  
7 1     1   27 use mro 'c3';
  1         1  
  1         6  
8 1     1   20 use English;
  1         1  
  1         6  
9 1     1   344 use Carp;
  1         2  
  1         51  
10             our $VERSION = 23;
11 1     1   7 use autodie qw( close );
  1         2  
  1         6  
12 1     1   323 use Array::Contains;
  1         2  
  1         51  
13 1     1   5 use utf8;
  1         2  
  1         9  
14 1     1   25 use Encode qw(is_utf8 encode_utf8 decode_utf8);
  1         5  
  1         52  
15 1     1   6 use feature 'signatures';
  1         1  
  1         89  
16 1     1   5 no warnings qw(experimental::signatures);
  1         3  
  1         28  
17             #---AUTOPRAGMAEND---
18              
19 1     1   719 use XML::Simple;
  1         7577  
  1         5  
20 1     1   58 use Time::HiRes qw(sleep usleep time);
  1         2  
  1         8  
21 1     1   113 use Sys::Hostname;
  1         2  
  1         35  
22 1     1   5 use Errno;
  1         2  
  1         23  
23 1     1   4 use IO::Socket::IP;
  1         2  
  1         7  
24 1     1   492 use IO::Select;
  1         2  
  1         41  
25 1     1   5 use IO::Socket::SSL;
  1         2  
  1         5  
26 1     1   609 use YAML::Syck;
  1         1572  
  1         46  
27 1     1   5 use MIME::Base64;
  1         2  
  1         31  
28 1     1   420 use File::Copy;
  1         3770  
  1         44  
29 1     1   514 use Data::Dumper;
  1         5057  
  1         49  
30              
31             # For turning off SSL session cache
32 1     1   475 use Readonly;
  1         3245  
  1         98  
33             Readonly my $SSL_SESS_CACHE_OFF => 0x0000;
34              
35             my %overheadflags = (
36             A => "auth_token", # Authentication token
37             O => "auth_ok", # Authentication OK
38             F => "auth_failed", # Authentication FAILED
39              
40             E => 'error_message', # Server to client error message
41              
42             C => "close_all_connections",
43             D => "discard_message",
44             G => "forward_message",
45             I => "set_interclacks_mode", # value: true/false, disables 'G' and 'U'
46             L => "lock_for_sync", # value: true/false, only available in interclacks client mode
47             M => "informal_message", # informal message, no further operation on it
48             N => "no_logging",
49             S => "shutdown_service", # value: positive number (number in seconds before shutdown). If interclacks clients are present, should be high
50             # enough to flush all buffers to them
51              
52             T => 'timestamp', # Used before KEYSYNC to compensate for time drift between different systems
53             U => "return_to_sender",
54             Z => "no_flags", # Only sent when no other flags are set
55             );
56              
57             BEGIN {
58             {
59             # We need to add some extra function to IO::Socket::SSL so we can track the client ID
60             # on both TCP and Unix Domain Sockets
61 1     1   6 no strict 'refs'; ## no critic (TestingAndDebugging::ProhibitNoStrict)
  1     1   3  
  1         166  
  1         4  
62 1         7 *{"IO::Socket::SSL::_setClientID"} = sub {
63 0     0   0 my ($self, $cid) = @_;
64            
65 0         0 ${*$self}{'__client_id'} = $cid; ## no critic (References::ProhibitDoubleSigils)
  0         0  
66 0         0 return;
67 1         3 };
68            
69 1         1283 *{"IO::Socket::SSL::_getClientID"} = sub {
70 0     0   0 my ($self) = @_;
71            
72 0   0     0 return ${*$self}{'__client_id'} || ''; ## no critic (References::ProhibitDoubleSigils)
73 1         3 };
74              
75             }
76            
77             }
78              
79 0     0 1   sub new($class, $isDebugging, $configfile) {
  0            
  0            
  0            
  0            
80              
81 0           my $self = bless {}, $class;
82              
83 0           $self->{isDebugging} = $isDebugging;
84 0           $self->{configfile} = $configfile;
85              
86 0           $self->{timeoffset} = 0;
87              
88 0 0         if(defined($ENV{CLACKS_SIMULATED_TIME_OFFSET})) {
89 0           $self->{timeoffset} = 0 + $ENV{CLACKS_SIMULATED_TIME_OFFSET};
90 0           print "****** RUNNING WITH A SIMULATED TIME OFFSET OF ", $self->{timeoffset}, " seconds ******\n";
91             }
92              
93 0           $self->{clackscache} = {};
94 0           $self->{clackscachetime} = {};
95 0           $self->{clackscacheaccesstime} = {};
96              
97 0           return $self;
98             }
99              
100 0     0 1   sub init($self) {
  0            
  0            
101              
102 0           my @paths;
103 0 0         if(defined($ENV{'PC_CONFIG_PATHS'})) {
104 0           push @paths, split/\:/, $ENV{'PC_CONFIG_PATHS'};
105 0           print "Found config paths:\n", Dumper(\@paths), " \n";
106             } else {
107 0           print("PC_CONFIG_PATHS undefined, falling back to legacy mode\n");
108 0           @paths = ('', 'configs/');
109             }
110              
111 0           my $filedata;
112 0           my $fname = $self->{configfile};
113 0           foreach my $path (@paths) {
114 0 0 0       if($path ne '' && $path !~ /\/$/) {
115 0           $path .= '/';
116             }
117 0           my $fullfname = $path . $fname;
118 0 0         next unless (-f $fullfname);
119 0           print " Loading config file $fullfname\n";
120              
121 0           $filedata = slurpBinFile($fullfname);
122              
123 0           foreach my $varname (keys %ENV) {
124 0 0         next unless $varname =~ /^PC\_/;
125              
126 0           my $newval = $ENV{$varname};
127 0           $filedata =~ s/$varname/$newval/g;
128             }
129              
130 0           last;
131             }
132              
133 0 0 0       if(!defined($filedata) || $filedata eq "") {
134 0           croak("Can't load config file: Not found or empty!");
135             }
136              
137 0           print "------- Parsing config file $fname ------\n";
138 0           my $config = XMLin($filedata, ForceArray => [ 'ip', 'socket' ]);
139              
140 0           my $hname = hostname;
141              
142             # Copy hostname-specific stuff to root if it exists
143 0 0         if(defined($config->{hosts}->{$hname})) {
144 0           foreach my $key (keys %{$config->{hosts}->{$hname}}) {
  0            
145 0           $config->{$key} = $config->{hosts}->{$hname}->{$key};
146             }
147             }
148              
149 0           $self->{config} = $config;
150              
151 0 0         if(!defined($self->{config}->{throttle}->{maxsleep})) {
152 0           $self->{config}->{throttle}->{maxsleep} = 100;
153             }
154 0 0         if(!defined($self->{config}->{throttle}->{step})) {
155 0           $self->{config}->{throttle}->{step} = 10;
156             }
157              
158 0           $self->{usleep} = 0;
159              
160 0 0 0       if(!defined($self->{config}->{ssl}) ||
      0        
161             !defined($self->{config}->{ssl}->{cert}) ||
162             !defined($self->{config}->{ssl}->{key})) {
163 0           croak("Missing or incomplete SSL config!");
164             }
165 0 0         if(!-f $self->{config}->{ssl}->{cert}) {
166 0           croak("SSL cert file " . $self->{config}->{ssl}->{cert} . " not found!");
167             }
168 0 0         if(!-f $self->{config}->{ssl}->{key}) {
169 0           croak("SSL key file " . $self->{config}->{ssl}->{key} . " not found!");
170             }
171              
172 0 0         if(!defined($self->{config}->{username})) {
173 0           croak("Username not defined!");
174             }
175 0 0         if(!defined($self->{config}->{password})) {
176 0           croak("Password not defined!");
177             }
178 0           $self->{authtoken} = encode_base64($self->{config}->{username}, '') . ':' . encode_base64($self->{config}->{password}, '');
179              
180 0 0         if(defined($self->{config}->{persistancefile})) {
181 0           $self->{persistance} = 1;
182             } else {
183 0           $self->{persistance} = 0;
184             }
185              
186 0 0         if(!defined($self->{config}->{persistanceinterval})) {
187 0           $self->{persistanceinterval} = 10;
188             } else {
189 0           $self->{persistanceinterval} = $self->{config}->{persistanceinterval};
190             }
191              
192 0 0         if(!defined($self->{config}->{interclacksreconnecttimeout})) {
193 0           $self->{config}->{interclacksreconnecttimeout} = 30;
194             }
195              
196 0 0         if(!defined($self->{config}->{authtimeout})) {
197 0           $self->{config}->{authtimeout} = 15;
198             }
199              
200 0 0         if(!defined($self->{config}->{deletedcachetime})) {
201 0           $self->{config}->{deletedcachetime} = 60 * 60; # 1 hour
202             }
203 0 0         if(!defined($self->{config}->{stalecachetime})) {
204 0           $self->{config}->{stalecachetime} = 60 * 60 * 24; # 1 day
205             }
206              
207 0           my @tcpsockets;
208              
209 0 0         if(defined($config->{ip})) {
210 0 0         if(!defined($config->{port})) {
211 0           croak("At least one IP defined, but no TCP port!");
212             }
213 0           foreach my $ip (@{$config->{ip}}) {
  0            
214             my $tcp = IO::Socket::IP->new(
215             LocalHost => $ip,
216             LocalPort => $config->{port},
217 0 0         Listen => 1,
218             Blocking => 0,
219             ReuseAddr => 1,
220             Proto => 'tcp',
221             ) or croak($ERRNO);
222             #binmode($tcp, ':bytes');
223 0           push @tcpsockets, $tcp;
224 0           print "Listening on $ip:", $config->{port}, "/tcp\n";
225             }
226             }
227              
228 0 0 0       if(defined($config->{socket}) || defined($self->{config}->{master}->{socket})) {
229 0           my $udsloaded = 0;
230 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
231 0           require IO::Socket::UNIX;
232 0           $udsloaded = 1;
233             };
234 0 0         if(!$udsloaded) {
235 0           croak("Specified a unix domain socket, but i couldn't load IO::Socket::UNIX!");
236             }
237              
238             # Add the ClientID stuff to Unix domain sockets as well. We don't do this in the BEGIN{} block
239             # since we are not yet sure we are going to load IO::Socket::UNIX in the first place
240             {
241 1     1   7 no strict 'refs'; ## no critic (TestingAndDebugging::ProhibitNoStrict)
  1         3  
  1         9926  
  0            
242 0           *{"IO::Socket::UNIX::_setClientID"} = sub {
243 0     0     my ($self, $cid) = @_;
244            
245 0           ${*$self}{'__client_id'} = $cid; ## no critic (References::ProhibitDoubleSigils)
  0            
246 0           return;
247 0           };
248            
249 0           *{"IO::Socket::UNIX::_getClientID"} = sub {
250 0     0     my ($self) = @_;
251            
252 0   0       return ${*$self}{'__client_id'} || ''; ## no critic (References::ProhibitDoubleSigils)
253 0           };
254             }
255             }
256              
257 0 0         if(defined($config->{socket})) {
258 0           foreach my $socket (@{$config->{socket}}) {
  0            
259 0 0         if(-S $socket) {
260 0           print "Removing old unix domain socket file $socket\n";
261 0           unlink $socket;
262             }
263 0 0         my $tcp = IO::Socket::UNIX->new(
264             Type => SOCK_STREAM(),
265             Local => $socket,
266             Listen => 1,
267             #Blocking => 0,
268             ) or croak($ERRNO);
269 0           $tcp->blocking(0);
270             #binmode($tcp, ':bytes');
271 0           push @tcpsockets, $tcp;
272 0           print "Listening on Unix domain socket $socket\n";
273              
274 0 0 0       if(defined($config->{socketchmod}) && $config->{socketchmod} ne '') {
275 0           my $cmd = 'chmod ' . $config->{socketchmod} . ' ' . $socket;
276 0           print $cmd, "\n";
277 0           `$cmd`;
278             }
279             }
280             }
281              
282 0           $self->{tcpsockets} = \@tcpsockets;
283              
284              
285 0           print "Ready.\n";
286              
287              
288 0           return;
289             }
290              
291 0     0 0   sub loadPersistanceFile($self, $fname) {
  0            
  0            
  0            
292 0           my %clackscache;
293             my %clackscachetime;
294 0           my %clackscacheaccesstime;
295              
296 0 0         if(open(my $ifh, '<', $fname)) {
297 0           my $line = <$ifh>;
298 0           my $timestampline = <$ifh>;
299 0           my $accesstimeline = <$ifh>;
300 0           my $endline = <$ifh>;
301 0           my $needupgrade = 0;
302 0           close $ifh;
303              
304 0           chomp $line;
305 0           chomp $timestampline;
306 0           chomp $accesstimeline;
307              
308 0 0 0       if(!defined($endline) && $accesstimeline eq 'ENDBYTES') {
309 0           $endline = 'ENDBYTES';
310 0           $accesstimeline = '';
311 0           $needupgrade = 1;
312             } else {
313 0           chomp $endline;
314             }
315              
316 0 0 0       if(!defined($line) || !defined($timestampline) || $endline ne 'ENDBYTES') {
      0        
317 0           carp("Invalid persistance file " . $fname . "! File is incomplete!");
318 0           return; # Fail
319             }
320              
321 0           my $loadok = 0;
322              
323 0 0         if($line ne '') {
324 0           eval {
325 0           $line = decode_base64($line);
326 0           $line = Load($line);
327 0           $loadok = 1;
328             };
329 0 0         if(!$loadok) {
330 0           carp("Invalid persistance file " . $fname . "! Failed to decode data line!");
331 0           return; # Fail
332             }
333             }
334 0           %clackscache = %{$line};
  0            
335              
336             # Mark all data as current as a fallback
337 0           my $now = $self->getTime();
338 0           foreach my $key (keys %clackscache) {
339 0           $clackscachetime{$key} = $now;
340             }
341              
342 0 0         if($timestampline ne '') {
343 0           $loadok = 0;
344 0           eval {
345 0           $timestampline = decode_base64($timestampline);
346 0           $timestampline = Load($timestampline);
347 0           $loadok = 1;
348             };
349 0 0         if(!$loadok) {
350 0           carp("Invalid persistance file " . $fname . "! Failed to decode timestamp line, using current time!");
351 0           return; # Fail
352             } else {
353 0           my %clackstemp = %{$timestampline};
  0            
354 0           foreach my $key (keys %clackstemp) {
355 0           $clackscachetime{$key} = $clackstemp{$key};
356             }
357             }
358             }
359              
360 0 0         if($needupgrade) {
    0          
361 0           print "Pre-Version 22 persistance file detected. Upgrading automatically.\n";
362 0           foreach my $key (keys %clackscache) {
363 0           $clackscacheaccesstime{$key} = $now;
364             }
365             } elsif($accesstimeline ne '') {
366 0           $loadok = 0;
367 0           eval {
368 0           $accesstimeline = decode_base64($accesstimeline);
369 0           $accesstimeline = Load($accesstimeline);
370 0           $loadok = 1;
371             };
372 0 0         if(!$loadok) {
373 0           carp("Invalid persistance file " . $fname . "! Failed to decode timestamp line, using current time!");
374 0           return; # Fail
375             } else {
376 0           %clackscacheaccesstime = %{$accesstimeline};
  0            
377             }
378             }
379             } else {
380             # Fail
381 0           return;
382             }
383              
384 0           return \%clackscache, \%clackscachetime, \%clackscacheaccesstime;
385             }
386              
387              
388 0     0 1   sub run($self) { ## no critic (Subroutines::ProhibitExcessComplexity)
  0            
  0            
389 0           my $savecache = 0;
390 0           my $lastsavecache = 0;
391              
392             # Let STDOUT/STDERR settle down first
393 0           sleep(0.1);
394              
395             # Need to ignore SIGPIPE, this can screw us over in certain circumstances
396             # while writing to the network. We can only detect certain types of disconnects
397             # after writing to the socket, but we will get a SIGPIPE if we try. So we just
398             # ignore the signal and carry on as usual...
399 0           $SIG{PIPE} = 'IGNORE';
400              
401 0           my @toremove;
402             my @outbox;
403 0           my %clients;
404              
405 0           my $shutdowntime;
406 0           my $selector = IO::Select->new();
407 0           my $interclackslock = 0;
408 0           my $nextinterclackscheck = 0;
409              
410 0           my $keepRunning = 1;
411 0     0     $SIG{INT} = sub { $keepRunning = 0; };
  0            
412 0     0     $SIG{TERM} = sub { $keepRunning = 0; };
  0            
413              
414             # Restore persistance file if required
415 0 0         if($self->{persistance}) {
416 0           my $previousfname = $self->{config}->{persistancefile} . '_bck';
417 0           my $tempfname = $self->{config}->{persistancefile} . '_';
418 0           my $loadok = 0;
419 0 0         if(-f $self->{config}->{persistancefile}) {
420 0           print "Trying to load persistance file ", $self->{config}->{persistancefile}, "\n";
421 0           my ($cc, $cct, $cca) = $self->loadPersistanceFile($self->{config}->{persistancefile});
422 0 0 0       if(defined($cc) && ref $cc eq 'HASH') {
423 0           $self->{clackscache} = $cc;
424 0           $self->{clackscachetime} = $cct;
425 0           $self->{clackscacheaccesstime} = $cca;
426 0           $savecache = 1; # Force saving a new persistance file
427 0           $loadok = 1;
428             }
429             }
430              
431 0 0 0       if(!$loadok && -f $previousfname) {
432 0           print "Trying to load backup (previous) persistance file ", $previousfname, "\n";
433 0           my ($cc, $cct, $cca) = $self->loadPersistanceFile($previousfname);
434 0 0 0       if(defined($cc) && ref $cc eq 'HASH') {
435 0           $self->{clackscache} = $cc;
436 0           $self->{clackscachetime} = $cct;
437 0           $self->{clackscacheaccesstime} = $cca;
438 0           $savecache = 2; # Force saving a new persistance file plus a new backup
439 0           $loadok = 1;
440             }
441             }
442 0 0 0       if(!$loadok && -f $tempfname) {
443 0           print "Oh no. As a final, desperate solution, trying to load a 'temporary file while saving' persistance file ", $tempfname, "\n";
444 0           my ($cc, $cct, $cca) = $self->loadPersistanceFile($tempfname);
445 0 0 0       if(defined($cc) && ref $cc eq 'HASH') {
446 0           $self->{clackscache} = $cc;
447 0           $self->{clackscachetime} = $cct;
448 0           $self->{clackscacheaccesstime} = $cca;
449 0           $savecache = 2; # Force saving a new persistance file plus a new backup
450 0           $loadok = 1;
451             }
452             }
453              
454 0 0         if(!$loadok) {
455 0           print "Sorry, no valid persistance file found. Starting server 'blankety-blank'\n";
456 0           $savecache = 2;
457             } else {
458 0           print "Persistance file loaded\n";
459             }
460             }
461              
462 0           while($keepRunning) {
463 0           my $workCount = 0;
464              
465             # Check for shutdown time
466 0 0 0       if($shutdowntime && $shutdowntime < time) {
467 0           print STDERR "Shutdown time has arrived!\n";
468 0           $keepRunning = 0;
469             }
470              
471 0           my $now = $self->getTime();
472 0 0 0       if($savecache && $now > ($lastsavecache + $self->{persistanceinterval})) {
473 0           $lastsavecache = $now;
474 0           $self->savePersistanceFile($savecache);
475 0           $savecache = 0;
476             }
477              
478             # We are in client mode. We need to add an interclacks link
479 0 0 0       if(defined($self->{config}->{master}->{socket}) || defined($self->{config}->{master}->{ip})) {
480 0           my $mcid;
481 0 0         if(defined($self->{config}->{master}->{socket})) {
482 0           $mcid = 'unixdomainsocket:interclacksmaster';
483             } else {
484 0           $mcid = $self->{config}->{master}->{ip}->[0] . ':' . $self->{config}->{master}->{port};
485             }
486 0 0 0       if(!defined($clients{$mcid}) && $nextinterclackscheck < $now) {
487 0           $nextinterclackscheck = $now + $self->{config}->{interclacksreconnecttimeout} + int(rand(10));
488              
489 0           print "Connect to master\n";
490 0           my $msocket;
491              
492 0 0         if(defined($self->{config}->{master}->{socket})) {
493             $msocket = IO::Socket::UNIX->new(
494 0           Peer => $self->{config}->{master}->{socket}->[0],
495             Type => SOCK_STREAM,
496             );
497             } else {
498             $msocket = IO::Socket::IP->new(
499             PeerHost => $self->{config}->{master}->{ip}->[0],
500             PeerPort => $self->{config}->{master}->{port},
501 0           Type => SOCK_STREAM,
502             Timeout => 5,
503             );
504             }
505 0 0         if(!defined($msocket)) {
506 0           print STDERR "Can't connect to MASTER via interclacks!\n";
507             } else {
508 0           print "connected to master\n";
509              
510 0 0         if(ref $msocket ne 'IO::Socket::UNIX') {
511             # ONLY USE SSL WHEN RUNNING OVER THE NETWORK
512             # There is simply no point in running it over a local socket.
513 0           my $encrypted = IO::Socket::SSL->start_SSL($msocket,
514             SSL_verify_mode => SSL_VERIFY_NONE,
515             );
516 0 0         if(!$encrypted) {
517 0           print "startSSL failed: ", $SSL_ERROR, "\n";
518 0           next;
519             }
520             }
521              
522 0           $msocket->blocking(0);
523             #binmode($msocket, ':bytes');
524             my %tmp = (
525             buffer => '',
526             charbuffer => [],
527             listening => {},
528             socket => $msocket,
529             lastping => $now,
530             mirror => 0,
531             outbuffer => "CLACKS PageCamel $VERSION in interclacks client mode\r\n" . # Tell the server we are using PageCamel Interclacks...
532             "OVERHEAD A " . $self->{authtoken} . "\r\n" . # ...send Auth token
533             "OVERHEAD I 1\r\n", # ...and turn interclacks master mode ON on remote side
534             clientinfo => 'Interclacks link',
535             client_timeoffset => 0,
536             interclacks => 1,
537             interclacksclient => 1,
538             lastinterclacksping => $now,
539             lastmessage => $now,
540             authtimeout => $now + $self->{config}->{authtimeout},
541 0           authok => 0,
542             failcount => 0,
543             outmessages => [],
544             inmessages => [],
545             messagedelay => 0,
546             inmessagedelay => 0,
547             outmessagedelay => 0,
548             );
549              
550 0 0         if(defined($self->{config}->{master}->{ip})) {
551 0           $tmp{host} = $self->{config}->{master}->{ip}->[0];
552 0           $tmp{port} = $self->{config}->{master}->{port};
553             }
554 0           $clients{$mcid} = \%tmp;
555 0           $msocket->_setClientID($mcid);
556 0           $selector->add($msocket);
557              
558 0           $workCount++;
559             }
560             }
561             }
562              
563 0           foreach my $tcpsocket (@{$self->{tcpsockets}}) {
  0            
564 0           my $clientsocket = $tcpsocket->accept;
565 0 0         if(defined($clientsocket)) {
566 0           $clientsocket->blocking(0);
567 0           my ($cid, $chost, $cport);
568 0 0         if(ref $tcpsocket eq 'IO::Socket::UNIX') {
569 0           $chost = 'unixdomainsocket';
570 0           $cport = $now . ':' . int(rand(1_000_000));
571             } else {
572 0           ($chost, $cport) = ($clientsocket->peerhost, $clientsocket->peerport);
573             }
574 0           print "Got a new client $chost:$cport!\n";
575 0           $cid = "$chost:$cport";
576 0           foreach my $debugcid (keys %clients) {
577 0 0         if($clients{$debugcid}->{mirror}) {
578 0           $clients{$debugcid}->{outbuffer} .= "DEBUG CONNECTED=" . $cid . "\r\n";
579             }
580             }
581              
582 0 0         if(ref $clientsocket ne 'IO::Socket::UNIX') {
583             # ONLY USE SSL WHEN RUNNING OVER THE NETWORK
584             # There is simply no point in running it over a local socket.
585             my $encrypted = IO::Socket::SSL->start_SSL($clientsocket,
586             SSL_server => 1,
587             SSL_cert_file => $self->{config}->{ssl}->{cert},
588             SSL_key_file => $self->{config}->{ssl}->{key},
589             SSL_cipher_list => 'ALL:!ADH:!RC4:+HIGH:+MEDIUM:!LOW:!SSLv2:!SSLv3!EXPORT',
590             SSL_create_ctx_callback => sub {
591 0     0     my $ctx = shift;
592              
593             # Enable workarounds for broken clients
594 0           Net::SSLeay::CTX_set_options($ctx, &Net::SSLeay::OP_ALL); ## no critic (Subroutines::ProhibitAmpersandSigils)
595              
596             # Disable session resumption completely
597 0           Net::SSLeay::CTX_set_session_cache_mode($ctx, $SSL_SESS_CACHE_OFF);
598              
599             # Disable session tickets
600 0           Net::SSLeay::CTX_set_options($ctx, &Net::SSLeay::OP_NO_TICKET); ## no critic (Subroutines::ProhibitAmpersandSigils)
601             },
602 0           );
603 0 0         if(!$encrypted) {
604 0           print "startSSL failed: ", $SSL_ERROR, "\n";
605 0           next;
606             }
607             }
608              
609 0           $clientsocket->blocking(0);
610             #binmode($clientsocket, ':bytes');
611             #$clientsocket->{clacks_cid} = $cid;
612             my %tmp = (
613             buffer => '',
614             charbuffer => [],
615             listening => {},
616             socket => $clientsocket,
617             lastping => $now,
618             mirror => 0,
619             outbuffer => "CLACKS PageCamel $VERSION\r\n" .
620             "OVERHEAD M Authentication required\r\n", # Informal message
621             clientinfo => 'UNKNOWN',
622             client_timeoffset => 0,
623             host => $chost,
624             port => $cport,
625             interclacks => 0,
626             interclacksclient => 0,
627             lastinterclacksping => 0,
628             lastmessage => $now,
629             authtimeout => $now + $self->{config}->{authtimeout},
630 0           authok => 0,
631             failcount => 0,
632             outmessages => [],
633             inmessages => [],
634             inmessagedelay => 0,
635             outmessagedelay => 0,
636             );
637 0           if(0 && $self->{isDebugging}) {
638             $tmp{authok} = 1;
639             $tmp{outbuffer} .= "OVERHEAD M debugmode_auth_not_really_required\r\n"
640             }
641 0           $clients{$cid} = \%tmp;
642 0           $clientsocket->_setClientID($cid);
643 0           $selector->add($clientsocket);
644 0           $workCount++;
645             }
646             }
647              
648             # Check if there are any clients to disconnect...
649              
650 0           my $pingtime = $now - $self->{config}->{pingtimeout};
651 0           my $interclackspingtime = $now - $self->{config}->{interclackspingtimeout};
652 0           my $interclackspinginterval = $now - int($self->{config}->{interclackspingtimeout} / 3);
653 0           foreach my $cid (keys %clients) {
654 0 0         if(!$clients{$cid}->{socket}->connected) {
655 0           push @toremove, $cid;
656 0           next;
657             }
658 0 0         if(!$clients{$cid}->{interclacks}) {
659 0 0 0       if($clients{$cid}->{lastping} > 0 && $clients{$cid}->{lastping} < $pingtime) {
660 0           $self->evalsyswrite($clients{$cid}->{socket}, "\r\nTIMEOUT\r\n");
661 0           push @toremove, $cid;
662 0           next;
663             }
664             } else {
665 0 0         if($clients{$cid}->{lastping} < $interclackspingtime) {
666 0           $self->evalsyswrite($clients{$cid}->{socket}, "\r\nTIMEOUT\r\n");
667 0           push @toremove, $cid;
668 0           next;
669             }
670             }
671              
672 0 0 0       if($clients{$cid}->{interclacks} && $clients{$cid}->{lastinterclacksping} < $interclackspinginterval) {
673 0           $clients{$cid}->{lastinterclacksping} = $now;
674 0           $clients{$cid}->{outbuffer} .= "PING\r\n";
675             }
676              
677 0 0 0       if(!$clients{$cid}->{authok} && $clients{$cid}->{authtimeout} < $now) {
678             # Authentication timeout!
679 0           push @toremove, $cid;
680             }
681             }
682              
683             # ...and disconnect them
684 0           while((my $cid = shift @toremove)) {
685             # In some circumstances, there may be multiple @toremove entries for the same client. Ignore them...
686 0 0         if(defined($clients{$cid})) {
687 0           print "Removing client $cid\n";
688 0           foreach my $debugcid (keys %clients) {
689 0 0         if($clients{$debugcid}->{mirror}) {
690 0           $clients{$debugcid}->{outbuffer} .= "DEBUG DISCONNECTED=" . $cid . "\r\n";
691             }
692             }
693              
694 0 0 0       if($clients{$cid}->{interclacksclient} && $interclackslock) {
695 0           print "...this one is interclacks master and has us locked - UNLOCKING mid-sync!\n";
696 0           $interclackslock = 0;
697             }
698              
699 0           $selector->remove($clients{$cid}->{socket});
700 0           delete $clients{$cid};
701             }
702              
703 0           $workCount++;
704             }
705              
706 0 0         if(!(scalar keys %clients)) {
707             # No clients to handle, let's sleep and try again later
708 0           sleep(0.1);
709 0           next;
710             }
711              
712              
713 0           my $hasoutbufferwork = 0;
714 0           foreach my $cid (keys %clients) {
715 0 0         if(length($clients{$cid}->{buffer}) > 0) {
716             # Found some work to do
717 0           $hasoutbufferwork = 1;
718 0           last;
719             }
720             }
721 0           my $selecttimeout = 0.5; # Half a second
722 0 0         if($hasoutbufferwork) {
723 0           $selecttimeout = 0.05;
724             }
725              
726 0           my @inclients = $selector->can_read($selecttimeout);
727 0           foreach my $clientsocket (@inclients) {
728 0           my $cid = $clientsocket->_getClientID();
729              
730 0           my $totalread = 0;
731 0           my $readchunksleft = 3;
732 0           while(1) {
733 0           my $rawbuffer;
734 0           my $readok = 0;
735 0           eval {
736 0           sysread($clients{$cid}->{socket}, $rawbuffer, 1_000_000); # Read at most 1 Meg at a time
737 0           $readok = 1;
738             };
739 0 0         if(!$readok) {
740 0           push @toremove, $cid;
741 0           last;
742             }
743 0 0 0       if(defined($rawbuffer) && length($rawbuffer)) {
744 0           $totalread += length($rawbuffer);
745 0           push @{$clients{$cid}->{charbuffer}}, split//, $rawbuffer;
  0            
746 0           $readchunksleft--;
747 0 0         if(!$readchunksleft) {
748 0           last;
749             }
750 0           next;
751             }
752 0           last;
753             }
754            
755             # Check if we could read data from a socket that was marked as readable.
756             # Thanks to SSL, this might ocxasionally fail. Don't bail out at the first
757             # error, only if multiple happen one after the other
758 0 0         if($totalread) {
759 0           $clients{$cid}->{failcount} = 0;
760             } else {
761 0           $clients{$cid}->{failcount}++;
762            
763 0 0         if($clients{$cid}->{failcount} > 5) {
764             # Socket was active multiple times but delivered no data?
765             # EOF, maybe, possible, perhaps?
766 0           push @toremove, $cid;
767             }
768             }
769             }
770              
771 0           foreach my $cid (keys %clients) {
772 0           while(@{$clients{$cid}->{charbuffer}}) {
  0            
773 0           my $buf = shift @{$clients{$cid}->{charbuffer}};
  0            
774              
775 0           $workCount++;
776 0 0         if($buf eq "\r") {
    0          
777 0           next;
778             } elsif($buf eq "\n") {
779 0 0         next if($clients{$cid}->{buffer} eq ''); # Empty lines
780              
781             my %inmsg = (
782             message => $clients{$cid}->{buffer},
783             releasetime => $now + $clients{$cid}->{inmessagedelay},
784 0           );
785 0           push @{$clients{$cid}->{inmessages}}, \%inmsg;
  0            
786 0           $clients{$cid}->{buffer} = '';
787             } else {
788 0           $clients{$cid}->{buffer} .= $buf;
789             }
790             }
791              
792 0 0 0       if($interclackslock && !$clients{$cid}->{interclacksclient}) {
793             # We are locked into interclacks sync lock, but this is not the connection to master,
794             # so we don't handle the input buffer for this client at the moment.
795 0           next;
796             }
797              
798              
799             # ******************************************************************************
800             # ******************************************************************************
801             # ******************************************************************************
802             # ******************************************************************************
803             # ******************************************************************************
804             # ******************************************************************************
805             # ******************************************************************************
806 0           while(scalar @{$clients{$cid}->{inmessages}}) {
  0            
807 0 0         last if($clients{$cid}->{inmessages}->[0]->{releasetime} > $now);
808 0           my $inmsgtmp = shift @{$clients{$cid}->{inmessages}};
  0            
809 0           my $inmsg = $inmsgtmp->{message};
810              
811             # Handle CLACKS identification header
812 0 0         if($inmsg =~ /^CLACKS\ (.+)/) {
813 0           $clients{$cid}->{clientinfo} = $1;
814 0           $clients{$cid}->{clientinfo} =~ s/\;/\_/g;
815 0           print "Client at ", $cid, " identified as ", $clients{$cid}->{clientinfo}, "\n";
816 0           next;
817             }
818              
819 0           my $nodebug = 0;
820 0           my $sendinterclacks = 1;
821 0           my $discardafterlogging = 0;
822             # Handle OVERHEAD messages before logging (for handling 'N' flag correctly)
823 0 0         if($inmsg =~ /^OVERHEAD\ (.+?)\ (.+)/) {
824 0           my ($flags, $value) = ($1, $2);
825 0           $sendinterclacks = 0;
826 0           my @flagparts = split//, $flags;
827 0           my %parsedflags;
828             my %newflags;
829 0           foreach my $key (sort keys %overheadflags) {
830 0 0         if(contains($key, \@flagparts)) {
831 0           $parsedflags{$overheadflags{$key}} = 1;
832 0           $newflags{$overheadflags{$key}} = 1;
833             } else {
834 0           $parsedflags{$overheadflags{$key}} = 0;
835 0           $newflags{$overheadflags{$key}} = 0;
836             }
837             }
838              
839 0 0         if($parsedflags{auth_token}) {
840 0 0         if($value eq $self->{authtoken}) {
841 0           $clients{$cid}->{authok} = 1;
842             #$clients{$cid}->{outbuffer} .= "OVERHEAD O Welcome!\r\n";
843 0           push @{$clients{$cid}->{outmessages}}, {releasetime => $now + $clients{$cid}->{outmessagedelay}, message => 'OVERHEAD O Welcome!'};
  0            
844             } else {
845 0           $clients{$cid}->{authok} = 0;
846             #$clients{$cid}->{outbuffer} .= "OVERHEAD F Login failed!\r\n";
847 0           push @{$clients{$cid}->{outmessages}}, {releasetime => $now + $clients{$cid}->{outmessagedelay}, message => 'OVERHEAD F Login failed!'};
  0            
848 0           push @{$clients{$cid}->{outmessages}}, {releasetime => $now + $clients{$cid}->{outmessagedelay}, message => 'EXIT'};
  0            
849 0           push @toremove, $cid; # Disconnect the client
850 0           last;
851             }
852             }
853              
854             # Ignore other command when not authenticated
855 0 0         if(!$clients{$cid}->{authok}) {
856 0           next;
857             }
858              
859 0 0         if($parsedflags{timestamp}) {
860 0           $now = $self->getTime(); # Make sure we are at the "latest" $now. This is one of the very few critical sections
861 0           $clients{$cid}->{client_timeoffset} = $now - $value;
862 0           print "**** CLIENT TIME OFFSET: ", $clients{$cid}->{client_timeoffset}, "\n";
863 0           next;
864             }
865              
866 0 0 0       if($parsedflags{lock_for_sync} && $clients{$cid}->{interclacksclient}) {
867 0 0         if($value) {
868 0           print "Interclacks sync lock ON.\n";
869 0           $interclackslock = 1;
870             } else {
871 0           print "Interclacks sync lock OFF.\n";
872 0           $interclackslock = 0;
873              
874             # Send server our keys AFTER we got everything FROM the server (e.g. after unlock)
875 0           $clients{$cid}->{outbuffer} .= "OVERHEAD T " . $self->getTime() . "\r\n"; # Send local time to server for offset calculation
876 0           foreach my $ckey (sort keys %{$self->{clackscache}}) {
  0            
877 0           $clients{$cid}->{outbuffer} .= "KEYSYNC " . $self->{clackscachetime}->{$ckey} . " " . $self->{clackscacheaccesstime}->{$ckey} . " U $ckey=" . $self->{clackscache}->{$ckey} . "\r\n";
878             }
879 0           foreach my $ckey (sort keys %{$self->{clackscachetime}}) {
  0            
880 0 0         next if(defined($self->{clackscache}->{$ckey}));
881 0           $clients{$cid}->{outbuffer} .= "KEYSYNC " . $self->{clackscachetime}->{$ckey} . " 0 D $ckey=REMOVED\r\n";
882             }
883             }
884 0           $parsedflags{forward_message} = 0; # Don't forward
885 0           $newflags{return_to_sender} = 0; # Don't return to sender
886             }
887              
888 0 0 0       if($parsedflags{close_all_connections} && $value) {
889 0           foreach my $closecid (keys %clients) {
890 0 0 0       if($clients{$closecid}->{interclacks} && $parsedflags{forward_message}) {
891 0           $self->evalsyswrite($clients{$closecid}->{socket}, "\r\nOVERHEAD GC 1\r\n");
892             }
893 0           $self->evalsyswrite($clients{$closecid}->{socket}, "\r\nQUIT\r\n");
894 0           push @toremove, $closecid;
895             }
896 0           $parsedflags{forward_message} = 0; # Already forwarded where needed
897             }
898              
899 0 0         if($parsedflags{shutdown_service}) {
900 0           $value = 0 + $value;
901 0 0         if($value > 0) {
902 0           $shutdowntime = $value + $now;
903 0           print STDERR "Shutting down in $value seconds\n";
904             }
905             }
906 0 0         if($parsedflags{discard_message}) {
907 0           $discardafterlogging = 1;
908             }
909 0 0         if($parsedflags{no_logging}) {
910 0           $nodebug = 1;
911             }
912              
913 0 0         if($parsedflags{error_message}) {
914 0           print STDERR 'ERROR from ', $cid, ': ', $value, "\n";
915             }
916              
917 0 0         if($parsedflags{set_interclacks_mode}) {
918 0           $newflags{forward_message} = 0;
919 0           $newflags{return_to_sender} = 0;
920              
921 0 0         if($value) {
922 0           $clients{$cid}->{interclacks} = 1;
923 0           $clients{$cid}->{lastping} = $now;
924              
925              
926             $clients{$cid}->{outbuffer} .= "CLACKS PageCamel $VERSION in interclacks master mode\r\n" . # Tell client we are in interclacks master mode
927             "OVERHEAD M Authentication required\r\n" . # Informal message
928 0           "OVERHEAD A " . $self->{authtoken} . "\r\n" . # ...and send Auth token...
929             "OVERHEAD L 1\r\n" . # ...and lock client for sync
930             "OVERHEAD T " . time . "\r\n"; # ... and send local timestamp
931              
932             # Make sure our new interclacks client has an *exact* copy of our buffer
933             #$clients{$cid}->{outbuffer} .= "CLEARCACHE\r\n";
934 0           foreach my $ckey (sort keys %{$self->{clackscache}}) {
  0            
935 0           $clients{$cid}->{outbuffer} .= "KEYSYNC " . $self->{clackscachetime}->{$ckey} . " " . $self->{clackscacheaccesstime}->{$ckey} . " U $ckey=" . $self->{clackscache}->{$ckey} . "\r\n";
936             }
937 0           foreach my $ckey (sort keys %{$self->{clackscachetime}}) {
  0            
938 0 0         next if(defined($self->{clackscache}->{$ckey}));
939 0           $clients{$cid}->{outbuffer} .= "KEYSYNC " . $self->{clackscachetime}->{$ckey} . " 0 D $ckey=REMOVED\r\n";
940             }
941 0           $clients{$cid}->{outbuffer} .= "OVERHEAD L 0\r\n"; # unlock client after sync
942 0           $clients{$cid}->{outbuffer} .= "PING\r\n";
943 0           $clients{$cid}->{lastinterclacksping} = $now;
944             } else {
945 0           $clients{$cid}->{interclacks} = 0;
946 0           $clients{$cid}->{lastping} = $now;
947             }
948             }
949              
950 0           my $newflagstring = '';
951 0           $newflags{return_to_sender} = 0;
952              
953 0           foreach my $key (sort keys %overheadflags) {
954 0 0         next if($key eq 'Z');
955 0 0         if($newflags{$overheadflags{$key}}) {
956 0           $newflagstring .= $key;
957             }
958             }
959 0 0         if($newflagstring eq '') {
960 0           $newflagstring = 'Z';
961             }
962              
963 0 0         if($parsedflags{forward_message}) {
964 0           foreach my $overheadcid (keys %clients) {
965 0 0 0       next if($cid eq $overheadcid && !$parsedflags{return_to_sender});
966              
967 0           $clients{$overheadcid}->{outbuffer} .= "OVERHEAD $newflagstring $value\r\n";
968             }
969             }
970             }
971              
972             # Ignore other command when not authenticated
973 0 0         if(!$clients{$cid}->{authok}) {
974 0           next;
975             }
976              
977 0 0         if(!$nodebug) {
978             # Add ALL incoming messages as debug-type messages to the outbox
979 0           my %tmp = (
980             sender => $cid,
981             type => 'DEBUG',
982             data => $inmsg,
983             );
984              
985 0           push @outbox, \%tmp;
986             }
987              
988 0 0         if($discardafterlogging) {
989 0           next;
990             }
991              
992              
993 0 0 0       if($inmsg =~ /^OVERHEAD\ /) { ## no critic (ControlStructures::ProhibitCascadingIfElse)
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
994             # Already handled
995             } elsif($inmsg =~ /^LISTEN\ (.*)/) {
996 0           $clients{$cid}->{listening}->{$1} = 1;
997 0           $sendinterclacks = 0;
998             } elsif($inmsg =~ /^UNLISTEN\ (.*)/) {
999 0           delete $clients{$cid}->{listening}->{$1};
1000 0           $sendinterclacks = 0;
1001             } elsif($inmsg =~ /^MONITOR/) {
1002 0           $clients{$cid}->{mirror} = 1;
1003 0           $sendinterclacks = 0;
1004             } elsif($inmsg =~ /^UNMONITOR/) {
1005 0           $clients{$cid}->{mirror} = 0;
1006 0           $sendinterclacks = 0;
1007             } elsif($inmsg =~ /^QUIT/) {
1008 0           print STDERR "Client disconnected cleanly!\n";
1009 0           push @toremove, $cid;
1010 0           $sendinterclacks = 0;
1011             } elsif($inmsg =~ /^TIMEOUT/ && $clients{$cid}->{interclacks}) {
1012 0           print STDERR "Ooops, didn't send timely PINGS through interclacks link!\n";
1013 0           push @toremove, $cid;
1014 0           $sendinterclacks = 0;
1015             } elsif($inmsg =~ /^PING/) {
1016 0           $clients{$cid}->{lastping} = $now;
1017 0           $sendinterclacks = 0;
1018             } elsif($inmsg =~ /^NOPING/) {
1019             # Disable PING check until next PING recieved
1020 0           $clients{$cid}->{lastping} = 0;
1021 0           $sendinterclacks = 0;
1022             } elsif($inmsg =~ /^NOTIFY\ (.*)/) {
1023 0           my %tmp = (
1024             sender => $cid,
1025             type => 'NOTIFY',
1026             name => $1,
1027             );
1028 0           push @outbox, \%tmp;
1029             } elsif($inmsg =~ /^SET\ (.+?)\=(.*)/) {
1030 0           my %tmp = (
1031             sender => $cid,
1032             type => 'SET',
1033             name => $1,
1034             value => $2,
1035             );
1036 0           push @outbox, \%tmp;
1037             } elsif($inmsg =~ /^KEYSYNC\ (.+?)\ (.+?)\ (.+?)\ (.+?)\=(.*)/) {
1038             #print "***** ", $inmsg, "\n";
1039 0           my ($ctimestamp, $atimestamp, $cmode, $ckey, $cval) = ($1, $2, $3, $4, $5);
1040 0           $clients{$cid}->{lastping} = $now; # KEYSYNC acts as a PING as well
1041              
1042 0           $ctimestamp += $clients{$cid}->{client_timeoffset}; # Take client time offset into account
1043 0 0         if($atimestamp) {
1044 0           $atimestamp += $clients{$cid}->{client_timeoffset}; # Take client time offset into account
1045             }
1046              
1047 0 0         if(!defined($self->{clackscachetime}->{$ckey})) {
1048 0           $self->{clackscachetime}->{$ckey} = 0;
1049             }
1050 0 0 0       if(!defined($self->{clackscachetime}->{$ckey}) || $ctimestamp > $self->{clackscachetime}->{$ckey}) {
1051             # If *we* have the older entry (or none at all), *only* then work on the keysync command
1052 0 0         if($cmode eq "U") { # "Update"
1053 0           $self->{clackscache}->{$ckey} = $cval;
1054 0           $self->{clackscachetime}->{$ckey} = $ctimestamp;
1055 0           $self->{clackscacheaccesstime}->{$ckey} = $atimestamp;
1056             } else { # REMOVE request from server
1057 0           $self->{clackscachetime}->{$ckey} = $ctimestamp;
1058 0 0         if(defined($self->{clackscache}->{$ckey})) {
1059 0           delete $self->{clackscache}->{$ckey};
1060             }
1061 0 0         if(defined($self->{clackscacheaccesstime}->{$ckey})) {
1062 0           delete $self->{clackscacheaccesstime}->{$ckey};
1063             }
1064             }
1065             }
1066              
1067 0           $savecache = 1;
1068 0           $sendinterclacks = 1;
1069             } elsif($inmsg =~ /^STORE\ (.+?)\=(.*)/) {
1070 0           $self->{clackscache}->{$1} = $2;
1071 0           $self->{clackscachetime}->{$1} = $now;
1072 0           $self->{clackscacheaccesstime}->{$1} = $now;
1073 0           $savecache = 1;
1074             } elsif($inmsg =~ /^SETANDSTORE\ (.+?)\=(.*)/) {
1075 0           my %tmp = (
1076             sender => $cid,
1077             type => 'SETANDSTORE',
1078             name => $1,
1079             value => $2,
1080             );
1081 0           push @outbox, \%tmp;
1082 0           $self->{clackscache}->{$tmp{name}} = $tmp{value};
1083 0           $self->{clackscachetime}->{$tmp{name}} = $now;
1084 0           $self->{clackscacheaccesstime}->{$tmp{name}} = $now;
1085 0           $savecache = 1;
1086             } elsif($inmsg =~ /^RETRIEVE\ (.+)/) {
1087             #$clients{$cid}->{outbuffer} .= "SET ". $line->{name} . "=" . $line->{value} . "\r\n";
1088 0           my $ckey = $1;
1089 0 0         if(defined($self->{clackscache}->{$ckey})) {
1090 0           $clients{$cid}->{outbuffer} .= "RETRIEVED $ckey=" . $self->{clackscache}->{$ckey} . "\r\n";
1091 0           $self->{clackscacheaccesstime}->{$ckey} = $now;
1092 0           $savecache = 1;
1093             } else {
1094 0           $clients{$cid}->{outbuffer} .= "NOTRETRIEVED $ckey\r\n";
1095             }
1096 0           $sendinterclacks = 0;
1097             } elsif($inmsg =~ /^REMOVE\ (.+)/) {
1098 0           my $ckey = $1;
1099 0 0         if(defined($self->{clackscache}->{$ckey})) {
1100 0           delete $self->{clackscache}->{$ckey};
1101 0           $self->{clackscachetime}->{$ckey} = $now;
1102             }
1103 0 0         if(defined($self->{clackscacheaccesstime}->{$ckey})) {
1104 0           delete $self->{clackscacheaccesstime}->{$ckey};
1105             }
1106 0           $savecache = 1;
1107             } elsif($inmsg =~ /^INCREMENT\ (.+)/) {
1108 0           my $ckey = $1;
1109 0           my $cval = 1;
1110 0 0         if($ckey =~ /(.+)\=(.+)/) {
1111 0           ($ckey, $cval) = ($1, $2);
1112 0           $cval = 0 + $cval;
1113             }
1114 0 0         if(defined($self->{clackscache}->{$ckey})) {
1115 0           $self->{clackscache}->{$ckey} += $cval;
1116             } else {
1117 0           $self->{clackscache}->{$ckey} = $cval;
1118             }
1119 0           $self->{clackscachetime}->{$ckey} = $now;
1120 0           $self->{clackscacheaccesstime}->{$ckey} = $now;
1121 0           $savecache = 1;
1122             } elsif($inmsg =~ /^DECREMENT\ (.+)/) {
1123 0           my $ckey = $1;
1124 0           my $cval = 1;
1125 0 0         if($ckey =~ /(.+)\=(.+)/) {
1126 0           ($ckey, $cval) = ($1, $2);
1127 0           $cval = 0 + $cval;
1128             }
1129 0 0         if(defined($self->{clackscache}->{$ckey})) {
1130 0           $self->{clackscache}->{$ckey} -= $cval;
1131             } else {
1132 0           $self->{clackscache}->{$ckey} = 0 - $cval;
1133             }
1134 0           $self->{clackscachetime}->{$ckey} = $now;
1135 0           $self->{clackscacheaccesstime}->{$ckey} = $now;
1136 0           $savecache = 1;
1137             } elsif($inmsg =~ /^KEYLIST/) {
1138 0           $clients{$cid}->{outbuffer} .= "KEYLISTSTART\r\n";
1139 0           foreach my $ckey (sort keys %{$self->{clackscache}}) {
  0            
1140 0           $clients{$cid}->{outbuffer} .= "KEY $ckey\r\n";
1141             }
1142 0           $clients{$cid}->{outbuffer} .= "KEYLISTEND\r\n";
1143 0           $sendinterclacks = 0;
1144             } elsif($inmsg =~ /^CLEARCACHE/) {
1145 0           %{$self->{clackscache}} = ();
  0            
1146 0           %{$self->{clackscachetime}} = ();
  0            
1147 0           %{$self->{clackscacheaccesstime}} = ();
  0            
1148 0           $savecache = 1;
1149              
1150             # local managment commands
1151             } elsif($inmsg =~ /^CLIENTLIST/) {
1152 0           $clients{$cid}->{outbuffer} .= "CLIENTLISTSTART\r\n";
1153 0           foreach my $lmccid (sort keys %clients) {
1154             $clients{$cid}->{outbuffer} .= "CLIENT CID=$lmccid;" .
1155             "HOST=" . $clients{$lmccid}->{host} . ";" .
1156             "PORT=" . $clients{$lmccid}->{port} . ";" .
1157             "CLIENTINFO=" . $clients{$lmccid}->{clientinfo} . ";" .
1158             "OUTBUFFER_LENGTH=" . length($clients{$lmccid}->{outbuffer}) . ";" .
1159             "INBUFFER_LENGTH=" . length($clients{$lmccid}->{buffer}) . ";" .
1160             "INTERCLACKS=" . $clients{$lmccid}->{interclacks} . ";" .
1161             "MONITOR=" . $clients{$lmccid}->{mirror} . ";" .
1162             "LASTPING=" . $clients{$lmccid}->{lastping} . ";" .
1163 0           "LASTINTERCLACKSPING=" . $clients{$lmccid}->{lastinterclacksping} . ";" .
1164             "\r\n";
1165             }
1166 0           $clients{$cid}->{outbuffer} .= "CLIENTLISTEND\r\n";
1167 0           $sendinterclacks = 0;
1168             } elsif($inmsg =~ /^CLIENTDISCONNECT\ (.+)/) {
1169 0           my $lmccid = $1;
1170 0 0         if(defined($clients{$lmccid})) {
1171             # Try to notify the client (may or may not work);
1172 0           $self->evalsyswrite($clients{$lmccid}->{socket}, "\r\nQUIT\r\n");
1173 0           push @toremove, $lmccid;
1174             }
1175 0           $sendinterclacks = 0;
1176             } elsif($inmsg =~ /^FLUSH\ (.+)/) {
1177 0           my $retid = $1;
1178 0           $clients{$cid}->{outbuffer} .= "FLUSHED $retid\r\n";
1179 0           $sendinterclacks = 0;
1180             } else {
1181 0           print STDERR "ERROR Unknown_command ", $inmsg, "\r\n";
1182 0           $sendinterclacks = 0;
1183 0           $clients{$cid}->{outbuffer} .= "OVERHEAD E unknown_command " . $inmsg . "\r\n";
1184             }
1185              
1186             # forward interclacks messages
1187 0 0         if($sendinterclacks) {
1188 0           foreach my $interclackscid (keys %clients) {
1189 0 0 0       if($cid eq $interclackscid || !$clients{$interclackscid}->{interclacks}) {
1190 0           next;
1191             }
1192 0           $clients{$interclackscid}->{outbuffer} .= $inmsg . "\r\n";
1193             }
1194             }
1195              
1196             }
1197              
1198             }
1199              
1200             # clean up very old "deleted" entries
1201 0           my $stillvalidtime = $now - $self->{config}->{deletedcachetime};
1202 0           foreach my $key (keys %{$self->{clackscachetime}}) {
  0            
1203 0 0         next if($self->{clackscachetime}->{$key} > $stillvalidtime);
1204 0 0         if(defined($self->{clackscache}->{$key})) { # Still has data? Fix clackscachetime entry
1205 0           $self->{clackscachetime}->{$key} = $now;
1206             }
1207 0           delete $self->{clackscachetime}->{$key};
1208 0           $savecache = 1;
1209             }
1210              
1211             # Clean up (forget) stale cached entries
1212 0           $stillvalidtime = $now - $self->{config}->{stalecachetime};
1213 0           foreach my $key (keys %{$self->{clackscacheaccesstime}}) {
  0            
1214 0 0         next if($self->{clackscacheaccesstime}->{$key} > $stillvalidtime);
1215 0           delete $self->{clackscacheaccesstime}->{$key};
1216 0 0         if(defined($self->{clackscache})) {
1217 0           delete $self->{clackscache}->{$key};
1218             }
1219 0 0         if(defined($self->{clackscachetime})) {
1220 0           delete $self->{clackscachetime}->{$key};
1221             }
1222              
1223 0           my %tmp = (
1224             sender => 'SERVERCACHE',
1225             type => 'DEBUG',
1226             data => 'FORGET=' . $key,
1227             );
1228              
1229 0           push @outbox, \%tmp;
1230             }
1231              
1232              
1233             # Outbox contains the messages that have to be forwarded to the clients when listening (or when the connection is in interclacks mode)
1234             # We iterate over the outbox and put those messages into the output buffers of the corresponding client connection
1235 0           while((my $line = shift @outbox)) {
1236 0           $workCount++;
1237 0           foreach my $cid (keys %clients) {
1238 0 0 0       if($line->{type} eq 'DEBUG' && $clients{$cid}->{mirror}) {
1239 0           $clients{$cid}->{outbuffer} .= "DEBUG " . $line->{sender} . "=". $line->{data} . "\r\n";
1240             }
1241              
1242 0 0         if($cid eq $line->{sender}) {
1243 0           next;
1244             }
1245              
1246 0 0 0       if($line->{type} ne 'DEBUG' && defined($clients{$cid}->{listening}->{$line->{name}})) {
1247             # Just buffer in the clients outbuffers
1248 0 0         if($line->{type} eq 'NOTIFY') {
    0          
    0          
1249 0           $clients{$cid}->{outbuffer} .= "NOTIFY ". $line->{name} . "\r\n";
1250             } elsif($line->{type} eq 'SET') {
1251 0           $clients{$cid}->{outbuffer} .= "SET ". $line->{name} . "=" . $line->{value} . "\r\n";
1252             } elsif($line->{type} eq 'SETANDSTORE') {
1253             # We forward SETANDSTORE as such only over interclacks connections. Basic clients don't have a cache,
1254             # so we only send a SET command
1255 0 0         if($clients{$cid}->{interclacks}) {
1256 0           $clients{$cid}->{outbuffer} .= "SETANDSTORE ". $line->{name} . "=" . $line->{value} . "\r\n";
1257             } else {
1258 0           $clients{$cid}->{outbuffer} .= "SET ". $line->{name} . "=" . $line->{value} . "\r\n";
1259             }
1260             }
1261             }
1262             }
1263             }
1264              
1265              
1266             # Push all messages that can be released at this time into the corresponding char based output for each client
1267 0           foreach my $cid (keys %clients) {
1268 0           while(scalar @{$clients{$cid}->{outmessages}}) {
  0            
1269 0 0         last if($clients{$cid}->{outmessages}->[0]->{releasetime} > $now);
1270              
1271 0           my $outmsg = shift @{$clients{$cid}->{outmessages}};
  0            
1272 0 0         if($outmsg->{message} eq 'EXIT') {
1273 0           push @toremove, $cid; # Disconnect the client
1274             } else {
1275 0           $clients{$cid}->{outbuffer} .= $outmsg->{message} . "\r\n";
1276             }
1277             }
1278             }
1279              
1280             # ******************************************************************************
1281             # ******************************************************************************
1282             # ******************************************************************************
1283             # ******************************************************************************
1284             # ******************************************************************************
1285             # ******************************************************************************
1286             # ******************************************************************************
1287              
1288             # Send as much as possible
1289 0           foreach my $cid (keys %clients) {
1290 0 0         if(length($clients{$cid}->{outbuffer})) {
    0          
1291 0           $clients{$cid}->{lastmessage} = $now;
1292             } elsif(($clients{$cid}->{lastmessage} + 60) < $now) {
1293 0           $clients{$cid}->{lastmessage} = $now;
1294 0           $clients{$cid}->{outbuffer} .= "NOP\r\n"; # send "No OPerations" command, just to
1295             # check if socket is still open
1296             }
1297              
1298 0 0         next if(!length($clients{$cid}->{outbuffer}));
1299              
1300             # Output bandwidth-limited stuff, in as big chunks as possible
1301 0           my $written;
1302 0           $workCount++;
1303 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
1304 0           $written = syswrite($clients{$cid}->{socket}, $clients{$cid}->{outbuffer});
1305             };
1306 0 0         if($EVAL_ERROR) {
1307 0           print STDERR "Write error: $EVAL_ERROR\n";
1308 0           push @toremove, $cid;
1309 0           next;
1310             }
1311 0 0 0       if(!$clients{$cid}->{socket}->opened || $clients{$cid}->{socket}->error || ($ERRNO ne '' && !$ERRNO{EWOULDBLOCK})) {
      0        
      0        
1312 0           print STDERR "webPrint write failure: $ERRNO\n";
1313 0           push @toremove, $cid;
1314 0           next;
1315             }
1316              
1317 0 0 0       if(defined($written) && $written) {
1318 0 0         if(length($clients{$cid}->{outbuffer}) == $written) {
1319 0           $clients{$cid}->{outbuffer} = '';
1320             } else {
1321 0           $clients{$cid}->{outbuffer} = substr($clients{$cid}->{outbuffer}, $written);
1322             }
1323             }
1324             }
1325              
1326 0 0         if($workCount) {
    0          
1327 0           $self->{usleep} = 0;
1328             } elsif($self->{usleep} < $self->{config}->{throttle}->{maxsleep}) {
1329 0           $self->{usleep} += $self->{config}->{throttle}->{step};
1330             }
1331 0 0         if($self->{usleep}) {
1332 0           sleep($self->{usleep} / 1000);
1333             }
1334             }
1335              
1336 0           print "Shutting down...\n";
1337              
1338             # Make sure we save the latest version of the persistance file
1339 0           $self->savePersistanceFile($savecache);
1340              
1341 0           sleep(0.5);
1342 0           foreach my $cid (keys %clients) {
1343 0           print "Removing client $cid\n";
1344             # Try to notify the client (may or may not work);
1345 0           $self->evalsyswrite($clients{$cid}->{socket}, "\r\nQUIT\r\n");
1346              
1347 0           delete $clients{$cid};
1348             }
1349 0           print "All clients removed\n";
1350              
1351              
1352 0           return;
1353             }
1354 0     0 0   sub savePersistanceFile($self, $savecache) {
  0            
  0            
  0            
1355 0 0         if(!$self->{persistance}) {
1356 0           return;
1357             }
1358              
1359 0           print "Saving persistance file\n";
1360 0           my $line = Dump($self->{clackscache});
1361 0           $line = encode_base64($line, '');
1362 0           my $timestampline = Dump($self->{clackscachetime});
1363 0           $timestampline = encode_base64($timestampline, '');
1364 0           my $accesstimeline = Dump($self->{clackscacheaccesstime});
1365 0           $accesstimeline = encode_base64($accesstimeline, '');
1366              
1367 0           my $tempfname = $self->{config}->{persistancefile} . '_';
1368 0           my $backfname = $self->{config}->{persistancefile} . '_bck';
1369 0 0         if($savecache == 1) {
1370             # Normal savecache operation only
1371 0           copy($self->{config}->{persistancefile}, $backfname);
1372             }
1373              
1374 0 0         if(open(my $ofh, '>', $tempfname)) {
1375 0           print $ofh $line, "\n";
1376 0           print $ofh $timestampline, "\n";
1377 0           print $ofh $accesstimeline, "\n";
1378 0           print $ofh "ENDBYTES\n";
1379 0           close $ofh;
1380             }
1381 0           move($tempfname, $self->{config}->{persistancefile});
1382              
1383 0 0         if($savecache == 2) {
1384             # Need to make sure we have a valid backup file, since we had a general problem while loading
1385 0           copy($self->{config}->{persistancefile}, $backfname);
1386             }
1387              
1388 0           return;
1389             }
1390              
1391 0     0 1   sub deref($self, $val) {
  0            
  0            
  0            
1392 0 0         return if(!defined($val));
1393              
1394 0   0       while(ref($val) eq "SCALAR" || ref($val) eq "REF") {
1395 0           $val = ${$val};
  0            
1396 0 0         last if(!defined($val));
1397             }
1398              
1399 0           return $val;
1400             }
1401              
1402 0     0 0   sub evalsyswrite($self, $socket, $buffer) {
  0            
  0            
  0            
  0            
1403 0 0         return 0 unless(length($buffer));
1404              
1405 0           my $written = 0;
1406 0           my $ok = 0;
1407 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
1408 0           $written = syswrite($socket, $buffer);
1409 0           $ok = 1;
1410             };
1411 0 0 0       if($EVAL_ERROR || !$ok) {
1412 0           print STDERR "Write error: $EVAL_ERROR\n";
1413 0           return -1;
1414             }
1415              
1416 0           return $written;
1417             }
1418              
1419 0     0 0   sub getTime($self) {
  0            
  0            
1420 0           my $now = time + $self->{timeoffset};
1421              
1422 0           return $now;
1423             }
1424              
1425 0     0 0   sub slurpBinFile($fname) {
  0            
  0            
1426             # Read in file in binary mode, slurping it into a single scalar.
1427             # We have to make sure we use binmode *and* turn on the line termination variable completly
1428             # to work around the multiple idiosynchrasies of Perl on Windows
1429 0 0         open(my $fh, "<", $fname) or croak($ERRNO);
1430 0           local $INPUT_RECORD_SEPARATOR = undef;
1431 0           binmode($fh);
1432 0           my $data = <$fh>;
1433 0           close($fh);
1434              
1435 0           return $data;
1436             }
1437              
1438              
1439              
1440             1;
1441             __END__