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