steam-condenser 0.12.0 → 0.13.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/README.md +3 -1
- data/lib/exceptions/web_api_exception.rb +49 -0
- data/lib/steam/community/app_news.rb +69 -0
- data/lib/steam/community/game_achievement.rb +29 -0
- data/lib/steam/community/steam_id.rb +1 -1
- data/lib/steam/community/tf2/tf2_golden_wrench.rb +43 -0
- data/lib/steam/community/tf2/tf2_inventory.rb +51 -0
- data/lib/steam/community/tf2/tf2_item.rb +106 -0
- data/lib/steam/community/tf2/tf2_stats.rb +11 -0
- data/lib/steam/community/web_api.rb +96 -0
- data/lib/steam/packets/a2s_player_packet.rb +1 -1
- data/lib/steam/packets/a2s_rules_packet.rb +3 -3
- data/lib/steam/packets/c2m_checkmd5_packet.rb +24 -0
- data/lib/steam/packets/m2c_isvalidmd5_packet.rb +25 -0
- data/lib/steam/packets/m2s_requestrestart_packet.rb +24 -0
- data/lib/steam/packets/rcon/rcon_packet.rb +1 -1
- data/lib/steam/packets/rcon/rcon_packet_factory.rb +0 -1
- data/lib/steam/packets/rcon/rcon_terminator.rb +20 -0
- data/lib/steam/packets/s2a_info2_packet.rb +3 -3
- data/lib/steam/packets/s2a_logstring_packet.rb +24 -0
- data/lib/steam/packets/s2a_player_packet.rb +1 -1
- data/lib/steam/packets/s2a_rules_packet.rb +2 -2
- data/lib/steam/packets/s2c_challenge_packet.rb +2 -2
- data/lib/steam/packets/s2m_heartbeat2_packet.rb +57 -0
- data/lib/steam/packets/steam_packet.rb +6 -3
- data/lib/steam/packets/steam_packet_factory.rb +12 -9
- data/lib/steam/servers/game_server.rb +57 -16
- data/lib/steam/servers/goldsrc_server.rb +7 -12
- data/lib/steam/servers/master_server.rb +63 -15
- data/lib/steam/servers/server.rb +66 -0
- data/lib/steam/servers/source_server.rb +14 -20
- data/lib/steam/sockets/goldsrc_socket.rb +13 -9
- data/lib/steam/sockets/rcon_socket.rb +35 -24
- data/lib/steam/sockets/steam_socket.rb +27 -21
- data/lib/steam/steam_player.rb +30 -15
- data/lib/steam-condenser/community.rb +17 -16
- data/lib/steam-condenser/version.rb +2 -2
- metadata +39 -15
- data/lib/datagram_channel.rb +0 -67
- data/lib/socket_channel.rb +0 -55
- data/lib/steam/packets/a2a_ack_packet.rb +0 -24
- data/lib/steam/packets/a2a_ping_packet.rb +0 -19
@@ -0,0 +1,24 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2011, Sebastian Staudt
|
5
|
+
|
6
|
+
require 'steam/packets/steam_packet'
|
7
|
+
|
8
|
+
# The S2A_LOGSTRING packet type is used to transfer log messages.
|
9
|
+
class S2A_LOGSTRING_Packet
|
10
|
+
|
11
|
+
include SteamPacket
|
12
|
+
|
13
|
+
# The log message contained in this packet
|
14
|
+
attr_reader :message
|
15
|
+
|
16
|
+
# Creates a new log message packet
|
17
|
+
def initialize(data)
|
18
|
+
super S2A_LOGSTRING_HEADER, data
|
19
|
+
|
20
|
+
@content_data.byte
|
21
|
+
@message = @content_data.string
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# This code is free software; you can redistribute it and/or modify it under the
|
2
2
|
# terms of the new BSD License.
|
3
3
|
#
|
4
|
-
# Copyright (c) 2008-
|
4
|
+
# Copyright (c) 2008-2011, Sebastian Staudt
|
5
5
|
|
6
6
|
require 'steam/packets/steam_packet'
|
7
7
|
|
@@ -15,7 +15,7 @@ class S2A_RULES_Packet
|
|
15
15
|
|
16
16
|
# Creates a S2A_RULES response object based on the data received.
|
17
17
|
def initialize(content_data)
|
18
|
-
raise Exception.new('Wrong formatted
|
18
|
+
raise Exception.new('Wrong formatted S2A_RULES response packet.') if content_data.nil?
|
19
19
|
|
20
20
|
super SteamPacket::S2A_RULES_HEADER, content_data
|
21
21
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# This code is free software; you can redistribute it and/or modify it under the
|
2
2
|
# terms of the new BSD License.
|
3
3
|
#
|
4
|
-
# Copyright (c) 2008-
|
4
|
+
# Copyright (c) 2008-2011, Sebastian Staudt
|
5
5
|
|
6
6
|
require 'steam/packets/steam_packet'
|
7
7
|
|
@@ -11,7 +11,7 @@ class S2C_CHALLENGE_Packet
|
|
11
11
|
|
12
12
|
include SteamPacket
|
13
13
|
|
14
|
-
# Creates a
|
14
|
+
# Creates a S2C_CHALLENGE response object based on the data received.
|
15
15
|
def initialize(challenge_number)
|
16
16
|
super S2C_CHALLENGE_HEADER, challenge_number
|
17
17
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under
|
2
|
+
# the terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2010-2011, Sebastian Staudt
|
5
|
+
|
6
|
+
require 'steam/packets/steam_packet'
|
7
|
+
|
8
|
+
# The S2M_HEARTBEAT2 packet type is used to signal a game servers availability
|
9
|
+
# and status to the master servers.
|
10
|
+
class S2M_HEARTBEAT2_Packet
|
11
|
+
|
12
|
+
include SteamPacket
|
13
|
+
|
14
|
+
DEFAULT_DATA = {
|
15
|
+
:appid => 320,
|
16
|
+
:bots => 0,
|
17
|
+
:challenge => nil,
|
18
|
+
:dedicated => 0,
|
19
|
+
:gamedir => 'hl2mp',
|
20
|
+
:gameport => 27015,
|
21
|
+
:gametype => 'ctf',
|
22
|
+
:lan => 1,
|
23
|
+
:map => 'null',
|
24
|
+
:max => 24,
|
25
|
+
:os => 'l',
|
26
|
+
:password => 0,
|
27
|
+
:players => 0,
|
28
|
+
:product => 'hl2dm',
|
29
|
+
:protocol => 7,
|
30
|
+
:region => 255,
|
31
|
+
:secure => 0,
|
32
|
+
:specport => 0,
|
33
|
+
:type => 'd',
|
34
|
+
:version => '1.0.0.0'
|
35
|
+
}
|
36
|
+
|
37
|
+
# Creates a new heartbeat packet to send to a master server
|
38
|
+
def initialize(data = {})
|
39
|
+
data = DEFAULT_DATA.merge data
|
40
|
+
|
41
|
+
if data[:challenge].nil?
|
42
|
+
raise SteamCondenserException.new 'You have to provide a challenge number when sending a heartbeat to a master server.'
|
43
|
+
end
|
44
|
+
|
45
|
+
bytes = 0x0A.chr
|
46
|
+
data.each { |k, v| bytes += "\\#{k}\\#{v}" }
|
47
|
+
bytes += 0x0A.chr
|
48
|
+
|
49
|
+
super S2M_HEARTBEAT2_HEADER, bytes
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a byte array representation of the packet data
|
53
|
+
def to_s
|
54
|
+
[@header_data, @content_data.string].pack('ca*')
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -1,30 +1,33 @@
|
|
1
1
|
# This code is free software; you can redistribute it and/or modify it under the
|
2
2
|
# terms of the new BSD License.
|
3
3
|
#
|
4
|
-
# Copyright (c) 2008-
|
4
|
+
# Copyright (c) 2008-2011, Sebastian Staudt
|
5
5
|
|
6
6
|
require 'stringio_additions'
|
7
7
|
|
8
8
|
# This class represents a packet used by the Source query protocol
|
9
9
|
module SteamPacket
|
10
10
|
|
11
|
-
A2A_ACK_HEADER = 0x6A
|
12
|
-
A2A_PING_HEADER = 0x69
|
13
11
|
A2M_GET_SERVERS_BATCH2_HEADER = 0x31
|
14
12
|
A2S_INFO_HEADER = 0x54
|
15
13
|
A2S_PLAYER_HEADER = 0x55
|
16
14
|
A2S_RULES_HEADER = 0x56
|
17
15
|
A2S_SERVERQUERY_GETCHALLENGE_HEADER = 0x57
|
16
|
+
C2M_CHECKMD5_HEADER = 0x4D
|
18
17
|
M2A_SERVER_BATCH_HEADER = 0x66
|
18
|
+
M2C_ISVALIDMD5_HEADER = 0x4E
|
19
|
+
M2S_REQUESTRESTART_HEADER = 0x4F
|
19
20
|
RCON_GOLDSRC_CHALLENGE_HEADER = 0x63
|
20
21
|
RCON_GOLDSRC_NO_CHALLENGE_HEADER = 0x39
|
21
22
|
RCON_GOLDSRC_RESPONSE_HEADER = 0x6c
|
22
23
|
S2A_INFO_DETAILED_HEADER = 0x6D
|
23
24
|
S2A_INFO2_HEADER = 0x49
|
25
|
+
S2A_LOGSTRING_HEADER = 0x52
|
24
26
|
S2A_PLAYER_HEADER = 0x44
|
25
27
|
S2A_RULES_HEADER = 0x45
|
26
28
|
S2C_CONNREJECT_HEADER = 0x39
|
27
29
|
S2C_CHALLENGE_HEADER = 0x41
|
30
|
+
S2M_HEARTBEAT2_HEADER = 0x30
|
28
31
|
|
29
32
|
# Creates a new SteamPacket object with given header and content data
|
30
33
|
def initialize(header_data, content_data = '')
|
@@ -1,16 +1,12 @@
|
|
1
1
|
# This code is free software; you can redistribute it and/or modify it under the
|
2
2
|
# terms of the new BSD License.
|
3
3
|
#
|
4
|
-
# Copyright (c) 2008-
|
5
|
-
|
6
|
-
require 'zlib'
|
4
|
+
# Copyright (c) 2008-2011, Sebastian Staudt
|
7
5
|
|
8
6
|
require 'exceptions/steam_condenser_exception'
|
9
7
|
require 'steam/packets/s2a_info_detailed_packet'
|
10
8
|
require 'steam/packets/a2s_info_packet'
|
11
9
|
require 'steam/packets/s2a_info2_packet'
|
12
|
-
require 'steam/packets/a2a_ping_packet'
|
13
|
-
require 'steam/packets/a2a_ack_packet'
|
14
10
|
require 'steam/packets/a2s_player_packet'
|
15
11
|
require 'steam/packets/s2a_player_packet'
|
16
12
|
require 'steam/packets/a2s_rules_packet'
|
@@ -19,6 +15,9 @@ require 'steam/packets/a2s_serverquery_getchallenge_packet'
|
|
19
15
|
require 'steam/packets/s2c_challenge_packet'
|
20
16
|
require 'steam/packets/a2m_get_servers_batch2_packet'
|
21
17
|
require 'steam/packets/m2a_server_batch_packet'
|
18
|
+
require 'steam/packets/m2c_isvalidmd5_packet'
|
19
|
+
require 'steam/packets/m2s_requestrestart_packet'
|
20
|
+
require 'steam/packets/s2a_logstring_packet'
|
22
21
|
require 'steam/packets/rcon/rcon_goldsrc_response'
|
23
22
|
|
24
23
|
module SteamPacketFactory
|
@@ -35,10 +34,6 @@ module SteamPacketFactory
|
|
35
34
|
return A2S_INFO_Packet.new
|
36
35
|
when SteamPacket::S2A_INFO2_HEADER
|
37
36
|
return S2A_INFO2_Packet.new(data)
|
38
|
-
when SteamPacket::A2A_PING_HEADER
|
39
|
-
return A2A_PING_Packet.new
|
40
|
-
when SteamPacket::A2A_ACK_HEADER
|
41
|
-
return A2A_ACK_Packet.new(data)
|
42
37
|
when SteamPacket::A2S_PLAYER_HEADER
|
43
38
|
return A2S_PLAYER_Packet.new
|
44
39
|
when SteamPacket::S2A_PLAYER_HEADER
|
@@ -55,10 +50,16 @@ module SteamPacketFactory
|
|
55
50
|
return A2M_GET_SERVERS_BATCH2_Packet.new(data)
|
56
51
|
when SteamPacket::M2A_SERVER_BATCH_HEADER
|
57
52
|
return M2A_SERVER_BATCH_Packet.new(data)
|
53
|
+
when SteamPacket::M2C_ISVALIDMD5_HEADER
|
54
|
+
return M2C_ISVALIDMD5_Packet.new(data)
|
55
|
+
when SteamPacket::M2S_REQUESTRESTART_HEADER
|
56
|
+
return M2S_REQUESTRESTART_Packet.new(data)
|
58
57
|
when SteamPacket::RCON_GOLDSRC_CHALLENGE_HEADER,
|
59
58
|
SteamPacket::RCON_GOLDSRC_NO_CHALLENGE_HEADER,
|
60
59
|
SteamPacket::RCON_GOLDSRC_RESPONSE_HEADER
|
61
60
|
return RCONGoldSrcResponse.new(data)
|
61
|
+
when SteamPacket::S2A_LOGSTRING_HEADER
|
62
|
+
return S2A_LOGSTRING_Packet.new(data)
|
62
63
|
else
|
63
64
|
raise SteamCondenserException.new("Unknown packet with header 0x#{header.to_s(16)} received.")
|
64
65
|
end
|
@@ -68,6 +69,8 @@ module SteamPacketFactory
|
|
68
69
|
packet_data = split_packets.join ''
|
69
70
|
|
70
71
|
if is_compressed
|
72
|
+
require 'zlib'
|
73
|
+
|
71
74
|
begin
|
72
75
|
require 'bz2'
|
73
76
|
packet_data = BZ2.uncompress(packet_data)
|
@@ -1,12 +1,11 @@
|
|
1
1
|
# This code is free software; you can redistribute it and/or modify it under the
|
2
2
|
# terms of the new BSD License.
|
3
3
|
#
|
4
|
-
# Copyright (c) 2008-
|
4
|
+
# Copyright (c) 2008-2011, Sebastian Staudt
|
5
5
|
|
6
6
|
require 'exceptions/steam_condenser_exception'
|
7
7
|
require 'steam/steam_player'
|
8
8
|
require 'steam/packets/a2s_info_packet'
|
9
|
-
require 'steam/packets/a2a_ping_packet'
|
10
9
|
require 'steam/packets/a2s_player_packet'
|
11
10
|
require 'steam/packets/a2s_rules_packet'
|
12
11
|
require 'steam/packets/a2s_serverquery_getchallenge_packet'
|
@@ -14,14 +13,59 @@ require 'steam/packets/s2a_info_base_packet'
|
|
14
13
|
require 'steam/packets/s2a_player_packet'
|
15
14
|
require 'steam/packets/s2a_rules_packet'
|
16
15
|
require 'steam/packets/s2c_challenge_packet'
|
16
|
+
require 'steam/servers/server'
|
17
17
|
|
18
18
|
module GameServer
|
19
19
|
|
20
|
+
include Server
|
21
|
+
|
20
22
|
REQUEST_CHALLENGE = 0
|
21
23
|
REQUEST_INFO = 1
|
22
24
|
REQUEST_PLAYER = 2
|
23
25
|
REQUEST_RULES = 3
|
24
26
|
|
27
|
+
# Parses the player attribute names supplied by +rcon status+
|
28
|
+
def self.player_status_attributes(status_header)
|
29
|
+
status_header.split.map do |attribute|
|
30
|
+
case attribute
|
31
|
+
when '#'
|
32
|
+
:id
|
33
|
+
when 'connected'
|
34
|
+
:time
|
35
|
+
when 'frag'
|
36
|
+
:score
|
37
|
+
else
|
38
|
+
attribute.to_sym
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Splits the player status obtained with +rcon status+
|
44
|
+
def self.split_player_status(attributes, player_status)
|
45
|
+
data = player_status[1..-1].strip.split '"'
|
46
|
+
data = [ data[0].split, data[1], data[2].split ]
|
47
|
+
data.flatten!
|
48
|
+
|
49
|
+
if attributes.size > data.size + 1
|
50
|
+
data.insert 1, nil
|
51
|
+
data.insert 4, nil, nil, nil
|
52
|
+
end
|
53
|
+
|
54
|
+
player_data = {}
|
55
|
+
data.each_index do |i|
|
56
|
+
player_data[attributes[i]] = data[i]
|
57
|
+
end
|
58
|
+
|
59
|
+
player_data
|
60
|
+
end
|
61
|
+
|
62
|
+
# Creates a new instance of a game server object
|
63
|
+
#
|
64
|
+
# The port defaults to 27015 for game servers
|
65
|
+
def initialize(address, port = 27015)
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
25
69
|
# Returns the last measured response time of this server
|
26
70
|
#
|
27
71
|
# If there's no data, update_ping is called to measure the current response
|
@@ -125,14 +169,18 @@ module GameServer
|
|
125
169
|
def update_player_info(rcon_password = nil)
|
126
170
|
handle_response_for_request GameServer::REQUEST_PLAYER
|
127
171
|
|
128
|
-
unless rcon_password.nil?
|
129
|
-
rcon_auth
|
130
|
-
players = rcon_exec('status').
|
131
|
-
|
172
|
+
unless rcon_password.nil? || @player_hash.nil? || @player_hash.empty?
|
173
|
+
rcon_auth rcon_password
|
174
|
+
players = rcon_exec('status').lines.select do |line|
|
175
|
+
line.start_with?('#') && line != "#end\n"
|
176
|
+
end
|
177
|
+
attributes = GameServer.player_status_attributes players.shift
|
132
178
|
|
133
179
|
players.each do |player|
|
134
|
-
player_data =
|
135
|
-
|
180
|
+
player_data = GameServer.split_player_status(attributes, player)
|
181
|
+
if @player_hash.key? player_data[:name]
|
182
|
+
@player_hash[player_data[:name]].add_info player_data
|
183
|
+
end
|
136
184
|
end
|
137
185
|
end
|
138
186
|
end
|
@@ -150,20 +198,13 @@ module GameServer
|
|
150
198
|
end
|
151
199
|
|
152
200
|
def update_ping
|
153
|
-
send_request
|
201
|
+
send_request A2S_INFO_Packet.new
|
154
202
|
start_time = Time.now
|
155
203
|
reply
|
156
204
|
end_time = Time.now
|
157
205
|
@ping = (end_time - start_time) * 1000
|
158
206
|
end
|
159
207
|
|
160
|
-
# Checks whether the listening port number of the server is in a valid range
|
161
|
-
def initialize(port_number = 27015)
|
162
|
-
unless port_number.to_i > 0 and port_number.to_i < 65536
|
163
|
-
raise ArgumentError.new('The listening port of the server has to be a number greater than 0 and less than 65536.')
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
208
|
def to_s
|
168
209
|
return_string = ''
|
169
210
|
|
@@ -10,20 +10,15 @@ class GoldSrcServer
|
|
10
10
|
|
11
11
|
include GameServer
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
player_data.flatten!
|
18
|
-
player_data[0] = player_data[2]
|
19
|
-
player_data.delete_at(2)
|
20
|
-
player_data.delete_at(4)
|
21
|
-
player_data
|
13
|
+
def initialize(address, port = 27015, is_hltv = false)
|
14
|
+
super address, port
|
15
|
+
|
16
|
+
@is_hltv = is_hltv
|
22
17
|
end
|
23
18
|
|
24
|
-
|
25
|
-
|
26
|
-
@socket = GoldSrcSocket.new ip_address,
|
19
|
+
# Initializes the socket to communicate with the GoldSrc server
|
20
|
+
def init_socket
|
21
|
+
@socket = GoldSrcSocket.new @ip_address, @port, @is_hltv
|
27
22
|
end
|
28
23
|
|
29
24
|
def rcon_auth(password)
|
@@ -1,13 +1,18 @@
|
|
1
1
|
# This code is free software; you can redistribute it and/or modify it under the
|
2
2
|
# terms of the new BSD License.
|
3
3
|
#
|
4
|
-
# Copyright (c) 2008-
|
4
|
+
# Copyright (c) 2008-2011, Sebastian Staudt
|
5
5
|
|
6
6
|
require 'steam/packets/a2m_get_servers_batch2_packet'
|
7
|
+
require 'steam/packets/c2m_checkmd5_packet'
|
8
|
+
require 'steam/packets/s2m_heartbeat2_packet'
|
9
|
+
require 'steam/servers/server'
|
7
10
|
require 'steam/sockets/master_server_socket'
|
8
11
|
|
9
12
|
class MasterServer
|
10
13
|
|
14
|
+
include Server
|
15
|
+
|
11
16
|
GOLDSRC_MASTER_SERVER = 'hl1master.steampowered.com', 27010
|
12
17
|
SOURCE_MASTER_SERVER = 'hl2master.steampowered.com', 27011
|
13
18
|
|
@@ -21,11 +26,30 @@ class MasterServer
|
|
21
26
|
REGION_AFRICA = 0x07
|
22
27
|
REGION_ALL = 0xFF
|
23
28
|
|
24
|
-
|
25
|
-
|
29
|
+
# Creates a new instance of a master server object
|
30
|
+
def initialize(address, port)
|
31
|
+
super
|
32
|
+
|
26
33
|
@server_array = []
|
27
34
|
end
|
28
35
|
|
36
|
+
# Request a challenge number from the master server. This is used for further
|
37
|
+
# communication with the master server.
|
38
|
+
#
|
39
|
+
# Please note that this is NOT needed for finding servers using the +servers+
|
40
|
+
# method
|
41
|
+
def challenge
|
42
|
+
failsafe do
|
43
|
+
@socket.send C2M_CHECKMD5_Packet.new
|
44
|
+
@socket.reply.challenge
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Initializes the socket to communicate with the master server
|
49
|
+
def init_socket
|
50
|
+
@socket = MasterServerSocket.new @ip_address, @port
|
51
|
+
end
|
52
|
+
|
29
53
|
def servers(region_code = MasterServer::REGION_ALL, filters = '')
|
30
54
|
update_servers region_code, filters if @server_array.empty?
|
31
55
|
@server_array
|
@@ -36,23 +60,47 @@ class MasterServer
|
|
36
60
|
finished = false
|
37
61
|
current_server = '0.0.0.0:0'
|
38
62
|
|
39
|
-
|
40
|
-
@socket.send A2M_GET_SERVERS_BATCH2_Packet.new(region_code, current_server, filters)
|
63
|
+
failsafe do
|
41
64
|
begin
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
65
|
+
@socket.send A2M_GET_SERVERS_BATCH2_Packet.new(region_code, current_server, filters)
|
66
|
+
begin
|
67
|
+
servers = @socket.reply.servers
|
68
|
+
servers.each do |server|
|
69
|
+
if server == '0.0.0.0:0'
|
70
|
+
finished = true
|
71
|
+
else
|
72
|
+
current_server = server
|
73
|
+
@server_array << server.split(':')
|
74
|
+
end
|
49
75
|
end
|
76
|
+
fail_count = 0
|
77
|
+
rescue TimeoutException
|
78
|
+
raise $! if (fail_count += 1) == 3
|
50
79
|
end
|
51
|
-
|
80
|
+
end while !finished
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Sends a constructed heartbeat to the master server
|
85
|
+
#
|
86
|
+
# This can be used to check server versions externally.
|
87
|
+
#
|
88
|
+
# The reply from the master server – usually zero or more packets. Zero means
|
89
|
+
# either the heartbeat was accepted by the master or there was a timeout. So
|
90
|
+
# usually it's best to repeat a heartbeat a few times when not receiving any
|
91
|
+
# packets.
|
92
|
+
def send_heartbeat(data)
|
93
|
+
failsafe do
|
94
|
+
@socket.send S2M_HEARTBEAT2_Packet.new(data)
|
95
|
+
|
96
|
+
reply_packets = []
|
97
|
+
begin
|
98
|
+
loop { reply_packets << @socket.reply }
|
52
99
|
rescue TimeoutException
|
53
|
-
raise $! if (fail_count += 1) == 3
|
54
100
|
end
|
55
|
-
end
|
101
|
+
end
|
102
|
+
|
103
|
+
reply_packets
|
56
104
|
end
|
57
105
|
|
58
106
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under
|
2
|
+
# the terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2011, Sebastian Staudt
|
5
|
+
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
# This module is included by all classes implementing server functionality
|
9
|
+
#
|
10
|
+
# It provides basic name resolution features.
|
11
|
+
module Server
|
12
|
+
|
13
|
+
# Creates a new server instance with the given address and port
|
14
|
+
def initialize(address, port = nil)
|
15
|
+
address = address.to_s
|
16
|
+
address, port = address.split(':', 2) if address.include? ':'
|
17
|
+
|
18
|
+
@host_names = []
|
19
|
+
@ip_addresses = []
|
20
|
+
@ip_index = 0
|
21
|
+
@port = port.to_i
|
22
|
+
|
23
|
+
Socket.getaddrinfo(address, port, Socket::AF_INET, Socket::SOCK_DGRAM).
|
24
|
+
each do |address|
|
25
|
+
@host_names << address[2]
|
26
|
+
@ip_addresses << address[3]
|
27
|
+
end
|
28
|
+
|
29
|
+
@ip_address = @ip_addresses.first
|
30
|
+
|
31
|
+
init_socket
|
32
|
+
end
|
33
|
+
|
34
|
+
# Rotate this server's IP address to the next one in the IP list
|
35
|
+
#
|
36
|
+
# This method will return +true+, if the IP list reached its end. If the list
|
37
|
+
# contains only one IP address, this method will instantly return +true+.
|
38
|
+
def rotate_ip
|
39
|
+
return true if @ip_addresses.size == 1
|
40
|
+
|
41
|
+
@ip_index = (@ip_index + 1) % @ip_addresses.size
|
42
|
+
@ip_address = @ip_addresses[@ip_index]
|
43
|
+
|
44
|
+
init_socket
|
45
|
+
|
46
|
+
@ip_index == 0
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
# Execute an action in the context of this server's current IP address
|
52
|
+
#
|
53
|
+
# Any failure, i.e. an exception, will cause the IP to rotate to the next IP
|
54
|
+
# in the server's IP list and the execution will be repeated for the next IP
|
55
|
+
# address. If the IP rotation reaches the end of the list, the exception will
|
56
|
+
# be reraised.
|
57
|
+
def failsafe(&proc)
|
58
|
+
begin
|
59
|
+
proc.call
|
60
|
+
rescue
|
61
|
+
raise $! if rotate_ip
|
62
|
+
failsafe &proc
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -1,12 +1,13 @@
|
|
1
1
|
# This code is free software; you can redistribute it and/or modify it under the
|
2
2
|
# terms of the new BSD License.
|
3
3
|
#
|
4
|
-
# Copyright (c) 2008-
|
4
|
+
# Copyright (c) 2008-2011, Sebastian Staudt
|
5
5
|
|
6
6
|
require 'exceptions/rcon_no_auth_exception'
|
7
7
|
require 'steam/packets/rcon/rcon_auth_request'
|
8
8
|
require 'steam/packets/rcon/rcon_auth_response'
|
9
9
|
require 'steam/packets/rcon/rcon_exec_request'
|
10
|
+
require 'steam/packets/rcon/rcon_terminator'
|
10
11
|
require 'steam/servers/game_server'
|
11
12
|
require 'steam/sockets/rcon_socket'
|
12
13
|
require 'steam/sockets/source_socket'
|
@@ -15,19 +16,14 @@ class SourceServer
|
|
15
16
|
|
16
17
|
include GameServer
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
player_data = player_status.match(/# *(\d+) +"(.*)" +(.*)/).to_a[1..-1]
|
21
|
-
player_data[2] = player_data[2].split
|
22
|
-
player_data.flatten!
|
23
|
-
player_data.delete_at(3)
|
24
|
-
player_data
|
19
|
+
def initialize(address, port = 27015)
|
20
|
+
super
|
25
21
|
end
|
26
22
|
|
27
|
-
|
28
|
-
|
29
|
-
@rcon_socket = RCONSocket.new ip_address,
|
30
|
-
@socket
|
23
|
+
# Initializes the sockets to communicate with the Source server
|
24
|
+
def init_socket
|
25
|
+
@rcon_socket = RCONSocket.new @ip_address, @port
|
26
|
+
@socket = SourceSocket.new @ip_address, @port
|
31
27
|
end
|
32
28
|
|
33
29
|
def rcon_auth(password)
|
@@ -44,17 +40,15 @@ class SourceServer
|
|
44
40
|
|
45
41
|
def rcon_exec(command)
|
46
42
|
@rcon_socket.send RCONExecRequest.new(@rcon_request_id, command)
|
43
|
+
@rcon_socket.send RCONTerminator.new(@rcon_request_id)
|
47
44
|
response_packets = []
|
48
45
|
|
49
46
|
begin
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
rescue TimeoutException
|
56
|
-
raise $! if response_packets.empty?
|
57
|
-
end
|
47
|
+
response_packet = @rcon_socket.reply
|
48
|
+
redo if response_packet.nil?
|
49
|
+
raise RCONNoAuthException.new if response_packet.is_a? RCONAuthResponse
|
50
|
+
response_packets << response_packet
|
51
|
+
end while response_packet.response.size > 0
|
58
52
|
|
59
53
|
response = ''
|
60
54
|
response_packets.each do |packet|
|
@@ -1,9 +1,11 @@
|
|
1
1
|
# This code is free software; you can redistribute it and/or modify it under the
|
2
2
|
# terms of the new BSD License.
|
3
3
|
#
|
4
|
-
# Copyright (c) 2008-
|
4
|
+
# Copyright (c) 2008-2011, Sebastian Staudt
|
5
5
|
|
6
6
|
require 'stringio_additions'
|
7
|
+
require 'exceptions/rcon_ban_exception'
|
8
|
+
require 'exceptions/rcon_no_auth_exception'
|
7
9
|
require 'steam/packets/steam_packet_factory'
|
8
10
|
require 'steam/packets/rcon/rcon_goldsrc_request'
|
9
11
|
require 'steam/sockets/steam_socket'
|
@@ -66,6 +68,7 @@ class GoldSrcSocket
|
|
66
68
|
rcon_challenge if @rcon_challenge.nil? or @is_hltv
|
67
69
|
|
68
70
|
rcon_send "rcon #{@rcon_challenge} #{password} #{command}"
|
71
|
+
rcon_send "rcon #{@rcon_challenge} #{password}"
|
69
72
|
if @is_hltv
|
70
73
|
begin
|
71
74
|
response = reply.response
|
@@ -76,17 +79,16 @@ class GoldSrcSocket
|
|
76
79
|
response = reply.response
|
77
80
|
end
|
78
81
|
|
79
|
-
if response.strip == 'Bad rcon_password.'
|
82
|
+
if response.strip == 'Bad rcon_password.'
|
80
83
|
raise RCONNoAuthException
|
84
|
+
elsif response.strip == 'You have been banned from this server.'
|
85
|
+
raise RCONBanException
|
81
86
|
end
|
82
87
|
|
83
88
|
begin
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
88
|
-
rescue TimeoutException
|
89
|
-
end
|
89
|
+
response_part = reply.response
|
90
|
+
response << response_part
|
91
|
+
end while response_part.size > 0
|
90
92
|
|
91
93
|
response
|
92
94
|
end
|
@@ -95,7 +97,9 @@ class GoldSrcSocket
|
|
95
97
|
rcon_send 'challenge rcon'
|
96
98
|
response = reply.response.strip
|
97
99
|
|
98
|
-
|
100
|
+
if response == 'You have been banned from this server.'
|
101
|
+
raise RCONBanException
|
102
|
+
end
|
99
103
|
|
100
104
|
@rcon_challenge = response[14..-1]
|
101
105
|
end
|