minecraft-query 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.
@@ -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: []