File Coverage

blib/lib/Net/Clacks/Server.pm
Criterion Covered Total %
statement 85 1145 7.4
branch 0 496 0.0
condition 0 172 0.0
subroutine 28 61 45.9
pod 5 5 100.0
total 118 1879 6.2


line stmt bran cond sub pod time code
1             package Net::Clacks::Server;
2             #---AUTOPRAGMASTART---
3 2     2   131149 use v5.36;
  2         8  
4 2     2   13 use strict;
  2         10  
  2         62  
5 2     2   612 use diagnostics;
  2         752652  
  2         19  
6 2     2   1143 use mro 'c3';
  2         7  
  2         17  
7 2     2   739 use English qw(-no_match_vars);
  2         3731  
  2         16  
8 2     2   1063 use Carp qw[carp croak confess cluck longmess shortmess];
  2         5  
  2         261  
9             our $VERSION = 35;
10 2     2   527 use autodie qw( close );
  2         22160  
  2         20  
11 2     2   1510 use Array::Contains;
  2         3578  
  2         155  
12 2     2   559 use utf8;
  2         289  
  2         17  
13 2     2   746 use Encode qw(is_utf8 encode_utf8 decode_utf8);
  2         20816  
  2         273  
14 2     2   716 use Data::Dumper;
  2         10030  
  2         188  
15 2     2   587 use builtin qw[true false is_bool];
  2         145  
  2         108  
16 2     2   13 no warnings qw(experimental::builtin); ## no critic (TestingAndDebugging::ProhibitNoWarnings)
  2         17  
  2         144  
17             #---AUTOPRAGMAEND---
18              
19 2     2   2371 use XML::Simple;
  2         26512  
  2         18  
20 2     2   227 use Time::HiRes qw(sleep usleep time);
  2         47  
  2         22  
21 2     2   817 use Sys::Hostname;
  2         1509  
  2         160  
22 2     2   18 use Errno;
  2         3  
  2         88  
23 2     2   717 use IO::Socket::IP;
  2         45800  
  2         19  
24 2     2   2202 use IO::Select;
  2         2001  
  2         153  
25 2     2   1010 use IO::Socket::SSL;
  2         84615  
  2         19  
26 2     2   1712 use YAML::Syck;
  2         4397  
  2         187  
27 2     2   354 use MIME::Base64;
  2         554  
  2         181  
28 2     2   1231 use File::Copy;
  2         10324  
  2         192  
29 2     2   18 use Scalar::Util qw(looks_like_number);
  2         3  
  2         97  
30              
31             # For turning off SSL session cache
32 2     2   1410 use Readonly;
  2         8944  
  2         294  
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 2     2   14 no strict 'refs'; ## no critic (TestingAndDebugging::ProhibitNoStrict)
  2     2   4  
  2         309  
  2         6  
