#!/usr/bin/perl # Copyright (c) 2008 Rudolf "divVerent" Polzer # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # parts copied from rcon2irc # MISC STRING UTILITY ROUTINES to convert between DarkPlaces and IRC conventions # convert mIRC color codes to DP color codes our $color_utf8_enable = 1; our @color_irc2dp_table = (7, 0, 4, 2, 1, 1, 6, 1, 3, 2, 5, 5, 4, 6, 7, 7); our @color_dp2irc_table = (-1, 4, 9, 8, 12, 11, 13, -1, -1, -1); # not accurate, but legible our @color_dp2ansi_table = ("m", "1;31m", "1;32m", "1;33m", "1;34m", "1;36m", "1;35m", "m", "1m", "1m"); # not accurate, but legible our %color_team2dp_table = (5 => 1, 14 => 4, 13 => 3, 10 => 6); our %color_team2irc_table = (5 => 4, 14 => 12, 13 => 8, 10 => 13); sub color_irc2dp($) { my ($message) = @_; $message =~ s/\^/^^/g; my $color = 7; $message =~ s{\003(\d\d?)(?:,(\d?\d?))?|(\017)}{ # $1 is FG, $2 is BG, but let's ignore BG my $oldcolor = $color; if($3) { $color = 7; } else { $color = $color_irc2dp_table[$1]; $color = $oldcolor if not defined $color; } ($color == $oldcolor) ? '' : '^' . $color; }esg; $message =~ s{[\000-\037]}{}gs; # kill bold etc. for now return $message; } our @text_qfont_table = ( # ripped from DP console.c qfont_table "\0", '#', '#', '#', '#', '.', '#', '#', '#', 9, 10, '#', ' ', 13, '.', '.', '[', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '<', '=', '>', ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '<', '<', '=', '>', '#', '#', '.', '#', '#', '#', '#', ' ', '#', ' ', '>', '.', '.', '[', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '<', '=', '>', ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '<' ); sub text_qfont_table($) { my ($char) = @_; my $o = ord $char; if($color_utf8_enable) { return ($o & 0xFF00 == 0xE000) ? $text_qfont_table[$o & 0xFF] : $char; } else { return $text_qfont_table[$o]; } } sub text_dp2ascii($) { my ($message) = @_; $message = join '', map { text_qfont_table $_ } split //, $message; } sub color_dp_transform(&$) { my ($block, $message) = @_; $message =~ s{(?:(\^\^)|\^x([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])|\^([0-9])|(.))(?=([0-9,]?))}{ defined $1 ? $block->(char => '^', $7) : defined $2 ? $block->(rgb => [hex $2, hex $3, hex $4], $7) : defined $5 ? $block->(color => $5, $7) : defined $6 ? $block->(char => $6, $7) : die "Invalid match"; }esg; return $message; } sub color_dp2none($) { my ($message) = @_; return color_dp_transform { my ($type, $data, $next) = @_; $type eq 'char' ? text_qfont_table $data : ""; } $message; } sub color_rgb2basic($) { my ($data) = @_; my ($R, $G, $B) = @$data; my $min = [sort { $a <=> $b } ($R, $G, $B)]->[0]; my $max = [sort { $a <=> $b } ($R, $G, $B)]->[-1]; my $v = $max / 15; my $s = ($max == $min) ? 0 : 1 - $min/$max; if($s < 0.2) { return 0 if $v < 0.5; return 7; } my $h; if($max == $min) { $h = 0; } elsif($max == $R) { $h = (60 * ($G - $B) / ($max - $min)) % 360; } elsif($max == $G) { $h = (60 * ($B - $R) / ($max - $min)) + 120; } elsif($max == $B) { $h = (60 * ($R - $G) / ($max - $min)) + 240; } return 1 if $h < 36; return 3 if $h < 80; return 2 if $h < 150; return 5 if $h < 200; return 4 if $h < 270; return 6 if $h < 330; return 1; } sub color_dp_rgb2basic($) { my ($message) = @_; return color_dp_transform { my ($type, $data, $next) = @_; $type eq 'char' ? ($data eq '^' ? '^^' : $data) : $type eq 'color' ? "^$data" : $type eq 'rgb' ? "^" . color_rgb2basic $data : die "Invalid type"; } $message; } sub color_dp2irc($) { my ($message) = @_; my $color = -1; return color_dp_transform { my ($type, $data, $next) = @_; if($type eq 'rgb') { $type = 'color'; $data = color_rgb2basic $data; } $type eq 'char' ? text_qfont_table $data : $type eq 'color' ? do { my $oldcolor = $color; $color = $color_dp2irc_table[$data]; $color == $oldcolor ? '' : $color < 0 ? "\017" : (index '0123456789,', $next) >= 0 ? "\003$color\002\002" : "\003$color"; } : die "Invalid type"; } $message; } sub color_dp2ansi($) { my ($message) = @_; my $color = -1; return color_dp_transform { my ($type, $data, $next) = @_; if($type eq 'rgb') { $type = 'color'; $data = color_rgb2basic $data; } $type eq 'char' ? text_qfont_table $data : $type eq 'color' ? do { my $oldcolor = $color; $color = $color_dp2ansi_table[$data]; $color eq $oldcolor ? '' : "\033[${color}" } : die "Invalid type"; } $message; } sub color_dpfix($) { my ($message) = @_; # if the message ends with an odd number of ^, kill one chop $message if $message =~ /(?:^|[^\^])\^(\^\^)*$/; return $message; } # Interfaces: # Connection: # $conn->sockname() returns a connection type specific representation # string of the local address, or undef if not applicable. # $conn->send("string") sends something over the connection. # $conn->recv() receives a string from the connection, or returns "" if no # data is available. # $conn->fds() returns all file descriptors used by the connection, so one # can use select() on them. # Channel: # Usually wraps around a connection and implements a command based # structure over it. It usually is constructed using new # ChannelType($connection, someparameters...) # @cmds = $chan->join_commands(@cmds) joins multiple commands to a single # command string if the protocol supports it, or does nothing and leaves # @cmds unchanged if the protocol does not support that usage (this is # meant to save send() invocations). # $chan->send($command, $nothrottle) sends a command over the channel. If # $nothrottle is sent, the command must not be left out even if the channel # is saturated (for example, because of IRC's flood control mechanism). # $chan->quote($str) returns a string in a quoted form so it can safely be # inserted as a substring into a command, or returns $str as is if not # applicable. It is assumed that the result of the quote method is used # as part of a quoted string, if the protocol supports that. # $chan->recv() returns a list of received commands from the channel, or # the empty list if none are available. # $conn->fds() returns all file descriptors used by the channel's # connections, so one can use select() on them. # Socket connection. # Represents a connection over a socket. # Mainly used to wrap a channel around it for, in this case, line based or rcon-like operation. package Connection::Socket; use strict; use warnings; use IO::Socket::INET; use IO::Handle; # Constructor: # my $conn = new Connection::Socket(tcp => "localaddress" => "remoteaddress" => 6667); # If the remote address does not contain a port number, the numeric port is # used (it serves as a default port). sub new($$) { my ($class, $proto, $local, $remote, $defaultport) = @_; my $sock = IO::Socket::INET->new( Proto => $proto, (length($local) ? (LocalAddr => $local) : ()), PeerAddr => $remote, PeerPort => $defaultport ) or die "socket $proto/$local/$remote/$defaultport: $!"; binmode $sock; $sock->blocking(0); my $you = { # Mortal fool! Release me from this wretched tomb! I must be set free # or I will haunt you forever! I will hide your keys beneath the # cushions of your upholstered furniture... and NEVERMORE will you be # able to find socks that match! sock => $sock, # My demonic powers have made me OMNIPOTENT! Bwahahahahahahaha! }; return bless $you, 'Connection::Socket'; } # $sock->sockname() returns the local address of the socket. sub sockname($) { my ($self) = @_; my ($port, $addr) = sockaddr_in $self->{sock}->sockname(); return "@{[inet_ntoa $addr]}:$port"; } # $sock->send($data) sends some data over the socket; on success, 1 is returned. sub send($$) { my ($self, $data) = @_; return 1 if not length $data; if(not eval { $self->{sock}->send($data); }) { warn "$@"; return 0; } return 1; } # $sock->recv() receives as much as possible from the socket (or at most 32k). Returns "" if no data is available. sub recv($) { my ($self) = @_; my $data = ""; if(defined $self->{sock}->recv($data, 32768, 0)) { return $data; } elsif($!{EAGAIN}) { return ""; } else { return undef; } } # $sock->fds() returns the socket file descriptor. sub fds($) { my ($self) = @_; return fileno $self->{sock}; } # QW rcon protocol channel. # Wraps around a UDP based Connection and sends commands as rcon commands as # well as receives rcon replies. The quote and join_commands methods are using # DarkPlaces engine specific rcon protocol extensions. package Channel::QW; use strict; use warnings; use Digest::HMAC; use Digest::MD4; # Constructor: # my $chan = new Channel::QW($connection, "password"); sub new($$$) { my ($class, $conn, $password, $secure, $timeout) = @_; my $you = { connector => $conn, password => $password, recvbuf => "", secure => $secure, timeout => $timeout, }; return bless $you, 'Channel::QW'; } # Note: multiple commands in one rcon packet is a DarkPlaces extension. sub join_commands($@) { my ($self, @data) = @_; return join "\0", @data; } sub send($$$) { my ($self, $line, $nothrottle) = @_; utf8::encode $line if $color_utf8_enable; if($self->{secure} > 1) { $self->{connector}->send("\377\377\377\377getchallenge"); my $c = $self->recvchallenge(); return 0 if not defined $c; my $key = Digest::HMAC::hmac("$c $line", $self->{password}, \&Digest::MD4::md4); return $self->{connector}->send("\377\377\377\377srcon HMAC-MD4 CHALLENGE $key $c $line"); } elsif($self->{secure}) { my $t = sprintf "%ld.%06d", time(), int rand 1000000; my $key = Digest::HMAC::hmac("$t $line", $self->{password}, \&Digest::MD4::md4); return $self->{connector}->send("\377\377\377\377srcon HMAC-MD4 TIME $key $t $line"); } else { return $self->{connector}->send("\377\377\377\377rcon $self->{password} $line"); } } # Note: backslash and quotation mark escaping is a DarkPlaces extension. sub quote($$) { my ($self, $data) = @_; $data =~ s/[\000-\037]//g; $data =~ s/([\\"])/\\$1/g; $data =~ s/\$/\$\$/g; return $data; } sub recvchallenge($) { my ($self) = @_; my $sel = IO::Select->new($self->fds()); my $endtime_max = Time::HiRes::time() + $self->{timeout}; my $endtime = $endtime_max; while((my $dt = $endtime - Time::HiRes::time()) > 0) { if($sel->can_read($dt)) { for(;;) { my $s = $self->{connector}->recv(); die "read error\n" if not defined $s; length $s or last; if($s =~ /^\377\377\377\377challenge (.*?)(?:$|\0)/s) { return $1; } next if $s !~ /^\377\377\377\377n(.*)$/s; $self->{recvbuf} .= $1; } } } return undef; } sub recv($) { my ($self) = @_; for(;;) { my $s = $self->{connector}->recv(); die "read error\n" if not defined $s; length $s or last; next if $s !~ /^\377\377\377\377n(.*)$/s; $self->{recvbuf} .= $1; } my @out = (); while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//) { my $s = $1; utf8::decode $s if $color_utf8_enable; push @out, $s; } return @out; } sub fds($) { my ($self) = @_; return $self->{connector}->fds(); } package main; use strict; use warnings; use IO::Select; use Time::HiRes; sub default($$) { my ($default, $value) = @_; return $value if defined $value; return $default; } my $server = default '', $ENV{rcon_address}; my $password = default '', $ENV{rcon_password}; my $secure = default '1', $ENV{rcon_secure}; my $timeout = default '5', $ENV{rcon_timeout}; my $timeouti = default '0.2', $ENV{rcon_timeout_inter}; my $timeoutc = default $timeout, $ENV{rcon_timeout_challenge}; my $colors = default '0', $ENV{rcon_colorcodes_raw}; my $utf8 = default '1', $ENV{rcon_utf8_enable}; if(!length $server) { print STDERR "Usage: rcon_address=SERVERIP:PORT rcon_password=PASSWORD $0 rconcommands...\n"; print STDERR "Optional: rcon_timeout=... (default: 5)\n"; print STDERR " rcon_timeout_inter=... (default: 0.2)\n"; print STDERR " rcon_timeout_challenge=... (default: 5)\n"; print STDERR " rcon_colorcodes_raw=1 (to disable color codes translation)\n"; print STDERR " rcon_secure=0 (to allow connecting to older servers not supporting secure rcon)\n"; print STDERR " rcon_utf8_enable=0 (to enable/disable engine UTF8 mode)\n"; exit 0; } $color_utf8_enable = $utf8; if($color_utf8_enable) { binmode STDOUT, ':utf8'; binmode STDERR, ':utf8'; } my $connection = Connection::Socket->new("udp", "", $server, 26000); my $rcon = Channel::QW->new($connection, $password, $secure, $timeoutc); if(!$rcon->send($rcon->join_commands(@ARGV))) { die "send: $!"; } if($timeout > 0) { my $sel = IO::Select->new($rcon->fds()); my $endtime_max = Time::HiRes::time() + $timeout; my $endtime = $endtime_max; while((my $dt = $endtime - Time::HiRes::time()) > 0) { if($sel->can_read($dt)) { for($rcon->recv()) { $_ = (color_dp2ansi $_) . "\033[m" unless $colors; print "$_\n" } $endtime = Time::HiRes::time() + $timeouti; $endtime = $endtime_max if $endtime > $endtime_max; } } } exit 0;