steam_mist 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,146 @@
1
+ module SteamMist
2
+ class Rcon
3
+
4
+ # Represents a packet either received from the server or sent by the
5
+ # client.
6
+ class Packet
7
+
8
+ # This is used as a {#type}. This is for the client, authenticating to
9
+ # the server.
10
+ SERVERDATA_AUTH = 3
11
+
12
+ # Used for {#type}. This is for the response from the server from the
13
+ # client.
14
+ SERVERDATA_AUTH_RESPONSE = 2
15
+
16
+ # Used for {#type}. This is for the client executing a response to the
17
+ # server.
18
+ SERVERDATA_EXECCOMMAND = 2
19
+
20
+ # Used for {#type}. This is for the server sending back response data
21
+ # for `EXECCOMMAND`.
22
+ SERVERDATA_RESPONSE_VALUE = 0
23
+
24
+ # This matches the requests with their responses.
25
+ RESPONSE_MATCH = { SERVERDATA_AUTH => SERVERDATA_AUTH_RESPONSE,
26
+ SERVERDATA_EXECCOMMAND => SERVERDATA_RESPONSE_VALUE }
27
+
28
+ # The id of the packet. This is mainly used to match request packets
29
+ # with their response.
30
+ #
31
+ # @return [Numeric]
32
+ attr_accessor :id
33
+
34
+ # The type of the packet.
35
+ #
36
+ # @return [Numeric]
37
+ attr_accessor :type
38
+
39
+ # The body of the packet.
40
+ #
41
+ # @return [Numeric]
42
+ attr_accessor :body
43
+
44
+ # Initialize the packet.
45
+ #
46
+ # @param id [Numeric] the id of the packet.
47
+ def initialize(id=nil)
48
+ @id = id || 0
49
+ @type = SERVERDATA_EXECCOMMAND
50
+ @body = ""
51
+ end
52
+
53
+ # Formats the packet for sending to the server. See
54
+ # [this](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol)
55
+ # on how it's done.
56
+ #
57
+ # @return [String] a formatted string containing the data.
58
+ def format
59
+ [size, id, type, body].pack("l<l<l<a#{body.bytesize+1}x")
60
+ end
61
+
62
+ # This returns the size of the packet. This is the size of the body, in
63
+ # bytes. It also adds 10 bytes for the type (4 bytes), id (4 bytes), and
64
+ # the two nul-terminators (one for the body and one for the packet).
65
+ #
66
+ # @return [Numeric]
67
+ def size
68
+ body.bytesize + 10
69
+ end
70
+
71
+ # Compare this with another object. Calls {#to_i} on the other object
72
+ # and this one and delegates to that.
73
+ #
74
+ # @return [Numeric]
75
+ def <=>(other)
76
+ self.to_i <=> other.to_i
77
+ end
78
+
79
+ # Shows whether or not the packet is empty. The packet is not empty if
80
+ # the body contains more than "" and the type is not 2.
81
+ #
82
+ # @return [Boolean]
83
+ def empty?
84
+ body.empty? && (type != 2)
85
+ end
86
+
87
+ # This checks to see if it is SRCDS's weird packet response.
88
+ #
89
+ # @return [Boolean]
90
+ def weird?
91
+ (type == 0) and (body == "\x00\x01\x00\x00")
92
+ end
93
+
94
+ # This loads the packet data from a hash. Overwrites the contents of the
95
+ # packet.
96
+ #
97
+ # @param hash [Hash]
98
+ # @return [self]
99
+ def load!(hash)
100
+ self.id = hash[:id] || hash["id"] || id
101
+ self.type = hash[:type] || hash["type"] || type
102
+ self.body = hash[:body] || hash["body"] || body
103
+ self
104
+ end
105
+
106
+ alias :to_i :id
107
+
108
+ # This takes a formatted string and turns it into a packet instance.
109
+ # This is mainly used for responses from the server.
110
+ #
111
+ # @param raw [String] the raw data from the server.
112
+ # @return [Packet] the packet representing the data.
113
+ def self.from_raw(raw)
114
+ packet = Packet.new
115
+ size, = raw.unpack("l<")
116
+ _, packet.id, packet.type, packet.body =
117
+ raw.unpack("l<l<l<a#{size - 10}xx")
118
+ packet
119
+ end
120
+
121
+ # This reads from a stream and converts it into a packet.
122
+ #
123
+ # @param socket [#read] the stream to read from.
124
+ # @return [Packet] the packet representing the data.
125
+ def self.from_stream(socket)
126
+ packet = Packet.new
127
+ size, = socket.read(4).unpack("l<")
128
+ packet.id, packet.type, packet.body =
129
+ socket.read(size).unpack("l<l<a#{size - 10}xx")
130
+ packet
131
+ end
132
+
133
+ # This sets up the packet from a given hash.
134
+ #
135
+ # @param data [Hash] the data to map to the packet.
136
+ # @return [Packet]
137
+ def self.from_hash(data)
138
+ packet = Packet.new
139
+ packet.load! data
140
+
141
+ packet
142
+ end
143
+
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,45 @@
1
+ require 'set'
2
+
3
+ module SteamMist
4
+ class Rcon
5
+
6
+ # Creates packets for use.
7
+ class PacketFactory
8
+
9
+ # An {SortedSet} containing all of the packets that have been created.
10
+ #
11
+ # @return [SortedSet]
12
+ attr_accessor :packets
13
+
14
+ # The current packet number.
15
+ #
16
+ # @return [Numeric]
17
+ attr_accessor :number
18
+
19
+ # initializes.
20
+ def initialize
21
+ @number = 1
22
+ @packets = SortedSet.new
23
+ end
24
+
25
+ # Creates a packet. Automatically assigns it an id, based off of the
26
+ # current packet number.
27
+ #
28
+ # @return [Packet]
29
+ def create_packet
30
+ packet = Packet.new(@number)
31
+ @packets << packet
32
+ @number = @number + 1
33
+ packet
34
+ end
35
+
36
+ # Pretty inspect
37
+ #
38
+ # @return [String]
39
+ def inspect
40
+ "#<SteamMist::Rcon::PacketFactory #{@number}>"
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,123 @@
1
+ module SteamMist
2
+ class Rcon
3
+
4
+ # This basically handles sending and receiving packets.
5
+ class Pass
6
+
7
+ # The packet factory that should be used when creating packets. This
8
+ # factory is used to increment the ID number on sequential packets.
9
+ #
10
+ # @return [PacketFactory]
11
+ attr_reader :packet_factory
12
+
13
+ # The listener for sending and receiving data from the server.
14
+ #
15
+ # @return [Listener]
16
+ attr_reader :listener
17
+
18
+ # Initialize.
19
+ #
20
+ # @param ip [String] the IP address of the Rcon server. Passed to
21
+ # Listener.
22
+ # @param port [Numeric] the port of the Rcon server. Passed to Listener.
23
+ def initialize(ip, port = 27015)
24
+ @packet_factory = PacketFactory.new
25
+ @listener = Listener.new ip, port
26
+ @empty_packet = Packet.from_hash :type => Packet::SERVERDATA_RESPONSE_VALUE
27
+ end
28
+
29
+ # Retrieves the next packet from the stream. If a block is given, it
30
+ # yields to that.
31
+ #
32
+ # @yieldparam packet [Packet]
33
+ # @return [Packet]
34
+ def on_packet(&block)
35
+ ensure_connected.on_data do |con|
36
+ packet = Packet.from_stream con
37
+
38
+ if block
39
+ block.call packet
40
+ end
41
+
42
+ packet
43
+ end
44
+ end
45
+
46
+ # Sends the given packet to the server.
47
+ #
48
+ # @param packet [Packet]
49
+ # @return [void, Numeric]
50
+ def write(packet)
51
+ ensure_connected.write packet.format
52
+ end
53
+
54
+ # Sends a packet along with an empty packet. This allows the client to
55
+ # figure out the server sent multiple packets to the client for the same
56
+ # request packet. Returns an array of packets if the server sent
57
+ # multiple packets.
58
+ #
59
+ # @param packet [Packet] the packet to send
60
+ # @return [Packet, Array<Packet>]
61
+ def send_packet(packet)
62
+ empty_packet = @empty_packet.dup
63
+ empty_packet.id = packet.id
64
+ write(packet) and write(empty_packet)
65
+ response_packets = []
66
+
67
+ begin
68
+ while response_packets.empty? || !response_packets.last.weird? do
69
+ response_packets << on_packet
70
+ end
71
+
72
+ rescue TimeoutError; end
73
+
74
+ response_packets.pop(2)
75
+
76
+ if response_packets.length == 1
77
+ response_packets[0]
78
+ else
79
+ response_packets
80
+ end
81
+ end
82
+
83
+ # This authenticates the connection with the server. A password is
84
+ # required.
85
+ #
86
+ # @param password [String] the password to authenticate with.
87
+ # @return [Boolean]
88
+ def auth(password)
89
+ packet = packet_factory.create_packet
90
+ packet.type = Packet::SERVERDATA_AUTH
91
+ packet.body = password
92
+
93
+ #response = send_packet packet
94
+ write packet
95
+
96
+ # discard
97
+ on_packet
98
+
99
+ response = on_packet
100
+
101
+ response.id == packet.id
102
+ end
103
+
104
+ # This closes the connection by calling {Listener#close}.
105
+ #
106
+ # @return [void]
107
+ def close
108
+ listener.close
109
+ end
110
+
111
+ private
112
+
113
+ # This makes sure the listener is connected before trying to perform an
114
+ # I/O operation.
115
+ #
116
+ # @return [Listener]
117
+ def ensure_connected
118
+ listener.bind! unless listener.connection
119
+ listener
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,47 @@
1
+ require 'forwardable'
2
+ require 'steam_mist/rcon/pass'
3
+ require 'steam_mist/rcon/packet'
4
+ require 'steam_mist/rcon/listener'
5
+ require 'steam_mist/rcon/packet_factory'
6
+
7
+ module SteamMist
8
+
9
+ # This class provides RCON capabilities.
10
+ class Rcon
11
+
12
+ extend Forwardable
13
+
14
+ # This is the class that handles reading and writing packets from the
15
+ # listener.
16
+ #
17
+ # @return [Pass]
18
+ attr_reader :pass
19
+
20
+ # Initialize.
21
+ #
22
+ # @param ip [String] the IP to connect to.
23
+ # @param port [Numeric] the port to connect to.
24
+ def initialize(ip, port = 27015)
25
+ @pass = Pass.new(ip, port)
26
+ end
27
+
28
+ # This takes a hash, turns it into a packet, and sends it.
29
+ #
30
+ # @param hash [Hash] the data to turn into a packet.
31
+ # @return [Packet, Array<Packet>]
32
+ def send(hash)
33
+ send_packet packet_factory.create_packet.load!(hash)
34
+ end
35
+
36
+ # Pretty inspect
37
+ #
38
+ # @return [String]
39
+ def inspect
40
+ "#<SteamMist::Rcon>"
41
+ end
42
+
43
+ def_delegators :@pass, :on_packet, :send_packet, :auth, :close,
44
+ :packet_factory
45
+
46
+ end
47
+ end
@@ -0,0 +1,77 @@
1
+ module SteamMist
2
+
3
+ # This represents a request that may be made to the steam api. It is mainly
4
+ # used for obtaining paths to request to.
5
+ class RequestUri
6
+
7
+ extend Forwardable
8
+
9
+ # This is the interface the request will be made to, like +ISteamUser+.
10
+ #
11
+ # @return [String]
12
+ attr_accessor :interface
13
+
14
+ # This is the method of the interface the request will be made to.
15
+ #
16
+ # @return [String]
17
+ attr_accessor :method
18
+
19
+ # These are the arguments that will be passed for the request.
20
+ #
21
+ # @return [Enumerable]
22
+ attr_accessor :arguments
23
+
24
+ # The version of the method to request.
25
+ #
26
+ # @return [Numeric]
27
+ attr_accessor :version
28
+
29
+ # The domain to make the reuqest to.
30
+ #
31
+ # @return [String]
32
+ attr_accessor :domain
33
+
34
+ # Initialize the request. Can take a hash. Options for the hash can be
35
+ # `:interface`, `:method`, `:version`, `:domain` and `:arguments`.
36
+ # Anything else will cause an argument error. See the attributes for
37
+ # each respectively on what they're for.
38
+ #
39
+ # @param options [Hash] the options to be used.
40
+ def initialize(options)
41
+ {
42
+ :interface => "",
43
+ :method => "",
44
+ :arguments => {},
45
+ :version => 0,
46
+ :domain => "api.steampowered.com"
47
+ }.merge(options).each do |k, v|
48
+ if respond_to? "#{k}="
49
+ send "#{k}=", v
50
+ else
51
+ raise ArgumentError, "don't know how to handle #{k}!"
52
+ end
53
+ end
54
+ end
55
+
56
+ # Takes the request data and formats it into an URI.
57
+ #
58
+ # @return [URI] the URI of the request.
59
+ def format_uri
60
+ basic = "http://%s/%s/%s/v%04d" % [domain, interface, method, version]
61
+
62
+ uri = URI(basic)
63
+ uri.query = URI.encode_www_form(arguments)
64
+
65
+ uri
66
+ end
67
+
68
+ # Outputs a string version of the request.
69
+ #
70
+ # @return [String] the fully formated URL of the request.
71
+ def to_s
72
+ format_uri.to_s
73
+ end
74
+
75
+ def_delegator :format_uri, :open
76
+ end
77
+ end
@@ -0,0 +1,42 @@
1
+ require 'set'
2
+ require 'hashie/mash'
3
+
4
+ module SteamMist
5
+
6
+ # Handles the Schema for games.
7
+ #
8
+ # @todo More tests.
9
+ class Schema
10
+
11
+ # Initialize the schema. The first argument is the application id,
12
+ # which valve uses internally to distinguish games from each other.
13
+ #
14
+ # @param app_id [Numeric]
15
+ # @param lang [String] the language the schema should return its
16
+ # results in.
17
+ def initialize(key, app_id, lang = nil)
18
+ @app_id = app_id
19
+ @session = Session.new
20
+ @get_schema = @session.get_interface("IEconItems_#{app_id}").get_schema
21
+
22
+ if lang
23
+ @get_schema.with_arguments!(:language => lang)
24
+ end
25
+ end
26
+
27
+ # Returns a list of items in the schema.
28
+ #
29
+ # @return [SortedSet<Item>]
30
+ def items
31
+ @_items ||= data.items
32
+ end
33
+
34
+ # Retrieves the data from the request.
35
+ #
36
+ # @return [Hashie::Mash]
37
+ def data
38
+ @_data ||= Hashie::Mash.new(@get_schema.get.data["result"])
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,60 @@
1
+ # We're placing these here so that if someone wants to only use this part of
2
+ # the library, they don't have to require the entire thing.
3
+ require 'oj'
4
+ require 'uri'
5
+ require 'forwardable'
6
+ require 'steam_mist/version'
7
+ require 'steam_mist/connector'
8
+ require 'steam_mist/connectors'
9
+ require 'steam_mist/request_uri'
10
+ require 'steam_mist/pseudo_interface'
11
+ require 'steam_mist/schema'
12
+
13
+ module SteamMist
14
+
15
+ # The session is used as a starting point for connecting to the Steam API.
16
+ class Session
17
+
18
+ # The connector that the session is going to use.
19
+ #
20
+ # @return [Class] the connector.
21
+ attr_accessor :connector
22
+
23
+ # The default arguments that the session will use.
24
+ #
25
+ # @return [Hash]
26
+ attr_accessor :default_arguments
27
+
28
+ # Initialize.
29
+ #
30
+ # @param connector [Class] the connector (should be subclass of {Connector}).
31
+ def initialize(connector = Connectors::LazyConnector)
32
+ @connector = connector
33
+ @_interfaces = {}
34
+ @default_arguments = {}
35
+ end
36
+
37
+ # Grabs an interface for use. These are cached, so every call with the
38
+ # same argument returns the same object.
39
+ #
40
+ # @param interface [Symbol] the interface name.
41
+ # @return [PseudoInterface]
42
+ def get_interface(interface)
43
+ @_interfaces[interface] ||= PseudoInterface.new(self, interface)
44
+ end
45
+
46
+ # Some {#method_missing} magic. Sorry @charliesome!
47
+ #
48
+ # @see {#get_interface}
49
+ def method_missing(method, *args)
50
+ super if args.length > 0 or block_given?
51
+ get_interface method
52
+ end
53
+
54
+ # pretty inspection
55
+ def inspect
56
+ "#<SteamMist::Session #{connector.inspect}>"
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,5 @@
1
+ module SteamMist
2
+
3
+ # The version of Steam Mist.
4
+ VERSION = "1.3.0"
5
+ end
data/lib/steam_mist.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'steam_mist/rcon'
2
+ require 'steam_mist/session'
3
+
4
+ # Steam Mist is an interface for the Web API.
5
+ module SteamMist
6
+
7
+ end
@@ -0,0 +1,25 @@
1
+ describe SteamMist::Connectors::LazyConnector do
2
+
3
+ subject { described_class.new(request_uri) }
4
+ let(:request_uri) { "https://api.twitter.com/1/statuses/oembed.json?id=133640144317198338" }
5
+
6
+ it "can cache" do
7
+ subject.enable_caching "tmp/example_cache.json"
8
+ expect(subject).to be_cache
9
+ end
10
+
11
+ it "does cache" do
12
+ subject.enable_caching "tmp/example_cache.json"
13
+ subject.data
14
+ expect { |p| subject.send(:with_cache, &p) }.to_not yield_control
15
+ end
16
+
17
+ it "loads from cache file" do
18
+ subject.enable_caching "tmp/example_cache.json"
19
+ expect { |p| subject.send(:with_cache, &p) }.to_not yield_control
20
+ end
21
+
22
+ after :all do
23
+ File.unlink "tmp/example_cache.json"
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ describe SteamMist::Rcon::PacketFactory do
2
+ it "should generate sequential packets" do
3
+ packet1 = subject.create_packet
4
+ packet2 = subject.create_packet
5
+
6
+ (packet1.id + 1).should be packet2.id
7
+ end
8
+ end
@@ -0,0 +1,26 @@
1
+ require 'stringio'
2
+
3
+ describe SteamMist::Rcon::Packet do
4
+ it "should format itself correctly" do
5
+ subject.id = 5
6
+ subject.type = 2
7
+ subject.body << "hello world"
8
+
9
+ subject.format.should eq "\x15\0\0\0\x05\0\0\0\x02\0\0\0hello world\0\0"
10
+ end
11
+
12
+ it "should properly deserialize data from raw" do
13
+ packet = described_class.from_raw "\x15\0\0\0\x05\0\0\0\x02\0\0\0hello world\0\0"
14
+ packet.id.should be 5
15
+ packet.type.should be 2
16
+ packet.body.should eq "hello world"
17
+ end
18
+
19
+ it "should properly deserialize data from a stream" do
20
+ s = StringIO.new "\x15\0\0\0\x05\0\0\0\x02\0\0\0hello world\0\0"
21
+ packet = described_class.from_stream s
22
+ packet.id.should be 5
23
+ packet.type.should be 2
24
+ packet.body.should eq "hello world"
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ describe SteamMist::PseudoInterface do
2
+
3
+ subject(:interface) do
4
+ described_class.new(nil, :some_interface)
5
+ end
6
+
7
+ it "turns the interface name into the correct api name" do
8
+ interface.api_name.should == "ISomeInterface"
9
+ end
10
+
11
+ it "gives a pseudo method" do
12
+ interface.get_method(:some_method) \
13
+ .should be_instance_of SteamMist::PseudoInterface::PseudoMethod
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ describe SteamMist::PseudoInterface::PseudoMethod do
2
+
3
+ subject(:pseudo_method) do
4
+ session = SteamMist::Session.new(SteamMist::Connectors::LazyConnector)
5
+ session.default_arguments[:something] = "value"
6
+ interface = SteamMist::PseudoInterface.new(session, :some_interface)
7
+ described_class.new(interface, :some_method, 4)
8
+ end
9
+
10
+ it "should turn the method name into an api name" do
11
+ pseudo_method.api_name.should == "SomeMethod"
12
+ end
13
+
14
+ it "should return a copy when adding arguments" do
15
+ pseudo_method.with_arguments(:hello => "world").should_not be pseudo_method
16
+ end
17
+
18
+ it "should return a copy when changing versions" do
19
+ pseudo_method.with_version(3).should_not be pseudo_method
20
+ end
21
+
22
+ it "should return a request uri" do
23
+ pseudo_method.request_uri.should be_instance_of SteamMist::RequestUri
24
+ end
25
+
26
+ it "should give a connector instance" do
27
+ pseudo_method.get.should be_instance_of SteamMist::Connectors::LazyConnector
28
+ end
29
+
30
+ it "should use default arguments" do
31
+ pseudo_method.request_uri.arguments.should include(:something => "value")
32
+ end
33
+
34
+ it "should give a connector with caching" do
35
+ expect(pseudo_method.with_caching("some_file").get).to be_cache
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ describe SteamMist::RequestUri do
2
+
3
+ subject(:request_uri) do
4
+ SteamMist::RequestUri.new(:interface => "ISomeInterface", :method => "SomeMethod",
5
+ :version => 1)
6
+ end
7
+
8
+ it "should raise an argument error" do
9
+ expect { described_class.new(:something => :else) }.to raise_error(ArgumentError)
10
+ end
11
+
12
+ it "should return a uri" do
13
+ request_uri.format_uri.should be_instance_of(URI::HTTP)
14
+ end
15
+
16
+ it "should format the right string" do
17
+ request_uri.to_s.should eq "http://api.steampowered.com/ISomeInterface/SomeMethod/v0001?"
18
+ end
19
+ end