File Coverage

blib/lib/Net/TCP/PtyServer.pm
Criterion Covered Total %
statement 18 157 11.4
branch 0 78 0.0
condition 0 18 0.0
subroutine 6 11 54.5
pod 2 2 100.0
total 26 266 9.7


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             =head1 NAME
4              
5             Net::TCP::PtyServer - Serves pseudo-terminals. Opens a listening
6             connection on a port, waits for network connections on that port, and
7             serves each one in a seperate PTY.
8              
9             =begin maintenance
10              
11             This is based on example code from both IO::Pty and
12             Net::TCP::Server.
13              
14             Lots of head-scratching has gone into getting this to work; it seems
15             okay to mebut your mileage may vary. I've tried to comment it where I
16             understand what it's doing, but networking code always seems to look
17             just like someone's hit random keys on the keyboard. Feedback from
18             anyone who understands this peoperly would be most welcome
19             (pause@rjlee.dyndns.org).
20              
21             =end maintenance
22              
23             =head1 HACKING
24              
25             =head2 ALGORITHM
26              
27             The actual algorithm is simple, although the implementation looks a
28             bit ickey.
29              
30             =over
31              
32             =item 1 Create a listening socket
33              
34             =item 2 Wait for the next connection on the socket (by calling B).
35              
36             =item 3 Fork.
37              
38             =over
39              
40             =item 3.1 Parent process closes its copy of the handle (by calling
41             B) then goes back to B<1>.
42              
43             =item 3.2 In the child process, we create a pseudo-TTY and fork
44              
45             =over
46              
47             =item 3.2.1 The child process runs the command by re-opening STDOUT,
48             STDERR and STDIN to the pseudo-TTY's slave terminal and then calling
49             B; this does not return
50              
51             This is necessary because the filehandles need to be exactly the same,
52             and we get buffering/crashing issues if we try an open3()
53              
54             =item 3.2.2 The parent process closes its copy of the pseudo-TTY's
55             slave terminal (using B).
56              
57             =item 3.2.3 The parent then repeatedly pipes the data between the
58             pseudo-TTY and the networked filehandle until the exec()ed process
59             completes.
60              
61             =item 3.2.4 The parent process then closes the pseudo-TTY (by implicit
62             destruction) and the networked filehandle (by B), and exits.
63              
64             =back
65              
66             =back
67              
68             =back
69              
70             =head2 Coping with terminal size changes
71              
72             To set the size of a terminal, you need to call ioctl(), and pass the
73             pseudo-TTY handle, the constant TIOCSWINSZ (defined in termio.h or
74             termios.h - or on my system, defined in the asm includes and imported
75             by one of them), and a winsize{} C-structure.
76              
77             The TIOCGWINSZ (G instead of S) can also be used to get the size of a
78             terminal. This is used to generate the structure passed to ioctl in
79             the case of the pseudo-TTY running on a real terminal; see this code
80             from IOS::TTY (referenced by IOS::PTY):
81              
82             sub clone_winsize_from {
83             my ($self, $fh) = @_;
84             my $winsize = "";
85             croak "Given filehandle is not a tty in clone_winsize_from, called"
86             if not POSIX::isatty($fh);
87             return 1 if not POSIX::isatty($self); # ignored for master ptys
88             ioctl($fh, &IO::Tty::Constant::TIOCGWINSZ, $winsize)
89             and ioctl($self, &IO::Tty::Constant::TIOCSWINSZ, $winsize)
90             and return 1;
91             warn "clone_winsize_from: error: $!" if $^W;
92             return undef;
93             }
94              
95             The structrure of winsize is defined in termios.h as follows:
96              
97             struct winsize {
98             unsigned short ws_row;
99             unsigned short ws_col;
100             unsigned short ws_xpixel;
101             unsigned short ws_ypixel;
102             };
103              
104             And the Internet tells me that ws_row is the number of rows, ws_col
105             the number of columns, ws_xpixel the number of horizontal pixels
106             across the terminal, and ws_ypixel the number of vertical pixels
107             across the terminal.
108              
109             After a little experiementing, this seems to work to create the
110             struct, although it should be noted that this assumes that the struct
111             has the same memory alignment as an array of unsigned shorts:
112              
113             my $winsize = pack("S*",$ws_row,$ws_col,$ws_xpixel,$ws_ypixel);
114              
115             So that's what I'm trying to use (thus saving an XS C function)
116              
117             =cut
118              
119             =head1 BUGS
120              
121             The module still has to handle the TELNET protocol properly. In
122             particular, the remapping of IAC and handling of TELNET escapes.
123              
124             For now, we just send the command to turn off echo and linemode, which
125             otherwise interferes with the UI (we also ignore the response, but
126             this seems to have no ill effects so far).
127              
128             Control characters (ctrl+q, ctrl+x) are coming in as 0x11 (17) and
129             0x18 (24); these seem to need translating into \C and the keycode for
130             some reason; the translation is not being picked up through the
131             pseudo-TTY. (For now I'll just use character codes in the code that
132             uses this; they seem simpler to me anyway).
133              
134             When the TCP connection is dropped, we don't currently SIGHUP. We may
135             be able to do this by close()ing the master terminal, but it's
136             probably better to send an explicit HUP signal as well.
137              
138             =cut
139              
140             package Net::TCP::PtyServer;
141              
142             our $VERSION = 1.0;
143              
144 1     1   22750 use IO::Pty;
  1         25577  
  1         42  
