rconrb 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []