Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/perl -w
- # BattlEye rcon in perl, developed for Arma2 DayZ mod.
- # This tool is part of DayZ4linux project.
- # This software is licensed under GPLv3 license
- # Author contact: facoptere-at-gmail.com
- use strict;
- use Socket;
- use Term::ReadPassword;
- # you may need to sudo perl -MCPAN -e 'install Term::ReadPassword'
- # also maybe perl -MCPAN -e 'install YAML::Any'
- use String::CRC32;
- # you may need to sudo perl -MCPAN -e 'install String::CRC32'
- use Getopt::Std;
- use vars qw( $opt_r $opt_i $opt_p $opt_k $opt_L $opt_b $opt_t);
- my ($socket,$dataUDP,$onebyte,$seq,$select,$rout,$nfound,$timeleft);
- my $datain="";
- my $timeout=40;
- my $PASS=0;
- my $COMM=1;
- my $KA=2;
- my $MULTI=9;
- my $IDLE=-1;
- my $state=$PASS;
- my $unresponsiveCount=0;
- my $rin = ''; # bitfield for fd select
- my @mbuffer; # buffer for multiple packets server response
- $SIG{'QUIT'} = sub { die "rcon.pl: SIGQUIT caught\n"; };
- # encapsulate to BattlEye datagram
- sub enc {
- my ($s)=@_;
- $s=pack("C",255).$s;
- return 'BE'.pack("L",crc32($s)).$s;
- }
- # decapsulate from BattlEye datagram
- sub dec {
- my ($s)=@_;
- my @r=unpack("a2LCa*",$s);
- #print STDERR $r[0].' '.$r[1].' '.crc32(pack("C",$r[2]).$r[3]).' '.$r[2]."\n";
- return $r[3] if ($r[0] eq 'BE' && $r[2] == 255 && crc32(pack("C",$r[2]).$r[3])==$r[1]);
- return pack("CCa*",0,99,"Can't connect");
- }
- # prepare a send request (password, command or console acknoledge)
- sub sendbe {
- my ($c,$q, $s)=@_;
- if ($c==$PASS) {
- return pack("C",0).$s;
- }
- elsif ($c==$COMM) {
- return pack("CCa*",1,$q,$s) if (defined $s);
- return pack("CC",1,$q);
- }
- elsif ($c==$KA) {
- return pack("CC",2,$q);
- }
- }
- # decode a response (password granted or not, command output, or console output)
- sub recvbe {
- my ($s)=@_;
- return (undef,undef,undef) if (!$s);
- my @r=unpack("CCCCCa*",$s);
- return ($MULTI, $r[1], $r[5], $r[4], $r[3]) if (scalar(@r)==6 && $r[0]==1 && $r[2]==0);
- @r=unpack("CCa*",$s);
- return ($r[0], $r[1], $r[2]);
- }
- #####
- # high level function!!
- my %HIpl;
- my %HIconnecting;
- my $HIpolling=3; # polling players command
- my $HImaxBEKO=9; # max minutes of BattlEye KO to start kicking
- my $HImaxBadname=20; # max minutes of BattlEye KO to start kicking
- my $HIminLobby=20; # dont kick player stayed less than this
- my $date=0;
- my $datePlayersSent=0;
- sub HIreadPlayers {
- my $state=0;
- $date=time();
- my ($block) = @_;
- #syswrite STDOUT, sprintf ("block \"%s\" \n", $block);
- my $ret="";
- while( $block =~ /([^\r\n]+)[\r\n]*/gs ) {
- $_=$1;
- # horizontal line detection
- if ($state == 0 && /^-{50}$/) {
- $state = 1; # go to player line parsing
- $datePlayersSent=$date; # reset datePlayersSent, acts as an aknowledge of the 'players' command
- # sometime server does not answer to commands
- next;
- }
- # parsing a player line
- elsif ($state == 1 && /^(\d+) +[0-9.:]+ +(-?\d+) +([a-f0-9]{32})\((.+?)\) +(.*?)$/) {
- my ($no, $ping, $uid, $ok, $name) = (int($1), int($2), $3, $4, $5);
- my $p = $HIpl{$uid};
- my $lobby = '';
- ($name, $lobby) = $name =~ /^(.*?)(?:| ?\((Lobby|Admin)\))$/ ;
- #print ">>> $no $ping $uid $ok '$name' / $lobby\n";
- if (!$p) {
- $p = $HIpl{ $uid } = [ $date, 0,"",0,0,0,0 ];
- #syswrite STDOUT, "new p\n";
- }
- if ($p) {
- $p->[1] = $no; # player number
- $p->[2] = $name; # player name
- $p->[3] = ($ok cmp 'OK')==0 ? 0 : $p->[3]+$date-$p->[0] ; # amount of consecutive seconds Battleye check is non OK
- $p->[4] = (defined $lobby && $lobby eq 'Lobby') ? $p->[4]+$date-$p->[0] : 0 ; # amount of consecutive seconds player stays in the lobby
- $p->[5] = (defined $lobby && $lobby eq 'Admin') ? $p->[5]+$date-$p->[0] : 0 ; # amount of consecutive seconds player is admin
- $p->[6] = ($name =~ /(:?Adm[il!|]n|[0o]wner)/i) ? $p->[6]+$date-$p->[0] : 0 ; # amount of consecutive seconds player has forbidden name
- #syswrite STDOUT, sprintf("%d %d %s %s %s %s / %d %d %d date=%ld p0=%ld\n", $no,$ping,$uid,$ok,$name,$lobby,$p->[3],$p->[4],$p->[5],$date,$p->[0]);
- $p->[0] = $date;
- # we dont check the ping.
- }
- next;
- }
- # parsing footer
- elsif ($state==1 && /^\((\d+) players in total\)$/) {
- my $nb=$1;
- #print STDERR "##### N.PLayers=$nb / $opt_L // $HIminLobby\n";
- foreach my $k (sort { $HIpl{$b}->[3]-$HIpl{$a}->[4] } keys %HIpl)
- {
- my $p = $HIpl{$k};
- if ($date!=$p->[0]) {
- delete($HIpl{k}) ; # delete player which were not in lastest player list
- }
- #elsif ($p->[3] >= $HImaxBEKO*60-1) {
- # $ret.= sprintf ("kick %d Sorry, BattlEye did not manage to verify your game files for %d minutes;",
- # $p->[1], $p->[3]/60 );
- # delete($HIpl{$k});
- # $nb--;
- #}
- #elsif ($p->[3] > ($HImaxBEKO-$HIpolling)*60) {
- # $ret.= sprintf ("say %d %s, you will be kicked if BattlEye can't verify your game files;",
- # $p->[1], $p->[2] );
- #}
- if ($p->[6] >= $HImaxBadname*60) {
- $ret.= sprintf ("kick %d Sorry, %s is an invalid player name;",
- $p->[1], $p->[2] );
- delete($HIpl{$k});
- $nb--;
- }
- elsif ($p->[6] > ($HImaxBadname-$HIpolling)*60) {
- $ret.= sprintf ("say %d %s, you will be kicked your don't change your name;",
- $p->[1], $p->[2] );
- }
- }
- if (defined $opt_L && $nb > $opt_L) {
- foreach my $k (sort { $HIpl{$b}->[4]-$HIpl{$a}->[4] } keys %HIpl)
- {
- my $p = $HIpl{$k};
- if ($p->[4] >= $HIminLobby*60) {
- $ret.= sprintf ("kick %d Sorry, server is almost full and you were AFK for %d minutes;",
- $p->[1], $p->[4]/60 );
- delete($HIpl{$k});
- $nb--;
- }
- last if ($nb <= $opt_L);
- }
- }
- $state=0;
- next;
- }
- elsif (/^Player #(\d+) (.*?) \(([0-9.]+):[0-9]+\) connected/) {
- my ($no, $name,$ip) = (int($1),$2, $3);
- $HIconnecting{$no} = $ip;
- }
- elsif (/^Verified GUID \(([a-f0-9]{32})\) of player #(\d+) ([^\r\n]+)$/) {
- #Verified GUID (a768de690a42fb45e938990a02a01bc9) of player #3 Pridezor - YouTube
- my ($uid, $no, $name) = ($1, int($2), $3);
- my $p = $HIpl{$uid};
- if (!$p) {
- $p = $HIpl{ $uid } = [ $date, 0,"",0,0,0,0 ];
- }
- if ($p) {
- $p->[1] = $no; # player number
- $p->[2] = $name; # player name
- $p->[0] = $date;
- my $ip=$HIconnecting{$no};
- $p->[7] = $ip if defined $ip;
- delete $HIconnecting{$no} if defined $ip;
- }
- }
- #else {
- # syswrite STDOUT, "unkonwn $_\n";
- #}
- }
- #syswrite STDOUT, "$ret\n";
- return "$ret";
- }
- # reading command line arguments
- getopts('r:i:p:L:kbt'); # -r for rconpassword, -i for ip address, -p for port, -k keepalive, -t timeout
- $opt_p=2302 if(!$opt_p);
- die "BattelEye admin console for ArmA2 OA\n".
- "Usage: rcon.pl -i ip [-p port] [-r rcon-password] [-k] [-b] [-t] [-L number]\n".
- " or: echo 'commands;say -1 get off all of you!;kickall Mouhahhaha' | rcon.pl -r rcon-password -i ip [-p port]\n".
- "-k: keep connection alive. By default connection is closed if idle for 40 seconds.\n".
- "-b: Don't send bell signals.\n".
- "-t: Don't quit at Stdin EOF, but wait for timeout\n".
- "-L: kick some players who stay in the lobby if 'number' players are in.\n"
- if(!$opt_i);
- $opt_k=1 if (defined $opt_L);
- $timeout=10 if (defined $opt_L); # TODO : the bes is to detect we are not on a terminal
- # connect to server, register to 'read' events from socket
- socket(RCON, PF_INET, SOCK_DGRAM, getprotobyname('udp')) || die $!;
- connect (RCON, sockaddr_in ($opt_p, inet_aton($opt_i))) || die $!;
- vec($rin,fileno(RCON),1) = 1;
- $opt_r = read_password('RConPassword: ') if (!$opt_r);
- # WARNING: from now, function such print() eof() <> are banned in code, dont use them since we use low level functions: select syswrite recv send
- while (1) {
- $date=time();
- my @ltime = localtime($date);
- #syswrite STDERR, "rcon.pl: $state datain: $datain\n";
- ## in this section, we should send something : PASS for a logon, COMM for a command, or KA to keep connection alive
- if ($state==$PASS) {
- $seq=0;
- syswrite STDERR, "Logging in...\n";
- send(RCON, enc(sendbe($PASS, undef, $opt_r)), 0);
- $datain="";
- }
- else { while ( $state!=$COMM && $datain =~ /^([^\r\t\n;]+)(?:$|[\r\t\n; ]+(.*)$)/ ) { # some commands in the buffer to send?
- my $cmd="$1";
- my $end= defined $2 ? "$2": "";
- if (substr($cmd,0,1) eq "!") {
- eval substr($cmd,1); # Warning: $cmd (such 'exit') is evaluated by Perl.
- $datain=$end;
- }
- elsif ($cmd =~ /^kickall +(.*)$/i) {
- %HIpl=();
- $datain="players;_kickall $1;$end";
- }
- # banbyname kickbyname saybyname
- elsif ($cmd =~ /^(?<cmd>[a-z]+)byname +(?:"(?<name>.*)"|'(?<name>.*)'|(?<name>[^ ]+)) +(?<msg>.*?)$/i) {
- my ($cmd,$name,$msg) = ($+{cmd},$+{name},$+{msg});
- $datain="";
- foreach my $k (keys %HIpl) {
- my $p = $HIpl{$k};
- $datain.="$cmd ".$p->[1]." $msg;" if ($p->[2] eq $name);
- }
- $datain.=$end;
- }
- elsif ($cmd =~ /^timeout +([0-9]+)$/i) {
- $timeout=$1;
- syswrite STDERR, "rcon.pl: timeout changed to $timeout\n";
- $datain=$end;
- }
- elsif ($cmd =~ /^_kickall +(.*)$/i) {
- $datain="";
- foreach my $k (keys %HIpl)
- {
- my $p = $HIpl{$k};
- $datain.="kick ".$p->[1]." $1;";
- }
- $datain.=$end;
- }
- else {
- syswrite STDERR, sprintf ("> %02d:%02d:%02d %s\n", $ltime[2],$ltime[1],$ltime[0], $cmd);
- send(RCON, enc(sendbe($COMM, $seq, $cmd)), 0) ;
- vec($rin,fileno(STDIN),1) = 0; ## switch off stdin to wait command response
- $state=$COMM;
- $datain=$end;
- last; # exit while
- }
- }}
- if ($state==$KA) {
- send(RCON, enc(sendbe($COMM, $seq, undef)), 0);
- syswrite STDOUT, "\a" if (! defined $opt_b) ; # Send a bell signal to the console
- $state=$IDLE;
- }
- ## In this section, we wait something from streams. We should if we sent something!
- ($nfound,$timeleft) = select($rout=$rin, undef, undef, $timeout);
- $date=time();
- @ltime = localtime($date);
- #syswrite STDOUT, sprintf ( "select: %d %f\n", $nfound, $timeleft );
- # select timed out? so no events during $timeout seconds...
- if ($timeleft==0) {
- die "rcon.pl: timeout\n" if (! defined $opt_k && ! defined $opt_L); # program exit if no keepalive
- # let's feed stdin with some evil commands if necessary
- if ( (defined $opt_L) && $date-$datePlayersSent>$HIpolling*60 ) {
- #syswrite STDERR, "(-L) Stdin fed with 'players' command\n";
- $datain="players;".$datain; # next loop, commands in the datain buffer will be processed
- }
- if ($state==$COMM) {
- syswrite STDERR, "rcon.pl: Hum, command timed out! Are we still logged on?\n";
- exit 18 if ($unresponsiveCount++>=2);
- $state=$PASS; # next loop, this will triger the logon process
- }
- elsif ($state==$PASS){
- syswrite STDERR, "rcon.pl: Hum, login timed out! Server unresponsive?\n";
- exit 17 if ($unresponsiveCount++>=2);
- }
- else { # we were idle
- $state=$KA; # next loop, this will trigger the sending of a keep-alive packet (if datain is empty)
- }
- next; # next loop...
- }
- ## In this section, we had some events from UDP socket or from STDIN... Let's process them!
- if (vec($rout,fileno(RCON),1) == 1) { # read event on UDP socket
- recv(RCON, $dataUDP,65507, 0) ;
- my ($type, $srvseq, $response, $idx, $qty) = recvbe(dec($dataUDP));
- if ($type == $COMM) { # the small result of a command
- if (length($response)>=0) {
- syswrite STDOUT, sprintf("< %02d:%02d:%02d %s\n", $ltime[2],$ltime[1],$ltime[0], $response);
- $datain.=HIreadPlayers($response);
- $seq=($seq+1)%255;
- $state=$IDLE;
- vec($rin,fileno(STDIN),1) = 1; # switch on stdin to consume another command
- }
- ###last if (vec($rin,fileno(STDIN),1) == 0); # if stdin has been closed then it is time to die
- $unresponsiveCount=0;
- }
- elsif ($type == $PASS) { # the response of a logon request
- die "rcon.pl: Bad password\n" if ($srvseq==0); # BAD PASSWORD
- if ($srvseq == 99) { # we can't connect since it is the fake datagram built by sub 'dec'
- if (defined $opt_k) {
- sleep 5; # wait a little and retry a logon later
- syswrite STDERR, "rcon.pl: retrying to logon...\n";
- }
- else {
- die "rcon.pl: $response\n";
- }
- }
- else { # We are connected and logon successfull
- syswrite STDERR, "Logon successful!\n";
- $state=$IDLE;
- }
- }
- elsif ($type == $KA) { # a message the server gave to us, that requires an acknowledge
- syswrite STDOUT, sprintf("< %02d:%02d:%02d %s\n", $ltime[2],$ltime[1],$ltime[0], $response);
- $datain.=HIreadPlayers($response);
- send(RCON, enc(sendbe($KA, $srvseq, undef)), 0);
- vec($rin,fileno(STDIN),1) = 1; # switch on stdin to consume another command
- $state=$IDLE;
- }
- elsif ($type == $MULTI) { # the result of a command, in multiple packets
- @mbuffer=() if (!$idx); # buffer of packets is reset if index=0 (first packet)
- $mbuffer[$idx]=$response;
- # syswrite STDOUT, sprintf("%s%s%s", ($idx)?'':'# ', $response, ($idx==$qty-1)?"\n":'');
- send(RCON, enc(sendbe($KA, $srvseq, undef)), 0);
- if ($idx==$qty-1) { # if it was the last packet, reception is complete
- $response=join('',@mbuffer);
- syswrite STDOUT, sprintf("< %02d:%02d:%02d %s\n",$ltime[2],$ltime[1],$ltime[0], $response);
- $datain.=HIreadPlayers($response);
- vec($rin,fileno(STDIN),1) = 1; # switch on stdin to consume another command
- $seq=($seq+1)%255;
- $state=$IDLE;
- }
- else {
- $state=$COMM; # let's keep on receipt the others packets
- }
- $unresponsiveCount=0;
- }
- }
- if (vec($rout,fileno(STDIN),1) == 1) { # event from stdin (can be data to read, or EOF if data to read is empty)
- my $dateSTDIN="";
- my $res=sysread(STDIN, $dateSTDIN,65507) ;
- $datain.=$dateSTDIN;
- if (!length($datain)) {
- die "rcon.pl: EOF for STDIN\n" if !(defined $opt_t); # exit if EOF for stdin AND datain buffer is empty
- vec($rin,fileno(STDIN),1) = 0;
- }
- #syswrite STDOUT, sprintf ("> STDIN \"%s\"+\"%s\" %d \n", $datain, $dateSTDIN, $res );
- }
- }
- close(RCON);
- exit 0;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement