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.
Files changed (2) hide show
  1. data/lib/rcon.rb +484 -0
  2. 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: []