steam-condenser 0.8.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 (72) hide show
  1. data/LICENSE +25 -0
  2. data/README.md +30 -0
  3. data/Rakefile +42 -0
  4. data/lib/abstract_class.rb +40 -0
  5. data/lib/byte_buffer.rb +126 -0
  6. data/lib/datagram_channel.rb +70 -0
  7. data/lib/exceptions/buffer_underflow_exception.rb +10 -0
  8. data/lib/exceptions/packet_format_exception.rb +12 -0
  9. data/lib/exceptions/rcon_no_auth_exception.rb +14 -0
  10. data/lib/exceptions/steam_condenser_exception.rb +8 -0
  11. data/lib/exceptions/timeout_exception.rb +10 -0
  12. data/lib/socket_channel.rb +54 -0
  13. data/lib/steam-condenser.rb +3 -0
  14. data/lib/steam/community/dods/dods_class.rb +32 -0
  15. data/lib/steam/community/dods/dods_stats.rb +47 -0
  16. data/lib/steam/community/dods/dods_weapon.rb +54 -0
  17. data/lib/steam/community/game_achievement.rb +26 -0
  18. data/lib/steam/community/game_class.rb +15 -0
  19. data/lib/steam/community/game_stats.rb +92 -0
  20. data/lib/steam/community/game_weapon.rb +29 -0
  21. data/lib/steam/community/l4d/l4d_explosive.rb +23 -0
  22. data/lib/steam/community/l4d/l4d_map.rb +34 -0
  23. data/lib/steam/community/l4d/l4d_stats.rb +166 -0
  24. data/lib/steam/community/l4d/l4d_weapon.rb +23 -0
  25. data/lib/steam/community/steam_group.rb +16 -0
  26. data/lib/steam/community/steam_id.rb +212 -0
  27. data/lib/steam/community/tf2/tf2_class.rb +31 -0
  28. data/lib/steam/community/tf2/tf2_class_factory.rb +38 -0
  29. data/lib/steam/community/tf2/tf2_engineer.rb +25 -0
  30. data/lib/steam/community/tf2/tf2_medic.rb +22 -0
  31. data/lib/steam/community/tf2/tf2_sniper.rb +20 -0
  32. data/lib/steam/community/tf2/tf2_spy.rb +23 -0
  33. data/lib/steam/community/tf2/tf2_stats.rb +39 -0
  34. data/lib/steam/packets/a2a_ack_packet.rb +21 -0
  35. data/lib/steam/packets/a2a_ping_packet.rb +18 -0
  36. data/lib/steam/packets/a2m_get_servers_batch2_packet.rb +25 -0
  37. data/lib/steam/packets/a2s_info_packet.rb +18 -0
  38. data/lib/steam/packets/a2s_player_packet.rb +22 -0
  39. data/lib/steam/packets/a2s_rules_packet.rb +21 -0
  40. data/lib/steam/packets/a2s_serverquery_getchallenge_packet.rb +18 -0
  41. data/lib/steam/packets/m2a_server_batch_packet.rb +37 -0
  42. data/lib/steam/packets/rcon/rcon_auth_request.rb +16 -0
  43. data/lib/steam/packets/rcon/rcon_auth_response.rb +16 -0
  44. data/lib/steam/packets/rcon/rcon_exec_request.rb +16 -0
  45. data/lib/steam/packets/rcon/rcon_exec_response.rb +20 -0
  46. data/lib/steam/packets/rcon/rcon_goldsrc_request.rb +20 -0
  47. data/lib/steam/packets/rcon/rcon_goldsrc_response.rb +20 -0
  48. data/lib/steam/packets/rcon/rcon_packet.rb +36 -0
  49. data/lib/steam/packets/rcon/rcon_packet_factory.rb +37 -0
  50. data/lib/steam/packets/request_with_challenge.rb +16 -0
  51. data/lib/steam/packets/s2a_info2_packet.rb +48 -0
  52. data/lib/steam/packets/s2a_info_base_packet.rb +29 -0
  53. data/lib/steam/packets/s2a_info_detailed_packet.rb +50 -0
  54. data/lib/steam/packets/s2a_player_packet.rb +32 -0
  55. data/lib/steam/packets/s2a_rules_packet.rb +33 -0
  56. data/lib/steam/packets/s2c_challenge_packet.rb +23 -0
  57. data/lib/steam/packets/steam_packet.rb +48 -0
  58. data/lib/steam/packets/steam_packet_factory.rb +90 -0
  59. data/lib/steam/servers/game_server.rb +222 -0
  60. data/lib/steam/servers/goldsrc_server.rb +36 -0
  61. data/lib/steam/servers/master_server.rb +60 -0
  62. data/lib/steam/servers/source_server.rb +65 -0
  63. data/lib/steam/sockets/goldsrc_socket.rb +108 -0
  64. data/lib/steam/sockets/master_server_socket.rb +20 -0
  65. data/lib/steam/sockets/rcon_socket.rb +50 -0
  66. data/lib/steam/sockets/source_socket.rb +72 -0
  67. data/lib/steam/sockets/steam_socket.rb +63 -0
  68. data/lib/steam/steam_player.rb +51 -0
  69. data/test/query_tests.rb +71 -0
  70. data/test/rcon_tests.rb +79 -0
  71. data/test/steam_community_tests.rb +36 -0
  72. metadata +132 -0
