| Category: | Networking |
| Author/Contact Info | David Glick (davisagli) <harvey@nonsensesoftware.com> |
| Description: | Chant is a little chatroom server I wrote to teach myself about TCP programming and Perl in general. I had trouble finding a good example of a non-forking multiplexing server using IO::Select, so hopefully this can help someone else. The server itself has been tested on both Linux and Win32, and can be accessed by any telnet client that supports ANSI color escapes--which actually could be easily disabled--on port 4242 by default. The chat commands are based on those of the Chatterbox here at Perlmonks; use the /help command to get more information on them. If you're concerned about security (and you should be!) then you should only run this in a trusted environment, i.e. a LAN. My main objective in writing this was education, as stated above, not security. If anybody wants to improve the security--or any other features for that matter--please be my guest. Comments and questions also welcome. Update, 7/20/2001: Chant now supports a number of new features (and bug fixes), including user accounts and multiple chantrooms! Try it out and let me know what you think--comments, etc. are still welcome. |
#!/usr/bin/perl -wT
require 5.005;
use strict;
use IO::Socket;
use IO::Select;
# IMPORTANT: You must change these settings before using Chant:
my $port = 4242; # (may also be specifed on command line)
my $superuser = 'davisagli'; # what do you want YOUR username to be?
my $user_file = 'users'; # where should user info be stored?
my $room_file = 'rooms'; # where should room info be stored?
# attempt to get port # from command line
$port = scalar(@ARGV)>0 ? $ARGV[0] : $port;
die "You must modify some basic settings for Chant" if grep {!defined}
+ ($port,$superuser,$user_file,$room_file);
$|++; # enable autoflush for logging messages to STDOUT
# create main socket
my $listener = IO::Socket::INET->new (
Proto => 'tcp',
LocalPort => $port,
Listen => 3, # number of sockets to queue
Reuse => 1 # allow immediate restart after cra
+sh
) or die $!;
print "Chantserver started on port $port\r\n";
# create socket selection
my $select = IO::Select->new($listener);
my @chanters; # chanter objects, keyed by fileno
my %accounts; # all users, keyed by username
my %users; # all connected users, by username
my %rooms; # all rooms, by room name
# create root chantroom (the name may be changed)
my $root = Chantroom->new('root', $superuser);
# reserve some usernames
$accounts{all} = undef;
$accounts{room} = undef;
$SIG{'PIPE'} = 'IGNORE';
# load saved user and room data
load_rooms();
load_users();
# set up interrupt handler -- this will only work in unix systems.
# If you want to stop chant in Windows, use the /shutdown command
# or put a .QUIT file in chant's working directory.
my $quit = 0;
use sigtrap handler => sub {$quit++}, qw(normal-signals);
# the main loop:
MAIN: until ($quit or -e '.QUIT') {
# loop while there are waiting connections:
while(scalar (my @ready = $select->can_read(3))) {
#loop through each waiting connection:
for my $socket (@ready) {
if($socket == $listener) {
# if it's a new connection, set up a new Chanter:
my $new_socket = $listener->accept;
$select->add($new_socket);
Chanter->new($new_socket);
} else {
# if there's input from an existing connection,
# call the chanter's current input handler:
my $chanter = $chanters[$socket->fileno];
defined $chanter ? &{$chanter->{handler}}() : print "unkno
+wn chanter: # $socket->fileno\n";
}
last MAIN if $quit or -e '.QUIT'; # exit main loop if necessa
+ry...
}
}
}
# (this runs after the main loop has ended:)
# save rooms and users
save_rooms();
save_users();
# clean up
print "Chant shutdown completed successfully.\n";
unlink '.QUIT' if -e '.QUIT';
# ___end of main program___
sub load_rooms {
# loads room data from the $room_file
-e $room_file or print("Room file $room_file not found. A new one
+will be created.\n"), return;
open ROOMS, $room_file or die "Error opening room file ($room_file)
+";
while (<ROOMS>) {
my ($name, $owner, $private, $ban_allow) = /^([^\r]*)\r([^\r]*)\
+r([^\r]*)\r([^\r]*)/;
exists $rooms{$name} and print("Duplicate room entry for $name f
+ound and ignored at $room_file line $..\n"), next;
my $room = {
name => $name,
owner => $owner,
private => $private,
$private ? 'allow' : 'ban' => {map {$_,''}(split /\s+/, $ban_
+allow)}
};
$rooms{$name} = $room;
}
close ROOMS;
print "Loaded rooms from $room_file.\n";
}
sub save_rooms {
# saves room data to $room_file
open ROOMS, ">$room_file" or print("Error opening room file ($room_
+file). Rooms not saved!!!\n"), return 0;
for (values %rooms) {
print ROOMS join("\r", $_->{name}, $_->{owner}, $_->{private}, j
+oin(' ', sort keys %{$_->{$_->{private}?'allow':'ban'}})), "\n" if de
+fined and $_ ne $rooms{root};
}
print "Rooms saved to $room_file.\n";
}
sub load_users {
# loads user data from $user_file
-e $user_file or print("User file $user_file not found. A new one
+will be created.\n"), return;
open USERS, $user_file or die "Error opening user file ($user_file)
+";
while (<USERS>) {
my ($name, $password, $room, $ignore, $awaiting, $awaitedby, $ms
+gbuf) = /^([^\x00]*)\x00([^\x00]*)\x00([^\x00]*)\x00([^\x00]*)\x00([^
+\x00]*)\x00([^\x00]*)\x00([^\x00]*)\n$/;
defined $name or next;
exists $accounts{$name} and print("Duplicate user entry for $nam
+e found and ignored at $user_file line $..\n"), next;
$msgbuf =~ s/\r\n|\r|\n/\r\n/g;
my $user = {
name => $name,
password => $password,
msgbuf => $msgbuf,
ignore => {map {$_,undef}(split /\s+/, $ignore)},
awaiting => {map {$_,undef}(split /\s+/, $awaiting)},
awaitedby => {map {$_,undef}(split /\s+/, $awaitedby)}
};
$user->{room} = ::resolve(exists $rooms{$room} ? $room : 'root',
+ \%rooms);
$accounts{$name} = $user;
}
close USERS;
print "Loaded user accounts from $user_file.\n\n";
}
sub save_users {
# saves user data to $user_file
open USERS, ">$user_file" or print("Error opening user file ($user_
+file). Users not saved!!!\n"), return 0;
for (values %accounts) {
defined or next;
$_->{msgbuf} =~ s/\r\n|\n/\r/g;
print USERS join("\x00", $_->{name}, $_->{password}, $_->{room}-
+>{name}, join(' ', sort keys %{$_->{ignore}}), join(' ', sort keys %{
+$_->{awaiting}}), join(' ', sort keys %{$_->{awaitedby}}), $_->{msgbu
+f}), "\n";
}
close USERS;
print "Users saved to $user_file.\n";
}
sub resolve {
# given an input string and a hash, finds the hash key for which the s
+tring
# is an abbreviation and returns it's associated value, or undef if no
+t found
my ($input, $target) = @_;
return $target->{$input} if exists $target->{$input};
for (sort keys %$target) { return $target->{$_} if /^\Q$input/i }
return undef;
}
{
package Chantroom;
use strict;
sub new {
my($class, $name, $owner, $private) = @_;
my $self = {
'name' => $name,
'owner' => defined $owner ? $owner : undef,
'users' => {},
'private' => defined $private ? $private : 0,
'ban' => {},
'allow' => {}
};
bless $self, $class;
$rooms{$name} = $self;
return $self;
}
} # end of package Chantroom
{
# this is the chanter object; one will be created for each connection
package Chanter;
use strict;
use Term::ANSIColor qw/:constants/;
sub new {
# starts setting up a new chanter
my($class,$socket) = @_;
# create new chanter object
my $self = {
'socket' => $socket,
'handler' => undef,
'nexthandlers' => [],
'quithandlers' => [],
'inbuf' => '',
'outbuf' => '',
'writable' => 1,
'lastcmd' => undef,
'lastmsger'=> undef,
'buflastmsger' => undef,
'user' => undef
};
bless $self,$class;
$chanters[$socket->fileno] = $self;
$self->log( '[', $self->{socket}->peerhost, '] connected');
# send welcome message and prompt for username
$self->welcome;
$self->write(BOLD, 'Username: ', RESET);
# set next connection handler to be the username-retriever
$self->{handler} = sub { $self->get_username };
return $self;
}
sub welcome {
# sends welcome/about message to a user
my ($self) = @_;
my ($bold, $reset) = (BOLD, RESET);
my $welcome = <<END;
$bold
CCC H H A N N TTTTT
C H H A A NN N T
C HHHHH AAAAA N N N T
C H H A A N NN T
CCC H H A A N N T
$reset
Welcome to the Chant chatroom server, written in 100% pure Perl.
Copyright 2001 David Glick; distributed under the terms of the GNU GPL
+.
Thanks to Brian Slesinsky for his excellent tutorial on which this is
+based:
http://hotwired.lycos.com/webmonkey/97/18/index2a.html
Enter ${bold}/help$reset for a list of commands.
Have fun chanting!
END
$welcome =~ s:\r\n|\n|\r:\r\n:g;
$self->write($welcome);
}
sub get_username {
# retrieves a valid username from the client
my($self) = @_;
# read in the username request
defined(my $name = $self->read) or return;
$name eq '' and $self->write(BOLD, "Username: ", RESET), return;
# make sure it's alphanumberic
($name) = $name =~ /^(\w+)$/;
defined $name or $self->write(BOLD, RED, "The username must only co
+ntain alphanumeric characters. Please pick another: ", RESET), return
+;
# make sure we don't duplicate usernames
exists $users{$name} and $self->write(BOLD, RED, "Username $name is
+ already in use. Please choose another name: ", RESET), return;
$self->log("user: $name");
# if this is an existing user, check their password
# if it's a new user, set up their account
exists $accounts{$name} ? $self->check_password($name,sub{$self->se
+tup_user($name)}) : $self->create_account($name);
}
sub check_password {
# performs a password check on a user; calls $success_handler if they
+pass it,
# and calls $fail_handler or disconnects them if they don't
my ($self, $name, $success_handler, $fail_handler, $step, $attempt)
+ = @_;
$step = 0 unless defined $step;
SWITCH: {
$step == 0 and do {
# first step: prompt for the password
my $prompt = RESET . BOLD . "Enter your password: " . CONCEALED;
$self->{socket}->syswrite($prompt, length $prompt); $self->{writ
+able} = 0;
$self->{handler} = sub { $self->check_password($name, $success_h
+andler, $fail_handler, 1) };
return };
$step == 1 and do {
# second step: check the password
defined(my $pw = $self->read) or return; # read in the password
$attempt = 1 unless defined $attempt;
# unless they entered the correct password:
unless ($pw eq $accounts{$name}->{password}) {
$self->log("$name failed password check");
$self->write(BOLD, RED, "Incorrect password. ", RESET);
# if they've failed 3 times, call $fail_handler or disconnect
+ them
delete $accounts{$name}, (defined $fail_handler ? &$fail_hand
+ler() : $self->leave), return if $attempt == 3;
# prompt to have them enter it again
my $prompt = RESET . BOLD . "Try again: " . CONCEALED;
$self->{socket}->syswrite($prompt, length $prompt); $self->{w
+ritable} = 0;
$self->{handler} = sub { $self->check_password($name, $succes
+s_handler, $fail_handler, 1, $attempt+1) };
return;
}
# if they entered it correctly, call the success handler
$self->log("$name succeeded password check");
&$success_handler();
return };
}
}
sub create_account {
# creates a new account for $name
my ($self, $name, $step, $password) = @_;
$step = 0 unless defined $step;
SWITCH: {
$step == 0 and do {
# step one: prompt to make sure they realize they're making a n
+ew account
$accounts{$name} = undef; # make sure no one else tries to crea
+te this account
$self->write(BOLD, "Create new user $name [Y/n]? ", RESET); $se
+lf->{writable} = 0;
$self->{handler} = sub { $self->create_account($name, 1) };
return };
$step == 1 and do {
#step two: if they want to continue, prompt them to enter a new
+ password
defined(my $confirm = $self->read) or return;
$confirm eq '' and $confirm = 'y';
$confirm = lc $confirm;
$confirm =~ /^y$|^n$/ or $self->write(BOLD, RED, "Please enter
+y or n: ", RESET), $self->{writable} = 0, return;
$self->leave, return if $confirm eq 'n';
$self->get_new_password( sub{$self->create_account($name, 2, $_
+[0])} );
return };
$step == 2 and do {
#step three: set up the account and call setup_user
my $user = {
name => $name,
password => $password,
msgbuf => '',
room => $root,
ignore => {},
awaiting => {},
awaitedby => {}
};
$accounts{$name} = $user;
$self->log("new user: $name");
$self->setup_user($name);
return };
}
}
sub get_new_password {
# retrieves a new password from a user, and passes it to $handler
my ($self, $handler, $step, $password) = @_;
$step = 0 unless defined $step;
SWITCH: {
$step == 0 and do {
# step one: prompt for the password
my $prompt = RESET . BOLD . "Please choose a new password: " . C
+ONCEALED;
$self->{socket}->syswrite($prompt, length $prompt); $self->{writ
+able} = 0;
$self->{handler} = sub { $self->get_new_password($handler, 1) };
return };
$step == 1 and do {
# step 2: if they entered a password, have them confirm it
defined(my $pw = $self->read) or return;
$self->get_new_password($handler, 0), return if $pw eq '';
my $prompt = RESET . BOLD . "Confirm your password: " . CONCEALE
+D;
$self->{socket}->syswrite($prompt, length $prompt); $self->{writ
+able} = 0;
$self->{handler} = sub { $self->get_new_password($handler,2,$pw)
+ };
return };
$step == 2 and do {
# step 3: make sure they match, and if so pass it to $handler
defined(my $pw = $self->read) or return;
unless ($pw eq $password) {
my $prompt = RESET . BOLD . RED . "The passwords did not match.
+ Please choose a new password: " . CONCEALED;
$self->{socket}->syswrite($prompt, length $prompt); $self->{wri
+table} = 0;
$self->{handler} = sub { $self->get_new_password($handler,1) };
return;
}
&$handler($password);
return };
}
}
sub create_room {
# creates a new room, with this user as the owner
my ($self, $name, $step) = @_;
$step = 0 unless defined $step;
SWITCH: {
$step == 0 and do {
# step one: prompt for public/private-ness
$self->write(BOLD, "Do you want the $name chantroom to be public
+ [Y/n]? ", RESET); $self->{writable} = 0;
$self->{handler} = sub{$self->create_room($name, 1)};
return };
$step == 1 and do {
# step two: retrieve the answer to the above, ...
defined(my $private = $self->read) or return;
$private eq '' and $private = 'y';
$private = lc $private;
$private =~ /^y$|^n$/ or $self->write(BOLD, RED, "Please enter y
+ or n: ", RESET), $self->{writable} = 0, return;
$private = ($private eq 'y') ^ 1;
# ...set up the new room, and send this user there
my $room = Chantroom->new($name,$self->{user}->{name},$private);
$self->log("$self->{user}->{name} created $name chantroom");
$self->goto_room($room);
$self->{handler} = $self->next_handler;
return };
}
}
sub setup_user {
# finishes logging a user into Chant
my ($self, $name) = @_;
# store and log user name
$self->{user} = $accounts{$name};
$users{$name} = $self;
# send welcome and stored messages
$self->write(GREEN, "Welcome to Chant, $name!", RESET, "\r\n");
$self->write(GREEN, "Here are your stored messages: ", RESET, "\r\n
+", $self->{user}->{msgbuf}) unless $self->{user}->{msgbuf} eq '';
$self->{user}->{msgbuf} = '';
# tell people on this user's awaitedby list that they have arrived
for my $recipient (keys %{$self->{user}->{awaitedby}}) {
defined (my $user = ::resolve($recipient, \%accounts)) or next;
defined (my $conn = ::resolve($user->{name}, \%users)) or next;
$conn->write(GREEN, "[$name just logged in.]", RESET, "\r\n") un
+less exists $user->{ignore}->{$name};
}
# send this user to the room they were in when they last logged out
$self->goto_room($self->{user}->{room}) or ($self->{user}->{room} e
+q $root or $self->goto_room($root)) or ($self->leave, return);
# tell controller to enter the main handler on subsequent calls
$self->{handler} = sub { $self->chant };
}
sub chant {
# the main handler while chanting
my($self) = @_;
# retrieve a line from the client
defined(my $line = $self->read) or return;
return if $line =~ /^\s*$/;
# handle commands
$self->log("$self->{user}->{name}: $line");
$self->resolve_cmd($line);
# tell controller where to go next
$self->next_handler;
# store this command
$self->{lastcmd} = $line;
}
sub next_handler {
# returns a reference to any queued input handlers, or to $self->chant
+()
my ($self) = @_;
if (defined (my $next = shift @{$self->{nexthandlers}})) { &$next()
+ }
else { $self->{handler} = sub{$self->chant} }
}
sub resolve_cmd {
# determines which command should be handled, and calls the handler
my ($self, $line, $to) = @_;
my $name = $self->{user}->{name};
# if line starts with '/':
if ($line =~ /^\/.*$/) {
my ($cmd, $data) = $line =~ /^\/(\S*)\s*(.*)$/;
# list of commands that can match -- in the order that they matc
+h
my @valid = qw/about await add goto help ignore invite list me m
+sg ping password pray repeat reply quit unignore unawait who where/;
($self->{room}->{owner} eq $name or $name eq $superuser) and pus
+h @valid, qw/allow ban delete kick private public unallow unban/;
$name eq $superuser and push @valid, qw/eval force save shutdown
+ su yell/;
# figure out which commmand the input is an abbreviation for
my $command;
unless ($cmd eq '') { /^\Q$cmd/i and $command = $_, last for @va
+lid }
# if a valid command was found, handle it
# if no valid command was found but one was entered in, handle i
+t as the recipient of a /msg
# if no command was entered (the line starts with / followed by
+whitespace), treat it as a /reply
defined $command ? $self->cmd($command, $data, $to) : ($cmd ne '
+' ? $self->cmd('msg', "$cmd $data") : $self->cmd('reply', $data));
} else {
# if command line didn't start with '/', broadcast it as a messa
+ge to everyone
$self->dispatch( (defined $to?$to:''), "$self->{user}->{name}> "
+, RESET, $line);
}
}
sub cmd {
# handles the various chat commands
my ($self, $cmd, $data, $to) = @_;
my $reset = RESET;
my $name = $self->{user}->{name};
my $room = $self->{room};
my $owner = ($name eq $room->{owner});
$cmd = lc $cmd;
SWITCH: for ($cmd) {
# commands for all users:
/^about$/ and $self->welcome, return;
/^add$/ and do {
exists $rooms{$data} and $self->write(BOLD, RED, "The $data chan
+troom already exists.$reset\r\n"), return;
push @{$self->{nexthandlers}}, sub {$self->create_room($data)};
return };
/^await$/ and do {
$data eq '' or $self->list( $self->{user}->{await}, $data, sub{$
+_[0]->{awaitedby}->{$name} = undef} );
$self->write(GREEN, "You will be notified whenever these users l
+og in: $reset", join(' ', sort keys %{$self->{user}->{await}}), "\r\n
+");
return };
/^goto$/ and do {
defined(my $room = ::resolve($data, \%rooms)) or $self->write(BO
+LD, RED, "The $data chantroom was not found.$reset\r\n"), return;
$self->goto_room($room);
return };
/^help$/ and $self->help, return;
/^ignore$/and do {
$data eq '' or $self->list( $self->{user}->{ignore}, $data, sub{
+defined ($_[1]) and $_[1]->write(GREEN, "$name has decided to ignore
+you, and will not receive any of your messages.$reset\r\n")} );
$self->write(GREEN, "You are currently ignoring these users: ",
+join(' ', sort keys %{$self->{user}->{ignore}}), "$reset\r\n");
return };
/^invite$/ and do {
($room->{private} and !$owner) and $self->write(BOLD, RED, "Unle
+ss you are the owner, you can only invite people to public chantrooms
+.$reset\r\n"), return;
defined (my $user = ::resolve($data, \%accounts)) or $self->writ
+e(BOLD, RED, "User $data not found.$reset\r\n"), return;
defined (my $conn = ::resolve($user->{name}, \%users)) or $self-
+>write(BOLD, RED, "User $user->{name} is not currently logged in.$res
+et\r\n"), return;
$conn->{room} eq $room and $self->write(BOLD, RED, "$user->{name
+} is already in this room.$reset\r\n"), return;
$room->{private} or exists $room->{ban}->{$user->{name}} and $se
+lf->write(RED, "$user->{name} has been banned from this chantroom.$re
+set\r\n"), return;
push @{$conn->{nexthandlers}}, sub{$conn->invite($room,$name)};
$self->write(GREEN, "$user->{name} has been invited to join the
+$room->{name} chantroom.$reset\r\n");
return };
/^list$/ and $self->write(GREEN, 'available chantrooms: ', RESET,
+ join(' ', map {$rooms{$_}->{private}?(exists $rooms{$_}->{allow}->{$
+name}?BOLD.BLUE.$_.RESET:$_):(exists $rooms{$_}->{ban}->{$name}?BOLD.
+RED.$_.RESET:BOLD.$_.RESET)} (sort keys %rooms)), "\r\n"), return;
/^repeat$/ and do {
defined $self->{lastcmd} or $self->write(BOLD, RED, "You haven't
+ used any commands yet!$reset\r\n"), return;
push @{$self->{nexthandlers}}, sub{ $self->write(BOLD, YELLOW, $
+self->{lastcmd}, RESET); $self->{handler} = sub{$self->chant} };
$self->{inbuf} .= $self->{lastcmd};
#$self->resolve_cmd( $self->{lastcmd} );
return };
/^me$/ and $self->dispatch((defined $to?$to:''), RESET, "$name
+$data"), return;
/^msg$/ and do {
my ($to, $msg) = $data =~ /^(\w+)\s*(.*)$/;
defined $to or $self->write(BOLD, RED, "You must specify a recip
+ient!$reset\r\n"), return;
my $recipient;
$msg eq '' and $self->write(GREEN, defined($recipient = ::resolv
+e($to, \%accounts)) ? "If you send a message to $to, it will be deliv
+ered to $recipient->{name}." : "User $to not found.", "$reset\r\n"),
+return;
$self->resolve_cmd( $msg, $to );
return };
/^ping$/ and do {
$data eq '' and $self->write(BOLD, RED, "You must specify who yo
+u want to ping.$reset\r\n"), return;
$self->dispatch($data, "$name>$reset [beep]\a"), return;
return };
/^password$/ and push(@{$self->{nexthandlers}}, sub{$self->check_p
+assword($name, sub {$self->get_new_password(sub{$self->{user}->{passw
+ord}=$_[0];$self->write(GREEN, "Password successfully changed.$reset\
+r\n");$self->{handler}=$self->next_handler;})}, sub{$self->{handler}
+= $self->next_handler})}), return;
/^pray$/ and $self->dispatch($room->{owner}, "Prayer from $room->
+{name}: $name>$reset $data"), return;
/^quit$/ and do {
my $next;
defined ($next = shift @{$self->{quithandlers}}) and &$next(), r
+eturn;
my $msg = RESET . GREEN . "So long, $self->{user}->{name}, and t
+hanks for all the fish!$reset\r\n";
$self->{socket}->syswrite($msg, length $msg);
$self->leave;
return };
/^reply$/ and do {
defined(my $to = $self->{buflastmsger}) or $self->write(BOLD, RE
+D, "No one has sent you a personal message yet, so I don't know who t
+o reply to.$reset\r\n"), return;
$data eq '' and $self->write(GREEN, "If you use the reply comman
+d, your message will be delivered to $to.$reset\r\n"), return;
exists $users{$to} or $self->write(BOLD, RED, "User $to is no lo
+nger connected.$reset\r\n"), return;
$self->resolve_cmd( $data, $to );
return };
/^unawait$/ and do {
$self->unlist( $self->{user}->{await}, $data, sub{delete $_[0]->
+{awaitedby}->{$name}} );
$self->write(GREEN, "You will be notified whenever these users l
+og in: $reset", join(' ', sort keys %{$self->{user}->{await}}), "\r\n
+");
return };
/^unignore$/ and do {
$self->unlist( $self->{user}->{ignore}, $data, sub{defined($_[1]
+) and $_[1]->write(GREEN, "$name has decided to stop ignoring you, so
+ you may resume your onslaught of messages.$reset\r\n")} );
$self->write(GREEN, "Here is your current ignore list: ", join('
+ ', sort keys %{$self->{user}->{ignore}}), "$reset\r\n");
return };
/^who$/ and do {
($data eq 'room' or $data eq '') and $self->write(GREEN, 'users
+in ', $self->{room}->{name}, ' chantroom: ', RESET, join(' ', sort ke
+ys %{$self->{room}->{users}}), "\r\n");
($data eq 'all' or $data eq '') and $self->write(GREEN, 'all cur
+rent chanters: ', RESET, join(' ', sort keys %users), "\r\n");
grep(/^$data$/, ('room','all','')) or $self->write(GREEN, "curre
+nt chanters matching $data: $reset", join(' ', sort grep /^$data/, ke
+ys %users), "\r\n");
return };
/^where$/ and do {
$data eq '' and $self->write(GREEN, "You are currently in the "
+, $room->{private}?'private':'public', " $self->{room}->{name} chantr
+oom, owned by $self->{room}->{owner}.$reset\r\n"), return;
defined(my $user = ::resolve($data, \%accounts)) or $self->writ
+e(BOLD, RED, "User $data not found.$reset\r\n"), return;
defined(my $conn = ::resolve($user->{name}, \%users)) or $self-
+>write(BOLD, RED, "$user->{name} is not currently logged in.$reset\r\
+n"), return;
$self->write(GREEN, "$user->{name} is currently in the ", $conn
+->{room}->{private}?'private':'public', " $conn->{room}->{name} chant
+room, owned by $conn->{room}->{owner}.$reset\r\n");
return };
# commands for the owner of each room:
if ($owner or $name eq $superuser) {
/^allow$/ and do {
$room->{private} or $self->write(BOLD, RED, "You can only cr
+eate an allow list for private chantrooms.$reset\r\n"), return;
$data eq '' or $self->list( $room->{allow}, $data, sub{defin
+ed($_[1]) and $_[1]->write(GREEN, "You have been granted free access
+to the private $room->{name} chantroom.$reset\r\n")} );
$self->write(GREEN, "These users are currently allowed in th
+e $room->{name} chantroom: ", join(' ', sort keys %{$room->{allow}}),
+ "$reset\r\n");
return };
/^ban$/ and do {
$room->{private} and $self->write(BOLD, RED, "You can only b
+an/unban users from public chantrooms.$reset\r\n"), return;
$data eq '' or $self->list( $room->{ban}, $data, sub{if (def
+ined $_[1]){ $_[1]->write(GREEN, "You have been banned from the $room
+->{name} chantroom.$reset\r\n"); ($_[1]->goto_room(::resolve('root',\
+%rooms)) or $_[1]->leave) if $_[1]->{room} eq $room}} );
$self->write(GREEN, "These users are currently banned from t
+he $room->{name} chantroom: ", join(' ', sort keys %{$room->{ban}}),
+"$reset\r\n");
return };
/^delete$/ and do {
$room->{name} eq 'root' and $self->write(BOLD, RED, "Cannot
+delete root chantroom!$reset\r\n"), return;
$self->write(GREEN, "The $room->{name} chantroom has been de
+leted.$reset\r\n");
$_->goto_room($root) or $_->leave for values %{$room->{users
+}};
delete $rooms{$room->{name}};
$self->log("$name deleted $room->{name} chantroom");
return };
/^kick$/ and do {
defined(my $user = ::resolve($data, \%accounts)) or $self->w
+rite(BOLD, RED, "User $data not found.$reset\r\n"), return;
defined(my $conn = ::resolve($user->{name}, \%users)) or $se
+lf->write(BOLD, RED, "User $user->{name} is not currently logged in.$
+reset\r\n"), return;
$conn eq $self and $self->write(BOLD, RED, "You can't kick y
+ourself out, stupid!$reset\r\n"), return;
$conn->{room} eq $room or $self->write(BOLD, RED, "User $use
+r->{name} is not currently in this room.$reset\r\n"), return;
$conn->write(GREEN, "You have been kicked out of the $room->
+{name} chantroom.$reset\r\n");
$conn->goto_room($root) or $conn->leave;
return };
/^private$/ and do {
$room->{name} eq 'root' and $self->write(BOLD, RED, "The roo
+t chantroom must remain public.$reset\r\n"), return;
$room->{private} = 1, $self->write(GREEN, "The $room->{name}
+ chantroom is now private.$reset\r\n");
return };
/^public$/ and $room->{private} = 0, $self->write(GREEN, "The $
+room->{name} chantroom is now publicly accessible.$reset\r\n"), retur
+n;
/^unallow$/ and do {
$room->{private} or $self->write(BOLD, RED, "You can only cr
+eate an allow list for private chantrooms.$reset\r\n"), return;
$self->unlist( $room->{allow}, $data, sub{if (defined $_[1])
+{ $_[1]->write(GREEN, "You may no longer freely access the private $r
+oom->{name} chantroom.$reset\r\n"); ($_[1]->goto_room(::resolve('root
+',\%rooms)) or $_[1]->leave) if $_[1]->{room} eq $room}} );
$self->write(GREEN, "These users are currently allowed in th
+e $room->{name} chantroom: ", join(' ', sort keys %{$room->{allow}}),
+ "$reset\r\n");
return };
/^unban$/ and do {
$room->{private} and $self->write(BOLD, RED, "You can only b
+an/unban users from public chantrooms.$reset\r\n"), return;
$self->unlist( $room->{ban}, $data, sub{defined($_[1]) and $
+_[1]->write(GREEN, "You are no longer banned from the $room->{name} c
+hantroom.$reset\r\n")} );
$self->write(GREEN, "These users are currently banned from t
+he $room->{name} chantroom: ", join(' ', sort keys %{$room->{ban}}),
+"$reset\r\n");
return };
}
# commands for the superuser -- that's you
if ($name eq $superuser) {
# MAJOR SECURITY RISK!!! Enable for debugging only:
# /^eval$/ and eval $data, return;
/^force$/ and do {
my ($to, $msg) = $data =~ /^(\w+)\s*(.*)$/;
defined $to or $self->write(BOLD, RED, "Who do you want to f
+orce?$reset\r\n"), return;
defined (my $user = ::resolve($to, \%accounts)) or $self->wr
+ite(BOLD, RED, "User $to not found.$reset\r\n"), return;
defined (my $conn = ::resolve($user->{name}, \%users)) or $s
+elf->write(BOLD, RED, "User $user->{name} not currently logged in.$re
+set\r\n"), return;
$msg eq '' and $self->write(GREEN, "If you try to force $to,
+ $user->{name} will carry out the action.$reset\r\n"), return;
$self->write(BOLD, RED, "You can't force yourself to do anyt
+hing, at least not that way.$reset\r\n"), return;
$self->write(GREEN, "$user->{name} has been forced to $msg.$
+reset\r\n");
$conn->write(GREEN, "$name has forced you to $msg.$reset\r\n
+");
$conn->resolve_cmd($msg);
return };
/^save$/ and do {
::save_rooms() && ::save_users() or $self->write(BOLD, RED,
+"Error saving data!$reset\r\n"), return;
$self->write(GREEN, "Rooms and users saved successfully.$res
+et\r\n"); return;
};
/^shutdown$/ and $quit++, return;
/^su$/ and do {
defined (my $user = ::resolve($data, \%accounts)) or $self->
+write(BOLD, RED, "User $data not found.$reset\r\n"), return;
defined (my $conn = ::resolve($user->{name}, \%users)) and $
+self->write(BOLD, RED, "$user->{name} is already logged in.$reset\r\n
+"), return;
push @{$self->{quithandlers}}, sub {
delete $users{$self->{user}->{name}};
$self->{user} = $accounts{$name};
$self->write(GREEN, "You have regained control of $name.$
+reset\r\n");
};
$self->setup_user($user->{name});
$self->write(GREEN, "You now control $user->{name}.$reset\r\
+n"), return;
return };
/^yell$/ and $self->resolve_cmd($data, '*'), return;
}
}
# theoretically you should never get here:
$self->write(BOLD, RED, "Command $cmd not recognized.$reset\r\n");
}
sub list {
# adds a list of users to a hash-list
my ($self, $list, $users, $on_add) = @_;
my %to_add;
(!defined $users or $users eq '') ? ($to_add{all} = undef) : %to_ad
+d = map {($_, undef)} (split /\s+/, $users);
if (scalar keys %to_add) {
%to_add = map {defined $accounts{$_} ? ($_ => undef) : ()} (keys
+ %accounts), delete $to_add{$self->{user}->{name}} if exists $to_add{
+all};
for (sort keys %to_add) {
defined(my $account = ::resolve($_, \%accounts)) or $self->wr
+ite(BOLD, RED, "User $_ not found.", RESET, "\r\n"), next;
$account eq $self->{user} and $self->write(BOLD, RED, "You ca
+n't do that to yourself. Sorry.", RESET, "\r\n"), next;
my $connection = ::resolve($account->{name}, \%users);
exists $list->{$account->{name}} or &$on_add($account, $conne
+ction) if defined $on_add;
$list->{$account->{name}} = undef;
}
}
}
sub unlist {
# removes a list of users from a hash-list
my ($self, $list, $users, $on_delete) = @_;
my %to_delete;
(!defined $users or $users eq '') ? ($to_delete{all} = undef) : %to
+_delete = map {($_, undef)} (split /\s+/, $users);
if (scalar keys %to_delete) {
%to_delete = %$list if exists $to_delete{all};
for (keys %to_delete) {
my $account = ::resolve($_, \%accounts) or $self->write(BOLD,
+ RED, "User $_ not found.", RESET, "\r\n"), next;
if (exists $list->{$account->{name}}) {
my $connection = ::resolve($account->{name}, \%users);
&$on_delete($account, $connection) if defined $on_delete;
delete $list->{$account->{name}};
}
}
}
}
sub invite {
# invites a user to enter a different chantroom
my ($self, $room, $from, $step) = @_;
$step = 0 unless defined $step;
SWITCH: {
$step == 0 and do {
# step one: prompt them to enter the room
$self->write(GREEN, "$from has invited you to join the $room->{n
+ame} chantroom. Do you want to go there now [Y/n]? ", RESET);
$self->{writable} = 0;
$self->{handler} = sub{$self->invite($room,$from,1)};
return };
$step == 1 and do {
# step two: handle the response to the above -- either go the ro
+om or don't
defined(my $conf = $self->read) or return;
$conf eq '' and $conf = 'y'; $conf = lc $conf;
$conf =~ /^y$|^n$/ or $self->write(BOLD, RED, "Please enter y or
+ n: ", RESET), return;
my $from = ::resolve($from,\%users);
if ($conf eq 'y') {
defined $from and $from->write(GREEN, "$self->{user}->{name}
+has accepted your invitation to the $room->{name} chantroom.", RESET,
+ "\r\n");
$self->goto_room($room, 1);
} else {
defined $from and $from->write(GREEN, "$self->{user}->{name}
+has declined your invitation to the $room->{name} chantroom.", RESET,
+ "\r\n");
}
$self->{handler} = $self->next_handler;
return };
}
}
sub goto_room {
# sends a user to a new chantroom; returns 1 if successful and 0 other
+wise
my ($self, $room, $override) = @_;
my $name = $self->{user}->{name};
$override = 0 unless defined $override;
# make sure the user is not already in this room, and that the room
+ exists
defined $self->{room} and $self->{room} eq $room and $self->write(B
+OLD, RED, "You are already in the $room->{name} chantroom.", RESET, "
+\r\n"), return 0;
exists $rooms{$room->{name}} or $self->write(BOLD, RED, "The $room-
+>{name} chantroom no longer exists.", RESET, "\r\n"), return;
# make sure the user has the privilege of entering this room (unles
+s $override is true)
unless ($room->{owner} eq $name or $override) {
if ($room->{private}) {
exists $room->{allow}->{$name} or $self->write(BOLD, RED, "Th
+e $room->{name} chantroom is private; you can not go there unless the
+ owner allows or invites you.", RESET, "\r\n"), return 0;
} else {
exists $room->{ban}->{$name} and $self->write(BOLD, RED, "Sor
+ry, you have been banned from the $room->{name} chantroom.", RESET, "
+\r\n"), return 0;
}
}
# alert users in the old room to this user's departure
if (defined $self->{room}) {
$self->broadcast(GREEN, "[$name has left this chantroom.]", RESE
+T);
delete $self->{room}->{users}->{$name};
}
# send them to the new room
$self->{room} = $self->{user}->{room} = $room;
$room->{users}->{$name} = $self;
$self->write(GREEN, "You have entered the ", $room->{private}?'priv
+ate':'public', " $room->{name} chantroom, owned by $room->{owner}.\r\
+nThe following users are here: ", RESET, join(' ', sort keys %{$room-
+>{users}}), "\r\n");
$self->broadcast(GREEN, "[$name has entered this chantroom.]", RESE
+T);
$self->log("$name entered $room->{name} chantroom");
return 1;
}
sub help {
# prints the happy little help message
my ($self) = @_;
my ($w, $b, $g, $r) = (BOLD, BOLD . BLUE, GREEN, RESET);
my $help = <<END;
${w}Chant help:$r
$w--------------------------------------------------------------------
+-----------$r
Commands and usernames may be shortened; Chant will try to guess what
+you mean.
${g}General commands:$r
${b}msg$r send$b msg$r to your chantroom
$w/me$b msg$r emote$b msg$r to your chantroom
$w/msg$b user msg$r send$b msg$r to$b user$r
$w/${b}user msg$r same as $w/msg$b user$r if$b user$r isn't
+ a valid command
$w/reply$b msg$r or $w/$b msg$r send$b msg$r to the last person who s
+ent you a $w/msg$r
$w/ping$b user$r send a beep to$b user$r
$w/invite$b user$r invite$b user$r to join your chantroom
$w/pray$b msg$r send$b msg$r to the owner of your current c
+hantroom
$w/about$r print the welcome message
$w/help$r print these help pages
$w/repeat$r repeat your last command
$w/password$r change your Chant password
$w/quit$r end your connection to Chant
$w/who$r [${w}all$r|${w}room$r|${b}user$r] lists chanters logged in, i
+n your room, or matching$b user$r
$w/where$b user$r print the name of the chantroom$b user$r is
+ currently in
$w/list$r list all available chantrooms
$w/goto$b room$r go to$b room$r
$w/add$b room$r add a chantroom named room
Typing one of the $w/un...$r commands without specifying$b users$r wil
+l clear the list.
$w/ignore$b users$r block all messages from$b users$r (separate
+d by spaces)
$w/unignore$b users$r stop ignoring$b users$r
$w/await$b users$r alert you when$b users$r log on
$w/unawait$b users$r remove$b users$r from the await list
${g}Commands for room owners:$r
$w/private$r or $w/public$r make this room private or public
$w/delete$r delete this room
$w/invite$b user$r invite$b user$r to join this room
$w/kick$b user$r kick$b user$r out of this room
$w/allow$b users$r allow$b users$r to enter this private room
+anytime they please
$w/unallow$b users$r remove$b users$r from the allow list (priva
+te rooms only)
$w/ban$b users$r ban$b users$r from this room (public rooms
+only)
$w/unban$b users$r remove$b users$r from the ban list (public
+rooms only)
END
$help .= <<END if $self->{user}->{name} eq $superuser;
${g}Commands for the superuser:$r
$w/save$r save the current rooms and users to disk
$w/shutdown$r shutdown Chant
$w/eval$b cmd$r eval$b cmd$r (you must specifically enable
+this!)
$w/force$b user cmd$r force$b user$r to perform$b cmd$r
$w/su$b user$r control$b user$r (only if they aren't alrea
+dy logged in!)
$w/yell$b msg$r send$b msg$r to everyone currently logged i
+n
END
$help =~ s:\r\n|\n|\r:\r\n:g;
#$self->write($help);
$self->page($help);
}
sub dispatch {
# sends a message to $to, which may be either a username, or 'room', '
+all', or ''
my ($self, $to) = (shift, shift);
my $msg = join '',@_;
if ($to eq '*') {
$self->write(BOLD, $msg, RESET, "\r\n");
$_ ne $self and $_->write(BOLD, BLUE, $msg, RESET, "\r\n") for v
+alues %users;
} elsif ($to eq '' or $to eq 'room' or $to eq 'all') {
# send the message to everyone in the current room
$self->write(BOLD, $msg, RESET, "\r\n");
$self->broadcast(BOLD, BLUE, $msg, RESET);
} else {
# send the message to one other user
my $reset = RESET;
# find the recipient
my $recipient = ::resolve($to, \%accounts) or $self->write(BOLD,
+ RED, "User $to not found.$reset\r\n"), return;
my $connection = ::resolve($recipient->{name}, \%users) or $self
+->write(BOLD, RED, "User $recipient->{name} not currently available.
+ Your message will be delivered the next time $recipient->{name} logs
+ on.$reset\r\n");
# send them the message unless they're ignoring this user
unless (exists $recipient->{ignore}->{$self->{user}->{name}}) {
if (defined $connection) {
$connection->write(BOLD, BLUE, "to $recipient->{name}: $ms
+g$reset\r\n");
$connection->{lastmsger} = $self->{user}->{name};
} else {
# add it to their stored messages if they're offline
$recipient->{msgbuf} .= BOLD . BLUE . "to $recipient->{nam
+e}: $msg$reset\r\n";
}
$self->write(BOLD, "to $recipient->{name}: $msg$reset\r\n") u
+nless $recipient eq $self->{user};
} else {
$self->write(BOLD, RED, "$recipient->{name} is ignoring you.
+Your message will not be delivered.$reset\r\n");
}
}
}
sub flush_output {
# sends buffered output to the client
my($self) = @_;
my $buf = $self->{outbuf};
$self->{outbuf} = '';
$self->write($buf) unless $buf eq '';
}
sub broadcast {
# sends a message to the other chanters in this room
my($self) = (shift);
my $msg = join '',@_;
for my $recipient (values %{$self->{room}->{users}}) {
$recipient->write("$msg\r\n") unless not defined $recipient or (
+exists $recipient->{user}->{ignore}->{$self->{user}->{name}} or $reci
+pient eq $self);
}
}
sub read {
# reads in line parts from this socket, returning '' until the line is
+ complete
my($self) = @_;
# turn on output buffering -- we don't want to send anything to the
# client until they are done typing
$self->{writable} = 0;
$self->{buflastmsger} = $self->{lastmsger};
# retrieve characters from the client
$self->{socket}->sysread(my $buf, 80);
$self->leave if $buf eq '';
# standardize line endings
$buf =~ s/\r\n|\r/\n/;
# add to the input buffer
$self->{inbuf} .= $buf;
# if the user has completed a line...
if ($self->{inbuf} =~ /\n/) {
# retrieve the line from the input buffer
($buf, $self->{inbuf}) = $self->{inbuf} =~ /(.*)\n(.*)/s;
# handle any backspace characters
$buf =~ s/[^\010]\010// while $buf =~ /[^\010]\010/;
$buf =~ s/\010//g;
# turn off output buffering and flush output buffer
$self->{writable} = 1;
$self->flush_output;
# return the line
return $buf;
} else {
# if the line is not complete, don't return any of it yet
return;
}
}
sub page {
# sends a string to the user, one 24-line page at a time
my ($self, $text, $in_progress) = @_;
my ($maxcols, $maxlines) = (80, 24);
my ($line, $col) = (1,0);
my $in_escape = 0;
$in_progress = 0 unless defined $in_progress;
if ($in_progress) {
defined(my $input = $self->read) or return;
$input =~ /^q$/i and $self->next_handler, return;
}
my $i = -1; my $char; my $len = length $text;
while (++$i < $len and defined($char = substr($text,$i,1))) {
$line > $maxlines and last;
$in_escape and ($char eq 'm' ? ($in_escape = 0, next) : next);
$char eq "\x1b" and $in_escape = 1, next;
$char eq "\n" and $line++, $col = 0, next;
"\r\a" =~ /\Q$char/ and next;
$col++; $col >= $maxcols and $line++, $col = 1, next;
}
my ($now, $later) = (substr($text,0,$i), substr($text,$i));
$self->write($now);
if (defined $later and $later !~ /^\s*$/) {
$self->write(BOLD, 'Press enter to continue or enter q to quit..
+.', RESET); $self->{writable} = 0;
$in_progress or push @{$self->{nexthandlers}}, sub{$self->page($
+later, 1)};
} else {
$in_progress ? $self->next_handler : return;
}
}
sub write {
# sends a message to the current user
my $self = shift;
my $buf = join '', @_;
return if $buf eq '';
# this is so the user's input will be yellow
$buf = RESET . $buf . BOLD . YELLOW;
# either send the message directly to the user or add it to the out
+put buffer
$self->{writable} ? ($self->{socket}->syswrite($buf, length $buf) o
+r $self->leave) : ($self->{outbuf}.=$buf);
}
sub leave {
# deletes this user and cleans up
my($self) = @_;
my $name = '';
if (defined $self->{user}) {
$name = $self->{user}->{name};
delete $users{$name};
delete $self->{room}->{users}->{$name};
$self->broadcast(GREEN, "[$name disconnected.]", RESET);
}
$self->log("$name [", $self->{socket}->peerhost, '] disconnected');
undef $chanters[$self->{socket}->fileno];
$select->remove($self->{socket});
$self->{socket}->close;
}
sub log {
# logs a message to STDOUT
my($self) = shift;
my $msg = join '', @_;
print $self->{socket}->fileno . ": $msg\n";
}
} # end of package Chanter
|
|
|
|---|