ff746b5e116667eb1bcd3954eaae913da5182155
[spider.git] / perl / DXProt.pm
1 #!/usr/bin/perl
2 #
3 # This module impliments the protocal mode for a dx cluster
4 #
5 # Copyright (c) 1998 Dirk Koopman G1TLH
6 #
7 # $Id$
8
9
10 package DXProt;
11
12 @ISA = qw(DXChannel);
13
14 use DXUtil;
15 use DXChannel;
16 use DXUser;
17 use DXM;
18 use DXCluster;
19 use DXProtVars;
20 use DXCommandmode;
21 use DXLog;
22 use Spot;
23 use DXProtout;
24 use DXDebug;
25 use Filter;
26 use Local;
27 use DXDb;
28 use Time::HiRes qw(gettimeofday tv_interval);
29
30 use Carp;
31
32 use strict;
33 use vars qw($me $pc11_max_age $pc23_max_age $pc11_dup_age $pc23_dup_age
34                         %spotdup %wwvdup $last_hour %pings %rcmds
35                         %nodehops @baddx $baddxfn $pc12_dup_age
36                         %anndup $allowzero $pc12_dup_lth $decode_dk0wcy);
37
38 $me = undef;                                    # the channel id for this cluster
39 $decode_dk0wcy = undef;                 # if set use this callsign to decode announces from the EU WWV data beacon
40 $pc11_max_age = 1*3600;                 # the maximum age for an incoming 'real-time' pc11
41 $pc23_max_age = 1*3600;                 # the maximum age for an incoming 'real-time' pc23
42 $pc11_dup_age = 3*3600;                 # the maximum time to keep the spot dup list for
43 $pc23_dup_age = 3*3600;                 # the maximum time to keep the wwv dup list for
44 $pc12_dup_age = 24*3600;                # the maximum time to keep the ann dup list for
45 $pc12_dup_lth = 60;                             # the length of ANN text to save for deduping 
46 %spotdup = ();                              # the pc11 and 26 dup hash 
47 %wwvdup = ();                               # the pc23 and 27 dup hash
48 %anndup = ();                               # the PC12 dup hash
49 $last_hour = time;                              # last time I did an hourly periodic update
50 %pings = ();                    # outstanding ping requests outbound
51 %rcmds = ();                    # outstanding rcmd requests outbound
52 %nodehops = ();                 # node specific hop control
53 @baddx = ();                    # list of illegal spotted callsigns
54
55
56 $baddxfn = "$main::data/baddx.pl";
57
58 sub init
59 {
60         my $user = DXUser->get($main::mycall);
61         $DXProt::myprot_version += $main::version*100;
62         $me = DXProt->new($main::mycall, 0, $user); 
63         $me->{here} = 1;
64         $me->{state} = "indifferent";
65         do "$main::data/hop_table.pl" if -e "$main::data/hop_table.pl";
66         confess $@ if $@;
67         #  $me->{sort} = 'M';    # M for me
68
69         # now prime the spot duplicates file with today's and yesterday's data
70     my @today = Julian::unixtoj(time);
71         my @spots = Spot::readfile(@today);
72         @today = Julian::sub(@today, 1);
73         push @spots, Spot::readfile(@today);
74         for (@spots) {
75                 my $dupkey = "$_->[0]$_->[1]$_->[2]$_->[3]$_->[4]";
76                 $spotdup{$dupkey} = $_->[2];
77         }
78
79         # now prime the wwv duplicates file with just this month's data
80         my @wwv = Geomag::readfile(time);
81         for (@wwv) {
82                 my $dupkey = "$_->[1].$_->[2]$_->[3]$_->[4]";
83                 $wwvdup{$dupkey} = $_->[1];
84         }
85
86         # load the baddx file
87         do "$baddxfn" if -e "$baddxfn";
88         print "$@\n" if $@;
89 }
90
91 #
92 # obtain a new connection this is derived from dxchannel
93 #
94
95 sub new 
96 {
97         my $self = DXChannel::alloc(@_);
98         $self->{'sort'} = 'A';          # in absence of how to find out what sort of an object I am
99         return $self;
100 }
101
102 # this is how a pc connection starts (for an incoming connection)
103 # issue a PC38 followed by a PC18, then wait for a PC20 (remembering
104 # all the crap that comes between).
105 sub start
106 {
107         my ($self, $line, $sort) = @_;
108         my $call = $self->{call};
109         my $user = $self->{user};
110         
111         # remember type of connection
112         $self->{consort} = $line;
113         $self->{outbound} = $sort eq 'O';
114         $self->{priv} = $user->priv;
115         $self->{lang} = $user->lang;
116         $self->{isolate} = $user->{isolate};
117         $self->{consort} = $line;       # save the connection type
118         $self->{here} = 1;
119
120         # get the INPUT filters (these only pertain to Clusters)
121         $self->{inspotfilter} = Filter::read_in('spots', $call, 1);
122         $self->{inwwvfilter} = Filter::read_in('wwv', $call, 1);
123         $self->{inannfilter} = Filter::read_in('ann', $call, 1);
124         
125         # set unbuffered and no echo
126         $self->send_now('B',"0");
127         $self->send_now('E',"0");
128         
129         # ping neighbour node stuff
130         $self->pingint($user->pingint || 3*60);
131         $self->nopings(3);
132         $self->pingtime([ ]);
133
134         # send initialisation string
135         unless ($self->{outbound}) {
136                 $self->send(pc38()) if DXNode->get_all();
137                 $self->send(pc18());
138                 $self->lastping($main::systime);
139         } else {
140                 # remove from outstanding connects queue
141                 @main::outstanding_connects = grep {$_->{call} ne $call} @main::outstanding_connects;
142                 $self->lastping($main::systime + $self->pingint / 2);
143         }
144         $self->state('init');
145         $self->pc50_t(time);
146
147         Log('DXProt', "$call connected");
148 }
149
150 #
151 # This is the normal pcxx despatcher
152 #
153 sub normal
154 {
155         my ($self, $line) = @_;
156         my @field = split /\^/, $line;
157         pop @field if $field[-1] eq '~';
158         
159 #       print join(',', @field), "\n";
160                                                 
161         # ignore any lines that don't start with PC
162         return if !$field[0] =~ /^PC/;
163         
164         # process PC frames
165         my ($pcno) = $field[0] =~ /^PC(\d\d)/; # just get the number
166         return unless $pcno;
167         return if $pcno < 10 || $pcno > 51;
168
169         # dump bad protocol messages unless it is a PC29
170         if ($line =~ /\%[0-9A-F][0-9A-F]/o && $pcno != 29) {
171                 dbg('chan', "CORRUPT protocol message - dumped");
172                 return;
173         }
174
175         # local processing 1
176         my $pcr;
177         eval {
178                 $pcr = Local::pcprot($self, $pcno, @field);
179         };
180 #       dbg('local', "Local::pcprot error $@") if $@;
181         return if $pcr;
182         
183  SWITCH: {
184                 if ($pcno == 10) {              # incoming talk
185                         
186                         # is it for me or one of mine?
187                         my $call = ($field[5] gt ' ') ? $field[5] : $field[2];
188                         if ($call eq $main::mycall || grep $_ eq $call, get_all_user_calls()) {
189                                 
190                                 # yes, it is
191                                 my $text = unpad($field[3]);
192                                 Log('talk', $call, $field[1], $field[6], $text);
193                                 $call = $main::myalias if $call eq $main::mycall;
194                                 my $ref = DXChannel->get($call);
195                                 $ref->send("$call de $field[1]: $text") if $ref && $ref->{talk};
196                         } else {
197                                 $self->route($field[2], $line); # relay it on its way
198                         }
199                         return;
200                 }
201                 
202                 if ($pcno == 11 || $pcno == 26) { # dx spot
203
204                         # route 'foreign' pc26s 
205                         if ($pcno == 26) {
206                                 if ($field[7] ne $main::mycall) {
207                                         $self->route($field[7], $line);
208                                         return;
209                                 }
210                         }
211                         
212                         # if this is a 'nodx' node then ignore it
213                         if (grep $field[7] =~ /^$_/,  @DXProt::nodx_node) {
214                                 dbg('chan', "Bad DXNode, dropped");
215                                 return;
216                         }
217                         
218                         # convert the date to a unix date
219                         my $d = cltounix($field[3], $field[4]);
220                         # bang out (and don't pass on) if date is invalid or the spot is too old (or too young)
221                         if (!$d || ($pcno == 11 && ($d < $main::systime - $pc11_max_age || $d > $main::systime + 900))) {
222                                 dbg('chan', "Spot ignored, invalid date or out of range ($field[3] $field[4])\n");
223                                 return;
224                         }
225
226                         # strip off the leading & trailing spaces from the comment
227                         my $text = unpad($field[5]);
228                         
229                         # store it away
230                         my $spotter = $field[6];
231                         $spotter =~ s/-[\@\d]+$//o;     # strip off the ssid from the spotter
232                         
233                         # do some de-duping
234                         my $freq = $field[1] - 0;
235                         my $dupkey = "$freq$field[2]$d$text$spotter";
236                         if ($spotdup{$dupkey}) {
237                                 dbg('chan', "Duplicate Spot ignored\n");
238                                 return;
239                         }
240                         
241                         $spotdup{$dupkey} = $d;
242
243                         # is it 'baddx'
244                         if (grep $field[2] eq $_, @baddx) {
245                                 dbg('chan', "Bad DX spot, ignored");
246                                 return;
247                         }
248
249                         # are any of the crucial fields invalid?
250             if ($field[2] =~ /[a-z]/ || $field[6] =~ /[a-z]/ || $field[7] =~ /[a-z]/) {
251                                 dbg('chan', "Spot contains lower case callsigns, rejected");
252                                 return;
253                         }
254                         
255                         my @spot = Spot::add($freq, $field[2], $d, $text, $spotter, $field[7]);
256
257             #
258                         # @spot at this point contains:-
259             # freq, spotted call, time, text, spotter, spotted cc, spotters cc, orig node
260                         # then  spotted itu, spotted cq, spotters itu, spotters cq
261                         # you should be able to route on any of these
262             #
263                         
264                         # local processing 
265                         my $r;
266                         eval {
267                                 $r = Local::spot($self, @spot);
268                         };
269 #                       dbg('local', "Local::spot1 error $@") if $@;
270                         return if $r;
271
272                         # DON'T be silly and send on PC26s!
273                         return if $pcno == 26;
274
275                         # send out the filtered spots
276                         send_dx_spot($self, $line, @spot) if @spot;
277                         return;
278                 }
279                 
280                 if ($pcno == 12) {              # announces
281                         # announce duplicate checking
282                         my $text = substr(uc unpad($field[3]), 0, $pc12_dup_lth);
283                         my $dupkey = $field[1].$field[2].$text;
284                         if ($anndup{$dupkey}) {
285                                 dbg('chan', "Duplicate Announce ignored\n");
286                                 return;
287                         }
288                         $anndup{$dupkey} = $main::systime;
289                         
290                         if ($field[2] eq '*' || $field[2] eq $main::mycall) {
291                                 
292                                 # global ann filtering on INPUT
293                                 if ($self->{inannfilter}) {
294                                         my ($filter, $hops) = Filter::it($self->{inannfilter}, @field[1..6], $self->{call} );
295                                         unless ($filter) {
296                                                 dbg('chan', "Rejected by filter");
297                                                 return;
298                                         }
299                                 }
300
301                                 # send it
302                                 $self->send_announce($line, @field[1..6]);
303                                 
304                                 if ($decode_dk0wcy && $field[1] eq $decode_dk0wcy) {
305                                         my ($hour, $k, $next, $a, $r, $sfi, $alarm) = $field[3] =~ /^Aurora Beacon\s+(\d+)UTC,\s+Kiel\s+K=(\d+),.*ed\s+K=(\d+),\s+A=(\d+),\s+R=(\d+),\s+SFI=(\d+),.*larm:\s+(\w+)/;
306                                         $alarm = ($alarm =~ /^Y/i) ? ', Aurora in DE' : ''; 
307                                         my $wwv = Geomag::update($main::systime, $hour, $sfi, $a, $k, "R=$r, Next K=$next$alarm", $decode_dk0wcy, $field[5], $r) if $sfi && $r;
308                                 }
309                                 
310                         } else {
311                                 $self->route($field[2], $line);
312                         }
313                         
314                         return;
315                 }
316                 
317                 if ($pcno == 13) {
318                         last SWITCH;
319                 }
320                 if ($pcno == 14) {
321                         last SWITCH;
322                 }
323                 if ($pcno == 15) {
324                         last SWITCH;
325                 }
326                 
327                 if ($pcno == 16) {              # add a user
328                         my $node = DXCluster->get_exact($field[1]); 
329                         my $dxchan;
330                         if (!$node && ($dxchan = DXChannel->get($field[1]))) {
331                                 # add it to the node table if it isn't present and it's
332                                 # connected locally
333                                 $node = DXNode->new($dxchan, $field[1], 0, 1, 5400);
334                                 broadcast_ak1a(pc19($dxchan, $node), $dxchan, $self) unless $dxchan->{isolate};
335                                 
336                         }
337                         return unless $node; # ignore if havn't seen a PC19 for this one yet
338                         return unless $node->isa('DXNode');
339                         if ($node->dxchan != $self) {
340                                 dbg('chan', "LOOP: $field[1] came in on wrong channel");
341                                 return;
342                         }
343                         if (($dxchan = DXChannel->get($field[1])) && $dxchan != $self) {
344                                 dbg('chan', "LOOP: $field[1] connected locally");
345                                 return;
346                         }
347                         my $i;
348                                                 
349                         for ($i = 2; $i < $#field; $i++) {
350                                 my ($call, $confmode, $here) = $field[$i] =~ /^(\S+) (\S) (\d)/o;
351                                 next if !$call || length $call < 3 || length $call > 8;
352                                 next if !$confmode;
353                                 $call = uc $call;
354                                 next if DXCluster->get_exact($call); # we already have this (loop?)
355                                 
356                                 $confmode = $confmode eq '*';
357                                 DXNodeuser->new($self, $node, $call, $confmode, $here);
358                                 
359                                 # add this station to the user database, if required
360                                 $call =~ s/-\d+$//o;        # remove ssid for users
361                                 my $user = DXUser->get_current($call);
362                                 $user = DXUser->new($call) if !$user;
363                                 $user->homenode($node->call) if !$user->homenode;
364                                 $user->node($node->call);
365                                 $user->lastin($main::systime) unless DXChannel->get($call);
366                                 $user->put;
367                         }
368                         
369                         # queue up any messages (look for privates only)
370                         DXMsg::queue_msg(1) if $self->state eq 'normal';     
371                         last SWITCH;
372                 }
373                 
374                 if ($pcno == 17) {              # remove a user
375                         my $node = DXCluster->get_exact($field[2]);
376                         my $dxchan;
377                         if (!$node && ($dxchan = DXChannel->get($field[2]))) {
378                                 # add it to the node table if it isn't present and it's
379                                 # connected locally
380                                 $node = DXNode->new($dxchan, $field[2], 0, 1, 5400);
381                                 broadcast_ak1a(pc19($dxchan, $node), $dxchan, $self) unless $dxchan->{isolate};
382                                 return;
383                         }
384                         return unless $node;
385                         return unless $node->isa('DXNode');
386                         if ($node->dxchan != $self) {
387                                 dbg('chan', "LOOP: $field[2] came in on wrong channel");
388                                 return;
389                         }
390                         if (($dxchan = DXChannel->get($field[2])) && $dxchan != $self) {
391                                 dbg('chan', "LOOP: $field[2] connected locally");
392                                 return;
393                         }
394                         my $ref = DXCluster->get_exact($field[1]);
395                         $ref->del() if $ref;
396                         last SWITCH;
397                 }
398                 
399                 if ($pcno == 18) {              # link request
400                         $self->state('init');   
401
402                         # first clear out any nodes on this dxchannel
403                         my @gonenodes = map { $_->dxchan == $self ? $_ : () } DXNode::get_all();
404                         foreach my $node (@gonenodes) {
405                                 next if $node->dxchan == $DXProt::me;
406                                 broadcast_ak1a(pc21($node->call, 'Gone, re-init') , $self) unless $self->{isolate}; 
407                                 $node->del();
408                         }
409                         $self->send_local_config();
410                         $self->send(pc20());
411                         return;             # we don't pass these on
412                 }
413                 
414                 if ($pcno == 19) {              # incoming cluster list
415                         my $i;
416                         my $newline = "PC19^";
417                         for ($i = 1; $i < $#field-1; $i += 4) {
418                                 my $here = $field[$i];
419                                 my $call = uc $field[$i+1];
420                                 my $confmode = $field[$i+2];
421                                 my $ver = $field[$i+3];
422
423                                 $ver = 5400 if !$ver && $allowzero;
424                                 
425                                 # now check the call over
426                                 my $node = DXCluster->get_exact($call);
427                                 if ($node) {
428                                         my $dxchan;
429                                         if (($dxchan = DXChannel->get($call)) && $dxchan != $self) {
430                                                 dbg('chan', "LOOP: $call connected locally");
431                                         }
432                                     if ($node->dxchan != $self) {
433                                                 dbg('chan', "LOOP: $call come in on wrong channel");
434                                                 next;
435                                         }
436                                         dbg('chan', "already have $call");
437                                         next;
438                                 }
439                                 
440                                 # check for sane parameters
441                                 next if $ver < 5000; # only works with version 5 software
442                                 next if length $call < 3; # min 3 letter callsigns
443
444                                 # add it to the nodes table and outgoing line
445                                 $newline .= "$here^$call^$confmode^$ver^";
446                                 DXNode->new($self, $call, $confmode, $here, $ver);
447                                 
448                                 # unbusy and stop and outgoing mail (ie if somehow we receive another PC19 without a disconnect)
449                                 my $mref = DXMsg::get_busy($call);
450                                 $mref->stop_msg($call) if $mref;
451                                 
452                                 # add this station to the user database, if required (don't remove SSID from nodes)
453                                 my $user = DXUser->get_current($call);
454                                 if (!$user) {
455                                         $user = DXUser->new($call);
456                                         $user->sort('A');
457                                         $user->priv(1);                   # I have relented and defaulted nodes
458                                         $self->{priv} = 1;                # to user RCMDs allowed
459                                         $user->homenode($call);
460                                         $user->node($call);
461                                 }
462                                 $user->lastin($main::systime) unless DXChannel->get($call);
463                                 $user->put;
464                         }
465                         
466                         return if $newline eq "PC19^";
467
468                         # add hop count 
469                         $newline .=  get_hops(19) . "^";
470                         $line = $newline;
471                         last SWITCH;
472                 }
473                 
474                 if ($pcno == 20) {              # send local configuration
475                         $self->send_local_config();
476                         $self->send(pc22());
477                         $self->state('normal');
478                         return;
479                 }
480                 
481                 if ($pcno == 21) {              # delete a cluster from the list
482                         my $call = uc $field[1];
483                         if ($call ne $main::mycall) { # don't allow malicious buggers to disconnect me!
484                                 my $node = DXCluster->get_exact($call);
485                                 if ($node) {
486                                         if ($node->dxchan != $self) {
487                                                 dbg('chan', "LOOP: $call come in on wrong channel");
488                                                 return;
489                                         }
490                                         my $dxchan;
491                                         if (($dxchan = DXChannel->get($call)) && $dxchan != $self) {
492                                                 dbg('chan', "LOOP: $call connected locally");
493                                                 return;
494                                         }
495                                         $node->del();
496                                 } else {
497                                         dbg('chan', "$call not in table, dropped");
498                                         return;
499                                 }
500                         }
501                         last SWITCH;
502                 }
503                 
504                 if ($pcno == 22) {
505                         $self->state('normal');
506                         return;
507                 }
508                                 
509                 if ($pcno == 23 || $pcno == 27) { # WWV info
510                         
511                         # route 'foreign' pc27s 
512                         if ($pcno == 27) {
513                                 if ($field[8] ne $main::mycall) {
514                                         $self->route($field[8], $line);
515                                         return;
516                                 }
517                         }
518
519                         # do some de-duping
520                         my $d = cltounix($field[1], sprintf("%02d18Z", $field[2]));
521                         my $sfi = unpad($field[3]);
522                         my $k = unpad($field[4]);
523                         my $i = unpad($field[5]);
524                         my ($r) = $field[6] =~ /R=(\d+)/;
525                         $r = 0 unless $r;
526                         my $dupkey = "$d.$sfi$k$i";
527                         if ($wwvdup{$dupkey}) {
528                                 dbg('chan', "Dup WWV Spot ignored\n");
529                                 return;
530                         }
531                         if (($pcno == 23 && $d < $main::systime - $pc23_max_age) || $d > $main::systime + 1500 || $field[2] < 0 || $field[2] > 23) {
532                                 dbg('chan', "WWV Date ($field[1] $field[2]) out of range");
533                                 return;
534                         }
535                         $wwvdup{$dupkey} = $d;
536                         $field[6] =~ s/-\d+$//o;            # remove spotter's ssid
537                 
538                         my $wwv = Geomag::update($d, $field[2], $sfi, $k, $i, @field[6..8], $r);
539
540                         my $rep;
541                         eval {
542                                 $rep = Local::wwv($self, $field[1], $field[2], $sfi, $k, $i, @field[6..8], $r);
543                         };
544 #                       dbg('local', "Local::wwv2 error $@") if $@;
545                         return if $rep;
546
547                         # DON'T be silly and send on PC27s!
548                         return if $pcno == 27;
549
550                         # broadcast to the eager world
551                         send_wwv_spot($self, $line, $d, $field[2], $sfi, $k, $i, @field[6..8]);
552                         return;
553                 }
554                 
555                 if ($pcno == 24) {              # set here status
556                         my $call = uc $field[1];
557                         my $ref = DXCluster->get_exact($call);
558                         $ref->here($field[2]) if $ref;
559                         last SWITCH;
560                 }
561                 
562                 if ($pcno == 25) {      # merge request
563                         if ($field[1] ne $main::mycall) {
564                                 $self->route($field[1], $line);
565                                 return;
566                         }
567                         if ($field[2] eq $main::mycall) {
568                                 dbg('chan', "Trying to merge to myself, ignored");
569                                 return;
570                         }
571
572                         Log('DXProt', "Merge request for $field[3] spots and $field[4] WWV from $field[1]");
573                         
574                         # spots
575                         if ($field[3] > 0) {
576                                 my @in = reverse Spot::search(1, undef, undef, 0, $field[3]);
577                                 my $in;
578                                 foreach $in (@in) {
579                                         $self->send(pc26(@{$in}[0..4], $field[2]));
580                                 }
581                         }
582
583                         # wwv
584                         if ($field[4] > 0) {
585                                 my @in = reverse Geomag::search(0, $field[4], time, 1);
586                                 my $in;
587                                 foreach $in (@in) {
588                                         $self->send(pc27(@{$in}[0..5], $field[2]));
589                                 }
590                         }
591                         return;
592                 }
593
594                 if (($pcno >= 28 && $pcno <= 33) || $pcno == 40 || $pcno == 42 || $pcno == 49) { # mail/file handling
595                         if ($pcno == 49 || $field[1] eq $main::mycall) {
596                                 DXMsg::process($self, $line);
597                         } else {
598                                 $self->route($field[1], $line);
599                         }
600                         return;
601                 }
602                 
603                 if ($pcno == 34 || $pcno == 36) { # remote commands (incoming)
604                         if ($field[1] eq $main::mycall) {
605                                 my $ref = DXUser->get_current($field[2]);
606                                 my $cref = DXCluster->get($field[2]);
607                                 Log('rcmd', 'in', $ref->{priv}, $field[2], $field[3]);
608                                 unless ($field[3] =~ /rcmd/i || !$cref || !$ref || $cref->mynode->call ne $ref->homenode) {    # not allowed to relay RCMDS!
609                                         if ($ref->{priv}) {     # you have to have SOME privilege, the commands have further filtering
610                                                 $self->{remotecmd} = 1; # for the benefit of any command that needs to know
611                                                 my $oldpriv = $self->{priv};
612                                                 $self->{priv} = $ref->{priv};     # assume the user's privilege level
613                                                 my @in = (DXCommandmode::run_cmd($self, $field[3]));
614                                                 $self->{priv} = $oldpriv;
615                                                 for (@in) {
616                                                         s/\s*$//og;
617                                                         $self->send(pc35($main::mycall, $field[2], "$main::mycall:$_"));
618                                                         Log('rcmd', 'out', $field[2], $_);
619                                                 }
620                                                 delete $self->{remotecmd};
621                                         } else {
622                                                 $self->send(pc35($main::mycall, $field[2], "$main::mycall:sorry...!"));
623                                         }
624                                 } else {
625                                         $self->send(pc35($main::mycall, $field[2], "$main::mycall:your attempt is logged, Tut tut tut...!"));
626                                 }
627                         } else {
628                                 $self->route($field[1], $line);
629                         }
630                         return;
631                 }
632                 
633                 if ($pcno == 35) {              # remote command replies
634                         if ($field[1] eq $main::mycall) {
635                                 my $s = $rcmds{$field[2]};
636                                 if ($s) {
637                                         my $dxchan = DXChannel->get($s->{call});
638                                         $dxchan->send($field[3]) if $dxchan;
639                                         delete $rcmds{$field[2]} if !$dxchan;
640                                 }
641                         } else {
642                                 $self->route($field[1], $line);
643                         }
644                         return;
645                 }
646                 
647                 # for pc 37 see 44 onwards
648
649                 if ($pcno == 38) {              # node connected list from neighbour
650                         return;
651                 }
652                 
653                 if ($pcno == 39) {              # incoming disconnect
654                         $self->disconnect();
655                         return;
656                 }
657                 
658                 if ($pcno == 41) {              # user info
659                         # add this station to the user database, if required
660                         my $user = DXUser->get_current($field[1]);
661                         if (!$user) {
662                                 # then try without an SSID
663                                 $field[1] =~ s/-\d+$//o;
664                                 $user = DXUser->get_current($field[1]);
665                         }
666                         $user = DXUser->new($field[1]) if !$user;
667                         
668                         if ($field[2] == 1) {
669                                 $user->name($field[3]);
670                         } elsif ($field[2] == 2) {
671                                 $user->qth($field[3]);
672                         } elsif ($field[2] == 3) {
673                                 my ($lat, $long) = DXBearing::stoll($field[3]);
674                                 $user->lat($lat);
675                                 $user->long($long);
676                         } elsif ($field[2] == 4) {
677                                 $user->homenode($field[3]);
678                         }
679                         $user->put;
680                         last SWITCH;
681                 }
682                 if ($pcno == 43) {
683                         last SWITCH;
684                 }
685                 if ($pcno == 37 || $pcno == 44 || $pcno == 45 || $pcno == 46 || $pcno == 47) {
686                         DXDb::process($self, $line);
687                         return;
688                 }
689                 
690                 if ($pcno == 50) {              # keep alive/user list
691                         my $node = DXCluster->get_exact($field[1]);
692                         if ($node) {
693                                 return unless $node->isa('DXNode');
694                                 return unless $node->dxchan == $self;
695                                 $node->update_users($field[2]);
696                         }
697                         last SWITCH;
698                 }
699                 
700                 if ($pcno == 51) {              # incoming ping requests/answers
701                         
702                         # is it for us?
703                         if ($field[1] eq $main::mycall) {
704                                 my $flag = $field[3];
705                                 if ($flag == 1) {
706                                         $self->send(pc51($field[2], $field[1], '0'));
707                                 } else {
708                                         # it's a reply, look in the ping list for this one
709                                         my $ref = $pings{$field[2]};
710                                         if ($ref) {
711                                                 my $tochan =  DXChannel->get($field[2]);
712                                                 while (@$ref) {
713                                                         my $r = shift @$ref;
714                                                         my $dxchan = DXChannel->get($r->{call});
715                                                         next unless $dxchan;
716                                                         my $t = tv_interval($r->{t}, [ gettimeofday ]);
717                                                         if ($dxchan->is_user) {
718                                                                 my $s = sprintf "%.2f", $t; 
719                                                                 my $ave = sprintf "%.2f", $tochan ? ($tochan->pingave || $t) : $t;
720                                                                 $dxchan->send($dxchan->msg('pingi', $field[2], $s, $ave))
721                                                         } elsif ($dxchan->is_ak1a) {
722                                                                 if ($tochan) {
723                                                                         $tochan->nopings(3); # pump up the timer
724                                                                         push @{$tochan->pingtime}, $t;
725                                                                         shift @{$tochan->pingtime} if @{$tochan->pingtime} > 6;
726                                                                         my $st;
727                                                                         for (@{$tochan->pingtime}) {
728                                                                                 $st += $_;
729                                                                         }
730                                                                         $tochan->{pingave} = $st / @{$tochan->pingtime};
731                                                                 }
732                                                         } 
733                                                 }
734                                         }
735                                 }
736                         } else {
737                                 # route down an appropriate thingy
738                                 $self->route($field[1], $line);
739                         }
740                         return;
741                 }
742         }
743          
744          # if get here then rebroadcast the thing with its Hop count decremented (if
745          # there is one). If it has a hop count and it decrements to zero then don't
746          # rebroadcast it.
747          #
748          # NOTE - don't arrive here UNLESS YOU WANT this lump of protocol to be
749          #        REBROADCAST!!!!
750          #
751          
752         unless ($self->{isolate}) {
753                 broadcast_ak1a($line, $self); # send it to everyone but me
754         }
755 }
756
757 #
758 # This is called from inside the main cluster processing loop and is used
759 # for despatching commands that are doing some long processing job
760 #
761 sub process
762 {
763         my $t = time;
764         my @dxchan = DXChannel->get_all();
765         my $dxchan;
766         
767         foreach $dxchan (@dxchan) {
768                 next unless $dxchan->is_ak1a();
769                 next if $dxchan == $me;
770                 
771                 # send a pc50 out on this channel
772                 if ($t >= $dxchan->pc50_t + $DXProt::pc50_interval) {
773                         $dxchan->send(pc50());
774                         $dxchan->pc50_t($t);
775                 } 
776
777                 # send a ping out on this channel
778                 if ($t >= $dxchan->pingint + $dxchan->lastping) {
779                         if ($dxchan->nopings <= 0) {
780                                 $dxchan->disconnect;
781                         } else {
782                                 addping($main::mycall, $dxchan->call);
783                                 $dxchan->nopings($dxchan->nopings - 1);
784                                 $dxchan->lastping($t);
785                         }
786                 }
787         }
788         
789         my $key;
790         my $val;
791         my $cutoff;
792         if ($main::systime - 3600 > $last_hour) {
793                 $cutoff  = $main::systime - $pc11_dup_age;
794                 while (($key, $val) = each %spotdup) {
795                         delete $spotdup{$key} if $val < $cutoff;
796                 }
797                 $cutoff = $main::systime - $pc23_dup_age;
798                 while (($key, $val) = each %wwvdup) {
799                         delete $wwvdup{$key} if $val < $cutoff;
800                 }
801                 $cutoff = $main::systime - $pc12_dup_age;
802                 while (($key, $val) = each %anndup) {
803                         delete $anndup{$key} if $val < $cutoff;
804                 }
805                 $last_hour = $main::systime;
806         }
807 }
808
809 #
810 # finish up a pc context
811 #
812 sub finish
813 {
814         my $self = shift;
815         my $call = $self->call;
816         my $ref = DXCluster->get_exact($call);
817         
818         $self->send_now("D", DXProt::pc39($main::mycall, $self->msg('disc1', "System Op")));
819         
820         # unbusy and stop and outgoing mail
821         my $mref = DXMsg::get_busy($call);
822         $mref->stop_msg($call) if $mref;
823         
824         # broadcast to all other nodes that all the nodes connected to via me are gone
825         my @gonenodes = map { $_->dxchan == $self ? $_ : () } DXNode::get_all();
826         my $node;
827         
828         foreach $node (@gonenodes) {
829                 next if $node->call eq $call;
830                 broadcast_ak1a(pc21($node->call, 'Gone') , $self) unless $self->{isolate}; 
831                 $node->del();
832         }
833
834         # remove outstanding pings
835         delete $pings{$call};
836         
837         # now broadcast to all other ak1a nodes that I have gone
838         broadcast_ak1a(pc21($call, 'Gone.'), $self) unless $self->{isolate};
839         
840         Log('DXProt', $call . " Disconnected");
841         $ref->del() if $ref;
842 }
843
844 #
845 # some active measures
846 #
847 sub send_dx_spot
848 {
849         my $self = shift;
850         my $line = shift;
851         my @dxchan = DXChannel->get_all();
852         my $dxchan;
853         
854         # send it if it isn't the except list and isn't isolated and still has a hop count
855         # taking into account filtering and so on
856         foreach $dxchan (@dxchan) {
857                 my $routeit;
858                 my ($filter, $hops);
859
860                 if ($dxchan->{spotfilter}) {
861                     ($filter, $hops) = Filter::it($dxchan->{spotfilter}, @_, $self->{call} );
862                         next unless $filter;
863                 }
864                 
865                 if ($dxchan->is_ak1a) {
866                         next if $dxchan == $self;
867                         if ($hops) {
868                                 $routeit = $line;
869                                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
870                         } else {
871                                 $routeit = adjust_hops($dxchan, $line);  # adjust its hop count by node name
872                                 next unless $routeit;
873                         }
874                         if ($filter) {
875                                 $dxchan->send($routeit) if $routeit;
876                         } else {
877                                 $dxchan->send($routeit) unless $dxchan->{isolate} || $self->{isolate};
878                         }
879                 } elsif ($dxchan->is_user && $dxchan->{dx}) {
880                         my $buf = Spot::formatb($_[0], $_[1], $_[2], $_[3], $_[4]);
881                         $buf .= "\a\a" if $dxchan->{beep};
882                         if ($dxchan->{state} eq 'prompt' || $dxchan->{state} eq 'convers') {
883                                 $dxchan->send($buf);
884                         } else {
885                                 $dxchan->delay($buf);
886                         }
887                 }                                       
888         }
889 }
890
891 sub send_wwv_spot
892 {
893         my $self = shift;
894         my $line = shift;
895         my @dxchan = DXChannel->get_all();
896         my $dxchan;
897         
898         # send it if it isn't the except list and isn't isolated and still has a hop count
899         # taking into account filtering and so on
900         foreach $dxchan (@dxchan) {
901                 my $routeit;
902                 my ($filter, $hops);
903
904                 if ($dxchan->{spotfilter}) {
905                          ($filter, $hops) = Filter::it($dxchan->{wwvfilter}, @_, $self->{call} );
906                          next unless $filter;
907                 }
908                 if ($dxchan->is_ak1a) {
909                         next if $dxchan == $self;
910                         if ($hops) {
911                                 $routeit = $line;
912                                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
913                         } else {
914                                 $routeit = adjust_hops($dxchan, $line);  # adjust its hop count by node name
915                                 next unless $routeit;
916                         }
917                         if ($filter) {
918                                 $dxchan->send($routeit) if $routeit;
919                         } else {
920                                 $dxchan->send($routeit) unless $dxchan->{isolate} || $self->{isolate};
921                                 
922                         }
923                 } elsif ($dxchan->is_user && $dxchan->{wwv}) {
924                         my $buf = "WWV de $_[6] <$_[1]>:   SFI=$_[2], A=$_[3], K=$_[4], $_[5]";
925                         $buf .= "\a\a" if $dxchan->{beep};
926                         if ($dxchan->{state} eq 'prompt' || $dxchan->{state} eq 'convers') {
927                                 $dxchan->send($buf);
928                         } else {
929                                 $dxchan->delay($buf);
930                         }
931                 }                                       
932         }
933 }
934
935 # send an announce
936 sub send_announce
937 {
938         my $self = shift;
939         my $line = shift;
940         my @dxchan = DXChannel->get_all();
941         my $dxchan;
942         my $text = unpad($_[2]);
943         my $target;
944         my $to = 'To ';
945                                 
946         if ($_[3] eq '*') {     # sysops
947                 $target = "SYSOP";
948         } elsif ($_[3] gt ' ') { # speciality list handling
949                 my ($name) = split /\./, $_[3]; 
950                 $target = "$name"; # put the rest in later (if bothered) 
951         } 
952         
953         if ($_[5] eq '1') {
954                 $target = "WX"; 
955                 $to = '';
956         }
957         $target = "All" if !$target;
958         
959         Log('ann', $target, $_[0], $text);
960
961         # send it if it isn't the except list and isn't isolated and still has a hop count
962         # taking into account filtering and so on
963         foreach $dxchan (@dxchan) {
964                 my $routeit;
965                 my ($filter, $hops);
966
967                 if ($dxchan->{annfilter}) {
968                         ($filter, $hops) = Filter::it($dxchan->{annfilter}, @_, $self->{call} );
969                         next unless $filter;
970                 } 
971                 if ($dxchan->is_ak1a && $_[1] ne $main::mycall) {  # i.e not specifically routed to me
972                         next if $dxchan == $self;
973                         if ($hops) {
974                                 $routeit = $line;
975                                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
976                         } else {
977                                 $routeit = adjust_hops($dxchan, $line);  # adjust its hop count by node name
978                                 next unless $routeit;
979                         }
980                         if ($filter) {
981                                 $dxchan->send($routeit) if $routeit;
982                         } else {
983                                 $dxchan->send($routeit) unless $dxchan->{isolate} || $self->{isolate};
984                                 
985                         }
986                 } elsif ($dxchan->is_user && $dxchan->{ann}) {
987                         next if $target eq 'SYSOP' && $dxchan->{priv} < 5;
988                         my $buf = "$to$target de $_[0]: $text";
989                         $buf .= "\a\a" if $dxchan->{beep};
990                         if ($dxchan->{state} eq 'prompt' || $dxchan->{state} eq 'convers') {
991                                 $dxchan->send($buf);
992                         } else {
993                                 $dxchan->delay($buf);
994                         }
995                 }                                       
996         }
997 }
998
999 sub send_local_config
1000 {
1001         my $self = shift;
1002         my $n;
1003         my @nodes;
1004         my @localnodes;
1005         my @remotenodes;
1006                 
1007         # send our nodes
1008         if ($self->{isolate}) {
1009                 @localnodes = (DXCluster->get_exact($main::mycall));
1010         } else {
1011                 # create a list of all the nodes that are not connected to this connection
1012                 # and are not themselves isolated, this to make sure that isolated nodes
1013         # don't appear outside of this node
1014                 @nodes = DXNode::get_all();
1015                 @nodes = grep { $_->{call} ne $main::mycall } @nodes;
1016                 @nodes = grep { $_->dxchan != $self } @nodes if @nodes;
1017                 @nodes = grep { !$_->dxchan->{isolate} } @nodes if @nodes;
1018                 @localnodes = grep { $_->dxchan->{call} eq $_->{call} } @nodes if @nodes;
1019                 unshift @localnodes, DXCluster->get_exact($main::mycall);
1020                 @remotenodes = grep { $_->dxchan->{call} ne $_->{call} } @nodes if @nodes;
1021         }
1022
1023         my @s = $me->pc19(@localnodes, @remotenodes);
1024         for (@s) {
1025                 my $routeit = adjust_hops($self, $_);
1026                 $self->send($routeit) if $routeit;
1027         }
1028         
1029         # get all the users connected on the above nodes and send them out
1030         foreach $n (@localnodes, @remotenodes) {
1031                 my @users = values %{$n->list};
1032                 my @s = pc16($n, @users);
1033                 for (@s) {
1034                         my $routeit = adjust_hops($self, $_);
1035                         $self->send($routeit) if $routeit;
1036                 }
1037         }
1038 }
1039
1040 #
1041 # route a message down an appropriate interface for a callsign
1042 #
1043 # is called route(to, pcline);
1044 #
1045 sub route
1046 {
1047         my ($self, $call, $line) = @_;
1048         my $cl = DXCluster->get_exact($call);
1049         if ($cl) {       # don't route it back down itself
1050                 if (ref $self && $call eq $self->{call}) {
1051                         dbg('chan', "Trying to route back to source, dropped");
1052                         return;
1053                 }
1054                 my $hops;
1055                 my $dxchan = $cl->{dxchan};
1056                 if ($dxchan) {
1057                         my $routeit = adjust_hops($dxchan, $line);   # adjust its hop count by node name
1058                         if ($routeit) {
1059                                 $dxchan->send($routeit) if $dxchan;
1060                         }
1061                 }
1062         }
1063 }
1064
1065 # broadcast a message to all clusters taking into account isolation
1066 # [except those mentioned after buffer]
1067 sub broadcast_ak1a
1068 {
1069         my $s = shift;                          # the line to be rebroadcast
1070         my @except = @_;                        # to all channels EXCEPT these (dxchannel refs)
1071         my @dxchan = get_all_ak1a();
1072         my $dxchan;
1073         
1074         # send it if it isn't the except list and isn't isolated and still has a hop count
1075         foreach $dxchan (@dxchan) {
1076                 next if grep $dxchan == $_, @except;
1077                 my $routeit = adjust_hops($dxchan, $s);      # adjust its hop count by node name
1078                 $dxchan->send($routeit) unless $dxchan->{isolate} || !$routeit;
1079         }
1080 }
1081
1082 # broadcast a message to all clusters ignoring isolation
1083 # [except those mentioned after buffer]
1084 sub broadcast_all_ak1a
1085 {
1086         my $s = shift;                          # the line to be rebroadcast
1087         my @except = @_;                        # to all channels EXCEPT these (dxchannel refs)
1088         my @dxchan = get_all_ak1a();
1089         my $dxchan;
1090         
1091         # send it if it isn't the except list and isn't isolated and still has a hop count
1092         foreach $dxchan (@dxchan) {
1093                 next if grep $dxchan == $_, @except;
1094                 my $routeit = adjust_hops($dxchan, $s);      # adjust its hop count by node name
1095                 $dxchan->send($routeit);
1096         }
1097 }
1098
1099 # broadcast to all users
1100 # storing the spot or whatever until it is in a state to receive it
1101 sub broadcast_users
1102 {
1103         my $s = shift;                          # the line to be rebroadcast
1104         my $sort = shift;           # the type of transmission
1105         my $fref = shift;           # a reference to an object to filter on
1106         my @except = @_;                        # to all channels EXCEPT these (dxchannel refs)
1107         my @dxchan = get_all_users();
1108         my $dxchan;
1109         my @out;
1110         
1111         foreach $dxchan (@dxchan) {
1112                 next if grep $dxchan == $_, @except;
1113                 push @out, $dxchan;
1114         }
1115         broadcast_list($s, $sort, $fref, @out);
1116 }
1117
1118 # broadcast to a list of users
1119 sub broadcast_list
1120 {
1121         my $s = shift;
1122         my $sort = shift;
1123         my $fref = shift;
1124         my $dxchan;
1125         
1126         foreach $dxchan (@_) {
1127                 my $filter = 1;
1128                 
1129                 if ($sort eq 'dx') {
1130                     next unless $dxchan->{dx};
1131                         ($filter) = Filter::it($dxchan->{spotfilter}, @{$fref}) if ref $fref;
1132                         next unless $filter;
1133                 }
1134                 next if $sort eq 'ann' && !$dxchan->{ann};
1135                 next if $sort eq 'wwv' && !$dxchan->{wwv};
1136                 next if $sort eq 'wx' && !$dxchan->{wx};
1137
1138                 $s =~ s/\a//og unless $dxchan->{beep};
1139
1140                 if ($dxchan->{state} eq 'prompt' || $dxchan->{state} eq 'convers') {
1141                         $dxchan->send($s);      
1142                 } else {
1143                         $dxchan->delay($s);
1144                 }
1145         }
1146 }
1147
1148 #
1149 # gimme all the ak1a nodes
1150 #
1151 sub get_all_ak1a
1152 {
1153         my @list = DXChannel->get_all();
1154         my $ref;
1155         my @out;
1156         foreach $ref (@list) {
1157                 push @out, $ref if $ref->is_ak1a;
1158         }
1159         return @out;
1160 }
1161
1162 # return a list of all users
1163 sub get_all_users
1164 {
1165         my @list = DXChannel->get_all();
1166         my $ref;
1167         my @out;
1168         foreach $ref (@list) {
1169                 push @out, $ref if $ref->is_user;
1170         }
1171         return @out;
1172 }
1173
1174 # return a list of all user callsigns
1175 sub get_all_user_calls
1176 {
1177         my @list = DXChannel->get_all();
1178         my $ref;
1179         my @out;
1180         foreach $ref (@list) {
1181                 push @out, $ref->call if $ref->is_user;
1182         }
1183         return @out;
1184 }
1185
1186 #
1187 # obtain the hops from the list for this callsign and pc no 
1188 #
1189
1190 sub get_hops
1191 {
1192         my $pcno = shift;
1193         my $hops = $DXProt::hopcount{$pcno};
1194         $hops = $DXProt::def_hopcount if !$hops;
1195         return "H$hops";       
1196 }
1197
1198
1199 # adjust the hop count on a per node basis using the user loadable 
1200 # hop table if available or else decrement an existing one
1201 #
1202
1203 sub adjust_hops
1204 {
1205         my $self = shift;
1206         my $s = shift;
1207         my $call = $self->{call};
1208         my $hops;
1209         
1210         if (($hops) = $s =~ /\^H(\d+)\^~?$/o) {
1211                 my ($pcno) = $s =~ /^PC(\d\d)/o;
1212                 confess "$call called adjust_hops with '$s'" unless $pcno;
1213                 my $ref = $nodehops{$call} if %nodehops;
1214                 if ($ref) {
1215                         my $newhops = $ref->{$pcno};
1216                         return "" if defined $newhops && $newhops == 0;
1217                         $newhops = $ref->{default} unless $newhops;
1218                         return "" if defined $newhops && $newhops == 0;
1219                         $newhops = $hops if !$newhops;
1220                         $s =~ s/\^H(\d+)(\^~?)$/\^H$newhops$2/ if $newhops;
1221                 } else {
1222                         # simply decrement it
1223                         $hops--;
1224                         return "" if !$hops;
1225                         $s =~ s/\^H(\d+)(\^~?)$/\^H$hops$2/ if $hops;
1226                 }
1227         }
1228         return $s;
1229 }
1230
1231
1232 # load hop tables
1233 #
1234 sub load_hops
1235 {
1236         my $self = shift;
1237         return $self->msg('lh1') unless -e "$main::data/hop_table.pl";
1238         do "$main::data/hop_table.pl";
1239         return $@ if $@;
1240         return 0;
1241 }
1242
1243 # remove leading and trailing spaces from an input string
1244 sub unpad
1245 {
1246         my $s = shift;
1247         $s =~ s/^\s+|\s+$//;
1248         return $s;
1249 }
1250
1251 # add a ping request to the ping queues
1252 sub addping
1253 {
1254         my ($from, $to) = @_;
1255         my $ref = $pings{$to} || [];
1256         my $r = {};
1257         $r->{call} = $from;
1258         $r->{t} = [ gettimeofday ];
1259         route(undef, $to, pc51($to, $main::mycall, 1));
1260         push @$ref, $r;
1261         $pings{$to} = $ref;
1262 }
1263
1264 # add a rcmd request to the rcmd queues
1265 sub addrcmd
1266 {
1267         my ($from, $to, $cmd) = @_;
1268         my $r = {};
1269         $r->{call} = $from;
1270         $r->{t} = $main::systime;
1271         $r->{cmd} = $cmd;
1272         route(undef, $to, pc34($main::mycall, $to, $cmd));
1273         $rcmds{$to} = $r;
1274 }
1275 1;
1276 __END__