steam_mist 1.3.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,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