RCON.pm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. # Minecraft::RCON - RCON remote console for Minecraft
  2. #
  3. # 1.x and above by Ryan Thompson <[email protected]>
  4. #
  5. # Original (0.1.x) by Fredrik Vold, no copyrights, no rights reserved.
  6. # This is absolutely free software, and you can do with it as you please.
  7. # If you do derive your own work from it, however, it'd be nice with some
  8. # credits to me somewhere in the comments of that work.
  9. #
  10. # Based on http:://wiki.vg/RCON documentation
  11. package Minecraft::RCON;
  12. our $VERSION = '1.03';
  13. use 5.010;
  14. use strict;
  15. use warnings;
  16. no warnings 'uninitialized';
  17. use Term::ANSIColor 3.01;
  18. use IO::Socket 1.18; # autoflush
  19. use Carp;
  20. use constant {
  21. # Packet types
  22. AUTH => 3, # Minecraft RCON login packet type
  23. AUTH_RESPONSE => 2, # Server auth response
  24. AUTH_FAIL => -1, # Auth failure (password invalid)
  25. COMMAND => 2, # Command packet type
  26. RESPONSE_VALUE => 0, # Server response
  27. };
  28. # Minecraft -> ANSI color map
  29. my %COLOR = map { $_->[1] => color($_->[0]) } (
  30. [black => '0'], [blue => '1'], [green => '2'],
  31. [cyan => '3'], [red => '4'], [magenta => '5'],
  32. [yellow => '6'], [white => '7'], [bright_black => '8'],
  33. [bright_blue => '9'], [bright_green => 'a'], [bright_cyan => 'b'],
  34. [bright_red => 'c'], [bright_magenta => 'd'], [yellow => 'e'],
  35. [bright_white => 'f'],
  36. [bold => 'l'], [concealed => 'm'], [underline => 'n'],
  37. [reverse => 'o'], [reset => 'r'],
  38. );
  39. # Defaults for new objects. Override in constructor or with accessors.
  40. sub _DEFAULTS(%) {
  41. (
  42. address => '127.0.0.1',
  43. port => 25575,
  44. password => '',
  45. color_mode => 'strip',
  46. request_id => 0,
  47. # DEPRECATED options
  48. strip_color => undef,
  49. convert_color => undef,
  50. @_, # Subclasses may override
  51. );
  52. }
  53. # DEPRECATED warning text for convenience/consistency
  54. my $DEP = 'deprecated and will be removed in a future release.';
  55. sub new {
  56. my $class = shift;
  57. my %opts = 'HASH' eq ref $_[0] ? %{$_[0]} : @_;
  58. my %DEFAULTS = _DEFAULTS();
  59. # DEPRECATED -- Warn and transition to new option
  60. if ($opts{convert_color}) {
  61. carp "convert_color $DEP\nConverted to color_mode => 'convert'.";
  62. $opts{color_mode} = 'convert';
  63. }
  64. if ($opts{strip_color}) {
  65. carp "strip_color $DEP\nConverted to color_mode => 'strip'.";
  66. $opts{color_mode} = 'strip';
  67. }
  68. my @unknowns = grep { not exists $DEFAULTS{$_} } sort keys %opts;
  69. carp "Ignoring unknown option(s): " . join(', ', @unknowns) if @unknowns;
  70. bless { %DEFAULTS, %opts }, $class;
  71. }
  72. sub connect {
  73. my ($s) = @_;
  74. return 1 if $s->connected;
  75. croak 'Password required' unless length $s->{password};
  76. $s->{socket} = IO::Socket::INET->new(
  77. PeerAddr => $s->{address},
  78. PeerPort => $s->{port},
  79. Proto => 'tcp',
  80. ) or croak "Connection to $s->{address}:$s->{port} failed: .$!";
  81. my $id = $s->_next_id;
  82. $s->_send_encode(AUTH, $id, $s->{password});
  83. my ($size,$res_id,$type,$payload) = $s->_recv_decode;
  84. # Force a reconnect if we're about to error out
  85. $s->disconnect unless $type == AUTH_RESPONSE and $id == $res_id;
  86. croak 'RCON authentication failed' if $res_id == AUTH_FAIL;
  87. croak "Expected AUTH_RESPONSE(2), got $type" if $type != AUTH_RESPONSE;
  88. croak "Expected ID $id, got $res_id" if $id != $res_id;
  89. croak "Non-blank payload <$payload>" if length $payload;
  90. return 1;
  91. }
  92. sub connected { $_[0]->{socket} and $_[0]->{socket}->connected }
  93. sub disconnect {
  94. $_[0]->{socket}->shutdown(2) if $_[0]->connected;
  95. delete $_[0]->{socket} if exists $_[0]->{socket};
  96. 1;
  97. }
  98. sub command {
  99. my ($s, $command, $mode) = @_;
  100. croak 'Command required' unless length $command;
  101. croak 'Not connected' unless $s->connected;
  102. my $id = $s->_next_id;
  103. my $nonce = 16 + int rand(2 ** 15 - 16); # Avoid 0..15
  104. $s->_send_encode(COMMAND, $id, $command);
  105. $s->_send_encode($nonce, $id, 'nonce');
  106. my $res = '';
  107. while (1) {
  108. my ($size,$res_id,$type,$payload) = $s->_recv_decode;
  109. if ($id != $res_id) {
  110. $s->disconnect;
  111. croak sprintf(
  112. "Desync. Expected %d (0x%4x), got %d (0x%4x). Disconnected.",
  113. $id, $id, $res_id, $res_id
  114. );
  115. }
  116. croak "size:$size id:$id got type $type, not RESPONSE_VALUE(0)"
  117. if $type != RESPONSE_VALUE;
  118. last if $payload eq sprintf 'Unknown request %x', $nonce;
  119. $res .= $payload;
  120. }
  121. $s->color_convert($res, defined $mode ? $mode : $s->{color_mode});
  122. }
  123. sub color_mode {
  124. my ($s, $mode, $code) = @_;
  125. return $s->{color_mode} if not defined $mode;
  126. croak 'Invalid color mode.'
  127. unless $mode =~ /^(strip|convert|ignore)$/;
  128. if ($code) {
  129. my $was = $s->{color_mode};
  130. $s->{color_mode} = $mode;
  131. $code->();
  132. $s->{color_mode} = $was;
  133. } else {
  134. $s->{color_mode} = $mode;
  135. }
  136. }
  137. sub color_convert {
  138. my ($s, $text, $mode) = @_;
  139. $mode = $s->{color_mode} if not defined $mode;
  140. my $re = qr/\x{00A7}(.)/o;
  141. $text =~ s/$re//g if $mode eq 'strip';
  142. $text =~ s/$re/$COLOR{$1}/g if $mode eq 'convert';
  143. $text .= $COLOR{r} if $mode eq 'convert' and $text =~ /\e\[/;
  144. $text;
  145. }
  146. sub DESTROY { $_[0]->disconnect }
  147. #
  148. # DEPRECATED methods
  149. #
  150. sub convert_color {
  151. my ($s, $val) = @_;
  152. carp "convert_color() is $DEP\nUse color_mode('convert') instead";
  153. $s->color_mode('convert') if $val;
  154. $s->color_mode eq 'convert';
  155. }
  156. sub strip_color {
  157. my ($s, $val) = @_;
  158. carp "strip_color() is $DEP\nUse color_mode('strip') instead";
  159. $s->color_mode('strip') if $val;
  160. $s->color_mode eq 'strip';
  161. }
  162. sub address {
  163. carp "address() is $DEP";
  164. $_[0]->{address} = $_[1] if defined $_[1];
  165. $_[0]->{address};
  166. }
  167. sub port {
  168. carp "port() is $DEP";
  169. $_[0]->{port} = $_[1] if defined $_[1];
  170. $_[0]->{port};
  171. }
  172. sub password {
  173. carp "password() is $DEP";
  174. $_[0]->{password} = $_[1] if defined $_[1];
  175. $_[0]->{password};
  176. }
  177. #
  178. # Private helpers
  179. #
  180. # Increment and return the next request ID, wrapping at 2**31-1
  181. sub _next_id { $_[0]->{request_id} = ($_[0]->{request_id} + 1) % 2**31 }
  182. # Form and send a packet of the specified type, request_id and payload
  183. sub _send_encode {
  184. my ($s, $type, $id, $payload) = @_;
  185. confess "Request ID `$id' is not an integer" unless $id =~ /^\d+$/;
  186. $payload = "" unless defined $payload;
  187. my $data = pack('V!V' => $id, $type) . $payload . "\0\0";
  188. $s->{socket}->send(pack(V => length $data) . $data);
  189. }
  190. # Grab a single packet.
  191. sub _recv_decode {
  192. my ($s) = @_;
  193. confess "_recv_decode when not connected" unless $s->connected;
  194. local $_; $s->{socket}->recv($_, 4);
  195. my $size = unpack 'V';
  196. $_ = '';
  197. my $frags = 0;
  198. croak "Zero length packet" unless $size;
  199. while ($size > length) {
  200. my $buf;
  201. $s->{socket}->recv($buf, $size);
  202. $_ .= $buf;
  203. $frags++;
  204. }
  205. croak 'Packet too short. ' . length($_) . ' < 10' if 10 > length($_);
  206. croak "Received packet missing terminator" unless s/\0\0$//;
  207. $size, unpack 'V!V(A*)';
  208. }
  209. 1;
  210. __END__
  211. =head1 NAME
  212. Minecraft::RCON - RCON remote console communication with Minecraft servers
  213. =head1 VERSION
  214. Version 1.03
  215. =head1 SYNOPSIS
  216. use Minecraft::RCON;
  217. my $rcon = Minecraft::RCON->new( { password => 'secret' } );
  218. eval { $rcon->connect };
  219. die "Connection failed: $@" if $@;
  220. my $response;
  221. eval { $response = $rcon->command('help') };
  222. say $@ ? "Error: $@" : "Response: $response";
  223. $rcon->disconnect;
  224. =head1 DESCRIPTION
  225. C<Minecraft::RCON> provides a nice object interface for talking to Mojang AB's
  226. game Minecraft. Intended for use with their multiplayer servers, specifically
  227. I<your> multiplayer server, as you will need the correct RCON password, and
  228. RCON must be enabled on said server.
  229. =head1 CONSTRUCTOR
  230. =head2 new( %options )
  231. Create a new RCON object. Note we do not connect automatically; see
  232. C<connect()> for that. The properties and their defaults are shown below:
  233. my $rcon = Minecraft::RCON->new({
  234. address => '127.0.0.1',
  235. port => 25575,
  236. password => '',
  237. color_mode => 'strip',
  238. error_mode => 'error',
  239. });
  240. We will C<carp()> but not die in the event that any unknown options are
  241. provided.
  242. =over 4
  243. =item address
  244. The hostname or IP address to connect to.
  245. =item port
  246. The TCP port number to connect to.
  247. =item password
  248. The plaintext password used to authenticate. This password must match the
  249. C<rcon.password=> line in the F<server.properties> file for your server.
  250. =item color_mode
  251. The color mode controls how C<Minecraft::RCON> handles color codes sent back
  252. by the Minecraft server. It must be one of C<strip>, C<convert>, or C<ignore>.
  253. constants. See C<color_mode()> for more information.
  254. =back
  255. =head1 METHODS
  256. =head2 connect
  257. eval { $rcon->connect }; # $@ will be set on error
  258. Attempt to connect to the configured address and port, and issue the
  259. configured password for authentication.
  260. If already connected, returns C<undef> (nothing to be done).
  261. This method will C<croak> if the connection fails for any reason.
  262. Otherwise, returns a true value.
  263. =head2 connected
  264. say "We are connected!" if $rcon->connected;
  265. Returns true if we have a connected socket, false otherwise. Note that we have
  266. no way to tell if there is a misbehaving Minecraft server on the other
  267. side of that socket, so it is entirely possible for this command (or
  268. C<connect()>) to succeed, but C<command()> calls to fail.
  269. =head2 disconnect
  270. $rcon->disconnect;
  271. Disconnects from the server by closing the socket. Always succeeds.
  272. =head2 command( $command, [ $color_mode ] )
  273. my $response = $rcon->command("data get block $x $y $z");
  274. my $ansi = $rcon->command('list', 'convert');
  275. Sends the C<$command> to the Minecraft server, and synchronously waits for the
  276. response. This method is capable of handling fragmented responses (spread over
  277. several response packets), and will concatenate them all before returning the
  278. result.
  279. The resulting server response will have its color codes stripped, converted,
  280. or ignored, according to the current C<color_mode()> setting, unless a
  281. C<$color_mode> is given, which will override the current setting for this
  282. command only.
  283. =head2 color_mode( $color_mode, [ $code ] )
  284. $rcon->color_mode('strip');
  285. When a command response is received, the color codes it contains can be
  286. stripped, converted to ANSI, or left alone, depending on this setting.
  287. C<$color_mode> is optional, unless C<$code> is also specified.
  288. The valid modes are as follows:
  289. =over 10
  290. =item strip
  291. Strip any color codes, returning the plaintext.
  292. =item convert
  293. Convert any color codes to the equivalent ANSI escape sequences, suitable for
  294. display in a terminal.
  295. =item ignore
  296. Ignore color codes, returning the full command response verbatim.
  297. =back
  298. The current mode will be returned.
  299. If C<$code> is specified and is a C<CODE> ref, C<color_mode()> will apply the
  300. new color mode, run C<$code-E<gt>()>, and then restore the original color
  301. mode. This is useful when you use one color mode most of the time, but have
  302. sections of code requiring a different mode:
  303. Example usage:
  304. # Color mode is 'convert'
  305. $rcon->color_mode(strip => sub {
  306. my $plaintext = $rcon->command('...');
  307. });
  308. But see also C<command($cmd, $mode)> for running single commands with
  309. another color mode.
  310. =head2 color_convert( $string, [ $color_mode ] )
  311. my $response = $rcon->command('list');
  312. my ($strip, $ansi) = map { $rcon->color_convert($response, $_) }
  313. qw<strip convert>;
  314. This method is used internally by C<command()> to convert command responses as
  315. configured in the object. However, C<color_convert()> itself may be useful in
  316. some applications where a stripped version of the response may be needed for
  317. parsing, while an ANSI version may be desired for display to a terminal, for
  318. example, without having to run the command itself (with possible side-effects)
  319. a second time. For C<color_convert()> to do anything meaningful, your object's
  320. C<color_mode> should be set to C<ignore>.
  321. =head1 ERROR HANDLING
  322. This module C<croak>s (see L<Carp>) for almost all errors.
  323. When an error does not affect control flow, we will C<carp> instead.
  324. Thus, C<command()> and C<connect()>, at minimum, should be wrapped in block
  325. C<eval>:
  326. eval { $result = $rcon->command('list'); };
  327. warn "I don't know who is online because: $@" if $@;
  328. If a little extra syntactic sugar is desired, you can use an exception handler
  329. like L<Try::Tiny> instead:
  330. use Try::Tiny;
  331. try {
  332. $result = $rcon->command('list');
  333. } catch {
  334. warn "I don't know who is online because: $_";
  335. }
  336. =head1 DEPRECATED METHODS
  337. The following methods have been deprecated. They will issue a warning to
  338. STDOUT when called, and will be removed in a future release.
  339. =head2 convert_color ( $enable )
  340. If C<$enable> is a true value, change the color mode to C<convert>.
  341. Returns 1 if the current color mode is C<convert>, undef otherwise.
  342. B<Deprecated.> Use C<color_mode('convert')> instead.
  343. =head2 strip_color
  344. If C<$enable> is a true value, change the color mode to C<strip>.
  345. Returns 1 if the current color mode is C<strip>, undef otherwise.
  346. B<Deprecated.> Use C<color_mode('strip')> instead.
  347. =head1 SUPPORT
  348. =over 4
  349. =item L<https://github.com/rjt-pl/Minecraft-RCON.git>: Source code repository
  350. =item L<https://github.com/rjt-pl/Minecraft-RCON/issues>: Bug reports and feature requests
  351. =back
  352. =head1 SEE ALSO
  353. =over 4
  354. =item L<Net::RCON::Minecraft> for an alternative API
  355. =item L<Terminal::ANSIColor>, L<IO::Socket::INET>
  356. =item L<https://developer.valvesoftware.com/wiki/Source_RCON_Protocol>
  357. =item L<https://wiki.vg/RCON>
  358. =back
  359. =head1 AFFILIATION WITH MOJANG
  360. I<Note from original author, Fredrik Vold:>
  361. I am in no way affiliated with Mojang or the development of Minecraft.
  362. I'm simply a fan of their work, and a server admin myself. I needed
  363. some RCON magic for my servers website, and there was no perl module.
  364. It is important that everyone using this module understands that if
  365. Mojang changes the way RCON works, I won't be notified any sooner than
  366. anyone else, and I have no special avenue of connection with them.
  367. =head1 AUTHORS
  368. =over 4
  369. =item B<Ryan Thompson> C<E<lt>[email protected]<gt>>
  370. Addition of unit test suite, fragmentation support, and other improvements.
  371. This program is free software; you can redistribute it
  372. and/or modify it under the same terms as Perl itself.
  373. L<http://dev.perl.org/licenses/artistic.html>
  374. =item B<Fredrik Vold> C<E<lt>[email protected]<gt>>
  375. Original (0.1.x) author.
  376. No copyright claimed, no rights reserved.
  377. You are absolutely free to do as you wish with this code, but mentioning me in
  378. your comments or whatever would be nice.
  379. Minecraft is a trademark of Mojang AB. Name used in accordance with my
  380. interpretation of L<http://www.minecraft.net/terms>, to the best of my
  381. knowledge.
  382. =back