+# incoming user
+sub handle_16
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+
+
+ # general checks
+ my $dxchan;
+ my $ncall = $_[1];
+ my $newline = "PC16^";
+
+ # do I want users from this channel?
+ unless ($self->user->wantpc16) {
+ dbg("PCPROT: don't send users to $self->{call}") if isdbg('chanerr');
+ return;
+ }
+
+ # is it me?
+ if ($ncall eq $main::mycall) {
+ dbg("PCPROT: trying to alter config on this node from outside!") if isdbg('chanerr');
+ return;
+ }
+
+ # do we believe this call?
+ unless ($ncall eq $self->{call} || $self->is_believed($ncall)) {
+ dbg("PCPROT: We don't believe $ncall on $self->{call}");
+ return;
+ }
+
+ my $node = Route::Node::get($ncall);
+ unless ($node) {
+ dbg("PCPROT: Node $ncall not in config") if isdbg('chanerr');
+ return;
+ }
+
+ # dedupe only that which we potentially process
+ if (eph_dup($line)) {
+ dbg("PCPROT: dup PC16 detected") if isdbg('chanerr');
+ return;
+ }
+
+ my $i;
+ my @rout;
+ my @new;
+ for ($i = 2; $i < $#_; $i++) {
+ my ($call, $conf, $here) = $_[$i] =~ /^(\S+) (\S) (\d)/o;
+ next unless $call && $conf && defined $here && is_callsign($call);
+ next if $call eq $main::mycall;
+
+ eph_del_regex("^PC17\\^$call\\^$ncall");
+
+ $conf = $conf eq '*';
+
+ # reject this if we think it is a node already
+ my $r = Route::Node::get($call);
+ my $u = DXUser->get_current($call) unless $r;
+ if ($r || ($u && $u->is_node)) {
+ dbg("PCPROT: $call is a node") if isdbg('chanerr');
+ next;
+ }
+
+ $r = Route::User::get($call) || Route::User->new($call);
+ $r->here($here);
+ $r->conf($conf);
+ $node->lastseen($main::systime);
+
+ push @new, $node->add_user($r);
+
+ # add this station to the user database, if required
+ $call =~ s/-\d+$//o; # remove ssid for users
+ my $user = DXUser->get_current($call);
+ $user = DXUser->new($call) if !$user;
+ $user->homenode($node->call) if !$user->homenode;
+ $user->node($node->call);
+ $user->lastin($main::systime) unless DXChannel->get($call);
+ $user->put;
+ }
+ $self->route_pc16($origin, $line, $node, @new) if @new;
+}
+
+# remove a user
+sub handle_17
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ my $dxchan;
+ my $ncall = $_[2];
+ my $ucall = $_[1];
+
+ eph_del_regex("^PC16\\^$ncall.*$ucall");
+
+ # do I want users from this channel?
+ unless ($self->user->wantpc16) {
+ dbg("PCPROT: don't send users to $self->{call}") if isdbg('chanerr');
+ return;
+ }
+ if ($ncall eq $main::mycall) {
+ dbg("PCPROT: trying to alter config on this node from outside!") if isdbg('chanerr');
+ return;
+ }
+
+ # do we believe this call?
+ unless ($ncall eq $self->{call} || $self->is_believed($ncall)) {
+ dbg("PCPROT: We don't believe $ncall on $self->{call}");
+ return;
+ }
+
+ my $uref = Route::User::get($ucall);
+ unless ($uref) {
+ dbg("PCPROT: Route::User $ucall not in config") if isdbg('chanerr');
+ }
+ my $node = Route::Node::get($ncall);
+ unless ($node) {
+ dbg("PCPROT: Route::Node $ncall not in config") if isdbg('chanerr');
+ }
+
+ return unless $node && $uref;
+
+ my @rout;
+ my @new;
+ if ($self->in_filter_route($node)) {
+
+ if (eph_dup($line)) {
+ dbg("PCPROT: dup PC17 detected") if isdbg('chanerr');
+ return;
+ }
+ push @new, $node->del_user($uref);
+ }
+
+ $self->route_pc17($origin, $line, $node, $uref) if @new;
+
+ # get rid of orphaned users;
+ $_->delete for @new;
+}
+
+# link request
+sub handle_18
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ $self->state('init');
+
+ # record the type and version offered
+ if ($_[1] =~ /DXSpider Version: (\d+\.\d+) Build: (\d+\.\d+)/) {
+ $self->version(53 + $1);
+ $self->user->version(53 + $1);
+ $self->build(0 + $2);
+ $self->user->build(0 + $2);
+ unless ($self->is_spider) {
+ $self->user->sort('S');
+ $self->user->put;
+ $self->sort('S');
+ }
+ } else {
+ $self->version(50.0);
+ $self->version($_[2] / 100) if $_[2] && $_[2] =~ /^\d+$/;
+ $self->user->version($self->version);
+ }
+ $self->newroute( $_[1] =~ /NewRoute/ );
+
+ # first clear out any nodes on this dxchannel
+ my $node = Route::Node::get($self->{call}) ;
+ my @rout;
+ foreach my $n ($node->nodes) {
+ next if $n eq $main::mycall;
+ next if $n eq $self->{call};
+ my $nref = Route::Node::get($n);
+ push @rout, $node->remove_route($nref, $self) if $nref;
+ }
+ $self->route_pc21($origin, $line, @rout) if @rout;
+ for (@rout) {
+ $_->delete;
+ };
+
+ # send the new config
+ $self->send_local_config();
+ $self->send(pc20());
+}
+
+# incoming cluster list
+sub handle_19
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+
+ my $i;
+ my $newline = "PC19^";
+
+ if (eph_dup($line)) {
+ dbg("PCPROT: dup PC19 detected") if isdbg('chanerr');
+ return;
+ }
+
+ # new routing list
+ my @rout;
+
+ # first get the INTERFACE node
+ my $parent = Route::Node::get($self->{call});
+ unless ($parent) {
+ dbg("DXPROT: my parent $self->{call} has disappeared");
+ $self->disconnect;
+ return;
+ }
+
+ my @new;
+
+ # parse the PC19
+ for ($i = 1; $i < $#_-1; $i += 4) {
+ my $here = $_[$i];
+ my $call = uc $_[$i+1];
+ my $conf = $_[$i+2];
+ my $ver = $_[$i+3];
+ next unless defined $here && defined $conf && is_callsign($call);
+
+ # check for sane parameters
+ # $ver = 5000 if $ver eq '0000';
+ next if $ver < 5000; # only works with version 5 software
+ next if length $call < 3; # min 3 letter callsigns
+ next if $call eq $main::mycall;
+
+ # do we believe this call?
+ unless ($call eq $self->{call} || $self->is_believed($call)) {
+ dbg("PCPROT: We don't believe $call on $self->{call}");
+ next;
+ }
+
+ eph_del_regex("^PC(?:21\\^$call|17\\^[^\\^]+\\^$call)");
+
+ # add this station to the user database, if required (don't remove SSID from nodes)
+ my $user = DXUser->get_current($call);
+ if (!$user) {
+ $user = DXUser->new($call);
+ $user->sort('A');
+ $user->priv(1); # I have relented and defaulted nodes
+ $user->lockout(1);
+ $user->homenode($call);
+ $user->node($call);
+ }
+ $user->wantroutepc19(1) unless defined $user->wantroutepc19;
+
+ my $r = Route::Node::get($call) || Route::Node->new($call);
+ $r->here($here);
+ $r->conf($conf);
+ $r->version($ver);
+ $r->lastseen($main::systime);
+
+ if ($self->in_filter_route($r)) {
+ push @new, $parent->link_node($r, $self);
+ push @rout, $r;
+ }
+
+ # unbusy and stop and outgoing mail (ie if somehow we receive another PC19 without a disconnect)
+ my $mref = DXMsg::get_busy($call);
+ $mref->stop_msg($call) if $mref;
+
+ $user->lastin($main::systime) unless DXChannel->get($call);
+ $user->put;
+ }
+
+ # route out new nodes to legacy nodes
+ $self->route_pc19($origin, $line, @new) if @new;
+ $self->route_pc59('A', 0, $self->{call}, @rout) if @rout;
+}
+
+# send local configuration
+sub handle_20
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ $self->send_local_config();
+ $self->send(pc22());
+ $self->state('normal');
+ $self->{lastping} = 0;
+}
+
+# delete a cluster from the list
+sub handle_21
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ my $call = uc $_[1];
+
+ return if $call eq $main::mycall; # don't allow malicious buggers to disconnect me (or ignore loops)!
+
+ unless ($call eq $self->{call} || $self->is_believed($call)) {
+ dbg("PCPROT: We don't believe $call on $self->{call}");
+ return;
+ }
+
+ eph_del_regex("^PC1[679].*$call");
+
+ # if I get a PC21 from the same callsign as self then treat it
+ # as a PC39: I have gone away
+ if ($call eq $self->call) {
+ $self->disconnect(1);
+ return;
+ }
+
+ my @rout;
+ my @new;
+ my $parent = Route::Node::get($self->{call});
+ unless ($parent) {
+ dbg("DXPROT: my parent $self->{call} has disappeared");
+ $self->disconnect;
+ return;
+ }
+ $parent->lastseen;
+
+ my $node = Route::Node::get($call);
+ if ($node) {
+ $node->lastseen($main::systime);
+
+ # input filter it
+ return unless $self->in_filter_route($node);
+ push @rout, $node;
+ push @new, $node->link_node($parent, $self);
+ }
+
+ $self->route_pc21($origin, $line, @new) if @new;
+ $self->route_pc59('D', 0, $self->{call}, @rout) if @rout;
+
+ # get rid of orphaned nodes;
+ $_->delete for @new;
+}
+
+
+sub handle_22
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ $self->state('normal');
+ $self->{lastping} = 0;
+}
+
+# WWV info
+sub handle_23
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+
+ # route foreign' pc27s
+ if ($pcno == 27) {
+ if ($_[8] ne $main::mycall) {
+ $self->route($_[8], $line);
+ return;
+ }
+ }
+
+ # only do a rspf check on PC23 (not 27)
+ if ($pcno == 23) {
+ return if $rspfcheck and !$self->rspfcheck(1, $_[8], $_[7])
+ }
+
+ # do some de-duping
+ my $d = cltounix($_[1], sprintf("%02d18Z", $_[2]));
+ my $sfi = unpad($_[3]);
+ my $k = unpad($_[4]);
+ my $i = unpad($_[5]);
+ my ($r) = $_[6] =~ /R=(\d+)/;
+ $r = 0 unless $r;
+ if (($pcno == 23 && $d < $main::systime - $pc23_max_age) || $d > $main::systime + 1500 || $_[2] < 0 || $_[2] > 23) {
+ dbg("PCPROT: WWV Date ($_[1] $_[2]) out of range") if isdbg('chanerr');
+ return;
+ }
+ if (Geomag::dup($d,$sfi,$k,$i,$_[6])) {
+ dbg("PCPROT: Dup WWV Spot ignored\n") if isdbg('chanerr');
+ return;
+ }
+ $_[7] =~ s/-\d+$//o; # remove spotter's ssid
+
+ my $wwv = Geomag::update($d, $_[2], $sfi, $k, $i, @_[6..8], $r);
+
+ my $rep;
+ eval {
+ $rep = Local::wwv($self, $_[1], $_[2], $sfi, $k, $i, @_[6..8], $r);
+ };
+ # dbg("Local::wwv2 error $@") if isdbg('local') if $@;
+ return if $rep;
+
+ # DON'T be silly and send on PC27s!
+ return if $pcno == 27;
+
+ # broadcast to the eager world
+ send_wwv_spot($self, $line, $d, $_[2], $sfi, $k, $i, @_[6..8]);
+}
+
+# set here status
+sub handle_24
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ my $call = uc $_[1];
+ my ($nref, $uref);
+ $nref = Route::Node::get($call);
+ $uref = Route::User::get($call);
+ return unless $nref || $uref; # if we don't know where they are, it's pointless sending it on
+
+ if (eph_dup($line)) {
+ dbg("PCPROT: Dup PC24 ignored\n") if isdbg('chanerr');
+ return;
+ }
+
+ $nref->here($_[2]) if $nref;
+ $uref->here($_[2]) if $uref;
+ my $ref = $nref || $uref;
+ return unless $self->in_filter_route($ref);
+
+ $self->route_pc24($origin, $line, $ref, $_[3]);
+}
+
+# merge request
+sub handle_25
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ if ($_[1] ne $main::mycall) {
+ $self->route($_[1], $line);
+ return;
+ }
+ if ($_[2] eq $main::mycall) {
+ dbg("PCPROT: Trying to merge to myself, ignored") if isdbg('chanerr');
+ return;
+ }
+
+ Log('DXProt', "Merge request for $_[3] spots and $_[4] WWV from $_[2]");
+
+ # spots
+ if ($_[3] > 0) {
+ my @in = reverse Spot::search(1, undef, undef, 0, $_[3]);
+ my $in;
+ foreach $in (@in) {
+ $self->send(pc26(@{$in}[0..4], $_[2]));
+ }
+ }
+
+ # wwv
+ if ($_[4] > 0) {
+ my @in = reverse Geomag::search(0, $_[4], time, 1);
+ my $in;
+ foreach $in (@in) {
+ $self->send(pc27(@{$in}[0..5], $_[2]));
+ }
+ }
+}
+
+sub handle_26 {goto &handle_11}
+sub handle_27 {goto &handle_23}
+
+# mail/file handling
+sub handle_28
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ if ($_[1] eq $main::mycall) {
+ no strict 'refs';
+ my $sub = "DXMsg::handle_$pcno";
+ &$sub($self, @_);
+ } else {
+ $self->route($_[1], $line) unless $self->is_clx;
+ }
+}
+
+sub handle_29 {goto &handle_28}
+sub handle_30 {goto &handle_28}
+sub handle_31 {goto &handle_28}
+sub handle_32 {goto &handle_28}
+sub handle_33 {goto &handle_28}
+
+sub handle_34
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ if (eph_dup($line, $eph_pc34_restime)) {
+ dbg("PCPROT: dupe PC34, ignored") if isdbg('chanerr');
+ } else {
+ $self->process_rcmd($_[1], $_[2], $_[2], $_[3]);
+ }
+}
+
+# remote command replies
+sub handle_35
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ eph_del_regex("^PC35\\^$_[2]\\^$_[1]\\^");
+ $self->process_rcmd_reply($_[1], $_[2], $_[1], $_[3]);
+}
+
+sub handle_36 {goto &handle_34}
+
+# database stuff
+sub handle_37
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ if ($_[1] eq $main::mycall) {
+ no strict 'refs';
+ my $sub = "DXDb::handle_$pcno";
+ &$sub($self, @_);
+ } else {
+ $self->route($_[1], $line) unless $self->is_clx;
+ }
+}
+
+# node connected list from neighbour
+sub handle_38
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+}
+
+# incoming disconnect
+sub handle_39
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ if ($_[1] eq $self->{call}) {
+ $self->disconnect(1);
+ } else {
+ dbg("PCPROT: came in on wrong channel") if isdbg('chanerr');
+ }
+}
+
+sub handle_40 {goto &handle_28}
+
+# user info
+sub handle_41
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ my $call = $_[1];
+
+ my $l = $line;
+ $l =~ s/[\x00-\x20\x7f-\xff]+//g; # remove all funny characters and spaces for dup checking
+ if (eph_dup($l, $eph_info_restime)) {
+ dbg("PCPROT: dup PC41, ignored") if isdbg('chanerr');
+ return;
+ }
+
+ # input filter if required
+ # my $ref = Route::get($call) || Route->new($call);
+ # return unless $self->in_filter_route($ref);
+
+ if ($_[3] eq $_[2] || $_[3] =~ /^\s*$/) {
+ dbg('PCPROT: invalid value') if isdbg('chanerr');
+ return;
+ }
+
+ # add this station to the user database, if required
+ my $user = DXUser->get_current($call);
+ $user = DXUser->new($call) unless $user;
+
+ if ($_[2] == 1) {
+ $user->name($_[3]);
+ } elsif ($_[2] == 2) {
+ $user->qth($_[3]);
+ } elsif ($_[2] == 3) {
+ if (is_latlong($_[3])) {
+ my ($lat, $long) = DXBearing::stoll($_[3]);
+ $user->lat($lat);
+ $user->long($long);
+ $user->qra(DXBearing::lltoqra($lat, $long));
+ } else {
+ dbg('PCPROT: not a valid lat/long') if isdbg('chanerr');
+ return;
+ }
+ } elsif ($_[2] == 4) {
+ $user->homenode($_[3]);
+ } elsif ($_[2] == 5) {
+ if (is_qra(uc $_[3])) {
+ my ($lat, $long) = DXBearing::qratoll(uc $_[3]);
+ $user->lat($lat);
+ $user->long($long);
+ $user->qra(uc $_[3]);
+ } else {
+ dbg('PCPROT: not a valid QRA locator') if isdbg('chanerr');
+ return;
+ }
+ }
+ $user->lastoper($main::systime); # to cut down on excessive for/opers being generated
+ $user->put;
+
+ unless ($self->{isolate}) {
+ DXChannel::broadcast_nodes($line, $self); # send it to everyone but me
+ }
+
+ # perhaps this IS what we want after all
+ # $self->route_pc41($ref, $call, $_[2], $_[3], $_[4]);
+}
+
+sub handle_42 {goto &handle_28}
+
+
+# database
+sub handle_44 {goto &handle_37}
+sub handle_45 {goto &handle_37}
+sub handle_46 {goto &handle_37}
+sub handle_47 {goto &handle_37}
+sub handle_48 {goto &handle_37}
+
+# message and database
+sub handle_49
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+
+ if (eph_dup($line)) {
+ dbg("PCPROT: Dup PC49 ignored\n") if isdbg('chanerr');
+ return;
+ }
+
+ if ($_[1] eq $main::mycall) {
+ DXMsg::handle_49($self, @_);
+ } else {
+ $self->route($_[1], $line) unless $self->is_clx;
+ }
+}
+
+# keep alive/user list
+sub handle_50
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+
+ my $call = $_[1];
+ my $node = Route::Node::get($call);
+ if ($node) {
+ return unless $node->call eq $self->{call};
+ $node->usercount($_[2]);
+
+ # input filter if required
+ return unless $self->in_filter_route($node);
+
+ $self->route_pc50($origin, $line, $node, $_[2], $_[3]) unless eph_dup($line);
+ }
+}
+
+# incoming ping requests/answers
+sub handle_51
+{
+ my $self = shift;
+ my $pcno = shift;
+ my $line = shift;
+ my $origin = shift;
+ my $to = $_[1];
+ my $from = $_[2];
+ my $flag = $_[3];
+
+
+ # is it for us?
+ if ($to eq $main::mycall) {
+ if ($flag == 1) {
+ $self->send(pc51($from, $to, '0'));
+ } else {
+ # it's a reply, look in the ping list for this one
+ my $ref = $pings{$from};
+ if ($ref) {
+ my $tochan = DXChannel->get($from);
+ while (@$ref) {
+ my $r = shift @$ref;
+ my $dxchan = DXChannel->get($r->{call});
+ next unless $dxchan;
+ my $t = tv_interval($r->{t}, [ gettimeofday ]);
+ if ($dxchan->is_user) {
+ my $s = sprintf "%.2f", $t;
+ my $ave = sprintf "%.2f", $tochan ? ($tochan->{pingave} || $t) : $t;
+ $dxchan->send($dxchan->msg('pingi', $from, $s, $ave))
+ } elsif ($dxchan->is_node) {
+ if ($tochan) {
+ my $nopings = $tochan->user->nopings || $obscount;
+ push @{$tochan->{pingtime}}, $t;
+ shift @{$tochan->{pingtime}} if @{$tochan->{pingtime}} > 6;
+
+ # cope with a missed ping, this means you must set the pingint large enough
+ if ($t > $tochan->{pingint} && $t < 2 * $tochan->{pingint} ) {
+ $t -= $tochan->{pingint};
+ }
+
+ # calc smoothed RTT a la TCP
+ if (@{$tochan->{pingtime}} == 1) {
+ $tochan->{pingave} = $t;
+ } else {
+ $tochan->{pingave} = $tochan->{pingave} + (($t - $tochan->{pingave}) / 6);
+ }
+ $tochan->{nopings} = $nopings; # pump up the timer
+ }
+ }