rcon 0.1.0
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.
- data/lib/rcon.rb +484 -0
- metadata +40 -0
data/lib/rcon.rb
ADDED
@@ -0,0 +1,484 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
#
|
4
|
+
# RCon is a module to work with Quake 1/2/3, Half-Life, and Half-Life
|
5
|
+
# 2 (Source Engine) RCon (Remote Console) protocols.
|
6
|
+
#
|
7
|
+
# Version:: 0.1.0
|
8
|
+
#
|
9
|
+
# Author:: Erik Hollensbe <erik@hollensbe.org>
|
10
|
+
#
|
11
|
+
# License:: BSD
|
12
|
+
#
|
13
|
+
# The relevant modules to query RCon are in the RCon::Query namespace,
|
14
|
+
# under RCon::Query::Original (for Quake 1/2/3 and Half-Life), and
|
15
|
+
# RCon::Query::Source (for HL2 and CS: Source, and other Source Engine
|
16
|
+
# games). The RCon::Packet namespace is used to manage complex packet
|
17
|
+
# structures if required. The Original protocol does not require
|
18
|
+
# this, but Source does.
|
19
|
+
#
|
20
|
+
# Usage is fairly simple:
|
21
|
+
#
|
22
|
+
# # Note: Other classes have different constructors
|
23
|
+
# rcon = RCon::Query::Source.new("10.0.0.1", 27015)
|
24
|
+
# rcon.auth("foobar") # source only
|
25
|
+
# rcon.command("mp_friendlyfire") => "mp_friendlyfire = 1"
|
26
|
+
# rcon.cvar("mp_friendlyfire") => 1
|
27
|
+
#
|
28
|
+
# ================================================================
|
29
|
+
#
|
30
|
+
# The compilation of software known as rcon.rb is distributed under the
|
31
|
+
# following terms:
|
32
|
+
# Copyright (C) 2005-2006 Erik Hollensbe. All rights reserved.
|
33
|
+
#
|
34
|
+
# Redistribution and use in source form, with or without
|
35
|
+
# modification, are permitted provided that the following conditions
|
36
|
+
# are met:
|
37
|
+
# 1. Redistributions of source code must retain the above copyright
|
38
|
+
# notice, this list of conditions and the following disclaimer.
|
39
|
+
#
|
40
|
+
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
41
|
+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
42
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
43
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
|
44
|
+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
45
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
46
|
+
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
47
|
+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
48
|
+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
49
|
+
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
50
|
+
# SUCH DAMAGE.
|
51
|
+
#
|
52
|
+
|
53
|
+
|
54
|
+
class RCon
|
55
|
+
class Packet
|
56
|
+
# placeholder so ruby doesn't bitch
|
57
|
+
end
|
58
|
+
class Query
|
59
|
+
|
60
|
+
#
|
61
|
+
# Convenience method to scrape input from cvar output and return that data.
|
62
|
+
# Returns integers as a numeric type if possible.
|
63
|
+
#
|
64
|
+
# ex: rcon.cvar("mp_friendlyfire") => 1
|
65
|
+
#
|
66
|
+
|
67
|
+
def cvar(cvar_name)
|
68
|
+
response = command(cvar_name)
|
69
|
+
match = /^.+?\s(?:is|=)\s"([^"]+)".*$/.match response
|
70
|
+
match = match[1]
|
71
|
+
if /\D/.match match
|
72
|
+
return match
|
73
|
+
else
|
74
|
+
return match.to_i
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# RCon::Packet::Source generates a packet structure useful for
|
82
|
+
# RCon::Query::Source protocol queries.
|
83
|
+
#
|
84
|
+
# This class is primarily used internally, but is available if you
|
85
|
+
# want to do something more advanced with the Source RCon
|
86
|
+
# protocol.
|
87
|
+
#
|
88
|
+
# Use at your own risk.
|
89
|
+
#
|
90
|
+
|
91
|
+
class RCon::Packet::Source
|
92
|
+
# execution command
|
93
|
+
COMMAND_EXEC = 2
|
94
|
+
# auth command
|
95
|
+
COMMAND_AUTH = 3
|
96
|
+
# auth response
|
97
|
+
RESPONSE_AUTH = 2
|
98
|
+
# normal response
|
99
|
+
RESPONSE_NORM = 0
|
100
|
+
# packet trailer
|
101
|
+
TRAILER = "\x00\x00"
|
102
|
+
|
103
|
+
# size of the packet (10 bytes for header + string1 length)
|
104
|
+
attr_accessor :packet_size
|
105
|
+
# Request Identifier, used in managing multiple requests at once
|
106
|
+
attr_accessor :request_id
|
107
|
+
# Type of command, normally COMMAND_AUTH or COMMAND_EXEC
|
108
|
+
attr_accessor :type
|
109
|
+
# First string, the only used one in the protocol, contains
|
110
|
+
# commands and responses. Null terminated.
|
111
|
+
attr_accessor :string1
|
112
|
+
# Second string, unused by the protocol. Null terminated.
|
113
|
+
attr_accessor :string2
|
114
|
+
|
115
|
+
#
|
116
|
+
# Generate a command packet to be sent to an already
|
117
|
+
# authenticated RCon connection. Takes the command as an
|
118
|
+
# argument.
|
119
|
+
#
|
120
|
+
def command(string)
|
121
|
+
@request_id = rand(1000)
|
122
|
+
@string1 = string
|
123
|
+
@string2 = TRAILER
|
124
|
+
@type = COMMAND_EXEC
|
125
|
+
|
126
|
+
@packet_size = build_packet.length
|
127
|
+
|
128
|
+
return self
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# Generate an authentication packet to be sent to a newly
|
133
|
+
# started RCon connection. Takes the RCon password as an
|
134
|
+
# argument.
|
135
|
+
#
|
136
|
+
def auth(string)
|
137
|
+
@request_id = rand(1000)
|
138
|
+
@string1 = string
|
139
|
+
@string2 = TRAILER
|
140
|
+
@type = COMMAND_AUTH
|
141
|
+
|
142
|
+
@packet_size = build_packet.length
|
143
|
+
|
144
|
+
return self
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# Builds a packet ready to deliver, without the size prepended.
|
149
|
+
# Used to calculate the packet size, use #to_s to get the packet
|
150
|
+
# that srcds actually needs.
|
151
|
+
#
|
152
|
+
def build_packet
|
153
|
+
return [@request_id, @type, @string1, @string2].pack("VVa#{@string1.length}a2")
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns a string representation of the packet, useful for
|
157
|
+
# sending and debugging. This include the packet size.
|
158
|
+
def to_s
|
159
|
+
packet = build_packet
|
160
|
+
@packet_size = packet.length
|
161
|
+
return [@packet_size].pack("V") + packet
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
#
|
167
|
+
# RCon::Query::Original queries Quake 1/2/3 and Half-Life servers
|
168
|
+
# with the rcon protocol. This protocol travels over UDP to the
|
169
|
+
# game server port, and requires an initial authentication step,
|
170
|
+
# the information of which is provided at construction time.
|
171
|
+
#
|
172
|
+
# Some of the work here (namely the RCon packet structure) was taken
|
173
|
+
# from the KKRcon code, which is written in perl.
|
174
|
+
#
|
175
|
+
# One query per authentication is allowed.
|
176
|
+
#
|
177
|
+
|
178
|
+
class RCon::Query::Original < RCon::Query
|
179
|
+
# HLDS-Based Servers
|
180
|
+
HLDS = "l"
|
181
|
+
# QuakeWorld/Quake 1 Servers
|
182
|
+
QUAKEWORLD = "n"
|
183
|
+
# Quake 2/3 Servers
|
184
|
+
NEWQUAKE = ""
|
185
|
+
|
186
|
+
# Request to be sent to server
|
187
|
+
attr_reader :request
|
188
|
+
# Response from server
|
189
|
+
attr_reader :response
|
190
|
+
# Challenge ID (served by server-side of connection)
|
191
|
+
attr_reader :challenge_id
|
192
|
+
# UDPSocket object
|
193
|
+
attr_reader :socket
|
194
|
+
# Host of connection
|
195
|
+
attr_reader :host
|
196
|
+
# Port of connection
|
197
|
+
attr_reader :port
|
198
|
+
# RCon password
|
199
|
+
attr_reader :password
|
200
|
+
|
201
|
+
#
|
202
|
+
# Creates a RCon::Query::Original object for use.
|
203
|
+
#
|
204
|
+
# The type (the default of which is HLDS), has multiple possible
|
205
|
+
# values:
|
206
|
+
#
|
207
|
+
# HLDS - Half Life 1 (will not work with older versions of HLDS)
|
208
|
+
# QUAKEWORLD - QuakeWorld/Quake 1
|
209
|
+
# NEWQUAKE - Quake 2/3 (and many derivatives)
|
210
|
+
#
|
211
|
+
|
212
|
+
def initialize(host, port, password, type=HLDS)
|
213
|
+
@host = host
|
214
|
+
@port = port
|
215
|
+
@password = password
|
216
|
+
@type = type
|
217
|
+
end
|
218
|
+
|
219
|
+
#
|
220
|
+
# Sends a request given as the argument, and returns the
|
221
|
+
# response as a string.
|
222
|
+
#
|
223
|
+
def command(request)
|
224
|
+
@request = request
|
225
|
+
@challenge_id = nil
|
226
|
+
|
227
|
+
establish_connection
|
228
|
+
|
229
|
+
@socket.print "\xFF" * 4 + "challenge rcon\n\x00"
|
230
|
+
|
231
|
+
tmp = retrieve_socket_data
|
232
|
+
challenge_id = /challenge rcon (\d+)/.match tmp
|
233
|
+
if challenge_id
|
234
|
+
@challenge_id = challenge_id[1]
|
235
|
+
end
|
236
|
+
|
237
|
+
if @challenge_id.nil?
|
238
|
+
raise RCon::NetworkException.new("RCon challenge ID never returned: wrong rcon password?")
|
239
|
+
end
|
240
|
+
|
241
|
+
@socket.print "\xFF" * 4 + "rcon #{@challenge_id} \"#{@password}\" #{@request}\n\x00"
|
242
|
+
@response = retrieve_socket_data
|
243
|
+
|
244
|
+
@response.sub! /^\xFF\xFF\xFF\xFF#{@type}/, ""
|
245
|
+
@response.sub! /\x00+$/, ""
|
246
|
+
|
247
|
+
return @response
|
248
|
+
end
|
249
|
+
|
250
|
+
#
|
251
|
+
# Disconnects the RCon connection.
|
252
|
+
#
|
253
|
+
def disconnect
|
254
|
+
if @socket
|
255
|
+
@socket.close
|
256
|
+
@socket = nil
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
protected
|
261
|
+
|
262
|
+
#
|
263
|
+
# Establishes the connection.
|
264
|
+
#
|
265
|
+
def establish_connection
|
266
|
+
if @socket.nil?
|
267
|
+
@socket = UDPSocket.new
|
268
|
+
@socket.connect(@host, @port)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
#
|
273
|
+
# Generic method to pull data from the socket.
|
274
|
+
#
|
275
|
+
|
276
|
+
def retrieve_socket_data
|
277
|
+
return "" if @socket.nil?
|
278
|
+
|
279
|
+
retval = ""
|
280
|
+
packet = ""
|
281
|
+
loop do
|
282
|
+
break unless IO.select([@socket], nil, nil, 10)
|
283
|
+
packet << @socket.recv(8192)
|
284
|
+
retval << packet
|
285
|
+
break if packet.length < 8192
|
286
|
+
end
|
287
|
+
|
288
|
+
return retval
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
|
293
|
+
#
|
294
|
+
# RCon::Query::Source sends queries to a "Source" Engine server,
|
295
|
+
# such as Half-Life 2: Deathmatch, Counter-Strike: Source, or Day
|
296
|
+
# of Defeat: Source.
|
297
|
+
#
|
298
|
+
# Note that one authentication packet needs to be sent to send
|
299
|
+
# multiple commands. Sending multiple authentication packets may
|
300
|
+
# damage the current connection and require it to be reset.
|
301
|
+
#
|
302
|
+
# Note: If the attribute 'return_packets' is set to true, the full
|
303
|
+
# RCon::Packet::Source object is returned, instead of just a string
|
304
|
+
# with the headers stripped. Useful for debugging.
|
305
|
+
#
|
306
|
+
|
307
|
+
class RCon::Query::Source < RCon::Query
|
308
|
+
# RCon::Packet::Source object that was sent as a result of the last query
|
309
|
+
attr_reader :packet
|
310
|
+
# TCPSocket object
|
311
|
+
attr_reader :socket
|
312
|
+
# Host of connection
|
313
|
+
attr_reader :host
|
314
|
+
# Port of connection
|
315
|
+
attr_reader :port
|
316
|
+
# Authentication Status
|
317
|
+
attr_reader :authed
|
318
|
+
# return full packet, or just data?
|
319
|
+
attr :return_packets
|
320
|
+
|
321
|
+
#
|
322
|
+
# Given a host and a port (dotted-quad or hostname OK), creates
|
323
|
+
# a RCon::Query::Source object. Note that this will still
|
324
|
+
# require an authentication packet (see the auth() method)
|
325
|
+
# before commands can be sent.
|
326
|
+
#
|
327
|
+
|
328
|
+
def initialize(host, port)
|
329
|
+
@host = host
|
330
|
+
@port = port
|
331
|
+
@socket = nil
|
332
|
+
@packet = nil
|
333
|
+
@authed = false
|
334
|
+
@return_packets = false
|
335
|
+
end
|
336
|
+
|
337
|
+
#
|
338
|
+
# See RCon::Query#cvar.
|
339
|
+
#
|
340
|
+
|
341
|
+
def cvar(cvar_name)
|
342
|
+
return_packets = @return_packets
|
343
|
+
@return_packets = false
|
344
|
+
response = super
|
345
|
+
@return_packets = return_packets
|
346
|
+
return response
|
347
|
+
end
|
348
|
+
|
349
|
+
#
|
350
|
+
# Sends a RCon command to the server. May be used multiple times
|
351
|
+
# after an authentication is successful.
|
352
|
+
#
|
353
|
+
# See the class-level documentation on the 'return_packet' attribute
|
354
|
+
# for return values. The default is to return a string containing
|
355
|
+
# the response.
|
356
|
+
#
|
357
|
+
|
358
|
+
def command(command)
|
359
|
+
|
360
|
+
if ! @authed
|
361
|
+
raise RCon::NetworkException.new("You must authenticate the connection successfully before sending commands.")
|
362
|
+
end
|
363
|
+
|
364
|
+
@packet = RCon::Packet::Source.new
|
365
|
+
@packet.command(command)
|
366
|
+
|
367
|
+
@socket.print @packet.to_s
|
368
|
+
rpacket = build_response_packet
|
369
|
+
|
370
|
+
if rpacket.type != RCon::Packet::Source::RESPONSE_NORM
|
371
|
+
raise RCon::NetworkException.new("error sending command: #{rpacket.type}")
|
372
|
+
end
|
373
|
+
|
374
|
+
if @return_packets
|
375
|
+
return rpacket
|
376
|
+
else
|
377
|
+
return rpacket.string1
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
#
|
382
|
+
# Requests authentication from the RCon server, given a
|
383
|
+
# password. Is only expected to be used once.
|
384
|
+
#
|
385
|
+
# See the class-level documentation on the 'return_packet' attribute
|
386
|
+
# for return values. The default is to return a true value if auth
|
387
|
+
# succeeded.
|
388
|
+
#
|
389
|
+
|
390
|
+
def auth(password)
|
391
|
+
establish_connection
|
392
|
+
|
393
|
+
@packet = RCon::Packet::Source.new
|
394
|
+
@packet.auth(password)
|
395
|
+
|
396
|
+
@socket.print @packet.to_s
|
397
|
+
# on auth, one junk packet is sent
|
398
|
+
rpacket = nil
|
399
|
+
2.times { rpacket = build_response_packet }
|
400
|
+
|
401
|
+
if rpacket.type != RCon::Packet::Source::RESPONSE_AUTH
|
402
|
+
raise RCon::NetworkException.new("error authenticating: #{rpacket.type}")
|
403
|
+
end
|
404
|
+
|
405
|
+
@authed = true
|
406
|
+
if @return_packet
|
407
|
+
return rpacket
|
408
|
+
else
|
409
|
+
return true
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
alias_method :authenticate, :auth
|
414
|
+
|
415
|
+
#
|
416
|
+
# Disconnects from the Source server.
|
417
|
+
#
|
418
|
+
|
419
|
+
def disconnect
|
420
|
+
if @socket
|
421
|
+
@socket.close
|
422
|
+
@socket = nil
|
423
|
+
@authed = false
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
protected
|
428
|
+
|
429
|
+
#
|
430
|
+
# Builds a RCon::Packet::Source packet based on the response
|
431
|
+
# given by the server.
|
432
|
+
#
|
433
|
+
def build_response_packet
|
434
|
+
rpacket = RCon::Packet::Source.new
|
435
|
+
total_size = 0
|
436
|
+
request_id = 0
|
437
|
+
type = 0
|
438
|
+
response = ""
|
439
|
+
message = ""
|
440
|
+
|
441
|
+
loop do
|
442
|
+
break unless IO.select([@socket], nil, nil, 1)
|
443
|
+
tmp = @socket.recv(14)
|
444
|
+
if tmp.nil?
|
445
|
+
return nil
|
446
|
+
end
|
447
|
+
size, request_id, type, message = tmp.unpack("VVVa*")
|
448
|
+
total_size += size
|
449
|
+
|
450
|
+
# special case for authentication
|
451
|
+
break if message.sub! /\x00\x00$/, ""
|
452
|
+
|
453
|
+
response << message
|
454
|
+
|
455
|
+
# the 'size - 10' here accounts for the fact that we've snarfed 14 bytes,
|
456
|
+
# the size (which is 4 bytes) is not counted, yet represents the rest
|
457
|
+
# of the packet (which we have already taken 10 bytes from)
|
458
|
+
|
459
|
+
tmp = @socket.recv(size - 10)
|
460
|
+
response << tmp
|
461
|
+
response.sub! /\x00\x00$/, ""
|
462
|
+
end
|
463
|
+
|
464
|
+
rpacket.packet_size = total_size
|
465
|
+
rpacket.request_id = request_id
|
466
|
+
rpacket.type = type
|
467
|
+
|
468
|
+
# strip nulls (this is actually the end of string1 and string2)
|
469
|
+
rpacket.string1 = response.sub /\x00\x00$/, ""
|
470
|
+
return rpacket
|
471
|
+
end
|
472
|
+
|
473
|
+
# establishes a connection to the server.
|
474
|
+
def establish_connection
|
475
|
+
if @socket.nil?
|
476
|
+
@socket = TCPSocket.new(@host, @port)
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
end
|
481
|
+
|
482
|
+
# Exception class for network errors
|
483
|
+
class RCon::NetworkException < Exception
|
484
|
+
end
|
metadata
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11
|
3
|
+
specification_version: 1
|
4
|
+
name: rcon
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2006-01-28 00:00:00 -08:00
|
8
|
+
summary: "Ruby class to work with Quake 1/2/3, Half-Life and Source Engine rcon (remote
|
9
|
+
console)"
|
10
|
+
require_paths:
|
11
|
+
- lib
|
12
|
+
email: erik@hollensbe.org
|
13
|
+
homepage:
|
14
|
+
rubyforge_project:
|
15
|
+
description:
|
16
|
+
autorequire: rcon
|
17
|
+
default_executable:
|
18
|
+
bindir: bin
|
19
|
+
has_rdoc: true
|
20
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
21
|
+
requirements:
|
22
|
+
-
|
23
|
+
- ">"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 0.0.0
|
26
|
+
version:
|
27
|
+
platform: ruby
|
28
|
+
signing_key:
|
29
|
+
cert_chain:
|
30
|
+
authors:
|
31
|
+
- Erik Hollensbe
|
32
|
+
files:
|
33
|
+
- lib/rcon.rb
|
34
|
+
test_files: []
|
35
|
+
rdoc_options: []
|
36
|
+
extra_rdoc_files: []
|
37
|
+
executables: []
|
38
|
+
extensions: []
|
39
|
+
requirements: []
|
40
|
+
dependencies: []
|