| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544 |
- # Minecraft::RCON - RCON remote console for Minecraft
- #
- # 1.x and above by Ryan Thompson <[email protected]>
- #
- # Original (0.1.x) by Fredrik Vold, no copyrights, no rights reserved.
- # This is absolutely free software, and you can do with it as you please.
- # If you do derive your own work from it, however, it'd be nice with some
- # credits to me somewhere in the comments of that work.
- #
- # Based on http:://wiki.vg/RCON documentation
- package Minecraft::RCON;
- our $VERSION = '1.03';
- use 5.010;
- use strict;
- use warnings;
- no warnings 'uninitialized';
- use Term::ANSIColor 3.01;
- use IO::Socket 1.18; # autoflush
- use Carp;
- use constant {
- # Packet types
- AUTH => 3, # Minecraft RCON login packet type
- AUTH_RESPONSE => 2, # Server auth response
- AUTH_FAIL => -1, # Auth failure (password invalid)
- COMMAND => 2, # Command packet type
- RESPONSE_VALUE => 0, # Server response
- };
- # Minecraft -> ANSI color map
- my %COLOR = map { $_->[1] => color($_->[0]) } (
- [black => '0'], [blue => '1'], [green => '2'],
- [cyan => '3'], [red => '4'], [magenta => '5'],
- [yellow => '6'], [white => '7'], [bright_black => '8'],
- [bright_blue => '9'], [bright_green => 'a'], [bright_cyan => 'b'],
- [bright_red => 'c'], [bright_magenta => 'd'], [yellow => 'e'],
- [bright_white => 'f'],
- [bold => 'l'], [concealed => 'm'], [underline => 'n'],
- [reverse => 'o'], [reset => 'r'],
- );
- # Defaults for new objects. Override in constructor or with accessors.
- sub _DEFAULTS(%) {
- (
- address => '127.0.0.1',
- port => 25575,
- password => '',
- color_mode => 'strip',
- request_id => 0,
- # DEPRECATED options
- strip_color => undef,
- convert_color => undef,
- @_, # Subclasses may override
- );
- }
- # DEPRECATED warning text for convenience/consistency
- my $DEP = 'deprecated and will be removed in a future release.';
- sub new {
- my $class = shift;
- my %opts = 'HASH' eq ref $_[0] ? %{$_[0]} : @_;
- my %DEFAULTS = _DEFAULTS();
- # DEPRECATED -- Warn and transition to new option
- if ($opts{convert_color}) {
- carp "convert_color $DEP\nConverted to color_mode => 'convert'.";
- $opts{color_mode} = 'convert';
- }
- if ($opts{strip_color}) {
- carp "strip_color $DEP\nConverted to color_mode => 'strip'.";
- $opts{color_mode} = 'strip';
- }
- my @unknowns = grep { not exists $DEFAULTS{$_} } sort keys %opts;
- carp "Ignoring unknown option(s): " . join(', ', @unknowns) if @unknowns;
- bless { %DEFAULTS, %opts }, $class;
- }
- sub connect {
- my ($s) = @_;
- return 1 if $s->connected;
- croak 'Password required' unless length $s->{password};
- $s->{socket} = IO::Socket::INET->new(
- PeerAddr => $s->{address},
- PeerPort => $s->{port},
- Proto => 'tcp',
- ) or croak "Connection to $s->{address}:$s->{port} failed: .$!";
- my $id = $s->_next_id;
- $s->_send_encode(AUTH, $id, $s->{password});
- my ($size,$res_id,$type,$payload) = $s->_recv_decode;
- # Force a reconnect if we're about to error out
- $s->disconnect unless $type == AUTH_RESPONSE and $id == $res_id;
- croak 'RCON authentication failed' if $res_id == AUTH_FAIL;
- croak "Expected AUTH_RESPONSE(2), got $type" if $type != AUTH_RESPONSE;
- croak "Expected ID $id, got $res_id" if $id != $res_id;
- croak "Non-blank payload <$payload>" if length $payload;
- return 1;
- }
- sub connected { $_[0]->{socket} and $_[0]->{socket}->connected }
- sub disconnect {
- $_[0]->{socket}->shutdown(2) if $_[0]->connected;
- delete $_[0]->{socket} if exists $_[0]->{socket};
- 1;
- }
- sub command {
- my ($s, $command, $mode) = @_;
- croak 'Command required' unless length $command;
- croak 'Not connected' unless $s->connected;
- my $id = $s->_next_id;
- my $nonce = 16 + int rand(2 ** 15 - 16); # Avoid 0..15
- $s->_send_encode(COMMAND, $id, $command);
- $s->_send_encode($nonce, $id, 'nonce');
- my $res = '';
- while (1) {
- my ($size,$res_id,$type,$payload) = $s->_recv_decode;
- if ($id != $res_id) {
- $s->disconnect;
- croak sprintf(
- "Desync. Expected %d (0x%4x), got %d (0x%4x). Disconnected.",
- $id, $id, $res_id, $res_id
- );
- }
- croak "size:$size id:$id got type $type, not RESPONSE_VALUE(0)"
- if $type != RESPONSE_VALUE;
- last if $payload eq sprintf 'Unknown request %x', $nonce;
- $res .= $payload;
- }
- $s->color_convert($res, defined $mode ? $mode : $s->{color_mode});
- }
- sub color_mode {
- my ($s, $mode, $code) = @_;
- return $s->{color_mode} if not defined $mode;
- croak 'Invalid color mode.'
- unless $mode =~ /^(strip|convert|ignore)$/;
- if ($code) {
- my $was = $s->{color_mode};
- $s->{color_mode} = $mode;
- $code->();
- $s->{color_mode} = $was;
- } else {
- $s->{color_mode} = $mode;
- }
- }
- sub color_convert {
- my ($s, $text, $mode) = @_;
- $mode = $s->{color_mode} if not defined $mode;
- my $re = qr/\x{00A7}(.)/o;
- $text =~ s/$re//g if $mode eq 'strip';
- $text =~ s/$re/$COLOR{$1}/g if $mode eq 'convert';
- $text .= $COLOR{r} if $mode eq 'convert' and $text =~ /\e\[/;
- $text;
- }
- sub DESTROY { $_[0]->disconnect }
- #
- # DEPRECATED methods
- #
- sub convert_color {
- my ($s, $val) = @_;
- carp "convert_color() is $DEP\nUse color_mode('convert') instead";
- $s->color_mode('convert') if $val;
- $s->color_mode eq 'convert';
- }
- sub strip_color {
- my ($s, $val) = @_;
- carp "strip_color() is $DEP\nUse color_mode('strip') instead";
- $s->color_mode('strip') if $val;
- $s->color_mode eq 'strip';
- }
- sub address {
- carp "address() is $DEP";
- $_[0]->{address} = $_[1] if defined $_[1];
- $_[0]->{address};
- }
- sub port {
- carp "port() is $DEP";
- $_[0]->{port} = $_[1] if defined $_[1];
- $_[0]->{port};
- }
- sub password {
- carp "password() is $DEP";
- $_[0]->{password} = $_[1] if defined $_[1];
- $_[0]->{password};
- }
- #
- # Private helpers
- #
- # Increment and return the next request ID, wrapping at 2**31-1
- sub _next_id { $_[0]->{request_id} = ($_[0]->{request_id} + 1) % 2**31 }
- # Form and send a packet of the specified type, request_id and payload
- sub _send_encode {
- my ($s, $type, $id, $payload) = @_;
- confess "Request ID `$id' is not an integer" unless $id =~ /^\d+$/;
- $payload = "" unless defined $payload;
- my $data = pack('V!V' => $id, $type) . $payload . "\0\0";
- $s->{socket}->send(pack(V => length $data) . $data);
- }
- # Grab a single packet.
- sub _recv_decode {
- my ($s) = @_;
- confess "_recv_decode when not connected" unless $s->connected;
- local $_; $s->{socket}->recv($_, 4);
- my $size = unpack 'V';
- $_ = '';
- my $frags = 0;
- croak "Zero length packet" unless $size;
- while ($size > length) {
- my $buf;
- $s->{socket}->recv($buf, $size);
- $_ .= $buf;
- $frags++;
- }
- croak 'Packet too short. ' . length($_) . ' < 10' if 10 > length($_);
- croak "Received packet missing terminator" unless s/\0\0$//;
- $size, unpack 'V!V(A*)';
- }
- 1;
- __END__
- =head1 NAME
- Minecraft::RCON - RCON remote console communication with Minecraft servers
- =head1 VERSION
- Version 1.03
- =head1 SYNOPSIS
- use Minecraft::RCON;
- my $rcon = Minecraft::RCON->new( { password => 'secret' } );
- eval { $rcon->connect };
- die "Connection failed: $@" if $@;
- my $response;
- eval { $response = $rcon->command('help') };
- say $@ ? "Error: $@" : "Response: $response";
- $rcon->disconnect;
- =head1 DESCRIPTION
- C<Minecraft::RCON> provides a nice object interface for talking to Mojang AB's
- game Minecraft. Intended for use with their multiplayer servers, specifically
- I<your> multiplayer server, as you will need the correct RCON password, and
- RCON must be enabled on said server.
- =head1 CONSTRUCTOR
- =head2 new( %options )
- Create a new RCON object. Note we do not connect automatically; see
- C<connect()> for that. The properties and their defaults are shown below:
- my $rcon = Minecraft::RCON->new({
- address => '127.0.0.1',
- port => 25575,
- password => '',
- color_mode => 'strip',
- error_mode => 'error',
- });
- We will C<carp()> but not die in the event that any unknown options are
- provided.
- =over 4
- =item address
- The hostname or IP address to connect to.
- =item port
- The TCP port number to connect to.
- =item password
- The plaintext password used to authenticate. This password must match the
- C<rcon.password=> line in the F<server.properties> file for your server.
- =item color_mode
- The color mode controls how C<Minecraft::RCON> handles color codes sent back
- by the Minecraft server. It must be one of C<strip>, C<convert>, or C<ignore>.
- constants. See C<color_mode()> for more information.
- =back
- =head1 METHODS
- =head2 connect
- eval { $rcon->connect }; # $@ will be set on error
- Attempt to connect to the configured address and port, and issue the
- configured password for authentication.
- If already connected, returns C<undef> (nothing to be done).
- This method will C<croak> if the connection fails for any reason.
- Otherwise, returns a true value.
- =head2 connected
- say "We are connected!" if $rcon->connected;
- Returns true if we have a connected socket, false otherwise. Note that we have
- no way to tell if there is a misbehaving Minecraft server on the other
- side of that socket, so it is entirely possible for this command (or
- C<connect()>) to succeed, but C<command()> calls to fail.
- =head2 disconnect
- $rcon->disconnect;
- Disconnects from the server by closing the socket. Always succeeds.
- =head2 command( $command, [ $color_mode ] )
- my $response = $rcon->command("data get block $x $y $z");
- my $ansi = $rcon->command('list', 'convert');
- Sends the C<$command> to the Minecraft server, and synchronously waits for the
- response. This method is capable of handling fragmented responses (spread over
- several response packets), and will concatenate them all before returning the
- result.
- The resulting server response will have its color codes stripped, converted,
- or ignored, according to the current C<color_mode()> setting, unless a
- C<$color_mode> is given, which will override the current setting for this
- command only.
- =head2 color_mode( $color_mode, [ $code ] )
- $rcon->color_mode('strip');
- When a command response is received, the color codes it contains can be
- stripped, converted to ANSI, or left alone, depending on this setting.
- C<$color_mode> is optional, unless C<$code> is also specified.
- The valid modes are as follows:
- =over 10
- =item strip
- Strip any color codes, returning the plaintext.
- =item convert
- Convert any color codes to the equivalent ANSI escape sequences, suitable for
- display in a terminal.
- =item ignore
- Ignore color codes, returning the full command response verbatim.
- =back
- The current mode will be returned.
- If C<$code> is specified and is a C<CODE> ref, C<color_mode()> will apply the
- new color mode, run C<$code-E<gt>()>, and then restore the original color
- mode. This is useful when you use one color mode most of the time, but have
- sections of code requiring a different mode:
- Example usage:
- # Color mode is 'convert'
- $rcon->color_mode(strip => sub {
- my $plaintext = $rcon->command('...');
- });
- But see also C<command($cmd, $mode)> for running single commands with
- another color mode.
- =head2 color_convert( $string, [ $color_mode ] )
- my $response = $rcon->command('list');
- my ($strip, $ansi) = map { $rcon->color_convert($response, $_) }
- qw<strip convert>;
- This method is used internally by C<command()> to convert command responses as
- configured in the object. However, C<color_convert()> itself may be useful in
- some applications where a stripped version of the response may be needed for
- parsing, while an ANSI version may be desired for display to a terminal, for
- example, without having to run the command itself (with possible side-effects)
- a second time. For C<color_convert()> to do anything meaningful, your object's
- C<color_mode> should be set to C<ignore>.
- =head1 ERROR HANDLING
- This module C<croak>s (see L<Carp>) for almost all errors.
- When an error does not affect control flow, we will C<carp> instead.
- Thus, C<command()> and C<connect()>, at minimum, should be wrapped in block
- C<eval>:
- eval { $result = $rcon->command('list'); };
- warn "I don't know who is online because: $@" if $@;
- If a little extra syntactic sugar is desired, you can use an exception handler
- like L<Try::Tiny> instead:
- use Try::Tiny;
- try {
- $result = $rcon->command('list');
- } catch {
- warn "I don't know who is online because: $_";
- }
- =head1 DEPRECATED METHODS
- The following methods have been deprecated. They will issue a warning to
- STDOUT when called, and will be removed in a future release.
- =head2 convert_color ( $enable )
- If C<$enable> is a true value, change the color mode to C<convert>.
- Returns 1 if the current color mode is C<convert>, undef otherwise.
- B<Deprecated.> Use C<color_mode('convert')> instead.
- =head2 strip_color
- If C<$enable> is a true value, change the color mode to C<strip>.
- Returns 1 if the current color mode is C<strip>, undef otherwise.
- B<Deprecated.> Use C<color_mode('strip')> instead.
- =head1 SUPPORT
- =over 4
- =item L<https://github.com/rjt-pl/Minecraft-RCON.git>: Source code repository
- =item L<https://github.com/rjt-pl/Minecraft-RCON/issues>: Bug reports and feature requests
- =back
- =head1 SEE ALSO
- =over 4
- =item L<Net::RCON::Minecraft> for an alternative API
- =item L<Terminal::ANSIColor>, L<IO::Socket::INET>
- =item L<https://developer.valvesoftware.com/wiki/Source_RCON_Protocol>
- =item L<https://wiki.vg/RCON>
- =back
- =head1 AFFILIATION WITH MOJANG
- I<Note from original author, Fredrik Vold:>
- I am in no way affiliated with Mojang or the development of Minecraft.
- I'm simply a fan of their work, and a server admin myself. I needed
- some RCON magic for my servers website, and there was no perl module.
- It is important that everyone using this module understands that if
- Mojang changes the way RCON works, I won't be notified any sooner than
- anyone else, and I have no special avenue of connection with them.
- =head1 AUTHORS
- =over 4
- =item B<Ryan Thompson> C<E<lt>[email protected]<gt>>
- Addition of unit test suite, fragmentation support, and other improvements.
- This program is free software; you can redistribute it
- and/or modify it under the same terms as Perl itself.
- L<http://dev.perl.org/licenses/artistic.html>
- =item B<Fredrik Vold> C<E<lt>[email protected]<gt>>
- Original (0.1.x) author.
- No copyright claimed, no rights reserved.
- You are absolutely free to do as you wish with this code, but mentioning me in
- your comments or whatever would be nice.
- Minecraft is a trademark of Mojang AB. Name used in accordance with my
- interpretation of L<http://www.minecraft.net/terms>, to the best of my
- knowledge.
- =back
|