62 2         20 *{"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 2         8 };
68            
69 2         8796 *{"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 2         7 };
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->{cache} = {};
94              
95 0           return $self;
96             }
97              
98 0     0 1   sub init($self) {
  0            
  0            
99             # Dummy function for backward compatibility
100 0           carp("Deprecated call to init(), you can remove that function from your code");
101 0           return;
102             }
103              
104 0     0 1   sub run($self) {
  0            
  0            
105 0 0 0       if(!defined($self->{initHasRun}) || !$self->{initHasRun}) {
106 0           $self->_init();
107             }
108              
109 0           while($self->{keepRunning}) {
110             # Check for shutdown time
111 0 0 0       if($self->{shutdowntime} && $self->{shutdowntime} < time) {
112 0           print STDERR "Shutdown time has arrived!\n";
113 0           $self->{keepRunning} = 0;
114             }
115              
116 0           $self->runOnce();
117              
118 0 0         if($self->{workCount}) {
    0          
119 0           $self->{usleep} = 0;
120             } elsif($self->{usleep} < $self->{config}->{throttle}->{maxsleep}) {
121 0           $self->{usleep} += $self->{config}->{throttle}->{step};
122             }
123 0 0         if($self->{usleep}) {
124 0           sleep($self->{usleep} / 1000);
125             }
126             }
127              
128 0           $self->runShutdown();
129              
130 0           return;
131             }
132              
133 0     0 1   sub runOnce($self) {
  0            
  0            
134 0 0 0       if(!defined($self->{initHasRun}) || !$self->{initHasRun}) {
135 0           $self->_init();
136             }
137              
138 0           $self->{workCount} = 0;
139              
140 0           my $now = $self->_getTime();
141 0 0 0       if($self->{savecache} && $now > ($self->{lastsavecache} + $self->{persistanceinterval})) {
142 0           $self->{lastsavecache} = $now;
143 0           $self->_savePersistanceFile();
144 0           $self->{savecache} = 0;
145             }
146              
147             # We are in client mode. We need to add an interclacks link
148 0 0 0       if(defined($self->{config}->{master}->{socket}) || defined($self->{config}->{master}->{ip})) {
149 0           $self->_addInterclacksLink();
150             }
151              
152 0           $self->_addNewClients();
153              
154 0           $self->_disconnectClients();
155              
156 0 0         if(!(scalar keys %{$self->{clients}})) {
  0            
157             # No clients to handle, let's sleep and try again later
158 0           sleep(0.1);
159 0           return $self->{workCount};
160             }
161              
162 0           $self->_clientInput();
163              
164              
165 0           foreach my $cid (keys %{$self->{clients}}) {
  0            
166 0           while(@{$self->{clients}->{$cid}->{charbuffer}}) {
  0            
167 0           my $buf = shift @{$self->{clients}->{$cid}->{charbuffer}};
  0            
168              
169 0           $self->{workCount}++;
170 0 0         if($buf eq "\r") {
    0          
171 0           next;
172             } elsif($buf eq "\n") {
173 0 0         next if($self->{clients}->{$cid}->{buffer} eq ''); # Empty lines
174              
175             my %inmsg = (
176             message => $self->{clients}->{$cid}->{buffer},
177             releasetime => $now + $self->{clients}->{$cid}->{inmessagedelay},
178 0           );
179 0           push @{$self->{clients}->{$cid}->{inmessages}}, \%inmsg;
  0            
180 0           $self->{clients}->{$cid}->{buffer} = '';
181             } else {
182 0           $self->{clients}->{$cid}->{buffer} .= $buf;
183             }
184             }
185              
186 0 0 0       if($self->{interclackslock} && !$self->{clients}->{$cid}->{interclacksclient}) {
187             # We are locked into interclacks sync lock, but this is not the connection to master,
188             # so we don't handle the input buffer for this client at the moment.
189 0           next;
190             }
191              
192              
193 0           while(scalar @{$self->{clients}->{$cid}->{inmessages}}) {
  0            
194 0 0         last if($self->{clients}->{$cid}->{inmessages}->[0]->{releasetime} > $now);
195 0           my $inmsgtmp = shift @{$self->{clients}->{$cid}->{inmessages}};
  0            
196 0           my $inmsg = $inmsgtmp->{message};
197              
198             # Handle CLACKS identification header
199 0 0         if($inmsg =~ /^CLACKS\ (.+)/) {
200 0           $self->{clients}->{$cid}->{clientinfo} = $1;
201 0           $self->{clients}->{$cid}->{clientinfo} =~ s/\;/\_/g;
202 0           print "Client at ", $cid, " identified as ", $self->{clients}->{$cid}->{clientinfo}, "\n";
203 0           next;
204             }
205              
206 0           $self->{nodebug} = 0;
207 0           $self->{sendinterclacks} = 1;
208 0           $self->{discardafterlogging} = 0;
209             # Handle OVERHEAD messages before logging (for handling 'N' flag correctly)
210 0           $self->_handleMessageOverhead($cid, $inmsg);
211              
212             # Ignore other command when not authenticated
213 0 0         if(!$self->{clients}->{$cid}->{authok}) {
214 0           next;
215             }
216              
217 0 0         if(!$self->{nodebug}) {
218             # Add ALL incoming messages as debug-type messages to the outbox
219 0           my %tmp = (
220             sender => $cid,
221             type => 'DEBUG',
222             data => $inmsg,
223             );
224              
225 0           push @{$self->{outbox}}, \%tmp;
  0            
226             }
227              
228 0 0         if($self->{discardafterlogging}) {
229 0           next;
230             }
231              
232              
233              
234 0 0         if($inmsg =~ /^OVERHEAD\ /) {
    0          
    0          
    0          
235             # Already handled
236 0           next;
237             } elsif($self->_handleMessageDirect($cid, $inmsg)) {
238             # Fallthrough
239             } elsif($self->_handleMessageCaching($cid, $inmsg)) {
240             # Fallthrough
241             } elsif($self->_handleMessageControl($cid, $inmsg)) {
242             # Fallthrough
243             # local managment commands
244             } else {
245 0           print STDERR "ERROR Unknown_command ", $inmsg, "\r\n";
246 0           $self->{sendinterclacks} = 0;
247 0           $self->{clients}->{$cid}->{outbuffer} .= "OVERHEAD E unknown_command " . $inmsg . "\r\n";
248             }
249              
250             # forward interclacks messages
251 0 0         if($self->{sendinterclacks}) {
252 0           foreach my $interclackscid (keys %{$self->{clients}}) {
  0            
253 0 0 0       if($cid eq $interclackscid || !$self->{clients}->{$interclackscid}->{interclacks}) {
254 0           next;
255             }
256 0           $self->{clients}->{$interclackscid}->{outbuffer} .= $inmsg . "\r\n";
257             }
258             }
259              
260             }
261              
262             }
263              
264             # Clean up algorithm, only run every so often
265 0 0         if($self->{nextcachecleanup} < $now) {
266 0           $self->_cacheCleanup();
267 0           $self->{nextcachecleanup} = $now + $self->{config}->{cachecleaninterval};
268              
269             }
270              
271 0           $self->_outboxToClientBuffer();
272 0           $self->_clientOutput();
273              
274 0           return $self->{workCount};
275             }
276              
277 0     0 1   sub runShutdown($self) {
  0            
  0            
278 0           print "Shutting down...\n";
279              
280             # Make sure we save the latest version of the persistance file
281 0           $self->_savePersistanceFile();
282              
283 0           sleep(0.5);
284 0           foreach my $cid (keys %{$self->{clients}}) {
  0            
285 0           print "Removing client $cid\n";
286             # Try to notify the client (may or may not work);
287 0           $self->_evalsyswrite($self->{clients}->{$cid}->{socket}, "\r\nQUIT\r\n");
288              
289 0           delete $self->{clients}->{$cid};
290             }
291 0           print "All clients removed\n";
292              
293 0           return;
294             }
295              
296 0     0     sub _savePersistanceFile($self) {
  0            
  0            
297 0 0         if(!$self->{persistance}) {
298 0           return;
299             }
300              
301 0           print "Saving persistance file\n";
302              
303 0           my $tempfname = $self->{config}->{persistancefile} . '_';
304 0           my $backfname = $self->{config}->{persistancefile} . '_bck';
305 0 0         if($self->{savecache} == 1) {
306             # Normal savecache operation only
307 0           copy($self->{config}->{persistancefile}, $backfname);
308             }
309              
310 0           my $persistancedata = chr(0) . 'CLACKSV3' . Dump($self->{cache}) . chr(0) . 'CLACKSV3';
311 0           $self->_writeBinFile($tempfname, $persistancedata);
312 0           move($tempfname, $self->{config}->{persistancefile});
313              
314 0 0         if($self->{savecache} == 2) {
315             # Need to make sure we have a valid backup file, since we had a general problem while loading
316 0           copy($self->{config}->{persistancefile}, $backfname);
317             }
318              
319 0           return;
320             }
321              
322 0     0     sub _evalsyswrite($self, $socket, $buffer) {
  0            
  0            
  0            
  0            
323 0 0         return false unless(length($buffer));
324              
325 0           my $written = 0;
326 0           my $ok = 0;
327 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
328 0           $written = syswrite($socket, $buffer);
329 0           $ok = 1;
330             };
331 0 0 0       if($EVAL_ERROR || !$ok) {
332 0           print STDERR "Write error: $EVAL_ERROR\n";
333 0           return -1;
334             }
335              
336 0           return $written;
337             }
338              
339 0     0     sub _getTime($self) {
  0            
  0            
340 0           my $now = time + $self->{timeoffset};
341              
342 0           return $now;
343             }
344              
345 0     0     sub _slurpBinFile($self, $fname) {
  0            
  0            
  0            
346             # Read in file in binary mode, slurping it into a single scalar.
347             # We have to make sure we use binmode *and* turn on the line termination variable completly
348             # to work around the multiple idiosynchrasies of Perl on Windows
349 0 0         open(my $fh, "<", $fname) or croak($ERRNO);
350 0           local $INPUT_RECORD_SEPARATOR = undef;
351 0           binmode($fh);
352 0           my $data = <$fh>;
353 0           close($fh);
354              
355 0           return $data;
356             }
357              
358 0     0     sub _writeBinFile($self, $fname, $data) {
  0            
  0            
  0            
  0            
359             # Write file in binmode
360             # We have to make sure we use binmode *and* turn on the line termination variable completly
361             # to work around the multiple idiosynchrasies of Perl on Windows
362 0 0         open(my $fh, ">", $fname) or croak($ERRNO);
363 0           local $INPUT_RECORD_SEPARATOR = undef;
364 0           binmode($fh);
365 0           print $fh $data;
366 0           close($fh);
367              
368 0           return true;
369             }
370              
371 0     0     sub _restorePersistanceFile($self) {
  0            
  0            
372 0           my $previousfname = $self->{config}->{persistancefile} . '_bck';
373 0           my $tempfname = $self->{config}->{persistancefile} . '_';
374 0           my $loadok = 0;
375 0 0         if(-f $self->{config}->{persistancefile}) {
376 0           print "Trying to load persistance file ", $self->{config}->{persistancefile}, "\n";
377 0           $loadok = $self->_loadPersistanceFile($self->{config}->{persistancefile});
378             }
379              
380 0 0 0       if(!$loadok && -f $previousfname) {
381 0           print "Trying to load backup (previous) persistance file ", $previousfname, "\n";
382 0           $loadok = $self->_loadPersistanceFile($previousfname);
383 0 0         if($loadok) {
384 0           $self->{savecache} = 2; # Force saving a new persistance file plus a new backup
385             }
386             }
387 0 0 0       if(!$loadok && -f $tempfname) {
388 0           print "Oh no. As a final, desperate solution, trying to load a 'temporary file while saving' persistance file ", $tempfname, "\n";
389 0           $loadok = $self->_loadPersistanceFile($tempfname);
390 0 0         if($loadok) {
391 0           $self->{savecache} = 2; # Force saving a new persistance file plus a new backup
392             }
393             }
394              
395 0 0         if(!$loadok) {
396 0           print "Sorry, no valid persistance file found. Starting server 'blankety-blank'\n";
397 0           $self->{savecache} = 2;
398             } else {
399 0           print "Persistance file loaded\n";
400             }
401            
402 0           return;
403             }
404              
405 0     0     sub _init($self) {
  0            
  0            
406 0 0 0       if(defined($self->{initHasRun}) && $self->{initHasRun}) {
407             # NOT ALLOWED!
408 0           croak("Multiple calls to _init() are not allowed!");
409             }
410              
411 0           my @paths;
412 0 0         if(defined($ENV{'PC_CONFIG_PATHS'})) {
413 0           push @paths, split/\:/, $ENV{'PC_CONFIG_PATHS'};
414 0           print "Found config paths:\n", Dumper(\@paths), " \n";
415             } else {
416 0           print("PC_CONFIG_PATHS undefined, falling back to legacy mode\n");
417 0           @paths = ('', 'configs/');
418             }
419              
420 0           my $filedata;
421 0           my $fname = $self->{configfile};
422 0           foreach my $path (@paths) {
423 0 0 0       if($path ne '' && $path !~ /\/$/) {
424 0           $path .= '/';
425             }
426 0           my $fullfname = $path . $fname;
427 0 0         next unless (-f $fullfname);
428 0           print " Loading config file $fullfname\n";
429              
430 0           $filedata = $self->_slurpBinFile($fullfname);
431              
432 0           foreach my $varname (keys %ENV) {
433 0 0         next unless $varname =~ /^PC\_/;
434              
435 0           my $newval = $ENV{$varname};
436 0           $filedata =~ s/$varname/$newval/g;
437             }
438              
439 0           last;
440             }
441              
442 0 0 0       if(!defined($filedata) || $filedata eq "") {
443 0           croak("Can't load config file: Not found or empty!");
444             }
445              
446 0           print "------- Parsing config file $fname ------\n";
447 0           my $config = XMLin($filedata, ForceArray => [ 'ip', 'socket', 'user', 'item' ]);
448              
449 0           my $hname = hostname;
450              
451             # Copy hostname-specific stuff to root if it exists
452 0 0         if(defined($config->{hosts}->{$hname})) {
453 0           foreach my $key (keys %{$config->{hosts}->{$hname}}) {
  0            
454 0           $config->{$key} = $config->{hosts}->{$hname}->{$key};
455             }
456             }
457              
458 0           $self->{config} = $config;
459              
460 0 0         if(!defined($self->{config}->{throttle}->{maxsleep})) {
461 0           $self->{config}->{throttle}->{maxsleep} = 100;
462             }
463 0 0         if(!defined($self->{config}->{throttle}->{step})) {
464 0           $self->{config}->{throttle}->{step} = 10;
465             }
466              
467 0           $self->{usleep} = 0;
468              
469 0 0         if(defined($config->{ip})) {
470             # SSL only needed for TCP. Unix domain sockets are unencrypted
471 0 0 0       if(!defined($self->{config}->{ssl}) ||
      0        
472             !defined($self->{config}->{ssl}->{cert}) ||
473             !defined($self->{config}->{ssl}->{key})) {
474 0           croak("Missing or incomplete SSL config!");
475             }
476 0 0         if(!-f $self->{config}->{ssl}->{cert}) {
477 0           croak("SSL cert file " . $self->{config}->{ssl}->{cert} . " not found!");
478             }
479 0 0         if(!-f $self->{config}->{ssl}->{key}) {
480 0           croak("SSL key file " . $self->{config}->{ssl}->{key} . " not found!");
481             }
482             }
483              
484 0 0         if(!defined($self->{config}->{username})) {
485 0           croak("Username not defined!");
486             }
487 0 0         if(!defined($self->{config}->{password})) {
488 0           croak("Password not defined!");
489             }
490 0           $self->{authtoken} = encode_base64($self->{config}->{username}, '') . ':' . encode_base64($self->{config}->{password}, '');
491              
492             # Add authtoken to the list of authorized users, giving it full permissions
493 0           $self->{userlist} = {};
494             $self->{userlist}->{$self->{authtoken}} = {
495 0           read => 1,
496             write => 1,
497             manage => 1,
498             interclacks => 1,
499             };
500              
501             # Configure additional users. They can NOT be used for interclacks connections, only the default user can
502 0 0         if(defined($self->{config}->{user})) {
503 0           foreach my $user (@{$self->{config}->{user}}) {
  0            
504 0 0 0       if(!defined($user->{username}) || !defined($user->{password})) {
505 0           croak("User config is missing username/password");
506             }
507 0           my $authtoken = encode_base64($user->{username}, '') . ':' . encode_base64($user->{password}, '');
508 0           $self->{userlist}->{$authtoken} = {
509             read => 0,
510             write => 0,
511             manage => 0,
512             interclacks => 0,
513             };
514 0           foreach my $key (qw[read write manage]) {
515 0 0 0       if(defined($user->{$key}) && $user->{$key}) {
516 0           $self->{userlist}->{$authtoken}->{$key} = 1;
517             }
518             }
519             }
520             }
521              
522 0 0         if(defined($self->{config}->{persistancefile})) {
523 0           $self->{persistance} = 1;
524             } else {
525 0           $self->{persistance} = 0;
526             }
527              
528 0 0         if(!defined($self->{config}->{persistanceinterval})) {
529 0           $self->{persistanceinterval} = 10;
530             } else {
531 0           $self->{persistanceinterval} = $self->{config}->{persistanceinterval};
532             }
533              
534 0 0         if(!defined($self->{config}->{interclacksreconnecttimeout})) {
535 0           $self->{config}->{interclacksreconnecttimeout} = 30;
536             }
537              
538 0 0         if(!defined($self->{config}->{authtimeout})) {
539 0           $self->{config}->{authtimeout} = 15;
540             }
541              
542 0 0         if(!defined($self->{config}->{deletedcachetime})) {
543 0           $self->{config}->{deletedcachetime} = 60 * 60; # 1 hour
544             }
545 0 0         if(!defined($self->{config}->{stalecachetime})) {
546 0           $self->{config}->{stalecachetime} = 60 * 60 * 24; # 1 day
547             }
548              
549 0 0         if(!defined($self->{config}->{cachecleaninterval})) {
550 0           $self->{config}->{cachecleaninterval} = 60; # 1 minute
551             }
552              
553 0 0         if(!defined($self->{config}->{readfailtimeout})) {
554 0           $self->{config}->{readfailtimeout} = 30; # 30 seconds - matches pingtimeout default
555             }
556              
557 0 0         if(!defined($self->{config}->{interclacksreadfailtimeout})) {
558 0           $self->{config}->{interclacksreadfailtimeout} = 60; # 60 seconds for interclacks (higher latency)
559             }
560              
561             # Init run() variables
562 0           $self->{savecache} = 0;
563 0           $self->{lastsavecache} = 0;
564 0           $self->{outbox} = [];
565 0           $self->{toremove} = [];
566 0           $self->{clients} = {};
567 0           $self->{shutdowntime} = 0;
568 0           $self->{selector} = IO::Select->new();
569 0           $self->{interclackslock} = 0;
570 0           $self->{nextinterclackscheck} = 0;
571 0           $self->{keepRunning} = 1;
572 0           $self->{nextcachecleanup} = 0;
573 0           $self->{initHasRun} = 1;
574              
575 0           my @tcpsockets;
576              
577 0 0         if(defined($config->{ip})) {
578 0 0         if(!defined($config->{port})) {
579 0           croak("At least one IP defined, but no TCP port!");
580             }
581 0           foreach my $ip (@{$config->{ip}}) {
  0            
582             my $tcp = IO::Socket::IP->new(
583             LocalHost => $ip,
584             LocalPort => $config->{port},
585 0 0         Listen => 20, # Listen queue of 20, just in case multiple clients try to connect at the same time
586             Blocking => 0,
587             ReuseAddr => 1,
588             Proto => 'tcp',
589             ) or croak($ERRNO);
590             #binmode($tcp, ':bytes');
591 0           push @tcpsockets, $tcp;
592 0           print "Listening on $ip:", $config->{port}, "/tcp\n";
593             }
594             }
595              
596 0 0 0       if(defined($config->{socket}) || defined($self->{config}->{master}->{socket})) {
597 0           my $udsloaded = 0;
598 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
599 0           require IO::Socket::UNIX;
600 0           $udsloaded = 1;
601             };
602 0 0         if(!$udsloaded) {
603 0           croak("Specified a unix domain socket, but i couldn't load IO::Socket::UNIX!");
604             }
605              
606             # Add the ClientID stuff to Unix domain sockets as well. We don't do this in the BEGIN{} block
607             # since we are not yet sure we are going to load IO::Socket::UNIX in the first place
608             {
609 2     2   17 no strict 'refs'; ## no critic (TestingAndDebugging::ProhibitNoStrict)
  2         3  
  2         24755  
  0            
610 0           *{"IO::Socket::UNIX::_setClientID"} = sub {
611 0     0     my ($self, $cid) = @_;
612            
613 0           ${*$self}{'__client_id'} = $cid; ## no critic (References::ProhibitDoubleSigils)
  0            
614 0           return;
615 0           };
616            
617 0           *{"IO::Socket::UNIX::_getClientID"} = sub {
618 0     0     my ($self) = @_;
619            
620 0   0       return ${*$self}{'__client_id'} || ''; ## no critic (References::ProhibitDoubleSigils)
621 0           };
622             }
623             }
624              
625 0 0         if(defined($config->{socket})) {
626 0           foreach my $socket (@{$config->{socket}}) {
  0            
627 0 0         if(-S $socket) {
628 0           print "Removing old unix domain socket file $socket\n";
629 0           unlink $socket;
630             }
631 0 0         my $tcp = IO::Socket::UNIX->new(
632             Type => SOCK_STREAM(),
633             Local => $socket,
634             Listen => 20, # Listen queue of 20, just in case multiple clients try to connect at the same time
635             #Blocking => 0,
636             ) or croak($ERRNO);
637 0           $tcp->blocking(0);
638             #binmode($tcp, ':bytes');
639 0           push @tcpsockets, $tcp;
640 0           print "Listening on Unix domain socket $socket\n";
641              
642 0 0 0       if(defined($config->{socketchmod}) && $config->{socketchmod} ne '') {
643 0           my $cmd = 'chmod ' . $config->{socketchmod} . ' ' . $socket;
644 0           print $cmd, "\n";
645 0           `$cmd`;
646             }
647              
648 0 0         if(defined($config->{socketcommands})) {
649 0           foreach my $cmd (@{$config->{socketcommands}->{item}}) {
  0            
650 0           print "Running Socket command: $cmd\n";
651 0           `$cmd`;
652             }
653             }
654             }
655             }
656              
657 0           $self->{tcpsockets} = \@tcpsockets;
658              
659             # Need to ignore SIGPIPE, this can screw us over in certain circumstances
660             # while writing to the network. We can only detect certain types of disconnects
661             # after writing to the socket, but we will get a SIGPIPE if we try. So we just
662             # ignore the signal and carry on as usual...
663 0           $SIG{PIPE} = 'IGNORE';
664              
665              
666 0     0     $SIG{INT} = sub { $self->{keepRunning} = 0; };
  0            
