rcon 0.1.0

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