more mods to get it to work better
[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 DXProtVars;
19 use DXCommandmode;
20 use DXLog;
21 use Spot;
22 use DXProtout;
23 use DXDebug;
24 use Filter;
25 use Local;
26 use DXDb;
27 use AnnTalk;
28 use Geomag;
29 use WCY;
30 use Time::HiRes qw(gettimeofday tv_interval);
31 use BadWords;
32 use DXHash;
33 use Route;
34 use Route::Node;
35 use Script;
36 use Investigate;
37 use DXProtHandler;
38
39 use strict;
40
41 use vars qw($VERSION $BRANCH);
42 $VERSION = sprintf( "%d.%03d", q$Revision$ =~ /:\s+(\d+)\.(\d+)/ );
43 $BRANCH = sprintf( "%d.%03d", (q$Revision$ =~ /:\s+\d+\.\d+\.(\d+)\.(\d+)/)  || (0,0));
44 $main::build += $VERSION;
45 $main::branch += $BRANCH;
46
47 use vars qw($pc11_max_age $pc23_max_age $last_pc50 $eph_restime $eph_info_restime $eph_pc34_restime
48                         $last_hour $last10 %eph  %pings %rcmds $ann_to_talk $pc19_version
49                         $pingint $obscount %pc19list $chatdupeage $investigation_int
50                         %nodehops $baddx $badspotter $badnode $censorpc $rspfcheck
51                         $allowzero $decode_dk0wcy $send_opernam @checklist);
52
53 $pc11_max_age = 1*3600;                 # the maximum age for an incoming 'real-time' pc11
54 $pc23_max_age = 1*3600;                 # the maximum age for an incoming 'real-time' pc23
55
56 $last_hour = time;                              # last time I did an hourly periodic update
57 %pings = ();                    # outstanding ping requests outbound
58 %rcmds = ();                    # outstanding rcmd requests outbound
59 %nodehops = ();                 # node specific hop control
60 %pc19list = ();                                 # list of outstanding PC19s that haven't had PC16s on them
61
62 $censorpc = 1;                                  # Do a BadWords::check on text fields and reject things
63                                                                 # loads of 'bad things'
64 $baddx = new DXHash "baddx";
65 $badspotter = new DXHash "badspotter";
66 $badnode = new DXHash "badnode";
67 $last10 = $last_pc50 = time;
68 $ann_to_talk = 1;
69 $rspfcheck = 1;
70 $eph_restime = 180;
71 $eph_info_restime = 60*60;
72 $eph_pc34_restime = 30;
73 $pingint = 5*60;
74 $obscount = 2;
75 $chatdupeage = 20 * 60 * 60;
76 $investigation_int = 7*86400;   # time between checks to see if we can see this node
77 $pc19_version = 5466;                   # the visible version no for outgoing PC19s generated from pc59
78
79
80 @checklist = 
81 (
82  [ qw(i c c m bp bc c) ],                       # pc10
83  [ qw(i f m d t m c c h) ],             # pc11
84  [ qw(i c bm m bm bm p h) ],            # pc12
85  [ qw(i c h) ],                                 # 
86  [ qw(i c h) ],                                 # 
87  [ qw(i c m h) ],                                       # 
88  undef ,                                                # pc16 has to be validated manually
89  [ qw(i c c h) ],                                       # pc17
90  [ qw(i m n) ],                                 # pc18
91  undef ,                                                # pc19 has to be validated manually
92  undef ,                                                # pc20 no validation
93  [ qw(i c m h) ],                                       # pc21
94  undef ,                                                # pc22 no validation
95  [ qw(i d n n n n m c c h) ],           # pc23
96  [ qw(i c p h) ],                                       # pc24
97  [ qw(i c c n n) ],                             # pc25
98  [ qw(i f m d t m c c bc) ],            # pc26
99  [ qw(i d n n n n m c c bc) ],  # pc27
100  [ qw(i c c m c d t p m bp n p bp bc) ], # pc28
101  [ qw(i c c n m) ],                             # pc29
102  [ qw(i c c n) ],                                       # pc30
103  [ qw(i c c n) ],                                       # pc31
104  [ qw(i c c n) ],                                       # pc32
105  [ qw(i c c n) ],                                       # pc33
106  [ qw(i c c m) ],                                       # pc34
107  [ qw(i c c m) ],                                       # pc35
108  [ qw(i c c m) ],                                       # pc36
109  [ qw(i c c n m) ],                             # pc37
110  undef,                                                 # pc38 not interested
111  [ qw(i c m) ],                                 # pc39
112  [ qw(i c c m p n) ],                           # pc40
113  [ qw(i c n m h) ],                             # pc41
114  [ qw(i c c n) ],                                       # pc42
115  undef,                                                 # pc43 don't handle it
116  [ qw(i c c n m m c) ],                 # pc44
117  [ qw(i c c n m) ],                             # pc45
118  [ qw(i c c n) ],                                       # pc46
119  undef,                                                 # pc47
120  undef,                                                 # pc48
121  [ qw(i c m h) ],                                       # pc49
122  [ qw(i c n h) ],                                       # pc50
123  [ qw(i c c n) ],                                       # pc51
124  undef,
125  undef,
126  undef,
127  undef,
128  undef,
129  undef,
130  undef,
131  undef,
132  undef,                                                 # pc60
133  undef,
134  undef,
135  undef,
136  undef,
137  undef,
138  undef,
139  undef,
140  undef,
141  undef,
142  undef,                                                 # pc70
143  undef,
144  undef,
145  [ qw(i d n n n n n n m m m c c h) ],   # pc73
146  undef,
147  undef,
148  undef,
149  undef,
150  undef,
151  undef,
152  undef,                                                 # pc80
153  undef,
154  undef,
155  undef,
156  [ qw(i c c c m) ],                             # pc84
157  [ qw(i c c c m) ],                             # pc85
158  undef,
159  undef,
160  undef,
161  undef,
162  [ qw(i c n) ],                                 # pc90
163 );
164
165 # use the entry in the check list to check the field list presented
166 # return OK if line NOT in check list (for now)
167 sub check
168 {
169         my $n = shift;
170         $n -= 10;
171         return 0 if $n < 0 || $n > @checklist; 
172         my $ref = $checklist[$n];
173         return 0 unless ref $ref;
174         
175         my $i;
176         for ($i = 1; $i < @$ref; $i++) {
177                 my ($blank, $act) = $$ref[$i] =~ /^(b?)(\w)$/;
178                 return 0 unless $act;
179                 next if $blank && $_[$i] =~ /^[ \*]$/;
180                 if ($act eq 'c') {
181                         return $i unless is_callsign($_[$i]);
182                 } elsif ($act eq 'i') {                 
183                         ;                                       # do nothing
184                 } elsif ($act eq 'm') {
185                         return $i unless is_pctext($_[$i]);
186                 } elsif ($act eq 'p') {
187                         return $i unless is_pcflag($_[$i]);
188                 } elsif ($act eq 'f') {
189                         return $i unless is_freq($_[$i]);
190                 } elsif ($act eq 'n') {
191                         return $i unless $_[$i] =~ /^[\d ]+$/;
192                 } elsif ($act eq 'h') {
193                         return $i unless $_[$i] =~ /^H\d\d?$/;
194                 } elsif ($act eq 'd') {
195                         return $i unless $_[$i] =~ /^\s*\d+-\w\w\w-[12][90]\d\d$/;
196                 } elsif ($act eq 't') {
197                         return $i unless $_[$i] =~ /^[012]\d[012345]\dZ$/;
198                 } 
199         }
200         return 0;
201 }
202
203 sub init
204 {
205         do "$main::data/hop_table.pl" if -e "$main::data/hop_table.pl";
206         confess $@ if $@;
207
208         my $user = DXUser->get($main::mycall);
209         $DXProt::myprot_version += $main::version*100;
210         $main::me = DXProt->SUPER::alloc($main::mycall, 0, $user); 
211         $main::me->{here} = 1;
212         $main::me->{state} = "indifferent";
213         $main::me->{sort} = 'S';    # S for spider
214         $main::me->{priv} = 9;
215         $main::me->{metric} = 0;
216         $main::me->{pingave} = 0;
217         $main::me->{registered} = 1;
218         $main::me->{version} = $main::version;
219         $main::me->{build} = $main::build;
220 }
221
222 #
223 # obtain a new connection this is derived from dxchannel
224 #
225
226 sub new 
227 {
228         my $self = DXChannel::alloc(@_);
229
230         # add this node to the table, the values get filled in later
231         my $pkg = shift;
232         my $call = shift;
233
234         my $uref = Route::Node::get($call) || Route::Node->new($call);
235         $uref->here(1);
236         $uref->conf(0);
237         $uref->version($pc19_version);
238         $main::routeroot->link_node($uref, $self);
239         return $self;
240 }
241
242 # this is how a pc connection starts (for an incoming connection)
243 # issue a PC38 followed by a PC18, then wait for a PC20 (remembering
244 # all the crap that comes between).
245 sub start
246 {
247         my ($self, $line, $sort) = @_;
248         my $call = $self->{call};
249         my $user = $self->{user};
250
251         # log it
252         my $host = $self->{conn}->{peerhost} || "unknown";
253         Log('DXProt', "$call connected from $host");
254         
255         # remember type of connection
256         $self->{consort} = $line;
257         $self->{outbound} = $sort eq 'O';
258         my $priv = $user->priv;
259         $priv = $user->priv(1) unless $priv;
260         $self->{priv} = $priv;     # other clusters can always be 'normal' users
261         $self->{lang} = $user->lang || 'en';
262         $self->{isolate} = $user->{isolate};
263         $self->{consort} = $line;       # save the connection type
264         $self->{here} = 1;
265         $self->{width} = 80;
266
267         # sort out registration
268         $self->{registered} = 1;
269
270         # get the output filters
271         $self->{spotsfilter} = Filter::read_in('spots', $call, 0) || Filter::read_in('spots', 'node_default', 0);
272         $self->{wwvfilter} = Filter::read_in('wwv', $call, 0) || Filter::read_in('wwv', 'node_default', 0);
273         $self->{wcyfilter} = Filter::read_in('wcy', $call, 0) || Filter::read_in('wcy', 'node_default', 0);
274         $self->{annfilter} = Filter::read_in('ann', $call, 0) || Filter::read_in('ann', 'node_default', 0) ;
275         $self->{routefilter} = Filter::read_in('route', $call, 0) || Filter::read_in('route', 'node_default', 0) unless $self->{isolate} ;
276
277
278         # get the INPUT filters (these only pertain to Clusters)
279         $self->{inspotsfilter} = Filter::read_in('spots', $call, 1) || Filter::read_in('spots', 'node_default', 1);
280         $self->{inwwvfilter} = Filter::read_in('wwv', $call, 1) || Filter::read_in('wwv', 'node_default', 1);
281         $self->{inwcyfilter} = Filter::read_in('wcy', $call, 1) || Filter::read_in('wcy', 'node_default', 1);
282         $self->{inannfilter} = Filter::read_in('ann', $call, 1) || Filter::read_in('ann', 'node_default', 1);
283         $self->{inroutefilter} = Filter::read_in('route', $call, 1) || Filter::read_in('route', 'node_default', 1) unless $self->{isolate};
284         
285         # set unbuffered and no echo
286         $self->send_now('B',"0");
287         $self->send_now('E',"0");
288         $self->conn->echo(0) if $self->conn->can('echo');
289         
290         # ping neighbour node stuff
291         my $ping = $user->pingint;
292         $ping = $pingint unless defined $ping;
293         $self->{pingint} = $ping;
294         $self->{nopings} = $user->nopings || $obscount;
295         $self->{pingtime} = [ ];
296         $self->{pingave} = 999;
297         $self->{metric} ||= 100;
298         $self->{lastping} = $main::systime;
299
300         # send initialisation string
301         unless ($self->{outbound}) {
302                 $self->sendinit;
303         }
304         
305         $self->state('init');
306         $self->{pc50_t} = $main::systime;
307
308         # send info to all logged in thingies
309         $self->tell_login('loginn');
310
311         # run a script send the output to the debug file
312         my $script = new Script(lc $call) || new Script('node_default');
313         $script->run($self) if $script;
314 }
315
316 #
317 # send outgoing 'challenge'
318 #
319
320 sub sendinit
321 {
322         my $self = shift;
323         $self->send(pc18());
324 }
325
326 sub removepc90
327 {
328         $_[0] =~ s/^PC90\^[-A-Z0-9]+\^\d+\^//;
329         $_[0] =~ s/^PC91\^[-A-Z0-9]+\^\d+\^[-A-Z0-9]+\^//;
330 }
331
332 #sub send
333 #{
334 #       my $self = shift;
335 #       while (@_) {
336 #               my $line = shift;
337 #               $self->SUPER::send($line);
338 #       }
339 #}
340
341 #
342 # This is the normal pcxx despatcher
343 #
344 sub normal
345 {
346         my ($self, $line) = @_;
347
348         # remove any incoming PC90 frames
349         removepc90($line);
350
351         my @field = split /\^/, $line;
352         return unless @field;
353         
354         pop @field if $field[-1] eq '~';
355         
356 #       print join(',', @field), "\n";
357                                                 
358         
359         # process PC frames, this will fail unless the frame starts PCnn
360         my ($pcno) = $field[0] =~ /^PC(\d\d)/; # just get the number
361         unless (defined $pcno && $pcno >= 10 && $pcno <= 99) {
362                 dbg("PCPROT: unknown protocol") if isdbg('chanerr');
363                 return;
364         }
365
366         # check for and dump bad protocol messages
367         my $n = check($pcno, @field);
368         if ($n) {
369                 dbg("PCPROT: bad field $n, dumped (" . parray($checklist[$pcno-10]) . ")") if isdbg('chanerr');
370                 return;
371         }
372
373         my $origin = $self->{call};
374         no strict 'subs';
375         my $sub = "handle_$pcno";
376
377         if ($self->can($sub)) {
378                 $self->$sub($pcno, $line, $origin, @field);
379         } else {
380                 $self->handle_default($pcno, $line, $origin, @field);
381         }
382 }
383         
384
385 #
386 # This is called from inside the main cluster processing loop and is used
387 # for despatching commands that are doing some long processing job
388 #
389 # It is called once per second
390 #
391 sub process
392 {
393         my $t = time;
394         my @dxchan = DXChannel->get_all();
395         my $dxchan;
396         my $pc50s;
397         
398         # send out a pc50 on EVERY channel all at once
399         if ($t >= $last_pc50 + $DXProt::pc50_interval) {
400                 $pc50s = pc50($main::me, scalar DXChannel::get_all_users);
401                 eph_dup($pc50s);
402                 $last_pc50 = $t;
403         }
404
405         foreach $dxchan (@dxchan) {
406                 next unless $dxchan->is_node();
407                 next if $dxchan == $main::me;
408
409                 # send the pc50
410                 $dxchan->send($pc50s) if $pc50s;
411                 
412                 # send a ping out on this channel
413                 if ($dxchan->{pingint} && $t >= $dxchan->{pingint} + $dxchan->{lastping}) {
414                         if ($dxchan->{nopings} <= 0) {
415                                 $dxchan->disconnect;
416                         } else {
417                                 addping($main::mycall, $dxchan->call);
418                                 $dxchan->{nopings} -= 1;
419                                 $dxchan->{lastping} = $t;
420                         }
421                 }
422         }
423
424         Investigate::process();
425         
426         # every ten seconds
427         if ($t - $last10 >= 10) {       
428                 # clean out ephemera 
429
430                 eph_clean();
431
432                 $last10 = $t;
433         }
434         
435         if ($main::systime - 3600 > $last_hour) {
436                 $last_hour = $main::systime;
437         }
438 }
439
440 #
441 # This the routine that does all routing table updates
442 # It determines which nodes have been created or
443 # deleted. 
444 #
445 # legacy protocol calls this routine after transforming
446 # the arguments to be compatible, viz: some scalar
447 # parameters and a bunch of routing objects.
448 #
449 # A list of 4 references to (any) adds/deletes that
450 # have occured is returned to allow generation of
451 # PC1[679] and PC21
452 #
453
454 sub process_pc59
455 {
456         my $self = shift;
457         my $pcno = shift;
458         my $sort = shift;
459         my $hexstamp = shift || hexstamp();
460         my $node = shift;
461         my $enode = shift;
462         my $line = shift;
463         
464         my $ncall = $node->call;
465         my $origin = $enode->call;
466         
467         # if it is a delete, disconnect all the entries mentioned
468         # from this node (which is a parent in this context).
469         my @delnode;
470         my @deluser;
471         if ($sort eq 'D') {
472                 for my $ref (@_) {
473                         next if $ref->call eq $ncall;
474                         next if $ref->call eq $main::mycall;
475                         if ($ref->isa('Route::Node')) {
476                                 push @delnode, $enode->unlink_node($ref, $self);
477                         } elsif ($ref->isa('Route::User')) {
478                                 push @deluser, $enode->del_user($ref);
479                         }
480                 }
481         }
482
483         # if it is an add, connect all the entries
484         my @addnode;
485         my @adduser;
486         if ($sort eq 'A') {
487                 for my $ref (@_) {
488                         next if $ref->call eq $ncall;
489                         next if $ref->call eq $main::mycall;
490                         if ($ref->isa('Route::Node')) {
491                                 my $new = $enode->link_node($ref, $self);
492                                 push @addnode, $new if $new;
493                         } elsif ($ref->isa('Route::User')) {
494                                 push @adduser, $enode->add_user($ref);
495                         }
496                 }
497         }
498
499         # if it is a configure, unlink all the nodes and users that 
500         # are not in @_ but are in the node, then add all the
501         # nodes and users that are @_ but not in the node.
502         #
503         if ($sort eq 'C') {
504                 my @dn;
505                 my @du;
506                 my @an;
507                 my @au;
508                 for my $r (map {Route::Node::get($_)} $node->nodes) {
509                         next unless $r;
510                         next if $r->call eq $ncall;
511                         next if $r->call eq $main::mycall;
512                         push @dn, $r unless grep $_->call eq $r->call, @_;
513                 }
514                 for my $r (map {Route::User::get($_)} $node->users) {
515                         next unless $r;
516                         push @du, $r unless grep $_->call eq $r->call, @_;
517                 }
518                 for my $r (@_) {
519                         next unless $r;
520                         next if $r->call eq $ncall;
521                         next if $r->call eq $main::mycall;
522                         if ($r->isa('Route::Node')) {
523                                 push @an, $r unless grep $r->call eq $_, $node->nodes;
524                         } elsif ($r->isa('Route::User')) {
525                                 push @au, $r unless grep $r->call eq $_, $node->users;
526                         }
527                 }
528                 push @addnode, $node if $self->{state} =~ /^init/;
529                 push @delnode, $enode->remove_route($_, $self) for @dn;
530                 push @deluser, $enode->del_user($_) for @du;
531                 push @addnode, $enode->add_route($_, $self) for @an;
532                 push @adduser, $enode->add_user($_) for @au;
533         }
534
535         $self->route_pc21($origin, $line, @delnode) if @delnode;
536         $self->route_pc19($origin, $line, @addnode) if @addnode;
537         $self->route_pc17($origin, $line, $enode, @deluser) if @deluser;
538         $self->route_pc16($origin, $line, $enode, @adduser) if @adduser;
539         
540         unshift @_, $enode if $ncall ne $origin;
541         $self->route_pc59($origin, $line, $sort, $hexstamp, $origin, @_) if @_;
542
543         $_->delete for @deluser;
544         $_->delete for @delnode;
545 }
546
547 #
548 # some active measures
549 #
550
551
552 sub send_dx_spot
553 {
554         my $self = shift;
555         my $line = shift;
556         my @dxchan = DXChannel->get_all();
557         my $dxchan;
558         
559         # send it if it isn't the except list and isn't isolated and still has a hop count
560         # taking into account filtering and so on
561         foreach $dxchan (@dxchan) {
562                 next if $dxchan == $main::me;
563                 next if $dxchan == $self && $self->is_node;
564                 $dxchan->dx_spot($line, $self->{isolate}, @_, $self->{call});
565         }
566 }
567
568 sub dx_spot
569 {
570         my $self = shift;
571         my $line = shift;
572         my $isolate = shift;
573         my ($filter, $hops);
574
575         if ($self->{spotsfilter}) {
576                 ($filter, $hops) = $self->{spotsfilter}->it(@_);
577                 return unless $filter;
578         }
579         send_prot_line($self, $filter, $hops, $isolate, $line);
580 }
581
582 sub send_prot_line
583 {
584         my ($self, $filter, $hops, $isolate, $line) = @_;
585         my $routeit;
586
587
588         if ($hops) {
589                 $routeit = $line;
590                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
591         } else {
592                 $routeit = adjust_hops($self, $line);  # adjust its hop count by node name
593                 return unless $routeit;
594         }
595         if ($filter) {
596                 $self->send($routeit);
597         } else {
598                 $self->send($routeit) unless $self->{isolate} || $isolate;
599         }
600 }
601
602
603 sub send_wwv_spot
604 {
605         my $self = shift;
606         my $line = shift;
607         my @dxchan = DXChannel->get_all();
608         my $dxchan;
609         my @dxcc = ((Prefix::cty_data($_[6]))[0..2], (Prefix::cty_data($_[7]))[0..2]);
610
611         # send it if it isn't the except list and isn't isolated and still has a hop count
612         # taking into account filtering and so on
613         foreach $dxchan (@dxchan) {
614                 next if $dxchan == $main::me;
615                 next if $dxchan == $self && $self->is_node;
616                 my $routeit;
617                 my ($filter, $hops);
618
619                 $dxchan->wwv($line, $self->{isolate}, @_, $self->{call}, @dxcc);
620         }
621 }
622
623 sub wwv
624 {
625         my $self = shift;
626         my $line = shift;
627         my $isolate = shift;
628         my ($filter, $hops);
629         
630         if ($self->{wwvfilter}) {
631                 ($filter, $hops) = $self->{wwvfilter}->it(@_);
632                 return unless $filter;
633         }
634         send_prot_line($self, $filter, $hops, $isolate, $line)
635 }
636
637 sub send_wcy_spot
638 {
639         my $self = shift;
640         my $line = shift;
641         my @dxchan = DXChannel->get_all();
642         my $dxchan;
643         my @dxcc = ((Prefix::cty_data($_[10]))[0..2], (Prefix::cty_data($_[11]))[0..2]);
644         
645         # send it if it isn't the except list and isn't isolated and still has a hop count
646         # taking into account filtering and so on
647         foreach $dxchan (@dxchan) {
648                 next if $dxchan == $main::me;
649                 next if $dxchan == $self;
650
651                 $dxchan->wcy($line, $self->{isolate}, @_, $self->{call}, @dxcc);
652         }
653 }
654
655 sub wcy
656 {
657         my $self = shift;
658         my $line = shift;
659         my $isolate = shift;
660         my ($filter, $hops);
661
662         if ($self->{wcyfilter}) {
663                 ($filter, $hops) = $self->{wcyfilter}->it(@_);
664                 return unless $filter;
665         }
666         send_prot_line($self, $filter, $hops, $isolate, $line) if $self->is_clx || $self->is_spider || $self->is_dxnet;
667 }
668
669 # send an announce
670 sub send_announce
671 {
672         my $self = shift;
673         my $line = shift;
674         my @dxchan = DXChannel->get_all();
675         my $dxchan;
676         my $target;
677         my $to = 'To ';
678         my $text = unpad($_[2]);
679                                 
680         if ($_[3] eq '*') {     # sysops
681                 $target = "SYSOP";
682         } elsif ($_[3] gt ' ') { # speciality list handling
683                 my ($name) = split /\./, $_[3]; 
684                 $target = "$name"; # put the rest in later (if bothered) 
685         } 
686         
687         if ($_[5] eq '1') {
688                 $target = "WX"; 
689                 $to = '';
690         }
691         $target = "ALL" if !$target;
692
693
694         # obtain country codes etc 
695         my @a = Prefix::cty_data($_[0]);
696         my @b = Prefix::cty_data($_[4]);
697         if ($self->{inannfilter}) {
698                 my ($filter, $hops) = 
699                         $self->{inannfilter}->it(@_, $self->{call}, 
700                                                                          @a[0..2],
701                                                                          @b[0..2], $a[3], $b[3]);
702                 unless ($filter) {
703                         dbg("PCPROT: Rejected by input announce filter") if isdbg('chanerr');
704                         return;
705                 }
706         }
707
708         if (AnnTalk::dup($_[0], $_[1], $_[2])) {
709                 dbg("PCPROT: Duplicate Announce ignored") if isdbg('chanerr');
710                 return;
711         }
712
713         Log('ann', $target, $_[0], $text);
714
715         # send it if it isn't the except list and isn't isolated and still has a hop count
716         # taking into account filtering and so on
717         foreach $dxchan (@dxchan) {
718                 next if $dxchan == $main::me;
719                 next if $dxchan == $self && $self->is_node;
720                 $dxchan->announce($line, $self->{isolate}, $to, $target, $text, @_, $self->{call},
721                                                   @a[0..2], @b[0..2]);
722         }
723 }
724
725 my $msgid = 0;
726
727 sub nextchatmsgid
728 {
729         $msgid++;
730         $msgid = 1 if $msgid > 999;
731         return $msgid;
732 }
733
734 # send a chat line
735 sub send_chat
736 {
737         my $self = shift;
738         my $line = shift;
739         my @dxchan = DXChannel->get_all();
740         my $dxchan;
741         my $target = $_[3];
742         my $text = unpad($_[2]);
743         my $ak1a_line;
744                                 
745         # munge the group and recast the line if required
746         if ($target =~ s/\.LST$//) {
747                 $ak1a_line = $line;
748         }
749         
750         # obtain country codes etc 
751         my @a = Prefix::cty_data($_[0]);
752         my @b = Prefix::cty_data($_[4]);
753         if ($self->{inannfilter}) {
754                 my ($filter, $hops) = 
755                         $self->{inannfilter}->it(@_, $self->{call}, 
756                                                                          @a[0..2],
757                                                                          @b[0..2], $a[3], $b[3]);
758                 unless ($filter) {
759                         dbg("PCPROT: Rejected by input announce filter") if isdbg('chanerr');
760                         return;
761                 }
762         }
763
764         if (AnnTalk::dup($_[0], $_[1], $_[2], $chatdupeage)) {
765                 dbg("PCPROT: Duplicate Announce ignored") if isdbg('chanerr');
766                 return;
767         }
768
769
770         Log('chat', $target, $_[0], $text);
771
772         # send it if it isn't the except list and isn't isolated and still has a hop count
773         # taking into account filtering and so on
774         foreach $dxchan (@dxchan) {
775                 my $is_ak1a = $dxchan->is_ak1a;
776                 
777                 if ($dxchan->is_node) {
778                         next if $dxchan == $main::me;
779                         next if $dxchan == $self;
780                         next unless $dxchan->is_spider || $is_ak1a;
781                         next if $target eq 'LOCAL';
782                         if (!$ak1a_line && $is_ak1a) {
783                                 $ak1a_line = DXProt::pc12($_[0], $text, $_[1], "$target.LST");
784                         }
785                 }
786                 
787                 $dxchan->chat($is_ak1a ? $ak1a_line : $line, $self->{isolate}, $target, $_[1], 
788                                           $text, @_, $self->{call}, @a[0..2], @b[0..2]);
789         }
790 }
791
792 sub announce
793 {
794         my $self = shift;
795         my $line = shift;
796         my $isolate = shift;
797         my $to = shift;
798         my $target = shift;
799         my $text = shift;
800         my ($filter, $hops);
801
802         if ($self->{annfilter}) {
803                 ($filter, $hops) = $self->{annfilter}->it(@_);
804                 return unless $filter;
805         }
806         send_prot_line($self, $filter, $hops, $isolate, $line) unless $_[1] eq $main::mycall;
807 }
808
809 sub chat
810 {
811         goto &announce;
812 }
813
814
815 sub send_local_config
816 {
817         my $self = shift;
818         my $node;
819         my @nodes;
820         my @localnodes;
821         my @remotenodes;
822
823         dbg('DXProt::send_local_config') if isdbg('trace');
824
825         if ($self->{newroute}) {
826                 my @nodes = $self->{isolate} ? ($main::routeroot) : grep { $_->call ne $main::mycall && $_ != $self && !$_->{isolate} } DXChannel::get_all_nodes();
827                 my @users = DXChannel::get_all_users();
828                 @localnodes = map { Route::Node::get($_->{call}) } @nodes;
829                 my @localusers = map { Route::User::get($_->{call}) } @users;
830                 $self->send_route($main::mycall, \&pc59, @nodes+@users+4, 'C', 0, $main::mycall, $main::routeroot, @localnodes, @localusers);
831         } else {
832                 # send our nodes
833                 if ($self->{isolate}) {
834                         @localnodes = ( $main::routeroot );
835                         $self->send_route($main::mycall, \&pc19, 1, $main::routeroot);
836                 } else {
837                         # create a list of all the nodes that are not connected to this connection
838                         # and are not themselves isolated, this to make sure that isolated nodes
839                         # don't appear outside of this node
840                         
841                         # send locally connected nodes
842                         my @dxchan = grep { $_->call ne $main::mycall && $_ != $self && !$_->{isolate} } DXChannel::get_all_nodes();
843                         @localnodes = map { my $r = Route::Node::get($_->{call}); $r ? $r : () } @dxchan if @dxchan;
844                         $self->send_route($main::mycall, \&pc19, scalar(@localnodes)+1, $main::routeroot, @localnodes);
845                         
846                         my $node;
847                         my @rawintcalls = map { $_->nodes } @localnodes if @localnodes;
848                         my @intcalls;
849                         for $node (@rawintcalls) {
850                                 push @intcalls, $node unless grep $node eq $_, @intcalls; 
851                         }
852                         my $ref = Route::Node::get($self->{call});
853                         my @rnodes = $ref->nodes;
854                         for $node (@intcalls) {
855                                 push @remotenodes, Route::Node::get($node) unless grep $node eq $_, @rnodes, @remotenodes;
856                         }
857                         $self->send_route($main::mycall, \&pc19, scalar(@remotenodes), @remotenodes);
858                 }
859                 
860                 # get all the users connected on the above nodes and send them out
861                 foreach $node ($main::routeroot, @localnodes, @remotenodes) {
862                         if ($node) {
863                                 my @rout = map {my $r = Route::User::get($_); $r ? ($r) : ()} $node->users;
864                                 $self->send_route($main::mycall, \&pc16, 1, $node, @rout) if @rout && $self->user->wantsendpc16;
865                         } else {
866                                 dbg("sent a null value") if isdbg('chanerr');
867                         }
868                 }
869         }
870 }
871
872 #
873 # route a message down an appropriate interface for a callsign
874 #
875 # is called route(to, pcline);
876 #
877 sub route
878 {
879         my ($self, $call, $line) = @_;
880
881         if (ref $self && $call eq $self->{call}) {
882                 dbg("PCPROT: Trying to route back to source, dropped") if isdbg('chanerr');
883                 return;
884         }
885
886         # always send it down the local interface if available
887         my $dxchan = DXChannel->get($call);
888         unless ($dxchan) {
889                 my $cl = Route::get($call);
890                 $dxchan = $cl->bestdxchan if $cl;
891                 if (ref $dxchan) {
892                         if (ref $self && $dxchan eq $self) {
893                                 dbg("PCPROT: Trying to route back to source, dropped") if isdbg('chanerr');
894                                 return;
895                         }
896                 }
897         }
898         if ($dxchan) {
899                 my $routeit = adjust_hops($dxchan, $line);   # adjust its hop count by node name
900                 if ($routeit) {
901                         $dxchan->send($routeit) unless $dxchan == $main::me;
902                 }
903         } else {
904                 dbg("PCPROT: No route available, dropped") if isdbg('chanerr');
905         }
906 }
907
908 #
909 # obtain the hops from the list for this callsign and pc no 
910 #
911
912 sub get_hops
913 {
914         my $pcno = shift;
915         my $hops = $DXProt::hopcount{$pcno};
916         $hops = $DXProt::def_hopcount if !$hops;
917         return "H$hops";       
918 }
919
920
921 # adjust the hop count on a per node basis using the user loadable 
922 # hop table if available or else decrement an existing one
923 #
924
925 sub adjust_hops
926 {
927         my $self = shift;
928         my $s = shift;
929         my $call = $self->{call};
930         my $hops;
931         
932         if (($hops) = $s =~ /\^H(\d+)\^~?$/o) {
933                 my ($pcno) = $s =~ /^PC(\d\d)/o;
934                 confess "$call called adjust_hops with '$s'" unless $pcno;
935                 my $ref = $nodehops{$call} if %nodehops;
936                 if ($ref) {
937                         my $newhops = $ref->{$pcno};
938                         return "" if defined $newhops && $newhops == 0;
939                         $newhops = $ref->{default} unless $newhops;
940                         return "" if defined $newhops && $newhops == 0;
941                         $newhops = $hops if !$newhops;
942                         $s =~ s/\^H(\d+)(\^~?)$/\^H$newhops$2/ if $newhops;
943                 } else {
944                         # simply decrement it
945                         $hops--;
946                         return "" if !$hops;
947                         $s =~ s/\^H(\d+)(\^~?)$/\^H$hops$2/ if $hops;
948                 }
949         }
950         return $s;
951 }
952
953
954 # load hop tables
955 #
956 sub load_hops
957 {
958         my $self = shift;
959         return $self->msg('lh1') unless -e "$main::data/hop_table.pl";
960         do "$main::data/hop_table.pl";
961         return $@ if $@;
962         return ();
963 }
964
965
966 # add a ping request to the ping queues
967 sub addping
968 {
969         my ($from, $to, $via) = @_;
970         my $ref = $pings{$to} || [];
971         my $r = {};
972         $r->{call} = $from;
973         $r->{t} = [ gettimeofday ];
974         if ($via && (my $dxchan = DXChannel->get($via))) {
975                 $dxchan->send(pc51($to, $main::mycall, 1));
976         } else {
977                 route(undef, $to, pc51($to, $main::mycall, 1));
978         }
979         push @$ref, $r;
980         $pings{$to} = $ref;
981         my $u = DXUser->get_current($to);
982         if ($u) {
983                 $u->lastping($main::systime);
984                 $u->put;
985         }
986 }
987
988 sub process_rcmd
989 {
990         my ($self, $tonode, $fromnode, $user, $cmd) = @_;
991         if ($tonode eq $main::mycall) {
992                 my $ref = DXUser->get_current($fromnode);
993                 my $cref = Route::Node::get($fromnode);
994                 Log('rcmd', 'in', $ref->{priv}, $fromnode, $cmd);
995                 if ($cmd !~ /^\s*rcmd/i && $cref && $ref && $cref->call eq $ref->homenode) { # not allowed to relay RCMDS!
996                         if ($ref->{priv}) {             # you have to have SOME privilege, the commands have further filtering
997                                 $self->{remotecmd} = 1; # for the benefit of any command that needs to know
998                                 my $oldpriv = $self->{priv};
999                                 $self->{priv} = $ref->{priv}; # assume the user's privilege level
1000                                 my @in = (DXCommandmode::run_cmd($self, $cmd));
1001                                 $self->{priv} = $oldpriv;
1002                                 $self->send_rcmd_reply($main::mycall, $fromnode, $user, @in);
1003                                 delete $self->{remotecmd};
1004                         } else {
1005                                 $self->send_rcmd_reply($main::mycall, $fromnode, $user, "sorry...!");
1006                         }
1007                 } else {
1008                         $self->send_rcmd_reply($main::mycall, $fromnode, $user, "your attempt is logged, Tut tut tut...!");
1009                 }
1010         } else {
1011                 my $ref = DXUser->get_current($tonode);
1012                 if ($ref && $ref->is_clx) {
1013                         $self->route($tonode, pc84($fromnode, $tonode, $user, $cmd));
1014                 } else {
1015                         $self->route($tonode, pc34($fromnode, $tonode, $cmd));
1016                 }
1017         }
1018 }
1019
1020 sub process_rcmd_reply
1021 {
1022         my ($self, $tonode, $fromnode, $user, $line) = @_;
1023         if ($tonode eq $main::mycall) {
1024                 my $s = $rcmds{$fromnode};
1025                 if ($s) {
1026                         my $dxchan = DXChannel->get($s->{call});
1027                         my $ref = $user eq $tonode ? $dxchan : (DXChannel->get($user) || $dxchan);
1028                         $ref->send($line) if $ref;
1029                         delete $rcmds{$fromnode} if !$dxchan;
1030                 } else {
1031                         # send unsolicited ones to the sysop
1032                         my $dxchan = DXChannel->get($main::myalias);
1033                         $dxchan->send($line) if $dxchan;
1034                 }
1035         } else {
1036                 my $ref = DXUser->get_current($tonode);
1037                 if ($ref && $ref->is_clx) {
1038                         $self->route($tonode, pc85($fromnode, $tonode, $user, $line));
1039                 } else {
1040                         $self->route($tonode, pc35($fromnode, $tonode, $line));
1041                 }
1042         }
1043 }
1044
1045 sub send_rcmd_reply
1046 {
1047         my $self = shift;
1048         my $tonode = shift;
1049         my $fromnode = shift;
1050         my $user = shift;
1051         while (@_) {
1052                 my $line = shift;
1053                 $line =~ s/\s*$//;
1054                 Log('rcmd', 'out', $fromnode, $line);
1055                 if ($self->is_clx) {
1056                         $self->send(pc85($main::mycall, $fromnode, $user, "$main::mycall:$line"));
1057                 } else {
1058                         $self->send(pc35($main::mycall, $fromnode, "$main::mycall:$line"));
1059                 }
1060         }
1061 }
1062
1063 # add a rcmd request to the rcmd queues
1064 sub addrcmd
1065 {
1066         my ($self, $to, $cmd) = @_;
1067
1068         my $r = {};
1069         $r->{call} = $self->{call};
1070         $r->{t} = $main::systime;
1071         $r->{cmd} = $cmd;
1072         $rcmds{$to} = $r;
1073         
1074         my $ref = Route::Node::get($to);
1075         my $dxchan = $ref->bestdxchan;
1076         if ($dxchan && $dxchan->is_clx) {
1077                 route(undef, $to, pc84($main::mycall, $to, $self->{call}, $cmd));
1078         } else {
1079                 route(undef, $to, pc34($main::mycall, $to, $cmd));
1080         }
1081 }
1082
1083 sub disconnect
1084 {
1085         my $self = shift;
1086         my $pc39flag = shift;
1087         my $call = $self->call;
1088
1089         return if $self->{disconnecting}++;
1090         
1091         unless ($pc39flag && $pc39flag == 1) {
1092                 $self->send_now("D", DXProt::pc39($main::mycall, $self->msg('disc1', "System Op")));
1093         }
1094
1095         # get rid of any PC16/17/19
1096         eph_del_regex("^PC1[679]*$call");
1097
1098         # do routing stuff, remove me from routing table
1099         my $node = Route::Node::get($call);
1100         my @rout;
1101         if ($node) {
1102
1103                 # remove the route from this node and return a list
1104                 # of nodes that have become orphanned as a result. 
1105                 push @rout, $main::routeroot->remove_route($node, $self);
1106
1107                 # remove all my ephemera as well
1108                 for (@rout) {
1109                         my $c = $_->call;
1110                         eph_del_regex("^PC1[679].*$c");
1111                 }
1112         }
1113         
1114         # unbusy and stop and outgoing mail
1115         my $mref = DXMsg::get_busy($call);
1116         $mref->stop_msg($call) if $mref;
1117         
1118         # broadcast to all other nodes that all the nodes connected to via me are gone
1119         unless ($pc39flag && $pc39flag == 2) {
1120                 $self->route_pc21($main::mycall, undef, @rout) if @rout;
1121                 $self->route_pc59($main::mycall, "", 'D', hexstamp(), $main::mycall, $node);
1122         }
1123
1124         # delete all the unwanted nodes
1125         $_->delete for @rout;
1126         
1127         # remove outstanding pings
1128         delete $pings{$call};
1129         
1130         # I was the last node visited
1131     $self->user->node($main::mycall);
1132
1133         # send info to all logged in thingies
1134         $self->tell_login('logoutn');
1135
1136         Log('DXProt', $call . " Disconnected");
1137
1138         $self->SUPER::disconnect;
1139 }
1140
1141
1142
1143 # send a talk message to this thingy
1144 #
1145 sub talk
1146 {
1147         my ($self, $from, $to, $via, $line, $origin) = @_;
1148         
1149         $line =~ s/\^/\\5E/g;                   # remove any ^ characters
1150         $self->send(DXProt::pc10($from, $to, $via, $line, $origin));
1151         Log('talk', $to, $from, $via?$via:$self->call, $line) unless $origin && $origin ne $main::mycall;
1152 }
1153
1154 # send it if it isn't the except list and isn't isolated and still has a hop count
1155 # taking into account filtering and so on
1156
1157 sub send_route
1158 {
1159         my $self = shift;
1160         my $origin = shift;
1161         my $generate = shift;
1162         my $no = shift;     # the no of things to filter on 
1163         my $routeit;
1164         my ($filter, $hops);
1165         my @rin;
1166         
1167         for (; @_ && $no; $no--) {
1168                 my $r = shift;
1169
1170                 # deal with non routing parameters
1171                 unless (ref $r && $r->isa('Route')) {
1172                         push @rin, $r;
1173                         next;
1174                 }
1175                 
1176                 if (!$self->{isolate} && $self->{routefilter}) {
1177                         $filter = undef;
1178                         if ($r) {
1179                                 ($filter, $hops) = $self->{routefilter}->it($self->{call}, $self->{dxcc}, $self->{itu}, $self->{cq}, $r->call, $r->dxcc, $r->itu, $r->cq, $self->{state}, $r->{state});
1180                                 if ($filter) {
1181                                         push @rin, $r;
1182                                 } else {
1183                                         dbg("DXPROT: $self->{call}/" . $r->call . " rejected by output filter") if isdbg('chanerr');
1184                                 }
1185                         } else {
1186                                 dbg("was sent a null value") if isdbg('chanerr');
1187                         }
1188                 } else {
1189                         push @rin, $r unless $self->{isolate} && $r->call ne $main::mycall;
1190                 }
1191         }
1192         if (@rin) {
1193                 foreach my $line (&$generate(@rin, @_)) {
1194                         if ($hops) {
1195                                 $routeit = $line;
1196                                 $routeit =~ s/\^H\d+\^\~$/\^H$hops\^\~/;
1197                         } else {
1198                                 $routeit = adjust_hops($self, $line);  # adjust its hop count by node name
1199                                 next unless $routeit;
1200                         }
1201                         
1202                         $self->send($routeit);
1203                 }
1204         }
1205 }
1206
1207 sub broadcast_route
1208 {
1209         my $self = shift;
1210         my $origin = shift;
1211         my $generate = shift;
1212         my $line = shift;
1213         my @dxchan = DXChannel::get_all_nodes();
1214         my $dxchan;
1215         
1216         unless ($self->{isolate}) {
1217                 foreach $dxchan (@dxchan) {
1218                         next if $dxchan == $self;
1219                         next if $dxchan == $main::me;
1220                         next unless $dxchan->isa('DXProt');
1221                         next if ($generate == \&pc16 || $generate==\&pc17) && !$dxchan->user->wantsendpc16;
1222                         if ($dxchan->{newroute}) {
1223                                 next if ($generate == \&pc19 || $generate==\&pc21 ||
1224                                                 $generate == \&pc16 || $generate==\&pc17);
1225                         } else {
1226                                 next if ($generate == \&pc19 || $generate==\&pc21) && !$dxchan->user->wantroutepc19;
1227                                 next if ($generate == \&pc59);
1228                         }
1229  
1230                         $dxchan->send_route($origin, $generate, @_);
1231                 }
1232         }
1233 }
1234
1235 sub route_pc16
1236 {
1237         my $self = shift;
1238         return unless $self->user->wantpc16;
1239         my $origin = shift;
1240         my $line = shift;
1241         broadcast_route($self, $origin, \&pc16, $line, 1, @_);
1242 }
1243
1244 sub route_pc17
1245 {
1246         my $self = shift;
1247         return unless $self->user->wantpc16;
1248         my $origin = shift;
1249         my $line = shift;
1250         broadcast_route($self, $origin, \&pc17, $line, 1, @_);
1251 }
1252
1253 sub route_pc19
1254 {
1255         my $self = shift;
1256         my $origin = shift;
1257         my $line = shift;
1258         broadcast_route($self, $origin, \&pc19, $line, scalar @_, @_);
1259 }
1260
1261 sub route_pc21
1262 {
1263         my $self = shift;
1264         my $origin = shift;
1265         my $line = shift;
1266         broadcast_route($self, $origin, \&pc21, $line, scalar @_, @_);
1267 }
1268
1269 sub route_pc24
1270 {
1271         my $self = shift;
1272         my $origin = shift;
1273         my $line = shift;
1274         broadcast_route($self, $origin, \&pc24, $line, 1, @_);
1275 }
1276
1277 sub route_pc41
1278 {
1279         my $self = shift;
1280         my $origin = shift;
1281         my $line = shift;
1282         broadcast_route($self, $origin, \&pc41, $line, 1, @_);
1283 }
1284
1285 sub route_pc50
1286 {
1287         my $self = shift;
1288         my $origin = shift;
1289         my $line = shift;
1290         broadcast_route($self, $origin, \&pc50, $line, 1, @_);
1291 }
1292
1293 sub route_pc59
1294 {
1295         my $self = shift;
1296         my $origin = shift;
1297         my $line = shift;
1298
1299         broadcast_route($self, $origin, \&pc59, $line, scalar @_, @_);
1300 }
1301
1302 sub in_filter_route
1303 {
1304         my $self = shift;
1305         my $r = shift;
1306         my ($filter, $hops) = (1, 1);
1307         
1308         if ($self->{inroutefilter}) {
1309                 ($filter, $hops) = $self->{inroutefilter}->it($self->{call}, $self->{dxcc}, $self->{itu}, $self->{cq}, $r->call, $r->dxcc, $r->itu, $r->cq, $self->state, $r->state);
1310                 dbg("PCPROT: $self->{call}/" . $r->call . ' rejected by in_filter_route') if !$filter && isdbg('chanerr');
1311         }
1312         return $filter;
1313 }
1314
1315 sub eph_dup
1316 {
1317         my $s = shift;
1318         my $t = shift || $eph_restime;
1319         my $r;
1320
1321         # chop the end off
1322         $s =~ s/\^H\d\d?\^?\~?$//;
1323         $r = 1 if exists $eph{$s};    # pump up the dup if it keeps circulating
1324         $eph{$s} = $main::systime + $t;
1325         return $r;
1326 }
1327
1328 sub eph_del_regex
1329 {
1330         my $regex = shift;
1331         my ($key, $val);
1332         while (($key, $val) = each %eph) {
1333                 if ($key =~ m{$regex}) {
1334                         delete $eph{$key};
1335                 }
1336         }
1337 }
1338
1339 sub eph_clean
1340 {
1341         my ($key, $val);
1342         
1343         while (($key, $val) = each %eph) {
1344                 if ($main::systime >= $val) {
1345                         delete $eph{$key};
1346                 }
1347         }
1348 }
1349
1350 sub eph_list
1351 {
1352         my ($key, $val);
1353         my @out;
1354
1355         while (($key, $val) = each %eph) {
1356                 push @out, $key, $val;
1357         }
1358         return @out;
1359 }
1360
1361 sub run_cmd
1362 {
1363         goto &DXCommandmode::run_cmd;
1364 }
1365 1;
1366 __END__