ec49fedd3e61069b5921ab3b4e26c37603182b91
[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 Carp;
25
26 use strict;
27 use vars qw($me $pc11_max_age $pc11_dup_age %dup $last_hour %pings %rcmds);
28
29 $me = undef;                                    # the channel id for this cluster
30 $pc11_max_age = 1*3600;                 # the maximum age for an incoming 'real-time' pc11
31 $pc11_dup_age = 24*3600;                # the maximum time to keep the dup list for
32 %dup = ();                                              # the pc11 and 26 dup hash 
33 $last_hour = time;                              # last time I did an hourly periodic update
34 %pings = ();                    # outstanding ping requests outbound
35 %rcmds = ();                    # outstanding rcmd requests outbound
36
37 sub init
38 {
39         my $user = DXUser->get($main::mycall);
40         $me = DXProt->new($main::mycall, undef, $user); 
41         #  $me->{sort} = 'M';    # M for me
42 }
43
44 #
45 # obtain a new connection this is derived from dxchannel
46 #
47
48 sub new 
49 {
50         my $self = DXChannel::alloc(@_);
51         $self->{sort} = 'A';            # in absence of how to find out what sort of an object I am
52         return $self;
53 }
54
55 # this is how a pc connection starts (for an incoming connection)
56 # issue a PC38 followed by a PC18, then wait for a PC20 (remembering
57 # all the crap that comes between).
58 sub start
59 {
60         my ($self, $line, $sort) = @_;
61         my $call = $self->{call};
62         my $user = $self->{user};
63         
64         # remember type of connection
65         $self->{consort} = $line;
66         $self->{outbound} = $sort eq 'O';
67         $self->{priv} = $user->priv;
68         $self->{lang} = $user->lang;
69         $self->{consort} = $line;       # save the connection type
70         $self->{here} = 1;
71         
72         # set unbuffered
73         $self->send_now('B',"0");
74         
75         # send initialisation string
76         if (!$self->{outbound}) {
77                 $self->send(pc38()) if DXNode->get_all();
78                 $self->send(pc18());
79         }
80         $self->state('init');
81         $self->pc50_t(time);
82         Log('DXProt', "$call connected");
83 }
84
85 #
86 # This is the normal pcxx despatcher
87 #
88 sub normal
89 {
90         my ($self, $line) = @_;
91         my @field = split /[\^\~]/, $line;
92         
93         # ignore any lines that don't start with PC
94         return if !$field[0] =~ /^PC/;
95         
96         # process PC frames
97         my ($pcno) = $field[0] =~ /^PC(\d\d)/; # just get the number
98         return if $pcno < 10 || $pcno > 51;
99         
100  SWITCH: {
101                 if ($pcno == 10) {              # incoming talk
102                         
103                         # is it for me or one of mine?
104                         my $call = ($field[5] gt ' ') ? $field[5] : $field[2];
105                         if ($call eq $main::mycall || grep $_ eq $call, get_all_user_calls()) {
106                                 
107                                 # yes, it is
108                                 my $text = unpad($field[3]);
109                                 Log('talk', $call, $field[1], $field[6], $text);
110                                 $call = $main::myalias if $call eq $main::mycall;
111                                 my $ref = DXChannel->get($call);
112                                 $ref->send("$call de $field[1]: $text") if $ref;
113                         } else {
114                                 route($field[2], $line); # relay it on its way
115                         }
116                         return;
117                 }
118                 
119                 if ($pcno == 11 || $pcno == 26) { # dx spot
120                         
121                         # if this is a 'nodx' node then ignore it
122                         last SWITCH if grep $field[7] =~ /^$_/,  @DXProt::nodx_node;
123                         
124                         # convert the date to a unix date
125                         my $d = cltounix($field[3], $field[4]);
126                         return if !$d || ($pcno == 11 && $d < $main::systime - $pc11_max_age); # bang out (and don't pass on) if date is invalid or the spot is too old
127                         
128                         # strip off the leading & trailing spaces from the comment
129                         my $text = unpad($field[5]);
130                         
131                         # store it away
132                         my $spotter = $field[6];
133                         $spotter =~ s/-\d+$//o; # strip off the ssid from the spotter
134                         
135                         # do some de-duping
136                         my $dupkey = "$field[1]$field[2]$d$text$field[6]";
137                         return if $dup{$dupkey};
138                         $dup{$dupkey} = $d;
139                         
140                         my $spot = Spot::add($field[1], $field[2], $d, $text, $spotter);
141                         
142                         # send orf to the users
143                         if ($spot && $pcno == 11) {
144                                 my $buf = Spot::formatb($field[1], $field[2], $d, $text, $spotter);
145                                 broadcast_users("$buf\a\a");
146                         }
147                         
148                         last SWITCH;
149                 }
150                 
151                 if ($pcno == 12) {              # announces
152                         
153                         if ($field[2] eq '*' || $field[2] eq $main::mycall) {
154                                 
155                                 # strip leading and trailing stuff
156                                 my $text = unpad($field[3]);
157                                 my $target;
158                                 my $to = 'To ';
159                                 my @list;
160                                 
161                                 if ($field[4] eq '*') { # sysops
162                                         $target = "Sysops";
163                                         @list = map { $_->priv >= 5 ? $_ : () } get_all_users();
164                                 } elsif ($field[4] gt ' ') { # speciality list handling
165                                         my ($name) = split /\./, $field[4]; 
166                                         $target = "$name"; # put the rest in later (if bothered) 
167                                 } 
168                                 
169                                 if ($field[6] eq '1') {
170                                         $target = "WX"; 
171                                         $to = '';
172                                 }
173                                 $target = "All" if !$target;
174                                 
175                                 if (@list > 0) {
176                                         broadcast_list("$to$target de $field[1]: $text", @list);
177                                 } else {
178                                         broadcast_users("$target de $field[1]: $text");
179                                 }
180                                 Log('ann', $target, $field[1], $text);
181                                 
182                                 return if $field[2] eq $main::mycall; # it's routed to me
183                         } else {
184                                 route($field[2], $line);
185                                 return;                 # only on a routed one
186                         }
187                         
188                         last SWITCH;
189                 }
190                 
191                 if ($pcno == 13) {
192                         last SWITCH;
193                 }
194                 if ($pcno == 14) {
195                         last SWITCH;
196                 }
197                 if ($pcno == 15) {
198                         last SWITCH;
199                 }
200                 
201                 if ($pcno == 16) {              # add a user
202                         my $node = DXCluster->get_exact($field[1]); 
203                         last SWITCH if !$node; # ignore if havn't seen a PC19 for this one yet
204                         my $i;
205                         
206                         for ($i = 2; $i < $#field; $i++) {
207                                 my ($call, $confmode, $here) = $field[$i] =~ /^(\S+) (-) (\d)/o;
208                                 next if length $call < 3;
209                                 next if !$confmode;
210                                 $call = uc $call;
211                                 next if DXCluster->get_exact($call); # we already have this (loop?)
212                                 
213                                 $confmode = $confmode eq '*';
214                                 DXNodeuser->new($self, $node, $call, $confmode, $here);
215                                 
216                                 # add this station to the user database, if required
217                                 $call =~ s/-\d+$//o;        # remove ssid for users
218                                 my $user = DXUser->get_current($call);
219                                 $user = DXUser->new($call) if !$user;
220                                 $user->node($node->call);
221                                 $user->homenode($node->call) if !$user->homenode;
222                                 $user->put;
223                         }
224                         
225                         # queue up any messages (look for privates only)
226                         DXMsg::queue_msg(1) if $self->state eq 'normal';     
227                         last SWITCH;
228                 }
229                 
230                 if ($pcno == 17) {              # remove a user
231                         
232                         my $ref = DXCluster->get_exact($field[1]);
233                         $ref->del() if $ref;
234                         last SWITCH;
235                 }
236                 
237                 if ($pcno == 18) {              # link request
238                         $self->send_local_config();
239                         $self->send(pc20());
240                         $self->state('init');   
241                         last SWITCH;
242                 }
243                 
244                 if ($pcno == 19) {              # incoming cluster list
245                         my $i;
246                         for ($i = 1; $i < $#field-1; $i += 4) {
247                                 my $here = $field[$i];
248                                 my $call = uc $field[$i+1];
249                                 my $confmode = $field[$i+2] eq '*';
250                                 my $ver = $field[$i+3];
251                                 
252                                 # now check the call over
253                                 next if DXCluster->get_exact($call); # we already have this
254                                 
255                                 # check for sane parameters
256                                 next if $ver < 5000; # only works with version 5 software
257                                 next if length $call < 3; # min 3 letter callsigns
258                                 DXNode->new($self, $call, $confmode, $here, $ver);
259                                 
260                                 # unbusy and stop and outgoing mail (ie if somehow we receive another PC19 without a disconnect)
261                                 my $mref = DXMsg::get_busy($call);
262                                 $mref->stop_msg($self) if $mref;
263                                 
264                                 # add this station to the user database, if required (don't remove SSID from nodes)
265                                 my $user = DXUser->get_current($call);
266                                 if (!$user) {
267                                         $user = DXUser->new($call);
268                                         $user->sort('A');
269                                         $user->node($call);
270                                         $user->homenode($call);
271                                         $user->put;
272                                 }
273                         }
274                         
275                         # queue up any messages
276                         DXMsg::queue_msg() if $self->state eq 'normal';     
277                         last SWITCH;
278                 }
279                 
280                 if ($pcno == 20) {              # send local configuration
281                         $self->send_local_config();
282                         $self->send(pc22());
283                         $self->state('normal');
284                         
285                         # queue mail
286                         DXMsg::queue_msg();
287                         return;
288                 }
289                 
290                 if ($pcno == 21) {              # delete a cluster from the list
291                         my $call = uc $field[1];
292                         if ($call ne $main::mycall) { # don't allow malicious buggers to disconnect me!
293                                 my $ref = DXCluster->get_exact($call);
294                                 $ref->del() if $ref;
295                         }
296                         last SWITCH;
297                 }
298                 
299                 if ($pcno == 22) {
300                         $self->state('normal');
301                         
302                         # queue mail
303                         DXMsg::queue_msg();
304                         return;
305                 }
306                 
307                 if ($pcno == 23 || $pcno == 27) { # WWV info
308                         Geomag::update(@field[1..$#field]);
309                         last SWITCH;
310                 }
311                 
312                 if ($pcno == 24) {              # set here status
313                         my $call = uc $field[1];
314                         my $ref = DXCluster->get_exact($call);
315                         $ref->here($field[2]) if $ref;
316                         last SWITCH;
317                 }
318                 
319                 if ($pcno == 25) {
320                         last SWITCH;
321                 }
322                 
323                 if (($pcno >= 28 && $pcno <= 33) || $pcno == 40 || $pcno == 42) { # mail/file handling
324                         DXMsg::process($self, $line);
325                         return;
326                 }
327                 
328                 if ($pcno == 34 || $pcno == 36) { # remote commands (incoming)
329                         if ($field[1] eq $main::mycall) {
330                                 my $ref = DXUser->get_current($field[2]);
331                                 Log('rcmd', 'in', $ref->{priv}, $field[2], $field[3]);
332                                 if ($ref->{priv}) {     # you have to have SOME privilege, the commands have further filtering
333                                         $self->{remotecmd} = 1; # for the benefit of any command that needs to know
334                                         my @in = (DXCommandmode::run_cmd($self, $field[3]));
335                                         for (@in) {
336                                                 s/\s*$//og;
337                                                 $self->send(pc35($main::mycall, $field[2], "$main::mycall:$_"));
338                                                 Log('rcmd', 'out', $field[2], $_);
339                                         }
340                                         delete $self->{remotecmd};
341                                 }
342                         } else {
343                                 route($field[1], $line);
344                         }
345                         return;
346                 }
347                 
348                 if ($pcno == 35) {              # remote command replies
349                         if ($field[1] eq $main::mycall) {
350                                 my $s = $rcmds{$field[2]};
351                                 if ($s) {
352                                         my $dxchan = DXChannel->get($s->{call});
353                                         $dxchan->send($field[3]) if $dxchan;
354                                         delete $rcmds{$field[2]} if !$dxchan;
355                                 }
356                         } else {
357                                 route($field[1], $line);
358                         }
359                         return;
360                 }
361                 
362                 if ($pcno == 37) {
363                         last SWITCH;
364                 }
365                 
366                 if ($pcno == 38) {              # node connected list from neighbour
367                         return;
368                 }
369                 
370                 if ($pcno == 39) {              # incoming disconnect
371                         $self->disconnect();
372                         return;
373                 }
374                 
375                 if ($pcno == 41) {              # user info
376                         # add this station to the user database, if required
377                         my $user = DXUser->get_current($field[1]);
378                         if (!$user) {
379                                 # then try without an SSID
380                                 $field[1] =~ s/-\d+$//o;
381                                 $user = DXUser->get_current($field[1]);
382                         }
383                         $user = DXUser->new($field[1]) if !$user;
384                         
385                         if ($field[2] == 1) {
386                                 $user->name($field[3]);
387                         } elsif ($field[2] == 2) {
388                                 $user->qth($field[3]);
389                         } elsif ($field[2] == 3) {
390                                 my ($latd, $latm, $latl, $longd, $longm, $longl) = split /\s+/, $field[3];
391                                 $longd += ($longm/60);
392                                 $longd = 0-$longd if (uc $longl) eq 'W'; 
393                                 $user->long($longd);
394                                 $latd += ($latm/60);
395                                 $latd = 0-$latd if (uc $latl) eq 'S';
396                                 $user->lat($latd);
397                         } elsif ($field[2] == 4) {
398                                 $user->homenode($field[3]);
399                         }
400                         $user->put;
401                         last SWITCH;
402                 }
403                 if ($pcno == 43) {
404                         last SWITCH;
405                 }
406                 if ($pcno == 44) {
407                         last SWITCH;
408                 }
409                 if ($pcno == 45) {
410                         last SWITCH;
411                 }
412                 if ($pcno == 46) {
413                         last SWITCH;
414                 }
415                 if ($pcno == 47) {
416                         last SWITCH;
417                 }
418                 if ($pcno == 48) {
419                         last SWITCH;
420                 }
421                 if ($pcno == 49) {
422                         last SWITCH;
423                 }
424                 
425                 if ($pcno == 50) {              # keep alive/user list
426                         my $ref = DXCluster->get_exact($field[1]);
427                         $ref->update_users($field[2]) if $ref;
428                         last SWITCH;
429                 }
430                 
431                 if ($pcno == 51) {              # incoming ping requests/answers
432                         
433                         # is it for us?
434                         if ($field[1] eq $main::mycall) {
435                                 my $flag = $field[3];
436                                 if ($flag == 1) {
437                                         $self->send(pc51($field[2], $field[1], '0'));
438                                 } else {
439                                         # it's a reply, look in the ping list for this one
440                                         my $ref = $pings{$field[2]};
441                                         if ($ref) {
442                                                 my $r = shift @$ref;
443                                                 my $dxchan = DXChannel->get($r->{call});
444                                                 $dxchan->send($dxchan->msg('pingi', $field[2], atime($main::systime), $main::systime - $r->{t})) if $dxchan;
445                                         }
446                                 }
447                                 
448                         } else {
449                                 # route down an appropriate thingy
450                                 route($field[1], $line);
451                         }
452                         return;
453                 }
454         }
455          
456          # if get here then rebroadcast the thing with its Hop count decremented (if
457          # there is one). If it has a hop count and it decrements to zero then don't
458          # rebroadcast it.
459          #
460          # NOTE - don't arrive here UNLESS YOU WANT this lump of protocol to be
461          #        REBROADCAST!!!!
462          #
463          
464          my $hops;
465         if (($hops) = $line =~ /H(\d+)\^\~?$/o) {
466                 my $newhops = $hops - 1;
467                 if ($newhops > 0) {
468                         $line =~ s/\^H$hops(\^\~?)$/\^H$newhops$1/;     # change the hop count
469                         broadcast_ak1a($line, $self); # send it to everyone but me
470                 }
471         }
472 }
473
474 #
475 # This is called from inside the main cluster processing loop and is used
476 # for despatching commands that are doing some long processing job
477 #
478 sub process
479 {
480         my $t = time;
481         my @chan = DXChannel->get_all();
482         my $chan;
483         
484         foreach $chan (@chan) {
485                 next if !$chan->is_ak1a();
486                 
487                 # send a pc50 out on this channel
488                 if ($t >= $chan->pc50_t + $DXProt::pc50_interval) {
489                         $chan->send(pc50());
490                         $chan->pc50_t($t);
491                 }
492         }
493         
494         my $key;
495         my $val;
496         my $cutoff;
497         if ($main::systime - 3600 > $last_hour) {
498                 $cutoff  = $main::systime - $pc11_dup_age;
499                 while (($key, $val) = each %dup) {
500                         delete $dup{$key} if $val < $cutoff;
501                 }
502                 $last_hour = $main::systime;
503         }
504 }
505
506 #
507 # finish up a pc context
508 #
509 sub finish
510 {
511         my $self = shift;
512         my $call = $self->call;
513         my $ref = DXCluster->get_exact($call);
514         
515         # unbusy and stop and outgoing mail
516         my $mref = DXMsg::get_busy($call);
517         $mref->stop_msg($self) if $mref;
518         
519         # broadcast to all other nodes that all the nodes connected to via me are gone
520         my @gonenodes = map { $_->dxchan == $self ? $_ : () } DXNode::get_all();
521         my $node;
522         
523         foreach $node (@gonenodes) {
524                 next if $node->call eq $call; 
525                 broadcast_ak1a(pc21($node->call, 'Gone'), $self); # done like this 'cos DXNodes don't have a pc21 method
526                 $node->del();
527         }
528
529         # remove outstanding pings
530         delete $pings{$call};
531         
532         # now broadcast to all other ak1a nodes that I have gone
533         broadcast_ak1a(pc21($call, 'Gone.'), $self);
534         Log('DXProt', $call . " Disconnected");
535         $ref->del() if $ref;
536 }
537
538 #
539 # some active measures
540 #
541
542 sub send_local_config
543 {
544         my $self = shift;
545         my $n;
546         
547         # send our nodes
548         my @nodes = DXNode::get_all();
549         
550         # create a list of all the nodes that are not connected to this connection
551         @nodes = grep { $_->dxchan != $self } @nodes;
552         $self->send($me->pc19(@nodes));
553         
554         # get all the users connected on the above nodes and send them out
555         foreach $n (@nodes) {
556                 my @users = values %{$n->list};
557                 $self->send(DXProt::pc16($n, @users));
558         }
559 }
560
561 #
562 # route a message down an appropriate interface for a callsign
563 #
564 # is called route(to, pcline);
565 #
566 sub route
567 {
568         my ($call, $line) = @_;
569         my $cl = DXCluster->get_exact($call);
570         if ($cl) {
571                 my $hops;
572                 my $dxchan = $cl->{dxchan};
573                 if (($hops) = $line =~ /H(\d+)\^\~?$/o) {
574                         my $newhops = $hops - 1;
575                         if ($newhops > 0) {
576                                 $line =~ s/\^H$hops(\^\~?)$/\^H$newhops$1/;     # change the hop count
577                                 $dxchan->send($line) if $dxchan;
578                         }
579                 } else {
580                         $dxchan->send($line) if $dxchan; # for them wot don't have Hops
581                 }
582         }
583 }
584
585 # broadcast a message to all clusters [except those mentioned after buffer]
586 sub broadcast_ak1a
587 {
588         my $s = shift;                          # the line to be rebroadcast
589         my @except = @_;                        # to all channels EXCEPT these (dxchannel refs)
590         my @chan = get_all_ak1a();
591         my $chan;
592         
593         foreach $chan (@chan) {
594                 next if grep $chan == $_, @except;
595                 $chan->send($s);                # send it if it isn't the except list
596         }
597 }
598
599 # broadcast to all users
600 sub broadcast_users
601 {
602         my $s = shift;                          # the line to be rebroadcast
603         my @except = @_;                        # to all channels EXCEPT these (dxchannel refs)
604         my @chan = get_all_users();
605         my $chan;
606         
607         foreach $chan (@chan) {
608                 next if grep $chan == $_, @except;
609                 $chan->send($s);                # send it if it isn't the except list
610         }
611 }
612
613 # broadcast to a list of users
614 sub broadcast_list
615 {
616         my $s = shift;
617         my $chan;
618         
619         foreach $chan (@_) {
620                 $chan->send($s);                # send it 
621         }
622 }
623
624 #
625 # gimme all the ak1a nodes
626 #
627 sub get_all_ak1a
628 {
629         my @list = DXChannel->get_all();
630         my $ref;
631         my @out;
632         foreach $ref (@list) {
633                 push @out, $ref if $ref->is_ak1a;
634         }
635         return @out;
636 }
637
638 # return a list of all users
639 sub get_all_users
640 {
641         my @list = DXChannel->get_all();
642         my $ref;
643         my @out;
644         foreach $ref (@list) {
645                 push @out, $ref if $ref->is_user;
646         }
647         return @out;
648 }
649
650 # return a list of all user callsigns
651 sub get_all_user_calls
652 {
653         my @list = DXChannel->get_all();
654         my $ref;
655         my @out;
656         foreach $ref (@list) {
657                 push @out, $ref->call if $ref->is_user;
658         }
659         return @out;
660 }
661
662 #
663 # obtain the hops from the list for this callsign and pc no 
664 #
665
666 sub get_hops
667 {
668         my ($pcno) = @_;
669         my $hops = $DXProt::hopcount{$pcno};
670         $hops = $DXProt::def_hopcount if !$hops;
671         return "H$hops";       
672 }
673
674 # remove leading and trailing spaces from an input string
675 sub unpad
676 {
677         my $s = shift;
678         $s =~ s/^\s+|\s+$//;
679         return $s;
680 }
681
682 # add a ping request to the ping queues
683 sub addping
684 {
685         my ($from, $to) = @_;
686         my $ref = $pings{$to};
687         $ref = $pings{$to} = [] if !$ref;
688         my $r = {};
689         $r->{call} = $from;
690         $r->{t} = $main::systime;
691         route($to, pc51($to, $main::mycall, 1));
692         push @$ref, $r;
693 }
694
695 # add a rcmd request to the rcmd queues
696 sub addrcmd
697 {
698         my ($from, $to, $cmd) = @_;
699         my $r = {};
700         $r->{call} = $from;
701         $r->{t} = $main::systime;
702         $r->{cmd} = $cmd;
703         route($to, pc34($main::mycall, $to, $cmd));
704         $rcmds{$to} = $r;
705 }
706 1;
707 __END__