minecraft-query 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cb4d3f89b9749589d922d925b8120fb71cc1f4f9
4
+ data.tar.gz: 7c7effd741c452d11bf5ea0d46c0ee57cb18715f
5
+ SHA512:
6
+ metadata.gz: a8c7986956e9273a2993eb1114876a96e7c9c6d36f3fa4d9902439a9fa873ce31a3defb51ff73d93bee389888abe3df2605e8eef16a9c22773741898d5f80dfa
7
+ data.tar.gz: 67e3a69b557a2309b21320cf25399e08f25b414cdee5fab2cbef71a261365b3b85bfa31f5670dba50a2deff2e0e56ab69d1ac8be610abaff2f5b51f0e36404ba
@@ -0,0 +1,25 @@
1
+ module minecraft-query
2
+ require 'socket'
3
+ require 'timeout'
4
+
5
+ require 'query/query'
6
+ require 'rcon/rcon'
7
+
8
+ #
9
+ # Connects to a Minecraft server's RCON or Query port to send commands or fetch data.
10
+ #
11
+ # Example:
12
+ # >> rcon = RCON::Minecraft.new('localhost', 25575)
13
+ # => #<RCON::Minecraft:0x007ff6e29e1228 @host="localhost", @port=25575, @socket=nil, @packet=nil, @authed=false, @return_packets=false>
14
+ # >> rcon.auth('password')
15
+ # => true
16
+ # >> rcon.command('say hi')
17
+ # => "\xA7d[Server] hi\n"
18
+ #
19
+ # >> query = Query::simpleQuery('localhost', 25565)
20
+ # => {:motd=>"ECS Survival", :gametype=>"SMP", :map=>"world", :numplayers=>"1", :maxplayers=>"20"}
21
+ # >> players = query[:numplayers] + '/' + query[:maxplayers]
22
+ # => 1/20
23
+ #
24
+
25
+ end
@@ -0,0 +1,90 @@
1
+ class Query
2
+ def self.init
3
+ @sock = UDPSocket.new
4
+ @sock.connect(@addr,@port)
5
+ @val = {}
6
+ @buff = nil
7
+ key
8
+ end
9
+
10
+ def self.key
11
+ begin
12
+ timeout(5) do
13
+ start = @sock.send("\xFE\xFD\x09\x01\x02\x03\x04".force_encoding(Encoding::ASCII_8BIT), 0)
14
+ t = @sock.recvfrom(1460)[0]
15
+ key = t[5...-1].to_i
16
+ @key = Array(key).pack('N')
17
+ end
18
+ rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
19
+ return false
20
+ end
21
+ end
22
+
23
+
24
+ def self.simpleQuery(addr = 'localhost', port = 25565)
25
+ @addr = addr
26
+ @port = port
27
+ init
28
+ begin
29
+ timeout(2) do
30
+ query = @sock.send("\xFE\xFD\x00\x01\x02\x03\x04".force_encoding(Encoding::ASCII_8BIT) + @key, 0)
31
+ data = @sock.recvfrom(1460)[0]
32
+ buffer = data[5...-1]
33
+ @val[:motd], @val[:gametype], @val[:map], @val[:numplayers], @val[:maxplayers], @buf = buffer.split("\x00", 6)
34
+ if @sock != nil
35
+ @sock.close
36
+ end
37
+ end
38
+ return @val
39
+ rescue StandardError => e
40
+ return false, e
41
+ end
42
+ end
43
+
44
+ def self.fullQuery(addr = 'localhost', port = 25565)
45
+ @addr = addr
46
+ @port = port
47
+ init
48
+ begin
49
+ timeout(2) do
50
+ query = @sock.send("\xFE\xFD\x00\x01\x02\x03\x04".force_encoding(Encoding::ASCII_8BIT) + @key + "\x01\x02\x03\x04".force_encoding(Encoding::ASCII_8BIT), 0)
51
+ data = @sock.recvfrom(1460)[0]
52
+ buffer = data[11...-1]
53
+ items, players = buffer.split("\x00\x00\x01player_\x00\x00".force_encoding(Encoding::ASCII_8BIT))
54
+ if items[0...8] == 'hostname'
55
+ items = 'motd' + items[8...-1]
56
+ end
57
+ vals = {}
58
+ items = items.split("\x00")
59
+ items.each_with_index do |key, idx|
60
+ next unless idx % 2 == 0
61
+ vals[key] = items[idx + 1]
62
+ end
63
+
64
+ vals["motd"] = vals["hostname"]
65
+ vals.delete("hostname")
66
+ vals.delete("um") if vals["um"]
67
+
68
+ players = players[0..-2] if players
69
+
70
+ if players
71
+ vals[:players] = players.split("\x00")
72
+ end
73
+ puts vals
74
+ vals["raw_plugins"] = vals["plugins"]
75
+ parts = vals["raw_plugins"].split(":")
76
+ server = parts[0].strip() if parts[0]
77
+ plugins = []
78
+ if parts.size == 2
79
+ plugins = parts[1].split(";").map {|value| value.strip() }
80
+ end
81
+ vals["plugins"] = plugins
82
+ vals["server"] = server
83
+ vals["timestamp"] = Time.now
84
+ return vals.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
85
+ end
86
+ rescue StandardError => e
87
+ return false, e
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,435 @@
1
+ module RCON
2
+
3
+ class Query
4
+
5
+ #
6
+ # Convenience method to scrape input from cvar output and return that data.
7
+ # Returns integers as a numeric type if possible.
8
+ #
9
+ # ex: rcon.cvar("mp_friendlyfire") => 1
10
+ #
11
+
12
+ def cvar(cvar_name)
13
+ response = command(cvar_name)
14
+ match = /^.+?\s(?:is|=)\s"([^"]+)".*$/.match response
15
+ match = match[1]
16
+ if /\D/.match match
17
+ return match
18
+ else
19
+ return match.to_i
20
+ end
21
+ end
22
+ end
23
+
24
+ class Packet
25
+ end
26
+
27
+ class Packet::Source
28
+ # execution command
29
+ COMMAND_EXEC = 2
30
+ # auth command
31
+ COMMAND_AUTH = 3
32
+ # auth response
33
+ RESPONSE_AUTH = 2
34
+ # normal response
35
+ RESPONSE_NORM = 0
36
+ # packet trailer
37
+ TRAILER = "\x00\x00"
38
+
39
+ # size of the packet (10 bytes for header + string1 length)
40
+ attr_accessor :packet_size
41
+ # Request Identifier, used in managing multiple requests at once
42
+ attr_accessor :request_id
43
+ # Type of command, normally COMMAND_AUTH or COMMAND_EXEC. In response packets, RESPONSE_AUTH or RESPONSE_NORM
44
+ attr_accessor :command_type
45
+ # First string, the only used one in the protocol, contains
46
+ # commands and responses. Null terminated.
47
+ attr_accessor :string1
48
+ # Second string, unused by the protocol. Null terminated.
49
+ attr_accessor :string2
50
+
51
+ #
52
+ # Generate a command packet to be sent to an already
53
+ # authenticated RCon connection. Takes the command as an
54
+ # argument.
55
+ #
56
+ def command(string)
57
+ @request_id = rand(1000)
58
+ @string1 = string
59
+ @string2 = TRAILER
60
+ @command_type = COMMAND_EXEC
61
+
62
+ @packet_size = build_packet.length
63
+
64
+ return self
65
+ end
66
+
67
+ #
68
+ # Generate an authentication packet to be sent to a newly
69
+ # started RCon connection. Takes the RCon password as an
70
+ # argument.
71
+ #
72
+ def auth(string)
73
+ @request_id = rand(1000)
74
+ @string1 = string
75
+ @string2 = TRAILER
76
+ @command_type = COMMAND_AUTH
77
+
78
+ @packet_size = build_packet.length
79
+
80
+ return self
81
+ end
82
+
83
+ #
84
+ # Builds a packet ready to deliver, without the size prepended.
85
+ # Used to calculate the packet size, use #to_s to get the packet
86
+ # that srcds actually needs.
87
+ #
88
+ def build_packet
89
+ return [@request_id, @command_type, @string1, @string2].pack("VVa#{@string1.length}a2")
90
+ end
91
+
92
+ # Returns a string representation of the packet, useful for
93
+ # sending and debugging. This include the packet size.
94
+ def to_s
95
+ packet = build_packet
96
+ @packet_size = packet.length
97
+ return [@packet_size].pack("V") + packet
98
+ end
99
+
100
+ end
101
+
102
+ class Source < Query
103
+ # Packet::Source object that was sent as a result of the last query
104
+ attr_reader :packet
105
+ # TCPSocket object
106
+ attr_reader :socket
107
+ # Host of connection
108
+ attr_reader :host
109
+ # Port of connection
110
+ attr_reader :port
111
+ # Authentication Status
112
+ attr_reader :authed
113
+ # return full packet, or just data?
114
+ attr_accessor :return_packets
115
+
116
+ #
117
+ # Given a host and a port (dotted-quad or hostname OK), creates
118
+ # a Query::Source object. Note that this will still
119
+ # require an authentication packet (see the auth() method)
120
+ # before commands can be sent.
121
+ #
122
+
123
+ def initialize(host = 'localhost', port = 25575)
124
+ @host = host
125
+ @port = port
126
+ @socket = nil
127
+ @packet = nil
128
+ @authed = false
129
+ @return_packets = false
130
+ end
131
+
132
+ #
133
+ # See Query#cvar.
134
+ #
135
+
136
+ def cvar(cvar_name)
137
+ return_packets = @return_packets
138
+ @return_packets = false
139
+ response = super
140
+ @return_packets = return_packets
141
+ return response
142
+ end
143
+
144
+ #
145
+ # Sends a RCon command to the server. May be used multiple times
146
+ # after an authentication is successful.
147
+ #
148
+
149
+ def command(command)
150
+
151
+ if ! @authed
152
+ raise NetworkException.new("You must authenticate the connection successfully before sending commands.")
153
+ end
154
+
155
+ @packet = Packet::Source.new
156
+ @packet.command(command)
157
+
158
+ @socket.print @packet.to_s
159
+ rpacket = build_response_packet
160
+
161
+ if rpacket.command_type != Packet::Source::RESPONSE_NORM
162
+ raise NetworkException.new("error sending command: #{rpacket.command_type}")
163
+ end
164
+
165
+ if @return_packets
166
+ return rpacket
167
+ else
168
+ return rpacket.string1
169
+ end
170
+ end
171
+
172
+ #
173
+ # Requests authentication from the RCon server, given a
174
+ # password. Is only expected to be used once.
175
+ #
176
+
177
+ def auth(password)
178
+ establish_connection
179
+
180
+ @packet = Packet::Source.new
181
+ @packet.auth(password)
182
+
183
+ @socket.print @packet.to_s
184
+ # on auth, one junk packet is sent
185
+ rpacket = nil
186
+ 2.times { rpacket = build_response_packet }
187
+
188
+ if rpacket.command_type != Packet::Source::RESPONSE_AUTH
189
+ raise NetworkException.new("error authenticating: #{rpacket.command_type}")
190
+ end
191
+
192
+ @authed = true
193
+ if @return_packets
194
+ return rpacket
195
+ else
196
+ return true
197
+ end
198
+ end
199
+
200
+ alias_method :authenticate, :auth
201
+
202
+ #
203
+ # Disconnects from the Source server.
204
+ #
205
+
206
+ def disconnect
207
+ if @socket
208
+ @socket.close
209
+ @socket = nil
210
+ @authed = false
211
+ end
212
+ end
213
+
214
+ protected
215
+
216
+ #
217
+ # Builds a Packet::Source packet based on the response
218
+ # given by the server.
219
+ #
220
+ def build_response_packet
221
+ rpacket = Packet::Source.new
222
+ total_size = 0
223
+ request_id = 0
224
+ type = 0
225
+ response = ""
226
+ message = ""
227
+
228
+
229
+ loop do
230
+ break unless IO.select([@socket], nil, nil, 10)
231
+
232
+ #
233
+ # TODO: clean this up - read everything and then unpack.
234
+ #
235
+
236
+ tmp = @socket.recv(14)
237
+ if tmp.nil?
238
+ return nil
239
+ end
240
+ size, request_id, type, message = tmp.unpack("VVVa*")
241
+ total_size += size
242
+
243
+ # special case for authentication
244
+ break if message.sub! /\x00\x00$/, ""
245
+
246
+ response << message
247
+
248
+ # the 'size - 10' here accounts for the fact that we've snarfed 14 bytes,
249
+ # the size (which is 4 bytes) is not counted, yet represents the rest
250
+ # of the packet (which we have already taken 10 bytes from)
251
+
252
+ tmp = @socket.recv(size - 10)
253
+ response << tmp
254
+ response.sub! /\x00\x00$/, ""
255
+ end
256
+
257
+ rpacket.packet_size = total_size
258
+ rpacket.request_id = request_id
259
+ rpacket.command_type = type
260
+
261
+ # strip nulls (this is actually the end of string1 and string2)
262
+ rpacket.string1 = response.sub /\x00\x00$/, ""
263
+ return rpacket
264
+ end
265
+
266
+ # establishes a connection to the server.
267
+ def establish_connection
268
+ if @socket.nil?
269
+ @socket = TCPSocket.new(@host, @port)
270
+ end
271
+ end
272
+
273
+ end
274
+
275
+ class Minecraft < Query
276
+ # Packet::Source object that was sent as a result of the last query
277
+ attr_reader :packet
278
+ # TCPSocket object
279
+ attr_reader :socket
280
+ # Host of connection
281
+ attr_reader :host
282
+ # Port of connection
283
+ attr_reader :port
284
+ # Authentication Status
285
+ attr_reader :authed
286
+ # return full packet, or just data?
287
+ attr_accessor :return_packets
288
+
289
+ #
290
+ # Given a host and a port (dotted-quad or hostname OK), creates
291
+ # a Query::Minecraft object. Note that this will still
292
+ # require an authentication packet (see the auth() method)
293
+ # before commands can be sent.
294
+ #
295
+
296
+ def initialize(host = 'localhost', port = 25575)
297
+ @host = host
298
+ @port = port
299
+ @socket = nil
300
+ @packet = nil
301
+ @authed = false
302
+ @return_packets = false
303
+ end
304
+
305
+ #
306
+ # See Query#cvar.
307
+ #
308
+
309
+ def cvar(cvar_name)
310
+ return_packets = @return_packets
311
+ @return_packets = false
312
+ response = super
313
+ @return_packets = return_packets
314
+ return response
315
+ end
316
+
317
+ #
318
+ # Sends a RCon command to the server. May be used multiple times
319
+ # after an authentication is successful.
320
+ #
321
+
322
+ def command(command)
323
+
324
+ if ! @authed
325
+ raise NetworkException.new("You must authenticate the connection successfully before sending commands.")
326
+ end
327
+
328
+ @packet = Packet::Source.new
329
+ @packet.command(command)
330
+
331
+ @socket.print @packet.to_s
332
+ rpacket = build_response_packet
333
+
334
+ if rpacket.command_type != Packet::Source::RESPONSE_NORM
335
+ raise NetworkException.new("error sending command: #{rpacket.command_type}")
336
+ end
337
+
338
+ if @return_packets
339
+ return rpacket
340
+ else
341
+ return rpacket.string1
342
+ end
343
+ end
344
+
345
+ #
346
+ # Requests authentication from the RCon server, given a
347
+ # password. Is only expected to be used once.
348
+ #
349
+
350
+
351
+ def auth(password)
352
+ establish_connection
353
+
354
+ @packet = Packet::Source.new
355
+ @packet.auth(password)
356
+
357
+ @socket.print @packet.to_s
358
+ rpacket = nil
359
+ rpacket = build_response_packet
360
+
361
+ if rpacket.command_type != Packet::Source::RESPONSE_AUTH
362
+ raise NetworkException.new("error authenticating: #{rpacket.command_type}")
363
+ end
364
+
365
+ @authed = true
366
+ if @return_packets
367
+ return rpacket
368
+ else
369
+ return true
370
+ end
371
+ end
372
+
373
+ alias_method :authenticate, :auth
374
+
375
+ #
376
+ # Disconnects from the Minecraft server.
377
+ #
378
+
379
+ def disconnect
380
+ if @socket
381
+ @socket.close
382
+ @socket = nil
383
+ @authed = false
384
+ end
385
+ end
386
+
387
+ protected
388
+
389
+ #
390
+ # Builds a Packet::Source packet based on the response
391
+ # given by the server.
392
+ #
393
+ def build_response_packet
394
+ rpacket = Packet::Source.new
395
+ total_size = 0
396
+ request_id = 0
397
+ type = 0
398
+ response = ""
399
+ message = ""
400
+ message2 = ""
401
+
402
+ tmp = @socket.recv(4)
403
+ if tmp.nil?
404
+ return nil
405
+ end
406
+ size = tmp.unpack("V1")
407
+ tmp = @socket.recv(size[0])
408
+ request_id, type, message, message2 = tmp.unpack("V1V1a*a*")
409
+ total_size = size[0]
410
+
411
+ rpacket.packet_size = total_size
412
+ rpacket.request_id = request_id
413
+ rpacket.command_type = type
414
+
415
+ # strip nulls (this is actually the end of string1 and string2)
416
+ message.sub! /\x00\x00$/, ""
417
+ message2.sub! /\x00\x00$/, ""
418
+ rpacket.string1 = message
419
+ rpacket.string2 = message2
420
+ return rpacket
421
+ end
422
+
423
+ # establishes a connection to the server.
424
+ def establish_connection
425
+ if @socket.nil?
426
+ @socket = TCPSocket.new(@host, @port)
427
+ end
428
+ end
429
+
430
+ end
431
+
432
+ class NetworkException < Exception
433
+ end
434
+
435
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minecraft-query
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tyler Doherty
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-07-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A gem to retrieve data from a Minecraft server's query interface and
14
+ manage a Minecraft or SRCDS server via RCON.
15
+ email: tyler@leonplay.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/RubyMinecraft.rb
21
+ - lib/query/query.rb
22
+ - lib/rcon/rcon.rb
23
+ homepage: http://github.com/Tylerjd/RubyMinecraft
24
+ licenses:
25
+ - New BSD License
26
+ metadata: {}
27
+ post_install_message:
28
+ rdoc_options: []
29
+ require_paths:
30
+ - lib
31
+ required_ruby_version: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - '>='
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ requirements: []
42
+ rubyforge_project:
43
+ rubygems_version: 2.0.3
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: Minecraft Querying lib
47
+ test_files: []