@@ -0,0 +1,36 @@
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) 2008-2009, Sebastian Staudt
5
+
6
+ require "steam/servers/game_server"
7
+ require "steam/sockets/goldsrc_socket"
8
+
9
+ class GoldSrcServer < GameServer
10
+
11
+ # Splits the player status obtained with +rcon status+
12
+ def self.split_player_status(player_status)
13
+ player_data = player_status.match(/#( \d|\d\d) +"(.*)" +(\d+) +(.*)/).to_a[1..-1]
14
+ player_data[3] = player_data[3].split
15
+ player_data.flatten!
16
+ player_data[0] = player_data[2]
17
+ player_data.delete_at(2)
18
+ player_data.delete_at(4)
19
+ player_data
20
+ end
21
+
22
+ def initialize(ip_address, port_number = 27015, is_hltv = false)
23
+ super port_number
24
+ @socket = GoldSrcSocket.new ip_address, port_number, is_hltv
25
+ end
26
+
27
+ def rcon_auth(password)
28
+ @rcon_password = password
29
+ return true
30
+ end
31
+
32
+ def rcon_exec(command)
33
+ return @socket.rcon_exec(@rcon_password, command).strip
34
+ end
35
+
36
+ end
@@ -0,0 +1,60 @@
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) 2008-2009, Sebastian Staudt
5
+ #
6
+ # $Id$
7
+
8
+ require "steam/packets/a2m_get_servers_batch2_packet"
9
+ require "steam/sockets/master_server_socket"
10
+
11
+ class MasterServer
12
+
13
+ GOLDSRC_MASTER_SERVER = "hl1master.steampowered.com", 27010
14
+ SOURCE_MASTER_SERVER = "hl2master.steampowered.com", 27011
15
+
16
+ REGION_US_EAST_COAST = 0x00
17
+ REGION_US_WEST_COAST = 0x01
18
+ REGION_SOUTH_AMERICA = 0x02
19
+ REGION_EUROPE = 0x03
20
+ REGION_ASIA = 0x04
21
+ REGION_AUSTRALIA = 0x05
22
+ REGION_MIDDLE_EAST = 0x06
23
+ REGION_AFRICA = 0x07
24
+ REGION_ALL = 0xFF
25
+
26
+ def initialize(master_server_address, master_server_port)
27
+ @socket = MasterServerSocket.new master_server_address, master_server_port
28
+ @server_array = Array.new
29
+ end
30
+
31
+ def get_servers(region_code = MasterServer::REGION_ALL, filters = "")
32
+ if @server_array.empty?
33
+ self.update_servers region_code, filters
34
+ end
35
+
36
+ return @server_array
37
+ end
38
+
39
+ def update_servers(region_code, filters)
40
+ finished = false
41
+ current_server = "0.0.0.0:0"
42
+
43
+ begin
44
+ @socket.send A2M_GET_SERVERS_BATCH2_Packet.new(region_code, current_server, filters)
45
+ begin
46
+ servers = @socket.get_reply.get_servers
47
+ servers.each do |server|
48
+ if server == "0.0.0.0:0"
49
+ finished = true
50
+ else
51
+ current_server = server
52
+ @server_array << server.split(":")
53
+ end
54
+ end
55
+ rescue TimeoutException
56
+ end
57
+ end while !finished
58
+ end
59
+
60
+ end
@@ -0,0 +1,65 @@
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) 2008-2009, Sebastian Staudt
5
+
6
+ require "exceptions/rcon_no_auth_exception"
7
+ require "steam/packets/rcon/rcon_auth_request"
8
+ require "steam/packets/rcon/rcon_auth_response"
9
+ require "steam/packets/rcon/rcon_exec_request"
10
+ require "steam/servers/game_server"
11
+ require "steam/sockets/rcon_socket"
12
+ require "steam/sockets/source_socket"
13
+
14
+ class SourceServer < GameServer
15
+
16
+ # Splits the player status obtained with +rcon status+
17
+ def self.split_player_status(player_status)
18
+ player_data = player_status.match(/#( \d|\d\d) +"(.*)" +(.*)/).to_a[1..-1]
19
+ player_data[2] = player_data[2].split
20
+ player_data.flatten
21
+ end
22
+
23
+ def initialize(ip_address, port_number = 27015)
24
+ super port_number
25
+ @rcon_socket = RCONSocket.new ip_address, port_number
26
+ @socket = SourceSocket.new ip_address, port_number
27
+ end
28
+
29
+ def rcon_auth(password)
30
+ @rcon_request_id = rand 2**16
31
+
32
+ @rcon_socket.send RCONAuthRequest.new(@rcon_request_id, password)
33
+ @rcon_socket.get_reply
34
+ reply = @rcon_socket.get_reply
35
+
36
+ if reply.request_id == -1
37
+ raise RCONNoAuthException.new
38
+ end
39
+
40
+ return reply.get_request_id == @rcon_request_id
41
+ end
42
+
43
+ def rcon_exec(command)
44
+ @rcon_socket.send RCONExecRequest.new(@rcon_request_id, command)
45
+ response_packets = Array.new
46
+
47
+ begin
48
+ begin
49
+ response_packet = @rcon_socket.get_reply
50
+ raise RCONNoAuthException.new if response_packet.is_a? RCONAuthResponse
51
+ response_packets << response_packet
52
+ end while true
53
+ rescue TimeoutException
54
+ raise $! if response_packets.empty?
55
+ end
56
+
57
+ response = String.new
58
+ response_packets.each do |packet|
59
+ response << packet.get_response
60
+ end
61
+
62
+ return response.strip
63
+ end
64
+
65
+ end
@@ -0,0 +1,108 @@
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) 2008-2009, Sebastian Staudt
5
+
6
+ require "byte_buffer"
7
+ require "steam/packets/steam_packet_factory"
8
+ require "steam/packets/rcon/rcon_goldsrc_request"
9
+ require "steam/sockets/steam_socket"
10
+
11
+ # The SourceSocket class is a sub class of SteamSocket respecting the
12
+ # specifications of the Source query protocol.
13
+ class GoldSrcSocket < SteamSocket
14
+
15
+ def initialize(ipaddress, port_number = 27015, is_hltv = false)
16
+ super ipaddress, port_number
17
+ @is_hltv = is_hltv
18
+ end
19
+
20
+ # Reads a packet from the channel. The Source query protocol specifies a
21
+ # maximum packet size of 1400 byte. Greater packets will be split over several
22
+ # UDP packets. This method reassembles split packets into single packet
23
+ # objects.
24
+ def get_reply
25
+ bytes_read = self.receive_packet 1400
26
+
27
+ if @buffer.get_long == 0xFFFFFFFE
28
+ split_packets = Array.new
29
+ begin
30
+ # Parsing of split packet headers
31
+ request_id = @buffer.get_long
32
+ packet_number_and_count = @buffer.get_byte.to_i
33
+ packet_count = packet_number_and_count & 0xF
34
+ packet_number = (packet_number_and_count >> 4) + 1
35
+
36
+ # Caching of split packet data
37
+ split_packets[packet_number - 1] = @buffer.get
38
+
39
+ warn "Received packet #{packet_number} of #{packet_count} for request ##{request_id}"
40
+
41
+ # Receiving the next packet
42
+ if split_packets.size < packet_count
43
+ begin
44
+ bytes_read = self.receive_packet
45
+ rescue TimeoutException
46
+ bytes_read = 0
47
+ end
48
+ else
49
+ bytes_read = 0
50
+ end
51
+ end while bytes_read > 0 && @buffer.get_long == 0xFFFFFFFE
52
+
53
+ packet = SteamPacketFactory.reassemble_packet(split_packets)
54
+
55
+ else
56
+ packet = SteamPacketFactory.get_packet_from_data(@buffer.get)
57
+ end
58
+
59
+ warn "Got reply of type \"#{packet.class.to_s}\"."
60
+
61
+ return packet
62
+ end
63
+
64
+ def rcon_exec(password, command)
65
+ self.rcon_get_challenge if @rcon_challenge.nil? or @is_hltv
66
+
67
+ self.rcon_send "rcon #{@rcon_challenge} #{password} #{command}"
68
+ if @is_hltv
69
+ begin
70
+ response = self.get_reply.get_response
71
+ rescue TimeoutException
72
+ response = ""
73
+ end
74
+ else
75
+ response = self.get_reply.get_response
76
+ end
77
+
78
+ if response.strip == "Bad rcon_password." or response.strip == "You have been banned from this server."
79
+ raise RCONNoAuthException.new
80
+ end
81
+
82
+ begin
83
+ while true
84
+ response_part = self.get_reply.get_response
85
+ response << response_part
86
+ end
87
+ rescue TimeoutException
88
+ end
89
+
90
+ return response
91
+ end
92
+
93
+ def rcon_get_challenge
94
+ self.rcon_send "challenge rcon"
95
+ response = self.get_reply.get_response.strip
96
+
97
+ if response == "You have been banned from this server."
98
+ raise RCONNoAuthException.new;
99
+ end
100
+
101
+ @rcon_challenge = response[14..-1]
102
+ end
103
+
104
+ def rcon_send(command)
105
+ self.send RCONGoldSrcRequest.new(command)
106
+ end
107
+
108
+ end
@@ -0,0 +1,20 @@
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) 2008-2009, Sebastian Staudt
5
+
6
+ require "steam/sockets/steam_socket"
7
+
8
+ class MasterServerSocket < SteamSocket
9
+
10
+ def get_reply
11
+ self.receive_packet 1500
12
+
13
+ if @buffer.get_long != 0xFFFFFFFF
14
+ raise Exception.new("Master query response has wrong packet header.")
15
+ end
16
+
17
+ return SteamPacketFactory.get_packet_from_data(@buffer.get)
18
+ end
19
+
20
+ end
@@ -0,0 +1,50 @@
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) 2008, Sebastian Staudt
5
+ #
6
+ # $Id$
7
+
8
+ require "socket_channel"
9
+ require "steam/packets/rcon/rcon_packet"
10
+ require "steam/packets/rcon/rcon_packet_factory"
11
+ require "steam/sockets/steam_socket"
12
+
13
+ class RCONSocket < SteamSocket
14
+
15
+ def initialize(ip_address, port_number)
16
+ super ip_address, port_number
17
+ @channel = SocketChannel.open
18
+ end
19
+
20
+ def send(data_packet)
21
+ if !@channel.connected?
22
+ @channel.connect @remote_socket
23
+ end
24
+
25
+ @buffer = ByteBuffer.wrap data_packet.get_bytes
26
+ @channel.write @buffer
27
+ end
28
+
29
+ def get_reply
30
+ self.receive_packet 1440
31
+ packet_data = @buffer.array[0..@buffer.limit]
32
+ packet_size = @buffer.get_long + 4
33
+
34
+ if packet_size > 1440
35
+ remaining_bytes = packet_size - 1440
36
+ begin
37
+ if remaining_bytes < 1440
38
+ self.receive_packet remaining_bytes
39
+ else
40
+ self.receive_packet 1440
41
+ end
42
+ packet_data << @buffer.array[0..@buffer.limit]
43
+ remaining_bytes -= @buffer.limit
44
+ end while remaining_bytes > 0
45
+ end
46
+
47
+ return RCONPacketFactory.get_packet_from_data(packet_data)
48
+ end
49
+
50
+ end
@@ -0,0 +1,72 @@
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) 2008-2009, Sebastian Staudt
5
+
6
+ require "byte_buffer"
7
+ require "steam/sockets/steam_socket"
8
+
9
+ # The SourceSocket class is a sub class of SteamSocket respecting the
10
+ # specifications of the Source query protocol.
11
+ class SourceSocket < SteamSocket
12
+
13
+ # Reads a packet from the channel. The Source query protocol specifies a
14
+ # maximum packet size of 1400 byte. Greater packets will be split over several
15
+ # UDP packets. This method reassembles split packets into single packet
16
+ # objects.
17
+ def get_reply
18
+ bytes_read = self.receive_packet 1400
19
+ is_compressed = false
20
+
21
+ if @buffer.get_long == 0xFFFFFFFE
22
+ split_packets = Array.new
23
+ begin
24
+ # Parsing of split packet headers
25
+ request_id = @buffer.get_long
26
+ is_compressed = ((request_id & 0x80000000) != 0)
27
+ packet_count = @buffer.get_byte.to_i
28
+ packet_number = @buffer.get_byte.to_i + 1
29
+
30
+ if is_compressed
31
+ split_size = @buffer.get_long
32
+ packet_checksum = @buffer.get_long
33
+ else
34
+ split_size = @buffer.get_short
35
+ end
36
+
37
+ # Caching of split packet data
38
+ split_packets[packet_number - 1] = @buffer.get
39
+
40
+ warn "Received packet #{packet_number} of #{packet_count} for request ##{request_id}"
41
+
42
+ # Receiving the next packet
43
+ if split_packets.size < packet_count
44
+ begin
45
+ bytes_read = self.receive_packet
46
+ rescue TimeoutException
47
+ bytes_read = 0
48
+ end
49
+ else
50
+ bytes_read = 0
51
+ end
52
+ end while bytes_read > 0 && @buffer.get_long == 0xFFFFFFFE
53
+
54
+ if is_compressed
55
+ packet = SteamPacketFactory.reassemble_packet(split_packets, true, packet_checksum)
56
+ else
57
+ packet = SteamPacketFactory.reassemble_packet(split_packets)
58
+ end
59
+ else
60
+ packet = SteamPacketFactory.get_packet_from_data(@buffer.get)
61
+ end
62
+
63
+ if is_compressed
64
+ warn "Got compressed reply of type \"#{packet.class.to_s}\"."
65
+ else
66
+ warn "Got reply of type \"#{packet.class.to_s}\"."
67
+ end
68
+
69
+ return packet
70
+ end
71
+
72
+ end
@@ -0,0 +1,63 @@
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) 2008, Sebastian Staudt
5
+ #
6
+ # $Id$
7
+
8
+ require "abstract_class"
9
+ require "byte_buffer"
10
+ require "datagram_channel"
11
+ require "ipaddr"
12
+ require "exceptions/timeout_exception"
13
+
14
+ # The SteamSocket class is a sub class of CondenserSocket respecting the
15
+ # specifications of the Source query protocol.
16
+ class SteamSocket
17
+
18
+ include AbstractClass
19
+
20
+ def initialize(*args)
21
+ @channel = DatagramChannel.open
22
+ @channel.connect *args
23
+ @channel.configure_blocking false
24
+
25
+ @remote_socket = Socket.getaddrinfo args[0].to_s, args[1]
26
+ end
27
+
28
+ # Abstract get_reply method
29
+ def get_reply
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def receive_packet(buffer_length = 0)
34
+ if select([@channel.socket], nil, nil, 1) == nil
35
+ raise TimeoutException.new
36
+ end
37
+
38
+ if buffer_length == 0
39
+ @buffer.clear
40
+ else
41
+ @buffer = ByteBuffer.allocate buffer_length
42
+ end
43
+
44
+ @channel.read @buffer
45
+ bytes_read = @buffer.position
46
+ @buffer.rewind
47
+ @buffer.limit = bytes_read
48
+
49
+ return bytes_read
50
+ end
51
+
52
+ def send(data_packet)
53
+ warn "Sending data packet of type \"#{data_packet.class.to_s}\"."
54
+
55
+ @buffer = ByteBuffer.wrap data_packet.to_s
56
+ @channel.write @buffer
57
+ end
58
+
59
+ def finalize
60
+ @channel.close
61
+ end
62
+
63
+ end