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.
@@ -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)
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ Gemfile.lock
14
+
15
+ vendor
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rcon.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
8
+ gem "rspec_junit_formatter"
@@ -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
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ module Rcon
2
+ # current version number
3
+ VERSION = "0.1.2"
4
+ end
@@ -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: []