add localhost client aliasing on spots and PC92A
[spider.git] / perl / Msg.pm
1 #
2 # This has been taken from the 'Advanced Perl Programming' book by Sriram Srinivasan 
3 #
4 # I am presuming that the code is distributed on the same basis as perl itself.
5 #
6 # I have modified it to suit my devious purposes (Dirk Koopman G1TLH)
7 #
8 #
9 #
10
11 package Msg;
12
13 use strict;
14
15 use DXUtil;
16
17 use Mojo::IOLoop;
18 use Mojo::IOLoop::Stream;
19
20 use DXDebug;
21 use DXTimer;
22
23 use vars qw($now %conns $noconns $cnum $total_in $total_out $total_lines_in $total_lines_out $connect_timeout $disc_waittime);
24
25 $total_in = $total_out = 0;
26 $total_lines_in = $total_lines_out = 0;
27
28 $now = time;
29
30 $cnum = 0;
31 $connect_timeout = 5;
32 $disc_waittime = 1.5;
33
34 our %delqueue;
35
36 #
37 #-----------------------------------------------------------------
38 # Generalised initializer
39
40 sub new
41 {
42     my ($pkg, $rproc) = @_;
43         my $obj = ref($pkg);
44         my $class = $obj || $pkg;
45
46     my $conn = {
47                                 rproc => $rproc,
48                                 inqueue => [],
49                                 outqueue => [],
50                                 state => 0,
51                                 lineend => "\r\n",
52                                 csort => 'telnet',
53                                 timeval => 60,
54                                 blocking => 0,
55                                 cnum => (($cnum < 999) ? (++$cnum) : ($cnum = 1)),
56                                 linesin => 0,
57                                 linesout => 0,
58                                 datain => 0,
59                                 dataout => 0,
60     };
61
62         $noconns++;
63         
64         dbg("$class Connection created (total $noconns)") if isdbg('connll');
65         return bless $conn, $class;
66 }
67
68 sub set_error
69 {
70         my $conn = shift;
71         my $callback = shift;
72         $conn->{sock}->on(error => sub {$callback->($_[1]);});
73 }
74
75 sub set_on_eof
76 {
77         my $conn = shift;
78         my $callback = shift;
79         $conn->{sock}->on(close => sub {$callback->()});
80 }
81
82 sub set_rproc
83 {
84         my $conn = shift;
85         my $callback = shift;
86         $conn->{rproc} = $callback;
87 }
88
89 # save it
90 sub conns
91 {
92         my $pkg = shift;
93         my $call = shift;
94         my $ref;
95         
96         if (ref $pkg) {
97                 $call = $pkg->{call} unless $call;
98                 return undef unless $call;
99                 dbg((ref $pkg) . " changing $pkg->{call} to $call") if isdbg('connll') && exists $pkg->{call} && $call ne $pkg->{call};
100                 delete $conns{$pkg->{call}} if exists $pkg->{call} && exists $conns{$pkg->{call}} && $pkg->{call} ne $call; 
101                 $pkg->{call} = $call;
102                 $ref = $conns{$call} = $pkg;
103                 dbg((ref $pkg) . " Connection $pkg->{cnum} $call stored") if isdbg('connll');
104         } else {
105                 $ref = $conns{$call};
106         }
107         return $ref;
108 }
109
110 # this is only called by any dependent processes going away unexpectedly
111 sub pid_gone
112 {
113         my ($pkg, $pid) = @_;
114         
115         my @pid = grep {$_->{pid} == $pid} values %conns;
116         foreach my $p (@pid) {
117                 &{$p->{eproc}}($p, "$pid has gorn") if exists $p->{eproc};
118                 $p->disconnect;
119         }
120 }
121
122 sub ax25
123 {
124         my $conn = shift;
125         return $conn->{csort} eq 'ax25';
126 }
127
128 sub peerhost
129 {
130         my $conn = shift;
131         unless ($conn->{peerhost}) {
132                 $conn->{peerhost} ||= 'ax25' if $conn->ax25;
133                 $conn->{peerhost} ||= $conn->{sock}->handle->peerhost if $conn->{sock};
134                 $conn->{peerhost} ||= 'UNKNOWN';
135         }
136         $conn->{peerhost} =~ s/^::ffff://;
137         return $conn->{peerhost};
138 }
139
140 sub sockhost
141 {
142         my $conn = shift;
143         unless ($conn->{sockhost}) {
144                 $conn->{sockhost} ||= 'ax25' if $conn->ax25;
145                 $conn->{sockhost} ||= $conn->{sock}->handle->sockhost if $conn->{sock};
146                 $conn->{sockhost} ||= 'UNKNOWN';
147         }
148         $conn->{sockhost} =~ s/^::ffff://;
149         if (! defined $main::localhost_alias_ipv4 && $conn->{sockhost} =~ /\./ && $conn->{sockhost} !~ /^127\./) {
150                 $main::localhost_alias_ipv4 = $conn->{sockhost};
151                 dbg("Msg: localhost_alias_ipv4 = '$main::localhost_alias_ipv4'");
152         } elsif (! defined $main::localhost_alias_ipv6 && $conn->{sockhost} =~ /:/ && $conn->{sockhost} !~ /^::1$/) {
153                 $main::localhost_alias_ipv6 = $conn->{sockhost};
154                 dbg("Msg: localhost_alias_ipv6 = '$main::localhost_alias_ipv6'");
155         }
156         return $conn->{sockhost};
157 }
158 #-----------------------------------------------------------------
159 # Send side routines
160
161 sub _on_connect
162 {
163         my $conn = shift;
164         my $handle = shift;
165         undef $conn->{sock};
166         my $sock = $conn->{sock} = Mojo::IOLoop::Stream->new($handle);
167         $sock->on(read => sub {$conn->_rcv($_[1]);} );
168         $sock->on(error => sub {delete $conn->{sock}; $conn->disconnect;});
169         $sock->on(close => sub {delete $conn->{sock}; $conn->disconnect;});
170         $sock->timeout(0);
171         $sock->start;
172         $conn->{peerhost} = eval { $handle->peerhost; };
173         $conn->{sockhost} = eval { $handle->sockhost; };
174         dbg((ref $conn) . " connected $conn->{cnum}:$conn->{sockhost} to $conn->{peerhost}:$conn->{peerport}") if isdbg('conn') || isdbg ('connect');
175         if ($conn->{on_connect}) {
176                 &{$conn->{on_connect}}($conn, $handle);
177         }
178 }
179
180 sub is_connected
181 {
182         my $conn = shift;
183         my $sock = $conn->{sock};
184         return ref $sock && $sock->isa('Mojo::IOLoop::Stream');
185 }
186
187 sub connect {
188     my ($pkg, $to_host, $to_port, %args) = @_;
189         my $timeout = delete $args{timeout} || $connect_timeout;
190         
191     # Create a connection end-point object
192     my $conn = $pkg;
193         unless (ref $pkg) {
194                 my $rproc = delete $args{rproc}; 
195                 $conn = $pkg->new($rproc);
196         }
197         $conn->{peerhost} = $to_host;
198         $conn->{peerport} = $to_port;
199         $conn->{sort} = 'Outgoing';
200
201         dbg((ref $conn) . " connecting $conn->{cnum} to $to_host:$to_port") if isdbg('connll');
202         
203         my $sock;
204         $conn->{sock} = $sock = Mojo::IOLoop::Client->new;
205         $sock->on(connect => sub {
206                                   $conn->_on_connect($_[1])
207                           } );
208         $sock->on(error => sub {
209                                   &{$conn->{eproc}}($conn, $_[1]) if exists $conn->{eproc};
210                                   delete $conn->{sock};
211                                   $conn->disconnect
212                           });
213         $sock->on(close => sub {
214                                   delete $conn->{sock};
215                                   $conn->disconnect}
216                          );
217
218         # copy any args like on_connect, on_disconnect etc
219         while (my ($k, $v) = each %args) {
220                 $conn->{$k} = $v;
221         }
222         
223         $sock->connect(address => $to_host, port => $to_port, timeout => $timeout);
224         
225     return $conn;
226 }
227
228 sub start_program
229 {
230         my ($conn, $line, $sort) = @_;
231         my $pid;
232         
233 #       local $^F = 10000;              # make sure it ain't closed on exec
234 #       my ($a, $b) = $io_socket->socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC);
235 #       if ($a && $b) {
236 #               $a->autoflush(1);
237 #               $b->autoflush(1);
238 #               $pid = fork;
239 #               if (defined $pid) {
240 #                       if ($pid) {
241 #                               close $b;
242 #                               $conn->{sock} = $a;
243 #                               $conn->{csort} = $sort;
244 #                               $conn->{lineend} = "\cM" if $sort eq 'ax25';
245 #                               $conn->{pid} = $pid;
246 #                               if ($conn->{rproc}) {
247 #                                       my $callback = sub {$conn->_rcv};
248 #                                       Msg::set_event_handler ($a, read => $callback);
249 #                               }
250 #                               dbg("connect $conn->{cnum}: started pid: $conn->{pid} as $line") if isdbg('connect');
251 #                       } else {
252 #                               $^W = 0;
253 #                               dbgclose();
254 #                               STDIN->close;
255 #                               STDOUT->close;
256 #                               STDOUT->close;
257 #                               *STDIN = IO::File->new_from_fd($b, 'r') or die;
258 #                               *STDOUT = IO::File->new_from_fd($b, 'w') or die;
259 #                               *STDERR = IO::File->new_from_fd($b, 'w') or die;
260 #                               close $a;
261 #                               unless ($main::is_win) {
262 #                                       #                                               $SIG{HUP} = 'IGNORE';
263 #                                       $SIG{HUP} = $SIG{CHLD} = $SIG{TERM} = $SIG{INT} = 'DEFAULT';
264 #                                       alarm(0);
265 #                               }
266 #                               exec "$line" or dbg("exec '$line' failed $!");
267 #                       } 
268 #               } else {
269 #                       dbg("cannot fork for $line");
270 #               }
271 #       } else {
272 #               dbg("no socket pair $! for $line");
273 #       }
274         return $pid;
275 }
276
277 sub disconnect
278 {
279         my $conn = shift;
280         my $count = $conn->{disconnecting}++;
281         my $dbg = isdbg('connll');
282         my ($pkg, $fn, $line) = caller if $dbg;
283
284         if ($count >= 2) {
285                 dbgtrace((ref $conn) . "::disconnect on call $conn->{call} attempt $conn->{disconnecting} called from ${pkg}::${fn} line $line FORCING CLOSE ") if $dbg;
286                 _close_it($conn);
287                 return;
288         }
289         dbg((ref $conn) . "::disconnect on call $conn->{call} attempt $conn->{disconnecting} called from ${pkg}::${fn} line $line ") if $dbg;
290         return if $count;
291
292         # remove this conn from the active queue
293         # be careful to delete the correct one
294         my $call;
295         if ($call = $conn->{call}) {
296                 my $ref = $conns{$call};
297                 delete $conns{$call} if $ref && $ref == $conn;
298         }
299         $call ||= 'unallocated';
300
301         $delqueue{$conn} = $conn; # save this connection until everything is finished
302         my $sock = $conn->{sock};
303         if ($sock) {
304                 if ($sock->{buffer}) {
305                         my $lth = length $sock->{buffer};
306                         Mojo::IOLoop->timer($disc_waittime, sub {
307                                                                         dbg("Buffer contained $lth characters, coordinated for $disc_waittime secs, now disconnecting $call") if $dbg;
308                                                                         _close_it($conn);
309                                                                 });
310                 } else {
311                         dbg("Buffer empty, just close $call") if $dbg;
312                         _close_it($conn);
313                 }
314         }
315         else {
316                 dbg((ref $conn) . " socket missing on $conn->{call}") if $dbg;
317                 _close_it($conn);
318         }
319 }
320
321 sub _close_it
322 {
323     my $conn = shift;
324     my $sock = delete $conn->{sock};
325         $conn->{state} = 'E';
326         $conn->{timeout}->del if $conn->{timeout};
327
328         my $call = $conn->{call};
329
330         if (isdbg('connll')) {
331                 my ($pkg, $fn, $line) = caller;
332                 dbg((ref $conn) . "::_close_it on call $conn->{call} attempt $conn->{disconnecting} called from ${pkg}::${fn} line $line ");
333         }
334
335
336         dbg((ref $conn) . " Connection $conn->{cnum} $call starting to close") if isdbg('connll');
337         
338         if ($conn->{on_disconnect}) {
339                 &{$conn->{on_disconnect}}($conn);
340         }
341
342         if ($sock) {
343                 dbg((ref $conn) . " Connection $conn->{cnum} $call closing gracefully") if isdbg('connll');
344                 $sock->close_gracefully if $sock->can('close_gracefully');
345         }
346         
347         # get rid of any references
348         for (keys %$conn) {
349                 if (ref($conn->{$_})) {
350                         delete $conn->{$_};
351                 }
352         }
353
354         delete $delqueue{$conn};        # finally remove the $conn
355         
356         unless ($main::is_win) {
357                 kill 'TERM', $conn->{pid} if exists $conn->{pid};
358         }
359 }
360
361 sub _send_stuff
362 {
363         my $conn = shift;
364         my $rq = $conn->{outqueue};
365     my $sock = $conn->{sock};
366         return unless defined $sock;
367         return if $conn->{disconnecting};
368         
369         while (@$rq) {
370                 my $data = shift @$rq;
371                 my $lth = length $data;
372                 my $call = $conn->{call} || 'none';
373                 if (isdbg('raw')) {
374                         dbgdump('raw', "$call send $lth:", $data);
375                 }
376                 if (defined $sock) {
377                         $sock->write($data);
378                         $total_out += $lth;
379                         $conn->{dataout} += $lth;
380                         ++$conn->{linesout};
381                         ++$total_lines_out;
382                 } else {
383                         dbg("_send_stuff $call ending data ignored: $data");
384                 }
385         }
386 }
387
388 sub send_now {
389     my ($conn, $msg) = @_;
390     $conn->enqueue($msg);
391     _send_stuff($conn);
392 }
393
394 sub send_later {
395         goto &send_now;
396 }
397
398 sub send_raw
399 {
400     my ($conn, $msg) = @_;
401         push @{$conn->{outqueue}}, $msg;
402         _send_stuff($conn);
403 }
404
405 sub enqueue {
406     my $conn = shift;
407     push @{$conn->{outqueue}}, defined $_[0] ? $_[0] : '';
408 }
409
410 sub _err_will_block 
411 {
412         return 0;
413 }
414
415 sub close_on_empty
416 {
417         my $conn = shift;
418         $conn->{sock}->on(drain => sub {$conn->disconnect;});
419 }
420
421 #-----------------------------------------------------------------
422 # Receive side routines
423
424 sub new_server 
425 {
426 #    @_ == 4 || die "Msg->new_server (myhost, myport, login_proc)\n";
427         my ($pkg, $my_host, $my_port, $login_proc) = @_;
428         my $conn = $pkg->new($login_proc);
429         
430     my $sock = $conn->{sock} = Mojo::IOLoop::Server->new;
431         $sock->on(accept=>sub{$conn->new_client($_[1]);});
432         $sock->listen(address=>$my_host, port=>$my_port);
433         $sock->start;
434         
435     die "Could not create socket: $! \n" unless $conn->{sock};
436         return $conn;
437 }
438
439
440 sub nolinger
441 {
442         my $conn = shift;
443 }
444
445 sub dequeue
446 {
447         my $conn = shift;
448         return if $conn->{disconnecting};
449         
450         if ($conn->{msg} =~ /\cJ/) {
451                 my @lines = split /\cM?\cJ/, $conn->{msg};
452                 if ($conn->{msg} =~ /\cM?\cJ$/) {
453                         delete $conn->{msg};
454                 } else {
455                         $conn->{msg} = pop @lines;
456                 }
457                 $conn->{linesin} += @lines;
458                 $total_lines_in += @lines;
459                 for (@lines) {
460                         last if $conn->{disconnecting};
461                         &{$conn->{rproc}}($conn, defined $_ ? $_ : '');
462                 }
463         }
464 }
465
466 sub _rcv {                     # Complement to _send
467     my $conn = shift; # $rcv_now complement of $flush
468         my $msg = shift;
469     my $sock = $conn->{sock};
470     return unless defined($sock);
471         return if $conn->{disonnecting};
472
473         $total_in += length $msg;
474         $conn->{datain} += length $msg;
475
476         if (isdbg('raw')) {
477                 my $call = $conn->{call} || 'none';
478                 my $lth = length $msg;
479                 dbgdump('raw', "$call read $lth: ", $msg);
480         }
481         if ($conn->{echo}) {
482                 my @ch = split //, $msg;
483                         my $out;
484                         for (@ch) {
485                                 if (/[\cH\x7f]/) {
486                                         $out .= "\cH \cH";
487                                         $conn->{msg} =~ s/.$//;
488                                 } else {
489                                         $out .= $_;
490                                         $conn->{msg} .= $_;
491                                 }
492                         }
493                         if (defined $out) {
494                                 $conn->send_raw($out);
495                         }
496         } else {
497                 $conn->{msg} .= $msg;
498         }
499
500         unless ($conn->{disable_read}) {
501                 $conn->dequeue if exists $conn->{msg};
502         }
503 }
504
505 sub new_client {
506         my $server_conn = shift;
507         my $handle = shift;
508         
509         my $conn = $server_conn->new($server_conn->{rproc});
510         my $sock = $conn->{sock} = Mojo::IOLoop::Stream->new($handle);
511         $sock->on(read => sub {$conn->_rcv($_[1])});
512         $sock->timeout(0);
513         $sock->start;
514         $conn->{peerhost} = $handle->peerhost || 'unknown';
515         $conn->{peerport} = $handle->peerport || 0;
516         $conn->{sockhost} = $handle->sockhost || '';
517         dbg((ref $conn) . " accept $conn->{cnum}:$conn->{sockhost} from $conn->{peerhost}:$conn->{peerport}") if isdbg('conn') || isdbg('connect');
518         my ($rproc, $eproc) = &{$server_conn->{rproc}} ($conn, $conn->{peerhost}, $conn->{peerport});
519         $conn->{sort} = 'Incoming';
520         if ($eproc) {
521                 $conn->{eproc} = $eproc;
522         }
523         if ($rproc) {
524                 $conn->{rproc} = $rproc;
525         } else {  # Login failed
526                 &{$conn->{eproc}}($conn, undef) if exists $conn->{eproc};
527                 $conn->disconnect();
528         }
529         return $conn;
530 }
531
532 sub close_server
533 {
534         my $conn = shift;
535         delete $conn->{sock};
536 }
537
538 # close all clients (this is for forking really)
539 sub close_all_clients
540 {
541         foreach my $conn (values %conns) {
542                 $conn->disconnect;
543         }
544 }
545
546 sub disable_read
547 {
548         my $conn = shift;
549         return defined $_[0] ? $conn->{disable_read} = $_[0] : $_[0];
550 }
551
552
553 #
554 #----------------------------------------------------
555 # Event loop routines used by both client and server
556
557 sub set_event_handler {
558         my $sock = shift;
559         my %args = @_;
560         my ($pkg, $fn, $line) = caller;
561         my $s;
562         foreach (my ($k,$v) = each %args) {
563                 $s .= "$k => $v, ";
564         }
565         $s =~ s/[\s,]$//;
566         dbg("Msg::set_event_handler called from ${pkg}::${fn} line $line doing $s");
567 }
568
569 sub sleep
570 {
571         my ($pkg, $interval) = @_;
572         my $now = time;
573         while (time - $now < $interval) {
574                 sleep 1;
575         }
576 }
577
578 sub DESTROY
579 {
580         my $conn = shift;
581         my $call = $conn->{call} || 'unallocated';
582
583         if (isdbg('connll')) {
584                 my ($pkg, $fn, $line) = caller;
585                 dbgtrace((ref $conn) . "::DESTROY on call $call called from ${pkg}::${fn} line $line ");
586         }
587
588         my $call = $conn->{call} || 'unallocated';
589         my $host = $conn->{peerhost} || '';
590         my $port = $conn->{peerport} || '';
591         my $sock = $conn->{sock};
592
593         if ($sock) {
594                 $sock->close_gracefully if $sock->can('close_gracefully');
595                 delete $conn->{sock};
596         }
597         
598         $noconns--;
599         dbg((ref $conn) . " Connection $conn->{cnum} $call [$host $port] being destroyed (total $noconns)") if isdbg('connll');
600 }
601
602 1;
603
604 __END__
605