rconrb 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +25 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/Gemfile +8 -0
- data/README.md +104 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/rcon.rb +12 -0
- data/lib/rcon/client.rb +130 -0
- data/lib/rcon/error/error.rb +87 -0
- data/lib/rcon/packet.rb +119 -0
- data/lib/rcon/response.rb +69 -0
- data/lib/rcon/socket_wrapper.rb +40 -0
- data/lib/rcon/version.rb +4 -0
- data/rconrb.gemspec +30 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b2d90b6a2bacdf1d213da0356764b3e911cd0c5caf8a6516fcc8562cc88aec1a
|
4
|
+
data.tar.gz: 77f7e0038e8a2aa2eb05265c07adae7b4b05ced177fa86fc226f7aa6b6ef4933
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2e54c3a135b6ab6df953bc05e893a30e495f4a9f20415edd3c6a4eb65c1c32887f2a8178bef0c259578584f708e9955a9445ec9376d5364ab1cb92c8c0fbf518
|
7
|
+
data.tar.gz: f13555248995c93017d61eba73685ed2999c37c5833c68ec59bcc51e30601e5f78f0b811d696f419076b1cd2c77ea6c17e9ef8cc69de73553bb3a323327d4ccc
|
@@ -0,0 +1,25 @@
|
|
1
|
+
version: 2
|
2
|
+
jobs:
|
3
|
+
build:
|
4
|
+
parallelism: 1
|
5
|
+
docker:
|
6
|
+
- image: circleci/ruby:2.6.0-stretch
|
7
|
+
environment:
|
8
|
+
BUNDLE_JOBS: 1
|
9
|
+
BUNDLE_RETRY: 3
|
10
|
+
BUNDLE_PATH: vendor/bundle
|
11
|
+
steps:
|
12
|
+
- checkout
|
13
|
+
|
14
|
+
- run:
|
15
|
+
name: run setup
|
16
|
+
command: bin/setup
|
17
|
+
|
18
|
+
- run:
|
19
|
+
name: Run rspec in parallel
|
20
|
+
command: |
|
21
|
+
bundle exec rspec --profile 10 \
|
22
|
+
--format RspecJunitFormatter \
|
23
|
+
--out test_results/rspec.xml \
|
24
|
+
--format progress \
|
25
|
+
$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# Rcon
|
2
|
+
|
3
|
+
[![hernanat](https://circleci.com/gh/hernanat/rconrb/tree/master.svg?style=svg)](https://circleci.com/gh/hernanat/rconrb/tree/master)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/rconrb.svg)](https://badge.fury.io/rb/rconrb)
|
5
|
+
|
6
|
+
[The Source RCON Protocol](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol) is a protocol
|
7
|
+
designed to allow for the remote execution of commands on a server that supports it.
|
8
|
+
|
9
|
+
It is used for many different game servers running on the [Source Dedicated Server](https://developer.valvesoftware.com/wiki/Source_Dedicated_Server), but other
|
10
|
+
types of game servers (Minecraft) support it (or flavors of it) as well.
|
11
|
+
|
12
|
+
This gem intends to provide a means of executing remote commands via the "vanilla" RCON protocol by default,
|
13
|
+
but also offers some configuration options to allow you to work with the more problematic implementations
|
14
|
+
of the protocol (i.e. Minecraft).
|
15
|
+
|
16
|
+
See the [docs](https://rubydoc.info/github/hernanat/rconrb) for more information.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'rconrb'
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle install
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
$ gem install rconrb
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
### Basic Usage
|
37
|
+
|
38
|
+
#### Vanilla
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
client = Rcon::Client.new(host: "1.2.3.4", port: 25575, password: "foreveryepsilonbiggerthanzero")
|
42
|
+
client.authenticate!
|
43
|
+
client.execute("list")
|
44
|
+
```
|
45
|
+
|
46
|
+
#### Minecraft
|
47
|
+
|
48
|
+
Minecraft implements the protocol in such a way that makes me want to tear my hair out. Anyways:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
client = Rcon::Client.new(host: "1.2.3.4", port: 25575, password: "foreveryepsilonbiggerthanzero")
|
52
|
+
client.authenticate!(ignore_first_packet: false) # Minecraft RCON does not send a preliminary auth packet
|
53
|
+
client.execute("list")
|
54
|
+
```
|
55
|
+
|
56
|
+
### Segmented Responses
|
57
|
+
|
58
|
+
Some responses are too large to send back in one packet, and so they are broken up across several.
|
59
|
+
We handle this by sending a "trash" packet along immediately following our initial packet. Since
|
60
|
+
SRCDS guarantees that packets will be processed in order, and responded to in order, so we basically
|
61
|
+
we build the response body across several packets until we encounter the trash packet id, in which
|
62
|
+
case we know that we are finished. It's worth noting that I'm not positive that Minecraft follows
|
63
|
+
this behavioral guarantee, but throughout the testing that I've done it has seemed to.
|
64
|
+
|
65
|
+
Note that the segmented response workflow is disabled by default since most commands won't result
|
66
|
+
in a segmented response.
|
67
|
+
|
68
|
+
#### Vanilla
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
client = Rcon::Client.new(host: "1.2.3.4", port: 25575, password: "foreveryepsilonbiggerthanzero")
|
72
|
+
client.authenticate!
|
73
|
+
client.execute("cvarlist", expect_segmented_response: true)
|
74
|
+
```
|
75
|
+
|
76
|
+
#### Minecraft
|
77
|
+
|
78
|
+
Minecraft RCON doesn't handle receiving multiple packets in quick succession very well, and seems
|
79
|
+
to get confused and just close the TCP connection. This has been a long standing issue. The solution
|
80
|
+
is basically to wait a brief period between the initial packet and the trash packet to give the
|
81
|
+
server some time to process. This isn't an exact science unfortunately.
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
client = Rcon::Client.new(host: "1.2.3.4", port: 25575, password: "foreveryepsilonbiggerthanzero")
|
85
|
+
client.authenticate!(ignore_first_packet: false) # Minecraft RCON does not send a preliminary auth packet
|
86
|
+
client.execute("banlist", expect_segmented_response: true, wait: 0.25)
|
87
|
+
```
|
88
|
+
|
89
|
+
## Development
|
90
|
+
|
91
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
92
|
+
|
93
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
94
|
+
|
95
|
+
## Documentation
|
96
|
+
|
97
|
+
Documentation can be viewed at https://rubydoc.info/github/hernanat/rconrb
|
98
|
+
|
99
|
+
## Contributing
|
100
|
+
|
101
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/hernanat/rconrb
|
102
|
+
|
103
|
+
TODO: contribution guidelines
|
104
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rcon"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/rcon.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "rcon/version"
|
2
|
+
require "rcon/error/error"
|
3
|
+
require "rcon/client"
|
4
|
+
|
5
|
+
# This module is based on the {https://developer.valvesoftware.com/wiki/Source_RCON_Protocol Source RCON Protocol}.
|
6
|
+
# It is to be used for executing remote commands on servers that implement this protocol, or various flavors of it.
|
7
|
+
#
|
8
|
+
# The goal was to design something that could be used to work with the default protocol implementation, but also offer
|
9
|
+
# the flexibility to work with problem-children implementations such as the one used by Minecraft.
|
10
|
+
#
|
11
|
+
module Rcon
|
12
|
+
end
|
data/lib/rcon/client.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require "rcon/packet"
|
2
|
+
require "rcon/response"
|
3
|
+
require "rcon/socket_wrapper"
|
4
|
+
require "socket"
|
5
|
+
require "securerandom"
|
6
|
+
|
7
|
+
module Rcon
|
8
|
+
# Basic client for executing commands on your server remotely using the Source RCON protocol.
|
9
|
+
# See {https://developer.valvesoftware.com/wiki/Source_RCON_Protocol here} for more details.
|
10
|
+
#
|
11
|
+
# This is intended to be flexible enough to suit the needs of various flavors of RCON (for
|
12
|
+
# example, Minecraft).
|
13
|
+
#
|
14
|
+
# See individual method summaries for more information.
|
15
|
+
class Client
|
16
|
+
# Instantiates an {Client}.
|
17
|
+
#
|
18
|
+
# @param host [String] IP address of the server running RCON
|
19
|
+
# @param port [Integer] RCON port
|
20
|
+
# @param password [String] RCON password
|
21
|
+
# @return [Client]
|
22
|
+
def initialize(host:, port:, password:)
|
23
|
+
@host = host
|
24
|
+
@port = port
|
25
|
+
@password = password
|
26
|
+
@socket = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# Opens a TCP socket and authenticates with RCON.
|
30
|
+
#
|
31
|
+
# According to the RCON spec, the server will respond to an authentication request with a
|
32
|
+
# SERVERDATA_RESPONSE_VALUE packet, followed by a SERVERDATA_AUTH_RESPONSE packet by
|
33
|
+
# default.
|
34
|
+
#
|
35
|
+
# However, this is not the case in every implementation (looking at you Minecraft). For the
|
36
|
+
# sake of being flexible, we include a param which allows us to enable / disable this default behavior (see below).
|
37
|
+
#
|
38
|
+
# It is not recommended to call this method more than once before ending the session.
|
39
|
+
#
|
40
|
+
# @param ignore_first_packet [Boolean]
|
41
|
+
# @return [AuthResponse]
|
42
|
+
# @raise [Error::AuthError] if authentication fails
|
43
|
+
def authenticate!(ignore_first_packet: true)
|
44
|
+
packet_id = SecureRandom.rand(1000)
|
45
|
+
auth_packet = Packet.new(packet_id, :SERVERDATA_AUTH, password)
|
46
|
+
@socket = SocketWrapper.new(TCPSocket.open(host, port))
|
47
|
+
socket.deliver_packet(auth_packet)
|
48
|
+
read_packet_from_socket if ignore_first_packet
|
49
|
+
|
50
|
+
read_packet_from_socket.
|
51
|
+
then { |packet| Response.from_packet(packet) }.
|
52
|
+
tap { |response| raise Error::AuthError unless response.success? }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Execute the given command.
|
56
|
+
#
|
57
|
+
# Some commands require their responses to be sent across several packets because
|
58
|
+
# they are larger than the maximum (default) RCON packet size of 4096 bytes.
|
59
|
+
#
|
60
|
+
# In order to deal with this, we send an additional "trash" packet immediately
|
61
|
+
# following the initial command packet. SRCDS guarantees that requests are processed
|
62
|
+
# in order, and the subsequent responses are also in order, so we use this fact to
|
63
|
+
# append the packet bodies to the result on the client side until we see the trash
|
64
|
+
# packet id.
|
65
|
+
#
|
66
|
+
# Many commands won't require a segmented response, so we disable this behavior by
|
67
|
+
# default. You can enable it if you'd like using the option describe below.
|
68
|
+
#
|
69
|
+
# Additionally, some implementations of RCON servers (MINECRAFT) cannot handle two
|
70
|
+
# packets in quick succession, so you may want to wait a short duration (i.e. <= 1 second)
|
71
|
+
# before sending the trash packet. We give the ability to do this using the
|
72
|
+
# wait option described below.
|
73
|
+
#
|
74
|
+
# @param [Hash] opts options for executing the command
|
75
|
+
# @option opts [Boolean] :expect_segmented_response follow segmented response logic described above if true
|
76
|
+
# @option opts [Integer] :wait seconds to wait before sending trash packet (i.e. Minecraft 😡)
|
77
|
+
# @return [CommandResponse]
|
78
|
+
def execute(command, opts = {})
|
79
|
+
packet_id = SecureRandom.rand(1000)
|
80
|
+
socket.deliver_packet(Packet.new(packet_id, :SERVERDATA_EXECCOMMAND, command))
|
81
|
+
trash_packet_id = build_and_send_trash_packet(opts) if opts[:expect_segmented_response]
|
82
|
+
build_response(trash_packet_id)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Close the TCP socket and end the RCON session.
|
86
|
+
# @return [nil]
|
87
|
+
def end_session!
|
88
|
+
@socket = socket.close
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
attr_reader :host, :port, :password, :socket
|
94
|
+
|
95
|
+
def build_response(trash_packet_id)
|
96
|
+
if trash_packet_id.nil?
|
97
|
+
read_packet_from_socket.then { |p| Response.from_packet(p) }
|
98
|
+
else
|
99
|
+
build_segmented_response(trash_packet_id, read_packet_from_socket)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def build_segmented_response(trash_packet_id, first_segment)
|
104
|
+
next_segment = read_packet_from_socket
|
105
|
+
response = Response.from_packet(first_segment)
|
106
|
+
loop do
|
107
|
+
break if next_segment.id == trash_packet_id
|
108
|
+
response.body = "#{response.body}#{next_segment.body}"
|
109
|
+
next_segment = read_packet_from_socket
|
110
|
+
end
|
111
|
+
response
|
112
|
+
end
|
113
|
+
|
114
|
+
def build_and_send_trash_packet(opts = {})
|
115
|
+
# some RCON implementations (I'm looking at you Minecraft)
|
116
|
+
# blow up if you send successive packets too quickly
|
117
|
+
# the work around (currently) is to allow the server some
|
118
|
+
# time to catch up. Note that this isn't an exact science.
|
119
|
+
wait = opts[:wait]
|
120
|
+
SecureRandom.rand(1000).tap do |packet_id|
|
121
|
+
sleep(wait) if wait
|
122
|
+
socket.deliver_packet(Packet.new(packet_id, :SERVERDATA_RESPONSE_VALUE, ""))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def read_packet_from_socket
|
127
|
+
Packet.read_from_socket_wrapper(socket)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rcon
|
4
|
+
# module for Rcon related errors and their messages.
|
5
|
+
module Error
|
6
|
+
# used for communicating that there was an issue authenticating
|
7
|
+
# with the RCON server.
|
8
|
+
class AuthError < StandardError
|
9
|
+
# @return [String]
|
10
|
+
def message
|
11
|
+
"error authenticating with server. is your password correct?"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# used for communicating that the packet type is not supported
|
16
|
+
#
|
17
|
+
# @attr_reader packet_type [Integer]
|
18
|
+
class InvalidPacketTypeError < StandardError
|
19
|
+
# @param packet_type [Symbol] the packet type
|
20
|
+
# @return [InvalidPacketTypeError]
|
21
|
+
def initialize(packet_type)
|
22
|
+
@packet_type = packet_type
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :packet_type
|
27
|
+
|
28
|
+
# @return [String]
|
29
|
+
def message
|
30
|
+
"invalid packet_type: #{packet_type}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# used for communicating that the integer packet type code is not supported
|
35
|
+
#
|
36
|
+
# @attr_reader type_code [Integer]
|
37
|
+
class InvalidResponsePacketTypeCodeError < StandardError
|
38
|
+
# @param type_code [Integer] packet type code
|
39
|
+
# @return [InvalidResponsePacketTypeCodeError]
|
40
|
+
def initialize(type_code)
|
41
|
+
@type_code = type_code
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :type_code
|
46
|
+
|
47
|
+
# @return [String]
|
48
|
+
def message
|
49
|
+
"invalid response packet type code: #{type_code}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# used for communicating that we timed out trying to read from the socket
|
54
|
+
class SocketReadTimeoutError < StandardError
|
55
|
+
# @return [String]
|
56
|
+
def message
|
57
|
+
"timed out waiting for socket to be read-ready"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# used for communicating that we timed out trying to write to the socket
|
62
|
+
class SocketWriteTimeoutError < StandardError
|
63
|
+
# @return [String]
|
64
|
+
def message
|
65
|
+
"timed out waiting for socket to be write-ready"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# used for communicating that the response type of the packet is unsupported
|
70
|
+
#
|
71
|
+
# @attr_reader response_type [Symbol]
|
72
|
+
class UnsupportedResponseTypeError < StandardError
|
73
|
+
# @param response_type [Symbol] the response type
|
74
|
+
# @return [UnsupportedResponseTypeError]
|
75
|
+
def initialize(response_type)
|
76
|
+
@response_type = response_type
|
77
|
+
end
|
78
|
+
|
79
|
+
attr_reader :response_type
|
80
|
+
|
81
|
+
# @return [String]
|
82
|
+
def message
|
83
|
+
"unsupported response type: #{response_type}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
data/lib/rcon/packet.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
module Rcon
|
2
|
+
# Data structure representing packets sent to / received from RCON server.
|
3
|
+
class Packet
|
4
|
+
INTEGER_PACK_DIRECTIVE = "l<".freeze
|
5
|
+
STR_PACK_DIRECTIVE = "a".freeze
|
6
|
+
PACKET_PACK_DIRECTIVE = "#{INTEGER_PACK_DIRECTIVE}2#{STR_PACK_DIRECTIVE}*#{STR_PACK_DIRECTIVE}".freeze
|
7
|
+
ENCODING = Encoding::ASCII
|
8
|
+
TRAILER = "\x00".freeze
|
9
|
+
INT_BYTE_SIZE = 4
|
10
|
+
TRAILER_BYTE_SIZE = 1
|
11
|
+
|
12
|
+
# Types of packets that the server expects to receive.
|
13
|
+
#
|
14
|
+
# The keys correspond with the Source RCON spec names, the values correspond with
|
15
|
+
# what the server expects to see in the type segment of a packet.
|
16
|
+
REQUEST_PACKET_TYPE = {
|
17
|
+
SERVERDATA_AUTH: 3,
|
18
|
+
SERVERDATA_EXECCOMMAND: 2
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# Types of packets that the client can expect to receive
|
22
|
+
# back from the server.
|
23
|
+
#
|
24
|
+
# The keys correspond with the Source RCON spec names, the values correspond with
|
25
|
+
# what the client expects to see in the type segment of a packet.
|
26
|
+
RESPONSE_PACKET_TYPE = {
|
27
|
+
SERVERDATA_AUTH_RESPONSE: 2,
|
28
|
+
SERVERDATA_RESPONSE_VALUE: 0
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
private_constant(
|
32
|
+
:INTEGER_PACK_DIRECTIVE, :STR_PACK_DIRECTIVE, :PACKET_PACK_DIRECTIVE,
|
33
|
+
:ENCODING, :TRAILER, :INT_BYTE_SIZE, :TRAILER_BYTE_SIZE
|
34
|
+
)
|
35
|
+
|
36
|
+
# Read a packet from the given {SocketWrapper}.
|
37
|
+
#
|
38
|
+
# @param socket_wrapper [SocketWrapper]
|
39
|
+
# @return [Packet]
|
40
|
+
# @raise [Error::SocketReadTimeoutError] if timeout occurs while waiting to read from socket
|
41
|
+
def self.read_from_socket_wrapper(socket_wrapper)
|
42
|
+
if socket_wrapper.ready_to_read?
|
43
|
+
size = socket_wrapper.recv(INT_BYTE_SIZE).unpack(INTEGER_PACK_DIRECTIVE).first
|
44
|
+
id_and_type_length = 2 * INT_BYTE_SIZE
|
45
|
+
body_length = size - id_and_type_length - (2 * TRAILER_BYTE_SIZE) # ignore trailing nulls
|
46
|
+
|
47
|
+
payload = socket_wrapper.recv(size)
|
48
|
+
id, type_int = payload[0...id_and_type_length].unpack("#{INTEGER_PACK_DIRECTIVE}*")
|
49
|
+
body = payload[id_and_type_length..].unpack("#{STR_PACK_DIRECTIVE}#{body_length}").first
|
50
|
+
type = RESPONSE_PACKET_TYPE.key(type_int) || raise(Error::InvalidResponsePacketTypeCodeError.new(type_int))
|
51
|
+
|
52
|
+
new(id, RESPONSE_PACKET_TYPE.key(type_int), body)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Instantiates a {Packet}
|
57
|
+
#
|
58
|
+
# @param id [Integer] the packet id
|
59
|
+
# @param type [Symbol] see {REQUEST_PACKET_TYPE} and {RESPONSE_PACKET_TYPE} keys
|
60
|
+
# @param body [String] the packet body
|
61
|
+
# @return [Packet]
|
62
|
+
def initialize(id, type, body)
|
63
|
+
@id = id
|
64
|
+
@type = type
|
65
|
+
@body = body
|
66
|
+
end
|
67
|
+
|
68
|
+
# Compares two objects to see if they are equal
|
69
|
+
#
|
70
|
+
# Returns true if other is a Packet and attributes match self, false otherwise.
|
71
|
+
# @param other [Packet, Object]
|
72
|
+
# @return [Boolean]
|
73
|
+
def ==(other)
|
74
|
+
eql?(other)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Compares two objects to see if they are equal
|
78
|
+
# Returns true if other is a Packet and attributes match self, false otherwise.
|
79
|
+
#
|
80
|
+
# @param other [Packet, Object]
|
81
|
+
# @return [Boolean]
|
82
|
+
def eql?(other)
|
83
|
+
if other.is_a?(Packet)
|
84
|
+
id == other.id && type == other.type && body == other.body
|
85
|
+
else
|
86
|
+
false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Converts the packet into an ASCII-encoded RCON Packet string for transmitting
|
91
|
+
# to the server.
|
92
|
+
#
|
93
|
+
# @return [String]
|
94
|
+
def to_s
|
95
|
+
[id, type_to_i, "#{body}#{TRAILER}", TRAILER].pack(PACKET_PACK_DIRECTIVE).then do |packet_str|
|
96
|
+
"#{[packet_str.length].pack(INTEGER_PACK_DIRECTIVE)}#{packet_str}".force_encoding(ENCODING)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get the integer representation of the packet's type, which is used in the string
|
101
|
+
# representation of the packet.
|
102
|
+
#
|
103
|
+
# @return [Integer]
|
104
|
+
# @raise [Error::InvalidPacketTypeError] if the packet type is unknown / invalid.
|
105
|
+
def type_to_i
|
106
|
+
type_sym = type.to_sym
|
107
|
+
case type_sym
|
108
|
+
when ->(t) { REQUEST_PACKET_TYPE.keys.include?(t) }
|
109
|
+
REQUEST_PACKET_TYPE[type_sym]
|
110
|
+
when ->(t) { RESPONSE_PACKET_TYPE.keys.include?(t) }
|
111
|
+
RESPONSE_PACKET_TYPE[type_sym]
|
112
|
+
else
|
113
|
+
raise Error::InvalidPacketTypeError.new(type)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
attr_reader :id, :type, :body
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Rcon
|
2
|
+
# wraps the response we receive from the server. It might not be obvious at
|
3
|
+
# first why have this additional datastructure. There are two main motivations.
|
4
|
+
#
|
5
|
+
# First, to separate how we deal with an {AuthResponse} vs a {CommandResponse}.
|
6
|
+
#
|
7
|
+
# Secondly, when we are dealing with segmented responses, instead of modifying
|
8
|
+
# the first packet in place to add subsequent parts of the body, we modify the
|
9
|
+
# {Response} object corresponding with the total response. i.e. a {Response} is
|
10
|
+
# a sum of {Packet}s.
|
11
|
+
#
|
12
|
+
# @attr_reader id [Integer] the initial request packet id corresponding to the response
|
13
|
+
# (except maybe for {AuthResponse}, see {AuthResponse#success?}
|
14
|
+
# @attr_reader type [Symbol] the type of response; see {Packet::RESPONSE_PACKET_TYPE}
|
15
|
+
# @attr body [String] the response body, which may be the concatenation of
|
16
|
+
# the bodies of several packets.
|
17
|
+
class Response
|
18
|
+
# instantiate an instance of a {Response} subclass given a packet.
|
19
|
+
#
|
20
|
+
# @param packet [Packet] the packet
|
21
|
+
# @return [AuthResponse, CommandResponse]
|
22
|
+
def self.from_packet(packet)
|
23
|
+
params = { id: packet.id, type: packet.type, body: packet.body }
|
24
|
+
case packet.type
|
25
|
+
when :SERVERDATA_AUTH_RESPONSE
|
26
|
+
AuthResponse.new(**params)
|
27
|
+
when :SERVERDATA_RESPONSE_VALUE
|
28
|
+
CommandResponse.new(**params)
|
29
|
+
else
|
30
|
+
raise Error::UnsupportedResponseTypeError.new(packet.type)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# instantiate a new {Response}
|
35
|
+
#
|
36
|
+
# @param id [Integer] the id of the initial request packet that the response
|
37
|
+
# corresponds to
|
38
|
+
# @param type [Symbol] the response type; see {Packet::RESPONSE_PACKET_TYPE}
|
39
|
+
# @param body [String] the response body
|
40
|
+
def initialize(id:, type:, body:)
|
41
|
+
@id = id
|
42
|
+
@type = type
|
43
|
+
@body = body
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :id, :type
|
47
|
+
attr_accessor :body
|
48
|
+
end
|
49
|
+
|
50
|
+
# the {Response} subclass corresponding with authentication response packets
|
51
|
+
# from the server.
|
52
|
+
class AuthResponse < Response
|
53
|
+
# when authentication fails, the ID field of the auth respone packet will
|
54
|
+
# be set to -1
|
55
|
+
AUTH_FAILURE_RESPONSE = -1
|
56
|
+
|
57
|
+
# determines whether or not authentication has succeeded.
|
58
|
+
#
|
59
|
+
# according to the RCON spec, when authentication fails, -1 is returned in the id field of the packet.
|
60
|
+
# @return [Boolean]
|
61
|
+
def success?
|
62
|
+
id != AUTH_FAILURE_RESPONSE
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# the {Response} subclass corresponding with response packets from the server
|
67
|
+
# that result from executing a command
|
68
|
+
class CommandResponse < Response; end
|
69
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "delegate"
|
2
|
+
|
3
|
+
module Rcon
|
4
|
+
# Simple wrapper to give some convenience methods around sockets.
|
5
|
+
class SocketWrapper < SimpleDelegator
|
6
|
+
TIMEOUT = 5
|
7
|
+
|
8
|
+
private_constant :TIMEOUT
|
9
|
+
|
10
|
+
# deliver the packet to the server if the socket is ready to
|
11
|
+
# be written.
|
12
|
+
#
|
13
|
+
# @param packet [Packet] the packet to be delivered
|
14
|
+
# @return [Integer] the number of bytes sent
|
15
|
+
# @raise [Error::SocketWriteTimeoutError] if a timeout occurs while waiting to write to socket
|
16
|
+
def deliver_packet(packet)
|
17
|
+
write(packet.to_s) if ready_to_write?
|
18
|
+
end
|
19
|
+
|
20
|
+
# check if socket is ready to read
|
21
|
+
#
|
22
|
+
# @return [Array] containing socket in first subarray if socket is ready to read
|
23
|
+
# @raise [Error::SocketReadTimeoutError] if timeout occurs
|
24
|
+
def ready_to_read?
|
25
|
+
IO.select([__getobj__], nil, nil, TIMEOUT).tap do |io|
|
26
|
+
raise Error::SocketReadTimeoutError if io.nil?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# check if socket is ready to write
|
31
|
+
#
|
32
|
+
# @return [Array] containing socket in second subarray if socket is ready to write
|
33
|
+
# @raise [Error::SocketReadTimeoutError] if timeout occurs
|
34
|
+
def ready_to_write?
|
35
|
+
IO.select(nil, [__getobj__], nil, TIMEOUT).tap do |io|
|
36
|
+
raise Error::SocketWriteTimeoutError if io.nil?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/rcon/version.rb
ADDED
data/rconrb.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'lib/rcon/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "rconrb"
|
5
|
+
spec.version = Rcon::VERSION
|
6
|
+
spec.authors = ["Anthony Felix Hernandez"]
|
7
|
+
spec.email = ["ant@antfeedr.com"]
|
8
|
+
|
9
|
+
spec.summary = %q{An flexible RCON client written in Ruby, based on the Source RCON protocol.}
|
10
|
+
spec.homepage = "https://github.com/hernanat/rconrb"
|
11
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
|
12
|
+
|
13
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
14
|
+
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
17
|
+
spec.metadata["documentation_uri"] = "https://rubydoc.info/github/hernanat/rconrb"
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
22
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
end
|
24
|
+
spec.bindir = "exe"
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
|
28
|
+
spec.add_development_dependency "pry"
|
29
|
+
spec.add_development_dependency "yard", "~> 0.9.9"
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rconrb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anthony Felix Hernandez
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-04-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pry
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: yard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.9.9
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.9.9
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- ant@antfeedr.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".circleci/config.yml"
|
49
|
+
- ".gitignore"
|
50
|
+
- ".rspec"
|
51
|
+
- Gemfile
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- bin/console
|
55
|
+
- bin/setup
|
56
|
+
- lib/rcon.rb
|
57
|
+
- lib/rcon/client.rb
|
58
|
+
- lib/rcon/error/error.rb
|
59
|
+
- lib/rcon/packet.rb
|
60
|
+
- lib/rcon/response.rb
|
61
|
+
- lib/rcon/socket_wrapper.rb
|
62
|
+
- lib/rcon/version.rb
|
63
|
+
- rconrb.gemspec
|
64
|
+
homepage: https://github.com/hernanat/rconrb
|
65
|
+
licenses: []
|
66
|
+
metadata:
|
67
|
+
allowed_push_host: https://rubygems.org
|
68
|
+
homepage_uri: https://github.com/hernanat/rconrb
|
69
|
+
source_code_uri: https://github.com/hernanat/rconrb
|
70
|
+
documentation_uri: https://rubydoc.info/github/hernanat/rconrb
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 2.6.0
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubygems_version: 3.1.2
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: An flexible RCON client written in Ruby, based on the Source RCON protocol.
|
90
|
+
test_files: []
|