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