#!/usr/bin/perl =head1 NAME who.cgi =head1 SYNOPSIS http[s]://<parent-url>/who.cgi[?host=<host>[:<port> || &port=<port>]&quiet=1] =head1 DESCRIPTION who.cgi is a I<Perl5> CGI program (that is fully compatible with Apache::Registry) that shows a highlighted list of players on a MUSH (with any URLS automatically converted into links), or any type of server that supports the WHO command at login. It recognizes PUEBLO enhanced servers and can show a prettier PUEBLO login screen for them. =head1 USAGE who.cgi Will show a screen allowing the user to supply all the possible options. who.cgi?host=mush.pennmush.org Shows the login screen and WHO listing for M*U*S*H, my home MUSH. Since I didn't give the port number 4201 (the default for PennMUSH) is assumed. "quiet=1" would tell it not to show the whole login screen. who.cgi?host=mush.pennmush.org:4201 Same thing, but the port is specified explicitly. who.cgi?host=mush.pennmush.org:4201&quiet=1 Same, but the login screen is not shown. =head1 TODO Add INFO command support with a header line that includes a telnet:/ link. Support for RWHO and any other WHO formats is needed. And for server-side bots is needed to support a greater number of functions (such as finger), this might be better done in a separate system. =head1 HISTORY 0.5 06/01/2000 Started keeping track. Made non-blocking on socket more portable, and improved the error messages. Released on SourceForge. 0.6 06/06/2000 Made "show login" and port 4201 the defaults. Highlights ads in @doings as invokations of the script. Links highlighted in login screen. Some very simple, but working ANSI support. Much more compatible with other types of MU*s. Doesn't use POST for form anymore, works better with history. SIGNIFICANTLY cleaner code. =head1 AUTHOR Rafael Kitover <caelum@debian.org> =head1 COPYRIGHT who.cgi Copyright (c) 2000 Rafael Kitover This program is free software; you can distribute and/or modify it under the same terms as Perl itself. =cut $VERSION = 0.6; use strict; use IO::Socket; use Fcntl; use CGI::Carp 'fatalsToBrowser'; use CGI qw/:all/; sub delay ($); sub splitLines ($); sub highlightLoginScreen (@); # Turn off buffering. local $| = 1; use constant SPACE => ' '; use constant CR => "\015"; use constant LF => "\012"; use constant CRLF => CR.LF; my $CR = CR; my $LF = LF; my $CRLF = CRLF; use constant ANSI => { '30' => 'black', '34' => 'blue', '37' => 'higray', '32' => 'green', '36' => 'cyan', '31' => 'red', '35' => 'purple', '33' => 'brown', '0;30' => 'black', '0;34' => 'blue', '0;37' => 'higray', '0;32' => 'green', '0;36' => 'cyan', '0;31' => 'red', '0;35' => 'purple', '0;33' => 'brown', '1;30' => 'dark gray', '1;34' => 'hiblue', '1;32' => 'higreen', '1;36' => 'hicyan', '1;31' => 'hired', '1;35' => 'hipurple', '1;33' => 'yellow', '1;37' => 'white' }; use constant LINK => qr{ (?<=[^\w"=/]) # Must begin with non-word that's probably not from an # <a> link, nor a part of http:// which could have # matched earlier. (http://([\w/\.-]+)|(www\.\S+)) }xi; # This will match MU* ads, which are of the form: # somewhere.something 4201 use constant MU_AD => qr{ (?<=[^\w"=]) # Must begin with non-word that's probably not from an # <a> link. ([\w-]+(?:\.[\w-]+)+) # Match a word followed by .words . (\s+) # Then some whitespace. (\d\d\d+) # Then the port number. (?=\W|$) # Ended by either EOL or non-word. }x; use constant HIGHLIGHT_NUMBERS => qr{ ( (?<=\s)(\d+)(?=[smhd:]) # eg.: 01d, 20h, 05m, 10: | (?<=\s\d\d:)(\d\d\W) # eg.: the 05 in 04:05 ) }sxi; use constant ANSI_COLOR => qr{ \033\[ # ANSI escape. (?:\d\d;)? # Possible background color. ([01];)? # $1 = possible bold/normal prefix. (3[0-7])m # $2 = the code. ([^\033]*) # $3 = what is highlighted up to next escape. (?:\033\[0m)? # Possibly terminated with reset. }x; use constant WHO_HEADER => qr{ ^(?:<[^>]*>|\s*)* # Preceded by any number of tags or whitespace. (?:player|name) # With the word "player" or "name" as first on # the line. }xi; # Help for those who want it. if (param('Help!')) { use Pod::Html; use IO::File; print header; pod2html( "--infile=$0", "--outfile=/tmp/who.cgi-help-$$", "--netscape", "--noindex", "--title=who.cgi help" ); print ((new IO::File "/tmp/who.cgi-help-$$")->getlines); unlink "/tmp/who.cgi-help-$$"; exit; } # Allow host:port syntax for the host param. (my $host = param('host')) =~ s/([^:]+)(?::(\d+))?/$1/; my $port = $2 || param('port') || 4201; if (!$host) { print header, start_html ({ title => 'MUSH Who', bgcolor => 'lightblue', }), start_form(-method => "GET"), table ( Tr(td(['Host:', textfield(-name => 'host', -default => 'mush.pennmush.org')])), Tr(td(['Port:', textfield(-name => 'port', -default => '4201')])), ), br, checkbox ({ name => 'quiet', label => " Don't show the login screen." }), br() x 2, submit("See who's on."), SPACE x 18, submit("Help!"), end_form, end_html; exit; } # The Timeout part of this requires Perl 5.6 or a newer IO::Socket to work # correctly in some instances. my $socket = new IO::Socket::INET(PeerAddr => "$host:$port", Timeout => 4) or do { print header, start_html ({ title => "$host:$port Who Listing", bgcolor => 'black', text => 'white' }), h2("Could not connect to $host:$port."), a({href => url(-query => 1)}, "Try again"), " if you like."; exit; }; print header, start_html ({ title => "$host:$port Who Listing", bgcolor => 'black', text => 'white' }), '<pre wrap>'; # Check whether or not to turn on Pueblo compatibility. my $text; my $have_pueblo = 0; $_ = scalar <$socket>; if (/pueblo/i) { $have_pueblo = 1; print $socket "PUEBLOCLIENT $CRLF"; } elsif (!/^\s*$/) { $text = $_; } delay(0.5); print $socket "WHO$CRLF"; delay(0.5); print $socket "QUIT$CRLF$CRLF"; # Read the rest, protect from unterminated lines. # Make socket non-blocking. fcntl($socket, F_SETFL, O_NDELAY || O_NONBLOCK); while ($socket->read(my $chunk, 10)) { $text .= $chunk; } # Give it 2 seconds in case of net-burp. # FIXME: this could be more elegant maybe? delay(1); while ($socket->read(my $chunk, 10)) { $text .= $chunk; } # Done with socket, close it. $socket->close; # If it's an unterminated line world, it's probably not a MUSH type. # Just dump out the login screen and error. if ($text !~ /$LF$/s) { my @lines = splitLines $text; unless (param('quiet')) { highlightLoginScreen @lines; print @lines; } print '</pre>', hr, br, h3("Sorry, either the connection is bad or I don't know how to get a WHO listing for that type of world."), a({href => url(-query => 1)}, "Try again"), " if you like.", hr, end_html; exit; } my @lines = splitLines($text); # Some display a big nasty DISCONNECT line, get rid of it. pop @lines if $lines[-1] =~ /DISCONNECT/i; # Find the Who portion. my ($line, @who); if (not $have_pueblo) { while ($_ = pop @lines) { last if /^\s*$/; # empty line unshift @who, $_; last if /@{[WHO_HEADER]}/; } push @lines, "$CRLF"; # Add a blank line. } else { # Get rid of non-pueblo login screen. while ((shift @lines) !~ /<body/i) {} while ($_ = pop @lines) { unshift @who, $_; last if /@{[WHO_HEADER]}/; } } unless (param('quiet')) { highlightLoginScreen @lines; print @lines; } # Beautify the 'WHO' output a bit. # Bold first line. $who[0] = '<b>'.$who[0].'</b>'; # Bold trailer with red player count. ($who[-1] = '<b>'.$who[-1].'</b>') =~ s{(\d+)}{<font color="red">$1</font>}; # Process the WHO list. for (@who) { s(@{[ANSI_COLOR]}) ('<font color="'.ANSI->{$1.$2}.qq|">$3</font>|)eg; s|@{[LINK]}|a({href => "http://$2$3/"}, $1)|e; s(@{[MU_AD]}) (a({href => url(-relative => 1)."?host=$1:$3"}, "$1$2$3"))e; s|@{[HIGHLIGHT_NUMBERS]}|<font color="lightblue">$1</font>|g; s|</?pre>||ig; # Remove any pre tags. } print @who, '</pre>', end_html; exit; ### Helper Subroutines # Waits $ seconds (where $ can be a decimal). sub delay ($) { select (undef, undef, undef, $_[0]) } # Split a text scalar into an array of lines. sub splitLines ($) { return map { "$_$CR$LF" } (split /$CR?$LF/s, $_[0]); } sub highlightLoginScreen (@) { for (@_) { s(@{[ANSI_COLOR]}) ('<font color="'.ANSI->{$1.$2}.qq|">$3</font>|)eg; s|@{[LINK]}|a({href => "http://$2$3/"}, $1)|e; s|@{[MU_AD]}|a({href => "telnet://$1:$3/"}, "$1$2$3")|e; } }