667 0     0     $SIG{TERM} = sub { $self->{keepRunning} = 0; };
  0            
668              
669             # Restore persistance file if required
670 0 0         if($self->{persistance}) {
671 0           $self->_restorePersistanceFile();
672             }
673              
674 0           print "Ready.\n";
675              
676              
677 0           return;
678             }
679              
680 0     0     sub _loadPersistanceFile($self, $fname) {
  0            
  0            
  0            
681 0           my $alreadyupgraded = 0;
682              
683             retry:
684 0 0         if(!-f $fname) {
685             # Does not exist
686 0           carp("$fname not found");
687 0           return false;
688             }
689              
690 0           my $data = $self->_slurpBinFile($fname);
691 0 0         if(length($data) < 11) {
692 0           carp("$fname too small");
693             # Invalid file
694 0           return false;
695             }
696 0 0         if(substr($data, 0, 9) ne chr(0) . 'CLACKSV3') {
697 0 0         if($alreadyupgraded) {
698 0           carp("Upgrade resulted in invalog file (wrong prefix)");
699             # Something went wrong with inplace upgrade
700 0           return;
701             }
702 0           $alreadyupgraded = 1;
703 0 0         if(!$self->_inplaceUpgrade($fname)) {
704 0           carp("Inplace upgrade failed");
705             # Could not upgrade
706 0           return false;
707             }
708 0           goto retry;
709             }
710              
711 0 0         if(length($data) < 18) {
712 0           carp("Incomplete V3 persistance file " . $fname . " (too small)!");
713 0           return false;
714             }
715              
716 0           substr($data, 0, 9, ''); # Remove header
717              
718 0 0         if(substr($data, -9, 9) ne chr(0) . 'CLACKSV3') {
719             # Missing end bytes
720 0           carp("Incomplete V3 persistance file " . $fname . " (missing end bytes)!");
721 0           return false; # Fail
722             }
723 0           substr($data, -9, 9, ''); # Remove end bytes
724              
725 0           $self->{cache} = {};
726 0 0         if(length($data)) {
727 0           my $loadok = 0;
728 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
729 0           $self->{cache} = Load($data);
730 0           $loadok = 1;
731             };
732 0 0         if(!$loadok) {
733 0           carp("Invalid V3 persistance file " . $fname . "!");
734 0           return false; # Fail
735             }
736             }
737              
738 0           return true;
739             }
740              
741 0     0     sub _inplaceUpgrade($self, $fname) {
  0            
  0            
  0            
742 0           my %clackscache;
743             my %clackscachetime;
744 0           my %clackscacheaccesstime;
745              
746 0           print "Inplace-upgrading persistance file $fname...\n";
747              
748 0           my $now = $self->_getTime();
749              
750             # Use old loader algorithm to load file
751 0 0         if(open(my $ifh, '<', $fname)) {
752 0           my $line = <$ifh>;
753 0           my $timestampline = <$ifh>;
754 0           my $accesstimeline = <$ifh>;
755 0           my $endline = <$ifh>;
756 0           my $needupgrade = 0;
757 0           close $ifh;
758              
759 0           chomp $line;
760 0           chomp $timestampline;
761 0           chomp $accesstimeline;
762              
763 0 0 0       if(!defined($endline) && $accesstimeline eq 'ENDBYTES') {
764 0           $endline = 'ENDBYTES';
765 0           $accesstimeline = '';
766 0           $needupgrade = 1;
767             } else {
768 0           chomp $endline;
769             }
770              
771 0 0 0       if(!defined($line) || !defined($timestampline) || $endline ne 'ENDBYTES') {
      0        
772 0           carp("Invalid persistance file " . $fname . "! File is incomplete!");
773 0           return false; # Fail
774             }
775              
776 0           my $loadok = 0;
777              
778 0 0         if($line ne '') {
779 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
780 0           $line = decode_base64($line);
781 0           $line = Load($line);
782 0           $loadok = 1;
783             };
784 0 0         if(!$loadok) {
785 0           carp("Invalid persistance file " . $fname . "! Failed to decode data line!");
786 0           return false; # Fail
787             }
788             }
789 0           %clackscache = %{$line};
  0            
790              
791             # Mark all data as current as a fallback
792 0           foreach my $key (keys %clackscache) {
793 0           $clackscachetime{$key} = $now;
794             }
795              
796 0 0         if($timestampline ne '') {
797 0           $loadok = 0;
798 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
799 0           $timestampline = decode_base64($timestampline);
800 0           $timestampline = Load($timestampline);
801 0           $loadok = 1;
802             };
803 0 0         if(!$loadok) {
804 0           carp("Invalid persistance file " . $fname . "! Failed to decode timestamp line, using current time!");
805 0           return false; # Fail
806             } else {
807 0           my %clackstemp = %{$timestampline};
  0            
808 0           foreach my $key (keys %clackstemp) {
809 0           $clackscachetime{$key} = $clackstemp{$key};
810             }
811             }
812             }
813              
814 0 0         if($needupgrade) {
    0          
815 0           print "Pre-Version 22 persistance file detected. Upgrading automatically.\n";
816 0           foreach my $key (keys %clackscache) {
817 0           $clackscacheaccesstime{$key} = $now;
818             }
819             } elsif($accesstimeline ne '') {
820 0           $loadok = 0;
821 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
822 0           $accesstimeline = decode_base64($accesstimeline);
823 0           $accesstimeline = Load($accesstimeline);
824 0           $loadok = 1;
825             };
826 0 0         if(!$loadok) {
827 0           carp("Invalid persistance file " . $fname . "! Failed to decode timestamp line, using current time!");
828 0           return false; # Fail
829             } else {
830 0           %clackscacheaccesstime = %{$accesstimeline};
  0            
831             }
832             }
833             } else {
834             # Fail
835 0           return false;
836             }
837              
838             # Turn into new format
839 0           my %cache;
840             # not-deleted sets
841 0           foreach my $key (keys %clackscache) {
842 0           my $cachetime = $now;
843 0           my $accesstime = $now;
844 0 0         if(defined($clackscachetime{$key})) {
845 0           $cachetime = $clackscachetime{$key};
846             }
847 0 0         if(defined($clackscacheaccesstime{$key})) {
848 0           $accesstime = $clackscacheaccesstime{$key};
849             }
850             $cache{$key} = {
851 0           data => $clackscache{$key},
852             cachetime => $cachetime,
853             accesstime => $accesstime,
854             deleted => 0,
855             };
856             }
857              
858             # deleted sets
859 0           foreach my $key (keys %clackscachetime) {
860 0 0         if(defined($cache{$key})) {
861             # not deleted
862 0           next;
863             }
864              
865 0           my $cachetime = $clackscachetime{$key};
866 0           my $accesstime = $now;
867 0 0         if(defined($clackscacheaccesstime{$key})) {
868 0           $accesstime = $clackscacheaccesstime{$key};
869             }
870 0           $cache{$key} = {
871             data => '',
872             cachetime => $cachetime,
873             accesstime => $accesstime,
874             deleted => 0,
875             };
876             }
877              
878 0           my $converted = chr(0) . 'CLACKSV3' . Dump(\%cache) . chr(0) . 'CLACKSV3';
879 0           $self->_writeBinFile($fname, $converted);
880              
881 0           print "...upgrade complete.\n";
882              
883 0           return true;
884             }
885              
886 0     0     sub _addInterclacksLink($self) {
  0            
  0            
887 0           my $now = $self->_getTime();
888              
889 0           my $mcid;
890 0 0         if(defined($self->{config}->{master}->{socket})) {
891 0           $mcid = 'unixdomainsocket:interclacksmaster';
892             } else {
893 0           $mcid = $self->{config}->{master}->{ip}->[0] . ':' . $self->{config}->{master}->{port};
894             }
895 0 0 0       if(!defined($self->{clients}->{$mcid}) && $self->{nextinterclackscheck} < $now) {
896 0           $self->{nextinterclackscheck} = $now + $self->{config}->{interclacksreconnecttimeout} + int(rand(10));
897              
898 0           print "Connect to master\n";
899 0           my $msocket;
900              
901 0 0         if(defined($self->{config}->{master}->{socket})) {
902             $msocket = IO::Socket::UNIX->new(
903 0           Peer => $self->{config}->{master}->{socket}->[0],
904             Type => SOCK_STREAM,
905             );
906             } else {
907             $msocket = IO::Socket::IP->new(
908             PeerHost => $self->{config}->{master}->{ip}->[0],
909             PeerPort => $self->{config}->{master}->{port},
910 0           Type => SOCK_STREAM,
911             Timeout => 5,
912             );
913             }
914 0 0         if(!defined($msocket)) {
915 0           print STDERR "Can't connect to MASTER via interclacks!\n";
916             } else {
917 0           print "connected to master\n";
918              
919 0 0         if(ref $msocket ne 'IO::Socket::UNIX') {
920             # ONLY USE SSL WHEN RUNNING OVER THE NETWORK
921             # There is simply no point in running it over a local socket.
922 0           my $encrypted = IO::Socket::SSL->start_SSL($msocket,
923             SSL_verify_mode => SSL_VERIFY_NONE,
924             );
925 0 0         if(!$encrypted) {
926 0           print "startSSL failed: ", $SSL_ERROR, "\n";
927 0           next;
928             }
929             }
930              
931 0           $msocket->blocking(0);
932             #binmode($msocket, ':bytes');
933             my %tmp = (
934             buffer => '',
935             charbuffer => [],
936             listening => {},
937             socket => $msocket,
938             lastping => $now,
939             mirror => 0,
940             outbuffer => "CLACKS PageCamel $VERSION in interclacks client mode\r\n" . # Tell the server we are using PageCamel Interclacks...
941             "OVERHEAD A " . $self->{authtoken} . "\r\n" . # ...send Auth token
942             "OVERHEAD I 1\r\n", # ...and turn interclacks master mode ON on remote side
943             clientinfo => 'Interclacks link',
944             client_timeoffset => 0,
945             interclacks => 1,
946             interclacksclient => 1,
947             lastinterclacksping => $now,
948             lastmessage => $now,
949             authtimeout => $now + $self->{config}->{authtimeout},
950 0           authok => 0,
951             failtime => 0,
952             outmessages => [],
953             inmessages => [],
954             messagedelay => 0,
955             inmessagedelay => 0,
956             outmessagedelay => 0,
957             permissions => {
958             read => 1,
959             write => 1,
960             manage => 1,
961             interclacks => 1,
962             },
963             );
964              
965 0 0         if(defined($self->{config}->{master}->{ip})) {
966 0           $tmp{host} = $self->{config}->{master}->{ip}->[0];
967 0           $tmp{port} = $self->{config}->{master}->{port};
968             }
969 0           $self->{clients}->{$mcid} = \%tmp;
970 0           $msocket->_setClientID($mcid);
971 0           $self->{selector}->add($msocket);
972              
973 0           $self->{workCount}++;
974             }
975             }
976 0           return;
977             }
978              
979 0     0     sub _addNewClients($self) {
  0            
  0            
980 0           my $now = $self->_getTime();
981 0           foreach my $tcpsocket (@{$self->{tcpsockets}}) {
  0            
982 0           my $clientsocket = $tcpsocket->accept;
983 0 0         if(defined($clientsocket)) {
984 0           $clientsocket->blocking(0);
985 0           my ($cid, $chost, $cport);
986 0 0         if(ref $tcpsocket eq 'IO::Socket::UNIX') {
987 0           $chost = 'unixdomainsocket';
988 0           $cport = $now . ':' . int(rand(1_000_000));
989             } else {
990 0           ($chost, $cport) = ($clientsocket->peerhost, $clientsocket->peerport);
991             }
992 0           print "Got a new client $chost:$cport!\n";
993 0           $cid = "$chost:$cport";
994 0           foreach my $debugcid (keys %{$self->{clients}}) {
  0            
995 0 0         if($self->{clients}->{$debugcid}->{mirror}) {
996 0           $self->{clients}->{$debugcid}->{outbuffer} .= "DEBUG CONNECTED=" . $cid . "\r\n";
997             }
998             }
999              
1000 0 0         if(ref $clientsocket ne 'IO::Socket::UNIX') {
1001             # ONLY USE SSL WHEN RUNNING OVER THE NETWORK
1002             # There is simply no point in running it over a local socket.
1003             my $encrypted = IO::Socket::SSL->start_SSL($clientsocket,
1004             SSL_server => 1,
1005             SSL_cert_file => $self->{config}->{ssl}->{cert},
1006             SSL_key_file => $self->{config}->{ssl}->{key},
1007             SSL_cipher_list => 'ALL:!ADH:!RC4:+HIGH:+MEDIUM:!LOW:!SSLv2:!SSLv3!EXPORT',
1008             SSL_create_ctx_callback => sub {
1009 0     0     my $ctx = shift;
1010              
1011             # Enable workarounds for broken clients
1012 0           Net::SSLeay::CTX_set_options($ctx, &Net::SSLeay::OP_ALL); ## no critic (Subroutines::ProhibitAmpersandSigils)
1013              
1014             # Disable session resumption completely
1015 0           Net::SSLeay::CTX_set_session_cache_mode($ctx, $SSL_SESS_CACHE_OFF);
1016              
1017             # Disable session tickets
1018 0           Net::SSLeay::CTX_set_options($ctx, &Net::SSLeay::OP_NO_TICKET); ## no critic (Subroutines::ProhibitAmpersandSigils)
1019             },
1020 0           );
1021 0 0         if(!$encrypted) {
1022 0           print "startSSL failed: ", $SSL_ERROR, "\n";
1023 0           next;
1024             }
1025             }
1026              
1027 0           $clientsocket->blocking(0);
1028             #binmode($clientsocket, ':bytes');
1029             #$clientsocket->{clacks_cid} = $cid;
1030             my %tmp = (
1031             buffer => '',
1032             charbuffer => [],
1033             listening => {},
1034             socket => $clientsocket,
1035             lastping => $now,
1036             mirror => 0,
1037             outbuffer => "CLACKS PageCamel $VERSION\r\n" .
1038             "OVERHEAD M Authentication required\r\n", # Informal message
1039             clientinfo => 'UNKNOWN',
1040             client_timeoffset => 0,
1041             host => $chost,
1042             port => $cport,
1043             interclacks => 0,
1044             interclacksclient => 0,
1045             lastinterclacksping => 0,
1046             lastmessage => $now,
1047             authtimeout => $now + $self->{config}->{authtimeout},
1048 0           authok => 0,
1049             failtime => 0,
1050             outmessages => [],
1051             inmessages => [],
1052             inmessagedelay => 0,
1053             outmessagedelay => 0,
1054             permissions => {
1055             read => 0,
1056             write => 0,
1057             manage => 0,
1058             interclacks => 0,
1059             },
1060             );
1061 0           if(0 && $self->{isDebugging}) {
1062             $tmp{authok} = 1;
1063             $tmp{outbuffer} .= "OVERHEAD M debugmode_auth_not_really_required\r\n"
1064             }
1065 0           $self->{clients}->{$cid} = \%tmp;
1066 0           $clientsocket->_setClientID($cid);
1067 0           $self->{selector}->add($clientsocket);
1068 0           $self->{workCount}++;
1069             }
1070             }
1071            
1072 0           return;
1073             }
1074              
1075 0     0     sub _disconnectClients($self) {
  0            
  0            
1076 0           my $now = $self->_getTime();
1077              
1078             # Check if there are any clients to disconnect...
1079 0           my $pingtime = $now - $self->{config}->{pingtimeout};
1080 0           my $interclackspingtime = $now - $self->{config}->{interclackspingtimeout};
1081 0           my $interclackspinginterval = $now - int($self->{config}->{interclackspingtimeout} / 3);
1082 0           foreach my $cid (keys %{$self->{clients}}) {
  0            
1083 0 0         if(!$self->{clients}->{$cid}->{socket}->connected) {
1084 0           push @{$self->{toremove}}, $cid;
  0            
1085 0           next;
1086             }
1087 0 0         if(!$self->{clients}->{$cid}->{interclacks}) {
1088 0 0 0       if($self->{clients}->{$cid}->{lastping} > 0 && $self->{clients}->{$cid}->{lastping} < $pingtime) {
1089 0           $self->_evalsyswrite($self->{clients}->{$cid}->{socket}, "\r\nTIMEOUT\r\n");
1090 0           push @{$self->{toremove}}, $cid;
  0            
1091 0           next;
1092             }
1093             } else {
1094 0 0         if($self->{clients}->{$cid}->{lastping} < $interclackspingtime) {
1095 0           $self->_evalsyswrite($self->{clients}->{$cid}->{socket}, "\r\nTIMEOUT\r\n");
1096 0           push @{$self->{toremove}}, $cid;
  0            
1097 0           next;
1098             }
1099             }
1100              
1101 0 0 0       if($self->{clients}->{$cid}->{interclacks} && $self->{clients}->{$cid}->{lastinterclacksping} < $interclackspinginterval) {
1102 0           $self->{clients}->{$cid}->{lastinterclacksping} = $now;
1103 0           $self->{clients}->{$cid}->{outbuffer} .= "PING\r\n";
1104             }
1105              
1106 0 0 0       if(!$self->{clients}->{$cid}->{authok} && $self->{clients}->{$cid}->{authtimeout} < $now) {
1107             # Authentication timeout!
1108 0           push @{$self->{toremove}}, $cid;
  0            
1109             }
1110             }
1111              
1112             # ...and disconnect them
1113 0 0         if(scalar @{$self->{toremove}}) {
  0            
1114             # Make sure we handle any last messages, or at least try to. This should allow us to at least try to adhere to the
1115             # protocol in some cases (auth failure, etc)
1116 0           $self->_outboxToClientBuffer();
1117 0           for(1..5) {
1118 0           my @flushed;
1119 0           foreach my $cid (@{$self->{toremove}}) {
  0            
1120 0 0         next if(contains($cid, \@flushed));
1121 0           push @flushed, $cid;
1122 0 0         next if(!length($self->{clients}->{$cid}->{outbuffer}));
1123 0           print "Flushing $cid for removal...\n";
1124 0           $self->_clientOutput($cid);
1125             }
1126 0           sleep(0.02);
1127             }
1128             }
1129              
1130 0           while((my $cid = shift @{$self->{toremove}})) {
  0            
1131             # In some circumstances, there may be multiple @{$self->{toremove}} entries for the same client. Ignore them...
1132 0 0         if(defined($self->{clients}->{$cid})) {
1133 0           print "Removing client $cid\n";
1134 0           foreach my $debugcid (keys %{$self->{clients}}) {
  0            
1135 0 0         if($self->{clients}->{$debugcid}->{mirror}) {
1136 0           $self->{clients}->{$debugcid}->{outbuffer} .= "DEBUG DISCONNECTED=" . $cid . " (" . length($self->{clients}->{$cid}->{outbuffer}) . " bytes discarded from outbuffer)\r\n";
1137 0 0 0       if($self->{clients}->{$cid}->{interclacksclient} && $self->{interclackslock}) {
1138 0           $self->{clients}->{$debugcid}->{outbuffer} .= "DEBUG DISCONNECTED=" . $cid . " (Unlocking interclacks mid-sync)\r\n";
1139             }
1140             }
1141             }
1142              
1143 0 0 0       if($self->{clients}->{$cid}->{interclacksclient} && $self->{interclackslock}) {
1144 0           print "...this one is interclacks master and has us locked - UNLOCKING mid-sync!\n";
1145 0           $self->{interclackslock} = 0;
1146             }
1147              
1148 0           $self->{selector}->remove($self->{clients}->{$cid}->{socket});
1149 0           delete $self->{clients}->{$cid};
1150             #print "# Clients left: ", scalar keys %{$self->{clients}}, "\n";
1151             }
1152              
1153 0           $self->{workCount}++;
1154             }
1155              
1156 0           return;
1157             }
1158              
1159 0     0     sub _clientInput($self) {
  0            
  0            
1160 0           my $now = $self->_getTime();
1161              
1162             # **** READ FROM CLIENTS ****
1163 0           my $hasoutbufferwork = 0;
1164 0           foreach my $cid (keys %{$self->{clients}}) {
  0            
1165 0 0         if(length($self->{clients}->{$cid}->{buffer}) > 0) {
1166             # Found some work to do
1167 0           $hasoutbufferwork = 1;
1168 0           last;
1169             }
1170             }
1171 0           my $selecttimeout = 0.5; # Half a second
1172 0 0         if($hasoutbufferwork) {
1173 0           $selecttimeout = 0.05;
1174             }
1175              
1176 0           my @inclients = $self->{selector}->can_read($selecttimeout);
1177 0           foreach my $clientsocket (@inclients) {
1178 0           my $cid = $clientsocket->_getClientID();
1179              
1180             # Skip if client already marked for removal or doesn't exist
1181 0 0         next if(!defined($self->{clients}->{$cid}));
1182 0 0         next if(contains($cid, $self->{toremove}));
1183              
1184 0           my $totalread = 0;
1185 0           my $readchunksleft = 3;
1186 0           while(1) {
1187 0           my $rawbuffer;
1188             my $bytes;
1189 0           my $readok = 0;
1190 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
1191 0           $bytes = sysread($self->{clients}->{$cid}->{socket}, $rawbuffer, 1_000_000); # Read at most 1 Meg at a time
1192 0           $readok = 1;
1193             };
1194 0 0         if(!$readok) {
1195             # Exception during read - disconnect
1196 0           push @{$self->{toremove}}, $cid;
  0            
1197 0           last;
1198             }
1199 0 0         if(!defined($bytes)) {
1200             # undef = error, check if temporary (EAGAIN/EWOULDBLOCK) or permanent
1201 0 0 0       if(!$ERRNO{EAGAIN} && !$ERRNO{EWOULDBLOCK}) {
1202             # Permanent error - disconnect
1203 0           push @{$self->{toremove}}, $cid;
  0            
1204             }
1205 0           last;
1206             }
1207 0 0         if($bytes == 0) {
1208             # EOF - peer closed connection, disconnect immediately
1209 0           push @{$self->{toremove}}, $cid;
  0            
1210 0           last;
1211             }
1212             # $bytes > 0, we have data
1213 0           $totalread += $bytes;
1214 0           push @{$self->{clients}->{$cid}->{charbuffer}}, split//, $rawbuffer;
  0            
1215 0           $readchunksleft--;
1216 0 0         if(!$readchunksleft) {
1217 0           last;
1218             }
1219             }
1220            
1221             # Check if we could read data from a socket that was marked as readable.
1222             # Thanks to SSL, this might occasionally fail. Don't bail out at the first
1223             # error, only if the condition persists for readfailtimeout seconds.
1224             # Note: EOF and permanent errors are now handled above, so this failtime
1225             # is mainly for SSL renegotiation edge cases and slow connections.
1226 0 0         if($totalread) {
1227             # Data received - reset fail timer
1228 0           $self->{clients}->{$cid}->{failtime} = 0;
1229             } else {
1230             # Socket marked readable but no data
1231             # Use longer timeout for interclacks connections (higher latency, more critical)
1232             my $failtimeout = $self->{clients}->{$cid}->{interclacks}
1233             ? $self->{config}->{interclacksreadfailtimeout}
1234 0 0         : $self->{config}->{readfailtimeout};
1235              
1236 0 0         if($self->{clients}->{$cid}->{failtime} == 0) {
    0          
1237             # First failure in this streak - record the time
1238 0           $self->{clients}->{$cid}->{failtime} = $now;
1239             } elsif(($now - $self->{clients}->{$cid}->{failtime}) > $failtimeout) {
1240             # Failure streak has exceeded timeout threshold
1241 0           push @{$self->{toremove}}, $cid;
  0            
1242             }
1243             }
1244             }
1245              
1246 0           return;
1247             }
1248              
1249             # $forceclientid let's us "force" working on a specific client only
1250 0     0     sub _clientOutput($self, $forceclientid = '') {
  0            
  0            
  0            
1251 0           my $now = $self->_getTime();
1252            
1253             # **** WRITE TO CLIENTS ****
1254 0           foreach my $cid (keys %{$self->{clients}}) {
  0            
1255 0 0 0       if($forceclientid ne '' && $forceclientid ne $cid) {
1256 0           next;
1257             }
1258 0 0 0       if($cid ne $forceclientid && contains($cid, $self->{toremove})) {
1259 0           next;
1260             }
1261 0 0         if(length($self->{clients}->{$cid}->{outbuffer})) {
    0          
1262 0           $self->{clients}->{$cid}->{lastmessage} = $now;
1263             } elsif(($self->{clients}->{$cid}->{lastmessage} + 60) < $now) {
1264 0           $self->{clients}->{$cid}->{lastmessage} = $now;
1265 0           $self->{clients}->{$cid}->{outbuffer} .= "NOP\r\n"; # send "No OPerations" command, just to
1266             # check if socket is still open
1267             }
1268              
1269 0 0         next if(!length($self->{clients}->{$cid}->{outbuffer}));
1270              
1271             # Output bandwidth-limited stuff, in as big chunks as possible
1272 0           my $written;
1273 0           $self->{workCount}++;
1274 0           eval { ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
1275 0           $written = syswrite($self->{clients}->{$cid}->{socket}, $self->{clients}->{$cid}->{outbuffer});
1276             };
1277 0 0         if($EVAL_ERROR) {
1278 0           print STDERR "Write error: $EVAL_ERROR\n";
1279 0           push @{$self->{toremove}}, $cid;
  0            
1280 0           next;
1281             }
1282 0 0         if(!defined($written)) {
1283             # syswrite returned undef - check error type
1284 0 0 0       if($ERRNO{EPIPE}) {
    0          
1285             # Peer closed connection
1286 0           print STDERR "Client $cid: connection closed by peer (EPIPE)\n";
1287 0           push @{$self->{toremove}}, $cid;
  0            
1288 0           next;
1289             } elsif(!$ERRNO{EAGAIN} && !$ERRNO{EWOULDBLOCK}) {
1290             # Other permanent error
1291 0           print STDERR "Write error for $cid: $ERRNO\n";
1292 0           push @{$self->{toremove}}, $cid;
  0            
1293 0           next;
1294             }
1295             # EAGAIN/EWOULDBLOCK - try again later
1296 0           next;
1297             }
1298 0 0 0       if(!$self->{clients}->{$cid}->{socket}->opened || $self->{clients}->{$cid}->{socket}->error) {
1299 0           print STDERR "Socket error for $cid\n";
1300 0           push @{$self->{toremove}}, $cid;
  0            
1301 0           next;
1302             }
1303              
1304 0 0 0       if(defined($written) && $written) {
1305 0 0         if(length($self->{clients}->{$cid}->{outbuffer}) == $written) {
1306 0           $self->{clients}->{$cid}->{outbuffer} = '';
1307             } else {
1308 0           $self->{clients}->{$cid}->{outbuffer} = substr($self->{clients}->{$cid}->{outbuffer}, $written);
1309             }
1310             }
1311             }
1312              
1313 0           return;
1314             }
1315              
1316 0     0     sub _cacheCleanup($self) {
  0            
  0            
1317 0           my $now = $self->_getTime();
1318              
1319 0           my $deletedtime = $now - $self->{config}->{deletedcachetime};
1320 0           my $staletime = $now - $self->{config}->{stalecachetime};
1321 0           foreach my $ckey (keys %{$self->{cache}}) {
  0            
1322             # Clean deleted entries. Ignore any non-"DELETED" entries as these are run from a different timer
1323 0 0 0       if($self->{cache}->{$ckey}->{deleted} && $self->{cache}->{$ckey}->{cachetime} < $deletedtime) {
1324 0           delete $self->{cache}->{$ckey};
1325              
1326 0           my %tmp = (
1327             sender => 'SERVERCACHE',
1328             type => 'DEBUG',
1329             data => 'CLEANDELETED=' . $ckey,
1330             );
1331              
1332 0           push @{$self->{outbox}}, \%tmp;
  0            
1333 0           $self->{savecache} = 1;
1334 0           next;
1335             }
1336              
1337             # Forget entries that have not been accesses in a long time (stale entries).
1338             # Ignore any "DELETED" entries, as these are run from a different timer
1339 0 0 0       if(!$self->{cache}->{$ckey}->{deleted} && $self->{cache}->{$ckey}->{accesstime} < $staletime) {
1340 0           delete $self->{cache}->{$ckey};
1341              
1342 0           my %tmp = (
1343             sender => 'SERVERCACHE',
1344             type => 'DEBUG',
1345             data => 'CLEANSTALE=' . $ckey,
1346             );
1347              
1348 0           push @{$self->{outbox}}, \%tmp;
  0            
1349 0           $self->{savecache} = 1;
1350 0           next;
1351             }
1352             }
1353              
1354 0           return;
1355             }
1356              
1357 0     0     sub _outboxToClientBuffer($self) {
  0            
  0            
1358 0           my $now = $self->_getTime();
1359              
1360             # Outbox contains the messages that have to be forwarded to the clients when listening (or when the connection is in interclacks mode)
1361             # We iterate over the outbox and put those messages into the output buffers of the corresponding client connection
1362 0           while((my $line = shift @{$self->{outbox}})) {
  0            
1363 0           $self->{workCount}++;
1364 0           foreach my $cid (keys %{$self->{clients}}) {
  0            
1365 0 0 0       if($line->{type} eq 'DEBUG' && $self->{clients}->{$cid}->{mirror}) {
1366 0           $self->{clients}->{$cid}->{outbuffer} .= "DEBUG " . $line->{sender} . "=". $line->{data} . "\r\n";
1367             }
1368              
1369 0 0         if($cid eq $line->{sender}) {
1370 0           next;
1371             }
1372              
1373 0 0 0       if($line->{type} ne 'DEBUG' && defined($self->{clients}->{$cid}->{listening}->{$line->{name}})) {
1374             # Just buffer in the clients outbuffers
1375 0 0         if($line->{type} eq 'NOTIFY') {
    0          
    0          
1376 0           $self->{clients}->{$cid}->{outbuffer} .= "NOTIFY ". $line->{name} . "\r\n";
1377             } elsif($line->{type} eq 'SET') {
1378 0           $self->{clients}->{$cid}->{outbuffer} .= "SET ". $line->{name} . "=" . $line->{value} . "\r\n";
1379             } elsif($line->{type} eq 'SETANDSTORE') {
1380             # We forward SETANDSTORE as such only over interclacks connections. Basic clients don't have a cache,
1381             # so we only send a SET command
1382 0 0         if($self->{clients}->{$cid}->{interclacks}) {
1383 0           $self->{clients}->{$cid}->{outbuffer} .= "SETANDSTORE ". $line->{name} . "=" . $line->{value} . "\r\n";
1384             } else {
1385 0           $self->{clients}->{$cid}->{outbuffer} .= "SET ". $line->{name} . "=" . $line->{value} . "\r\n";
1386             }
1387             }
1388             }
1389             }
1390             }
1391              
1392              
1393             # Push all messages that can be released at this time into the corresponding char based output for each client
1394 0           foreach my $cid (keys %{$self->{clients}}) {
  0            
1395 0           while(scalar @{$self->{clients}->{$cid}->{outmessages}}) {
  0            
1396 0 0         last if($self->{clients}->{$cid}->{outmessages}->[0]->{releasetime} > $now);
1397              
1398 0           my $outmsg = shift @{$self->{clients}->{$cid}->{outmessages}};
  0            
1399 0 0         if($outmsg->{message} eq 'EXIT') {
1400 0           push @{$self->{toremove}}, $cid; # Disconnect the client
  0            
1401             } else {
1402 0           $self->{clients}->{$cid}->{outbuffer} .= $outmsg->{message} . "\r\n";
1403             }
1404             }
1405             }
1406              
1407 0           return;
1408             }
1409              
1410 0     0     sub _requirePermission($self, $cid, $type) {
  0            
  0            
  0            
  0            
1411 0 0 0       if(defined($self->{clients}->{$cid}->{permissions}->{$type}) && $self->{clients}->{$cid}->{permissions}->{$type}) {
1412             # Permission OK
1413 0           return 1;
1414             }
1415              
1416 0           my $now = $self->_getTime();
1417 0           push @{$self->{clients}->{$cid}->{outmessages}}, {releasetime => $now + $self->{clients}->{$cid}->{outmessagedelay}, message => 'OVERHEAD E permission_denied'};
  0            
1418              
1419 0           return 0;
1420             }
1421              
1422 0     0     sub _handleMessageOverhead($self, $cid, $inmsg) {
  0            
  0            
  0            
  0            
1423 0           my $now = $self->_getTime();
1424              
1425 0 0         if($inmsg =~ /^OVERHEAD\ (.+?)\ (.+)/) {
1426 0           my ($flags, $value) = ($1, $2);
1427 0           $self->{sendinterclacks} = 0;
1428 0           my @flagparts = split//, $flags;
1429 0           my %parsedflags;
1430             my %newflags;
1431 0           foreach my $key (sort keys %overheadflags) {
1432 0 0         if(contains($key, \@flagparts)) {
1433 0           $parsedflags{$overheadflags{$key}} = 1;
1434 0           $newflags{$overheadflags{$key}} = 1;
1435             } else {
1436 0           $parsedflags{$overheadflags{$key}} = 0;
1437 0           $newflags{$overheadflags{$key}} = 0;
1438             }
1439             }
1440              
1441 0 0         if($parsedflags{auth_token}) {
1442 0 0         if(defined($self->{userlist}->{$value})) {
1443 0           $self->{clients}->{$cid}->{authok} = 1;
1444              
1445             # Copy user permissions to client session
1446 0           foreach my $key (qw[read write manage interclacks]) {
1447 0           $self->{clients}->{$cid}->{permissions}->{$key} = $self->{userlist}->{$value}->{$key};
1448             }
1449              
1450             #$self->{clients}->{$cid}->{outbuffer} .= "OVERHEAD O Welcome!\r\n";
1451 0           push @{$self->{clients}->{$cid}->{outmessages}}, {releasetime => $now + $self->{clients}->{$cid}->{outmessagedelay}, message => 'OVERHEAD O Welcome!'};
  0            
1452 0           return true; # NO LOGGING OF CREDENTIALS
1453             } else {
1454 0           $self->{clients}->{$cid}->{authok} = 0;
1455             #$self->{clients}->{$cid}->{outbuffer} .= "OVERHEAD F Login failed!\r\n";
1456 0           push @{$self->{clients}->{$cid}->{outmessages}}, {releasetime => $now + $self->{clients}->{$cid}->{outmessagedelay}, message => 'OVERHEAD F Login failed!'};
  0            
1457 0           push @{$self->{clients}->{$cid}->{outmessages}}, {releasetime => $now + $self->{clients}->{$cid}->{outmessagedelay}, message => 'EXIT'};
  0            
1458 0           push @{$self->{toremove}}, $cid; # Disconnect the client
  0            
1459 0           return true; # NO LOGGING OF CREDENTIALS
1460             }
1461             }
1462              
1463             # Ignore other command when not authenticated
1464 0 0         if(!$self->{clients}->{$cid}->{authok}) {
1465 0           return true;
1466             }
1467              
1468 0 0         if($parsedflags{timestamp}) {
1469 0           $now = $self->_getTime(); # Make sure we are at the "latest" $now. This is one of the very few critical sections
1470 0           $self->{clients}->{$cid}->{client_timeoffset} = $now - $value;
1471 0           print "**** CLIENT TIME OFFSET: ", $self->{clients}->{$cid}->{client_timeoffset}, "\n";
1472 0           return true;
1473             }
1474              
1475 0 0 0       if($parsedflags{lock_for_sync} && $self->{clients}->{$cid}->{interclacksclient}) {
1476 0 0         return true unless($self->_requirePermission($cid, 'interclacks'));
1477 0 0         if($value) {
1478 0           print "Interclacks sync lock ON.\n";
1479 0           $self->{interclackslock} = 1;
1480             } else {
1481 0           print "Interclacks sync lock OFF.\n";
1482 0           $self->{interclackslock} = 0;
1483              
1484             # Send server our keys AFTER we got everything FROM the server (e.g. after unlock)
1485 0           $self->{clients}->{$cid}->{outbuffer} .= "OVERHEAD T " . $self->_getTime() . "\r\n"; # Send local time to server for offset calculation
1486              
1487 0           $now = $self->_getTime();
1488 0           foreach my $ckey (sort keys %{$self->{cache}}) {
  0            
1489             # Sanity checks
1490 0 0 0       if(!defined($self->{cache}->{$ckey}->{cachetime}) || !looks_like_number($self->{cache}->{$ckey}->{cachetime})) {
1491 0           $self->{cache}->{$ckey}->{cachetime} = $now;
1492             }
1493 0 0 0       if(!defined($self->{cache}->{$ckey}->{accesstime}) || !looks_like_number($self->{cache}->{$ckey}->{accesstime})) {
1494 0           $self->{cache}->{$ckey}->{accesstime} = $now;
1495             }
1496 0 0 0       if(!defined($self->{cache}->{$ckey}->{deleted}) || !$self->{cache}->{$ckey}->{deleted}) {
1497 0           $self->{cache}->{$ckey}->{deleted} = 0;
1498             }
1499 0 0         if(!defined($self->{cache}->{$ckey}->{data})) {
1500 0           $self->{cache}->{$ckey}->{data} = '';
1501             }
1502            
1503             # Send KEYSYNC commands
1504 0 0         if(!$self->{cache}->{$ckey}->{deleted}) {
1505 0           $self->{clients}->{$cid}->{outbuffer} .= "KEYSYNC " . $self->{cache}->{$ckey}->{cachetime} . " " . $self->{cache}->{$ckey}->{accesstime} . " U $ckey=" . $self->{cache}->{$ckey}->{data} . "\r\n";
1506             } else {
1507 0           $self->{clients}->{$cid}->{outbuffer} .= "KEYSYNC " . $self->{cache}->{$ckey}->{cachetime} . " 0 D $ckey=REMOVED\r\n";
1508             }
1509             }
1510             }
1511 0           $parsedflags{forward_message} = 0; # Don't forward
1512 0           $newflags{return_to_sender} = 0; # Don't return to sender
1513             }
1514              
1515 0 0 0       if($parsedflags{close_all_connections} && $value) {
1516 0 0         return true unless($self->_requirePermission($cid, 'manage'));
1517 0           foreach my $closecid (keys %{$self->{clients}}) {
  0            
1518 0 0 0       if($self->{clients}->{$closecid}->{interclacks} && $parsedflags{forward_message}) {
1519 0           $self->_evalsyswrite($self->{clients}->{$closecid}->{socket}, "\r\nOVERHEAD GC 1\r\n");
1520             }
1521 0           $self->_evalsyswrite($self->{clients}->{$closecid}->{socket}, "\r\nQUIT\r\n");
1522 0           push @{$self->{toremove}}, $closecid;
  0            
1523             }
1524 0           $parsedflags{forward_message} = 0; # Already forwarded where needed
1525             }
1526              
1527 0 0         if($parsedflags{shutdown_service}) {
1528 0 0         return true unless($self->_requirePermission($cid, 'manage'));
1529 0           $value = 0 + $value;
1530 0 0         if($value > 0) {
1531 0           $self->{shutdowntime} = $value + $now;
1532 0           print STDERR "Shutting down in $value seconds\n";
1533             }
1534             }
1535 0 0         if($parsedflags{discard_message}) {
1536 0           $self->{discardafterlogging} = 1;
1537             }
1538 0 0         if($parsedflags{no_logging}) {
1539 0           $self->{nodebug} = 1;
1540             }
1541              
1542 0 0         if($parsedflags{error_message}) {
1543 0           print STDERR 'ERROR from ', $cid, ': ', $value, "\n";
1544             }
1545              
1546 0 0         if($parsedflags{set_interclacks_mode}) {
1547 0 0         return true unless($self->_requirePermission($cid, 'interclacks'));
1548 0           $newflags{forward_message} = 0;
1549 0           $newflags{return_to_sender} = 0;
1550              
1551 0 0         if($value) {
1552 0           $self->{clients}->{$cid}->{interclacks} = 1;
1553 0           $self->{clients}->{$cid}->{lastping} = $now;
1554              
1555              
1556             $self->{clients}->{$cid}->{outbuffer} .= "CLACKS PageCamel $VERSION in interclacks master mode\r\n" . # Tell client we are in interclacks master mode
1557             "OVERHEAD M Authentication required\r\n" . # Informal message
1558 0           "OVERHEAD A " . $self->{authtoken} . "\r\n" . # ...and send Auth token...
1559             "OVERHEAD L 1\r\n" . # ...and lock client for sync
1560             "OVERHEAD T " . time . "\r\n"; # ... and send local timestamp
1561              
1562             # Make sure our new interclacks client has an *exact* copy of our buffer
1563             #$self->{clients}->{$cid}->{outbuffer} .= "CLEARCACHE\r\n";
1564            
1565 0           $now = $self->_getTime();
1566 0           foreach my $ckey (sort keys %{$self->{cache}}) {
  0            
1567             # Sanity checks
1568 0 0 0       if(!defined($self->{cache}->{$ckey}->{cachetime}) || !looks_like_number($self->{cache}->{$ckey}->{cachetime})) {
1569 0           $self->{cache}->{$ckey}->{cachetime} = $now;
1570             }
1571 0 0 0       if(!defined($self->{cache}->{$ckey}->{accesstime}) || !looks_like_number($self->{cache}->{$ckey}->{accesstime})) {
1572 0           $self->{cache}->{$ckey}->{accesstime} = $now;
1573             }
1574 0 0 0       if(!defined($self->{cache}->{$ckey}->{deleted}) || !$self->{cache}->{$ckey}->{deleted}) {
1575 0           $self->{cache}->{$ckey}->{deleted} = 0;
1576             }
1577 0 0         if(!defined($self->{cache}->{$ckey}->{data})) {
1578 0           $self->{cache}->{$ckey}->{data} = '';
1579             }
1580              
1581             # Send KEYSYNC commands
1582 0 0         if(!$self->{cache}->{$ckey}->{deleted}) {
1583 0           $self->{clients}->{$cid}->{outbuffer} .= "KEYSYNC " . $self->{cache}->{$ckey}->{cachetime} . " " . $self->{cache}->{$ckey}->{accesstime} . " U $ckey=" . $self->{cache}->{$ckey}->{data} . "\r\n";
1584             } else {
1585 0           $self->{clients}->{$cid}->{outbuffer} .= "KEYSYNC " . $self->{cache}->{$ckey}->{cachetime} . " 0 D $ckey=REMOVED\r\n";
1586             }
1587             }
1588 0           $self->{clients}->{$cid}->{outbuffer} .= "OVERHEAD L 0\r\n"; # unlock client after sync
1589 0           $self->{clients}->{$cid}->{outbuffer} .= "PING\r\n";
1590 0           $self->{clients}->{$cid}->{lastinterclacksping} = $now;
1591             } else {
1592 0           $self->{clients}->{$cid}->{interclacks} = 0;
1593 0           $self->{clients}->{$cid}->{lastping} = $now;
1594             }
1595             }
1596              
1597 0           my $newflagstring = '';
1598 0           $newflags{return_to_sender} = 0;
1599              
1600 0           foreach my $key (sort keys %overheadflags) {
1601 0 0         next if($key eq 'Z');
1602 0 0         if($newflags{$overheadflags{$key}}) {
1603 0           $newflagstring .= $key;
1604             }
1605             }
1606 0 0         if($newflagstring eq '') {
1607 0           $newflagstring = 'Z';
1608             }
1609              
1610 0 0         if($parsedflags{forward_message}) {
1611 0 0         return true unless($self->_requirePermission($cid, 'write'));
1612 0           foreach my $overheadcid (keys %{$self->{clients}}) {
  0            
1613 0 0 0       next if($cid eq $overheadcid && !$parsedflags{return_to_sender});
1614              
1615 0           $self->{clients}->{$overheadcid}->{outbuffer} .= "OVERHEAD $newflagstring $value\r\n";
1616             }
1617             }
1618              
1619 0           return true;
1620             }
1621              
1622 0           return false;
1623             }
1624              
1625 0     0     sub _handleMessageCaching($self, $cid, $inmsg) {
  0            
  0            
  0            
  0            
1626 0           my $now = $self->_getTime();
1627              
1628 0 0         if($inmsg =~ /^KEYSYNC\ (.+?)\ (.+?)\ (.+?)\ (.+?)\=(.*)/) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
1629 0 0         return true unless($self->_requirePermission($cid, 'interclacks'));
1630             #print "***** ", $inmsg, "\n";
1631 0           my ($ctimestamp, $atimestamp, $cmode, $ckey, $cval) = ($1, $2, $3, $4, $5);
1632 0           $self->{clients}->{$cid}->{lastping} = $now; # KEYSYNC acts as a PING as well
1633              
1634 0           $ctimestamp += $self->{clients}->{$cid}->{client_timeoffset}; # Take client time offset into account
1635 0 0         if($atimestamp) {
1636 0           $atimestamp += $self->{clients}->{$cid}->{client_timeoffset}; # Take client time offset into account
1637             }
1638              
1639 0 0 0       if(!defined($self->{cache}->{$ckey}) || $ctimestamp > $self->{cache}->{$ckey}->{cachetime}) {
1640             # If *we* have the older entry (or none at all), *only* then work on the keysync command
1641 0           $self->{cache}->{$ckey} = {
1642             data => $cval,
1643             cachetime => $ctimestamp,
1644             accesstime => $atimestamp,
1645             deleted => 0,
1646             };
1647 0 0         if($cmode eq 'D') {
1648 0           $self->{cache}->{$ckey}->{data} = '';
1649 0           $self->{cache}->{$ckey}->{deleted} = 1;
1650             }
1651             }
1652              
1653 0           $self->{savecache} = 1;
1654 0           $self->{sendinterclacks} = 1;
1655             } elsif($inmsg =~ /^STORE\ (.+?)\=(.*)/) {
1656 0 0         return true unless($self->_requirePermission($cid, 'write'));
1657 0           my ($ckey, $cval) = ($1, $2);
1658 0           $self->{cache}->{$ckey} = {
1659             data => $cval,
1660             cachetime => $now,
1661             accesstime => $now,
1662             deleted => 0,
1663             };
1664 0           $self->{savecache} = 1;
1665             } elsif($inmsg =~ /^SETANDSTORE\ (.+?)\=(.*)/) {
1666 0 0         return true unless($self->_requirePermission($cid, 'write'));
1667 0           my ($ckey, $cval) = ($1, $2);
1668 0           my %tmp = (
1669             sender => $cid,
1670             type => 'SETANDSTORE',
1671             name => $ckey,
1672             value => $cval,
1673             );
1674 0           push @{$self->{outbox}}, \%tmp;
  0            
1675 0           $self->{cache}->{$ckey} = {
1676             data => $cval,
1677             cachetime => $now,
1678             accesstime => $now,
1679             deleted => 0,
1680             };
1681 0           $self->{savecache} = 1;
1682             } elsif($inmsg =~ /^RETRIEVE\ (.+)/) {
1683 0 0         return true unless($self->_requirePermission($cid, 'read'));
1684             #$self->{clients}->{$cid}->{outbuffer} .= "SET ". $line->{name} . "=" . $line->{value} . "\r\n";
1685 0           my $ckey = $1;
1686 0 0 0       if(defined($self->{cache}->{$ckey}) && !$self->{cache}->{$ckey}->{deleted}) {
1687 0           $self->{clients}->{$cid}->{outbuffer} .= "RETRIEVED $ckey=" . $self->{cache}->{$ckey}->{data} . "\r\n";
1688 0           $self->{cache}->{$ckey}->{accesstime} = $now;
1689 0           $self->{savecache} = 1;
1690             } else {
1691 0           $self->{clients}->{$cid}->{outbuffer} .= "NOTRETRIEVED $ckey\r\n";
1692             }
1693 0           $self->{sendinterclacks} = 0;
1694             } elsif($inmsg =~ /^REMOVE\ (.+)/) {
1695 0 0         return true unless($self->_requirePermission($cid, 'write'));
1696 0           my $ckey = $1;
1697 0           $self->{cache}->{$ckey} = {
1698             data => '',
1699             cachetime => $now,
1700             accesstime => $now,
1701             deleted => 1,
1702             };
1703 0           $self->{savecache} = 1;
1704             } elsif($inmsg =~ /^INCREMENT\ (.+)/) {
1705 0 0         return true unless($self->_requirePermission($cid, 'write'));
1706 0           my $ckey = $1;
1707 0           my $cval = 1;
1708 0 0         if($ckey =~ /(.+)\=(.+)/) {
1709 0           ($ckey, $cval) = ($1, $2);
1710 0           $cval = 0 + $cval;
1711             }
1712              
1713 0           my $oldval = 0;
1714 0 0 0       if(defined($self->{cache}->{$ckey}) && !$self->{cache}->{$ckey}->{deleted} && looks_like_number($self->{cache}->{$ckey}->{data})) {
      0        
1715 0           $oldval = $self->{cache}->{$ckey}->{data};
1716             }
1717              
1718 0           $self->{cache}->{$ckey} = {
1719             data => $oldval + $cval,
1720             cachetime => $now,
1721             accesstime => $now,
1722             deleted => 0,
1723             };
1724              
1725 0           $self->{savecache} = 1;
1726             } elsif($inmsg =~ /^DECREMENT\ (.+)/) {
1727 0 0         return true unless($self->_requirePermission($cid, 'write'));
1728 0           my $ckey = $1;
1729 0           my $cval = 1;
1730 0 0         if($ckey =~ /(.+)\=(.+)/) {
1731 0           ($ckey, $cval) = ($1, $2);
1732 0           $cval = 0 + $cval;
1733             }
1734              
1735 0           my $oldval = 0;
1736 0 0 0       if(defined($self->{cache}->{$ckey}) && !$self->{cache}->{$ckey}->{deleted} && looks_like_number($self->{cache}->{$ckey}->{data})) {
      0        
1737 0           $oldval = $self->{cache}->{$ckey}->{data};
1738             }
1739              
1740 0           $self->{cache}->{$ckey} = {
1741             data => $oldval - $cval,
1742             cachetime => $now,
1743             accesstime => $now,
1744             deleted => 0,
1745             };
1746              
1747 0           $self->{savecache} = 1;
1748             } elsif($inmsg =~ /^KEYLIST/) {
1749 0 0         return true unless($self->_requirePermission($cid, 'read'));
1750 0           $self->{clients}->{$cid}->{outbuffer} .= "KEYLISTSTART\r\n";
1751 0           foreach my $ckey (sort keys %{$self->{cache}}) {
  0            
1752 0           $self->{clients}->{$cid}->{outbuffer} .= "KEY $ckey\r\n";
1753             }
1754 0           $self->{clients}->{$cid}->{outbuffer} .= "KEYLISTEND\r\n";
1755 0           $self->{sendinterclacks} = 0;
1756             } elsif($inmsg =~ /^CLEARCACHE/) {
1757 0 0         return true unless $self->_requirePermission($cid, 'manage');
1758 0           $self->{cache} = {};
1759 0           $self->{savecache} = 1;
1760             } else {
1761             # "not handled in this sub"
1762 0           return false;
1763             }
1764              
1765 0           return true;
1766             }
1767              
1768 0     0     sub _handleMessageControl($self, $cid, $inmsg) {
  0            
  0            
  0            
  0            
1769 0           my $now = $self->_getTime();
1770              
1771 0 0 0       if($inmsg =~ /^LISTEN\ (.*)/) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
1772 0 0         return true unless($self->_requirePermission($cid, 'read'));
1773 0           $self->{clients}->{$cid}->{listening}->{$1} = 1;
1774 0           $self->{sendinterclacks} = 0;
1775             } elsif($inmsg =~ /^UNLISTEN\ (.*)/) {
1776 0 0         return true unless($self->_requirePermission($cid, 'read'));
1777 0           delete $self->{clients}->{$cid}->{listening}->{$1};
1778 0           $self->{sendinterclacks} = 0;
1779             } elsif($inmsg =~ /^MONITOR/) {
1780 0 0         return true unless($self->_requirePermission($cid, 'manage'));
1781 0           $self->{clients}->{$cid}->{mirror} = 1;
1782 0           $self->{sendinterclacks} = 0;
1783             } elsif($inmsg =~ /^UNMONITOR/) {
1784 0 0         return true unless($self->_requirePermission($cid, 'manage'));
1785 0           $self->{clients}->{$cid}->{mirror} = 0;
1786 0           $self->{sendinterclacks} = 0;
1787             } elsif($inmsg =~ /^QUIT/) {
1788 0           print STDERR "Client disconnected cleanly!\n";
1789 0           push @{$self->{toremove}}, $cid;
  0            
1790 0           $self->{sendinterclacks} = 0;
1791             } elsif($inmsg =~ /^TIMEOUT/ && $self->{clients}->{$cid}->{interclacks}) {
1792 0 0         return true unless($self->_requirePermission($cid, 'interclacks'));
1793 0           print STDERR "Ooops, didn't send timely PINGS through interclacks link!\n";
1794 0           push @{$self->{toremove}}, $cid;
  0            
1795 0           $self->{sendinterclacks} = 0;
1796             } elsif($inmsg =~ /^PING/) {
1797 0           $self->{clients}->{$cid}->{lastping} = $now;
1798 0           $self->{sendinterclacks} = 0;
1799             } elsif($inmsg =~ /^NOPING/) {
1800             # Disable PING check until next PING recieved
1801 0           $self->{clients}->{$cid}->{lastping} = 0;
1802 0           $self->{sendinterclacks} = 0;
1803             } elsif($inmsg =~ /^CLIENTLIST/) {
1804 0 0         return true unless($self->_requirePermission($cid, 'manage'));
1805 0           $self->{clients}->{$cid}->{outbuffer} .= "CLIENTLISTSTART\r\n";
1806 0           foreach my $lmccid (sort keys %{$self->{clients}}) {
  0            
1807             $self->{clients}->{$cid}->{outbuffer} .= "CLIENT CID=$lmccid;" .
1808             "HOST=" . $self->{clients}->{$lmccid}->{host} . ";" .
1809             "PORT=" . $self->{clients}->{$lmccid}->{port} . ";" .
1810             "CLIENTINFO=" . $self->{clients}->{$lmccid}->{clientinfo} . ";" .
1811             "OUTBUFFER_LENGTH=" . length($self->{clients}->{$lmccid}->{outbuffer}) . ";" .
1812             "INBUFFER_LENGTH=" . length($self->{clients}->{$lmccid}->{buffer}) . ";" .
1813             "INTERCLACKS=" . $self->{clients}->{$lmccid}->{interclacks} . ";" .
1814             "MONITOR=" . $self->{clients}->{$lmccid}->{mirror} . ";" .
1815             "LASTPING=" . $self->{clients}->{$lmccid}->{lastping} . ";" .
1816 0           "LASTINTERCLACKSPING=" . $self->{clients}->{$lmccid}->{lastinterclacksping} . ";" .
1817             "\r\n";
1818             }
1819 0           $self->{clients}->{$cid}->{outbuffer} .= "CLIENTLISTEND\r\n";
1820 0           $self->{sendinterclacks} = 0;
1821             } elsif($inmsg =~ /^CLIENTDISCONNECT\ (.+)/) {
1822 0 0         return true unless($self->_requirePermission($cid, 'manage'));
1823 0           my $lmccid = $1;
1824 0 0         if(defined($self->{clients}->{$lmccid})) {
1825             # Try to notify the client (may or may not work);
1826 0           $self->_evalsyswrite($self->{clients}->{$lmccid}->{socket}, "\r\nQUIT\r\n");
1827 0           push @{$self->{toremove}}, $lmccid;
  0            
1828             }
1829 0           $self->{sendinterclacks} = 0;
1830             } elsif($inmsg =~ /^FLUSH\ (.+)/) {
1831 0           my $retid = $1;
1832 0           $self->{clients}->{$cid}->{outbuffer} .= "FLUSHED $retid\r\n";
1833 0           $self->{sendinterclacks} = 0;
1834             } else {
1835             # "not handled in this sub"
1836 0           return false;
1837             }
1838              
1839 0           return true;
1840             }
1841              
1842              
1843 0     0     sub _handleMessageDirect($self, $cid, $inmsg) {
  0            
  0            
  0            
  0            
1844 0 0         if($inmsg =~ /^NOTIFY\ (.*)/) {
    0          
1845 0 0         return true unless($self->_requirePermission($cid, 'write'));
1846 0           my %tmp = (
1847             sender => $cid,
1848             type => 'NOTIFY',
1849             name => $1,
1850             );
1851 0           push @{$self->{outbox}}, \%tmp;
  0            
1852             } elsif($inmsg =~ /^SET\ (.+?)\=(.*)/) {
1853 0 0         return true unless($self->_requirePermission($cid, 'write'));
1854 0           my %tmp = (
1855             sender => $cid,
1856             type => 'SET',
1857             name => $1,
1858             value => $2,
1859             );
1860 0           push @{$self->{outbox}}, \%tmp;
  0            
1861             } else {
1862             # "not handled in this sub"
1863 0           return false;
1864             }
1865              
1866 0           return true;
1867             }
1868              
1869             1;
1870             __END__