rconrb 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
[](https://circleci.com/gh/hernanat/rconrb/tree/master)
|
4
|
+
[](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: []
|