145 1     1   856 use Net::TCP::Server;
  1         42732  
  1         49  
146             require POSIX;
147              
148 1     1   1888 use Time::HiRes qw(usleep);
  1         1767  
  1         5  
149              
150             #use constant DOLOG => 0; # Log network traffic (bytes and diagnostics)
151              
152 1     1   189 use constant TIMEOUT => undef;#36600 # Idle Timeout (undef means forever)
  1         2  
  1         58  
153              
154             # To find the best number here, test the response speed of multiple
155             # users connecting simultaneously, and watch the CPU load. Bigger
156             # numbers mean a faster response time for a single user, while lower
157             # numbers mean less CPU load and (in the limit) a faster response when
158             # multiple users are logged in.
159 1     1   5 use constant NSLEEP => 200; # number of loops to go through before
  1         1  
  1         35  
160             # sleeping
161              
162             # Niceness priority. 20 seems to be the best way of stopping the
163             # process from swamping the CPU without causing serious latency (SuSE
164             # Linux 2.6.5-7.201-default). 20 is nicest (lowest scheduling
165             # priority), 0 means don't renice (normal scheduling priority)
166 1     1   4 use constant RENICE => 20;
  1         1  
  1         1633  
167              
168             =head1 METHODS
169              
170             # Don't make zombies when we don't wait for forks (see perlipc):
171             $SIG{CHLD} = 'IGNORE';
172              
173             =head2 setTerminalSize
174              
175             Used internally in response to an incoming NAWS command
176              
177             Takes the terminal as the first argument, followed by the number of
178             rows, then the number of columns. The number of horizontal and
179             vertical pixels can also be specified, but the default is to assume an
180             8x8 pixel character.
181              
182             =cut
183              
184             sub setTerminalSize {
185 0     0 1   my $term = shift;
186 0           my ($ws_row,$ws_col,$ws_xpixel,$ws_ypixel) = @_;
187 0 0         $ws_xpixel = $ws_col * 8 unless $ws_xpixel;
188 0 0         $ws_ypixel = $ws_col * 8 unless $ws_ypixel;
189 0           my $winsize = pack("S*",$ws_row,$ws_col,$ws_xpixel,$ws_ypixel);
190 0           return ioctl($term, &IO::Tty::Constant::TIOCSWINSZ, $winsize);
191             }
192              
193             =head2 run
194              
195             Takes a port number as the first argument, followed by a command and
196             its arguments.
197              
198             Listens for connections on the given port. Bs the given
199             command on a pseudo-terminal on the given port in a child process for
200             each connection.
201              
202             Does not return (but it could die if something really goes wrong)
203              
204             =cut
205              
206             sub run {
207 0     0 1   my $port = shift;
208 0           my @command = @_;
209              
210 0           $^W = 1;
211              
212 0           my $pid;
213              
214             # Create a listening socket
215             my $socket;
216 0           until ($socket) { # wait for port
217 0           $socket = Net::TCP::Server->new($port);
218 0 0         sleep(1) unless $socket;
219             }
220              
221             # Accept connections on each socket and process in children
222 0           while (my $fh = $socket->accept) {
223 0           my $ppid = fork;
224 0 0         die "Cannot fork" if not defined $ppid;
225              
226 0 0         if ($ppid) {
227 0           $fh->stopio;
228             } else {
229             # Create a new PTY:
230 0           my $pty = new IO::Pty;
231              
232             # open a pair of connected pipes to get status from child to parent:
233 0 0         pipe(STAT_RDR, STAT_WTR)
234             or die "Cannot open pipe: $!";
235              
236             ## Allow buffering; it has no noticable effect on response times:
237             #autoflush the write handle
238 0           STAT_WTR->autoflush(1);
239              
240             # The child for the pseudoTTY
241 0           $pid = fork();
242              
243 0 0         die "Cannot fork" if not defined $pid;
244 0 0         unless ($pid) {
245             # Child process, connect stdio to the slave of the psTTY and execute
246             # command
247 0           close STAT_RDR;
248 0           $pty->make_slave_controlling_terminal();
249 0           my $slave = $pty->slave();
250 0           close $pty;
251             # $slave->clone_winsize_from(\*STDIN);
252 0           setTerminalSize($slave,24,80);
253 0           $slave->set_raw();
254              
255 0 0         open(STDIN,"<&". $slave->fileno())
256             or die "Couldn't reopen STDIN for reading, $!\n";
257 0 0         open(STDOUT,">&". $slave->fileno())
258             or die "Couldn't reopen STDOUT for writing, $!\n";
259             # open(STDERR,">&". $slave->fileno())
260 0 0         open STDERR, ">>log.stderr"
261             or die "Couldn't reopen STDERR for writing, $!\n";
262              
263             # Log stuff:
264 0           print STDERR ('*'x20)."\n";
265 0           print STDERR "$0 @ARGV [".gmtime()."]\n";
266              
267 0           close $slave;
268              
269              
270 0           my $telneg = "";
271             # Let's *try* to turn echo off on the remote side:
272 0           $telneg .= chr(255).chr(254).chr(1); # IAC DONT ECHO
273 0           $telneg .= chr(255).chr(251).chr(1); # IAC WILL ECHO
274             # Also, we can't handle the GA signal:
275 0           $telneg .= chr(255).chr(253).chr(3); # IAC DO SUPPRESS-GA
276 0           $telneg .= chr(255).chr(251).chr(3); # IAC WILL SUPPRESS-GA
277             # Try to turn off LINEMODE negotiation:
278 0           $telneg .= chr(255).chr(254).chr(34); # IAC DONT LINEMODE
279 0           $telneg .= chr(255).chr(252).chr(34); # IAC WONT LINEMODE
280             # Ask for Negotiate About Window Size from the client:
281 0           $telneg .= chr(255).chr(253).chr(31); # IAC DO NAWS
282              
283 0           syswrite($fh,$telneg);
284              
285             # Decrement network port's I/O count:
286 0           $fh->stopio;
287              
288 0           eval {
289 0           exec(@command);
290             };
291              
292             # An error occurred (exec only returns on error); tell the
293             # parent process for pTTY:
294 0           print STAT_WTR $!+0;
295 0           die "Cannot exec(@command): $!";
296             }
297              
298             # Parent process for pTTY:
299              
300 0           close STAT_WTR; # we only want to read from the pipe
301 0           $pty->close_slave(); # close the clone of the pTTY's slave
302              
303             # Raw mode:
304             # - characters typed are passed through immediately
305             # - control characters (interrupt, quit, etc.) passed without signal
306 0           $pty->set_raw();
307              
308             # now wait for child exec (eof due to close-on-exit) or exec error
309 0           my $errstatus = sysread(STAT_RDR, $errno, 256);
310 0 0         die "Cannot sync with child: $!" if not defined $errstatus;
311 0           close STAT_RDR;
312 0 0         if ($errstatus) {
313 0           $! = $errno+0;
314 0           die "Cannot exec(@command): $!";
315             }
316              
317             # POSIX::nice(RENICE);
318              
319             # Pump data around:
320 0           _parent($pty,$fh,$pid);
321 0           $fh->stopio; # All I/O is done; stop I/O
322 0           exit(0);
323             }
324              
325             }
326             }
327              
328             # Read a character
329             sub _readChar {
330 0     0     my $src = shift;
331 0           my $buf = shift;
332 0           my $rtn = 0;
333 0           do {
334 0           $rtn = sysread($src,$$buf,1);
335 0 0 0       die "HUP" if $rtn && $rtn == 0;
336 0 0         vec($rin, fileno($src), 1) = 0 unless $rtn;
337             } until ($rtn);
338 0           return $rtn;
339             }
340              
341             # Process I/O
342             sub _process {
343 0     0     my ($rin,$src,$dst,$pid,$toPTY) = @_;
344 0           my $buf = '';
345 0           my $read = sysread($src, $buf, 1);
346 0 0         die "HUP" unless $read;
347              
348 0 0         if ($toPTY) {
349             # Filter standard input to cope with TELNET sequences
350              
351 0 0         if ($buf eq "\015") {
352 0           $read = _readChar($src,\$buf);
353 0 0         if ($buf eq "\012") {
354             # print LOG " - CR for CRLF - discarding CR" if DOLOG;
355             } else {
356 0           $buf = "\015" . $buf;
357 0           $read = 2;
358             }
359             }
360              
361 0 0         if ($buf eq chr(255)) {
362             # print LOG " - TELNET SEQUENCE ON stdin\n" if DOLOG;
363 0           $read = _readChar($src,\$buf);
364 0 0 0       if ($buf eq chr(255)) {
    0 0        
    0 0        
    0          
    0          
    0          
365             # print LOG " - IAC IAC => IAC" if DOLOG;
366             } elsif ($buf eq chr(254) || $buf eq chr(253) || $buf eq chr(252) || $buf eq chr(251)) {
367             # print LOG " - IAC WILL|WONT|DO|DONT - safely ignored\n" if DOLOG;
368 0           _readChar($src,\$buf);
369 0           return $rin;
370             } elsif ($buf eq chr(250)) {
371 0           _readChar($src,\$buf);
372 0 0         if ($buf eq chr(31)) {
373             # print LOG " - IAC SB NAWS - reading terminal size\n" if DOLOG;
374 0           my ($w0,$w1,$h0,$h1);
375 0           _readChar($src,\$buf);
376 0 0         _readChar($src,\$buf) if $buf eq chr(255);
377 0           $w0 = ord($buf);
378 0           _readChar($src,\$buf);
379 0 0         _readChar($src,\$buf) if $buf eq chr(255);
380 0           $w1 = ord($buf);
381 0           _readChar($src,\$buf);
382 0 0         _readChar($src,\$buf) if $buf eq chr(255);
383 0           $h0 = ord($buf);
384 0           _readChar($src,\$buf);
385 0 0         _readChar($src,\$buf) if $buf eq chr(255);
386 0           $h1 = ord($buf);
387 0           my $w = ($w0 << 8) | $w1;
388 0           my $h = ($h0 << 8) | $h1;
389 0           do {
390 0           _readChar($src,\$buf);
391             } until $buf eq chr(240); # Discard the SE, junk out else
392             # print LOG " -- new terminal size cols=$w, rows=$h\n" if DOLOG;
393 0           setTerminalSize($dst,$h,$w);
394 0 0         kill WINCH => $pid if $pid;
395 0           return $rin;
396             } else {
397             # print LOG " - IAC SB ".ord($buf)." - ignoring until SE\n" if DOLOG;
398 0           while ($buf ne chr(240)) {
399 0           _readChar($src,\$buf);
400             }
401             }
402 0           return $rin;
403             } elsif ($buf eq chr(246)) {
404             # AYT
405             # } elsif ($buf eq chr(245)) {
406             # print LOG " - IAC AO - aborting output by sending SIGHUP\n" if DOLOG;
407             # # AO, Abort Output
408             # kill HUP => $pid, $$;
409             # $dst->close;
410             # return $rin;
411             } elsif ($buf eq chr(244)) {
412             # Interrupt Process
413             } elsif ($buf eq chr(241)) {
414             # print LOG " - IAC NOP - doing nothing\n" if DOLOG;
415 0           return $rin;
416             }
417             }
418             }
419              
420             # Write output buffer from child to parent:
421 0           syswrite($dst,$buf,$read);
422             # syswrite(LOG,$buf,$read) if DOLOG;
423             # print LOG "RIN: <$rin>; DST: <".ref($dst).">; BUFFER: <$buf>\n" if DOLOG;
424              
425 0           return $rin;
426             }
427              
428             # Pump data from pseudo-terminal to network pipe:
429             sub _parent {
430             # if (DOLOG) {
431             # open(LOG,">log") || die;
432             # # safely unbuffer LOG then revert to old selected filehandle:
433             # my $f = select(LOG);
434             # $| = 1;
435             # select($f);
436             # }
437 0     0     my $tty = shift;
438 0           my $fh = shift;
439 0           my $pid = shift;
440 0           my ($rin,$win,$ein) = ('','','');
441 0           vec($rin, fileno($fh), 1) = 1;
442 0           vec($rin, fileno($tty), 1) = 1;
443 0           vec($win, fileno($tty), 1) = 1;
444 0           vec($ein, fileno($tty), 1) = 1;
445             # Do not unbuffer the filehandles as it seems to have no
446             # noticable effect
447 0           select($tty); # unbuffer $tty
448 0           $| = 1;
449 0           select($fh); # unbuffer $fh
450 0           $| = 1;
451 0           eval {
452 0           while (1) {
453 0           my ($rout,$wout,$eout);
454             # Wait for $fh or $tty to have a non-blocking read or $tty to
455             # have a non-blocking write to stdout or stderr; $nfound will be
456             # the number of flags set in $rin and $win to indicate
457             # non-blocking read/write status:
458 0           $nfound = select($rout=$rin,$wout=$win,undef#$eout=$ein
459             ,TIMEOUT);
460              
461 0 0         die "select failed:$!" if ($nfound < 0);
462 0 0         if ($nfound > 0) {
463             #if (vec($eout, fileno($tty), 1)) {
464             # print STDERR "Exception on $tty\n";
465             #}
466 0 0 0       if (vec($rout, fileno($tty), 1)) {
    0          
467             # input from net to PTY
468 0           $rin = _process($rin,$tty,$fh,$pid,0);
469 0 0         last unless (vec($rin, fileno($tty), 1)); # exit on close TTY
470             } elsif (vec($rout, fileno($fh), 1) && vec($wout, fileno($tty), 1)) {
471             # output from PTY to net
472 0           $rin = _process($rin,$fh,$tty,$pid,1);
473             } else {
474             # No I/O is waiting.
475              
476             # Explicitly yield the thread to try and reduce load
477              
478             # I've tried various combinations of POSIX::yield,
479             # usleep(0/1/10/100) and sleep(0), but this permitation seems
480             # best
481 0           usleep(0);
482             }
483             }
484             }
485             };
486 0 0 0       if ($@ && $@ =~ /HUP/) {
    0          
487             # terminal has gone away; kill the child with a HUP
488             # print LOG "SENDING HUP ($!)" if DOLOG;
489 0           kill HUP => $pid;
490             } elsif ($@) {
491 0           die;
492             }
493             # close(LOG) if DOLOG;
494             }
495              
496             1;